mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
docs: synchronize controller transport documentation
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md
|
||||
2026-08-10-remote-event-delivery.md: c0bc459eb5f96dccd135417c0d5d4d2f743aad5e
|
||||
2026-08-10-remote-event-delivery.zh.md: bb02addbf92cd4db477ed5c6e9627469faedd89f
|
||||
2026-08-10-remote-event-delivery.md: 5e6e04bdf2c6b685bbf10f05ede9c96ea8104429
|
||||
2026-08-10-remote-event-delivery.zh.md: d744b92d47f5778397b519fa09d4b91f620dcd7e
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Remote event delivery (ctx.remote.$on)
|
||||
# Agent Note: Remote event delivery (`ctx.remote.$on`)
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,39 +6,51 @@ English | [中文](2026-08-10-remote-event-delivery.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
[Typert Gateway targeted method calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) cover only the request/response shape and deliberately leave Session event streams and stateful interactions to separate designs. Every **one-way Host-to-consumer push** therefore still rides the legacy API Proxy.
|
||||
[Typert Remote method calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) initially cover targeted calls with one result per request and deliberately leave Session streams and stateful interactions elsewhere. Host-to-consumer events need a delivery mechanism that is not owned by the API Proxy domain.
|
||||
|
||||
The Host owns a family of one-way events whose payloads are already JSON and whose emission never binds an AgentScope: `agent-preset/selected`, `commands/change`, `credentials/reference-updated`, `llm/adapters-updated`, and `settings/document-updated`. Reaching one UI subscriber took four hops: the Host cordis event, a hand-written `HostFrame` variant plus its zod branch in apiproxy, a hand-written bridge in client/runtime that re-emitted it as a Client cordis event, and finally the consumer's `ctx.on(...)`. Adding one such event edited five places (frame union, zod union, host-stream listener, client bridge, a duplicated Client-side `Events` declaration), and not one of them stated a new fact: the name, the payload type, and the emission point were all declared by the owner package's cordis `Events` merge.
|
||||
The Host owns one-way events such as `agent-preset/selected`, `commands/change`, `credentials/reference-updated`, `llm/adapters-updated`, and `settings/document-updated`. They do not depend on AgentScope, and their payloads are already JSON. Requiring every event to cross a handwritten API Proxy frame, a handwritten Client Runtime bridge, and a Client event alias adds no fact beyond the owner event declaration.
|
||||
|
||||
That duplicated declaration is also **lossy**: the Client side restates it as `settings/changed(ns: string)`, flattening a branded type into bare `string` — the opposite of the Remote method contract, where a consumer type points at the business package's one canonical symbol.
|
||||
That duplicate declaration is also lossy: the Client side restates an event as `settings/changed(ns: string)`, flattening a branded type to bare `string`, contrary to the Remote-method rule that consumer types point to the business package's one canonical symbol.
|
||||
|
||||
## Decision
|
||||
|
||||
The consumer Remote surface carries one one-way subscription verb, `ctx.remote.$on(event, listener)`, driven by an allowlist and forwarding verbatim:
|
||||
The consumer Remote surface has one event-subscription verb, `ctx.remote.$on(event, listener)`, with allowlist-driven, verbatim forwarding:
|
||||
|
||||
- `packages/api/remotes/src/remote-events.ts` holds the allowlist of forwardable Host events, and it is the single control point over what a consumer may subscribe to. `src/types.ts` beside it derives the type projection and fills the selection seat, staying type-only per the package convention. Both files are listed in the `files` of **both** of this package's faces, so the Host forwarding loop and the consumer key surface read one declaration.
|
||||
- The wire event name **is** the Host cordis event name (`settings/document-updated`) with no `host/` prefix, and the payload **is** the Host argument list, element for element, with no projection, redaction, or renaming.
|
||||
- The carrier reuses the existing host stream: `HostFrame` gains one wrapper variant, `host/remote-event`. No new downlink.
|
||||
- Event **signatures** get no second table. Each owner package moves its cordis `Events` declaration into its client-safe, type-only `./types` export, so both faces read the same declaration and `$on`'s listener type is `Events[Event]` itself. "Verbatim" then holds by construction rather than by proof.
|
||||
- Only cordis's *type shape* is borrowed, not its event system: delivery semantics, the subscription registry, and failure containment belong to Typert.
|
||||
- `packages/api/remotes/src/remote-events.ts` owns one list of forwardable Host events with explicit `emit`/`waterfall` modes. It is also the sole control point for what consumers may subscribe to. Adjacent `src/types.ts` derives the type projection and fills the selection seat while remaining type-only. Both files appear in the `files` of the package's Host and Client faces, so both read one declaration.
|
||||
- The event name on the wire is the original Host Cordis name (`settings/document-updated`) without a `host/` prefix. The payload is the Host argument list, element for element through JSON, without projection, redaction, or renaming.
|
||||
- `api/remotes` registers the Host source with API Gateway. Gateway reserves internal logical endpoint `$events` on the existing `/api/remote.mux`, adding no physical connection and giving API Proxy no event interpretation. Waterfall results return through HTTP unary endpoint `$events/result`.
|
||||
- Event signatures have no second table. Owner packages place their Cordis `Events` declarations in Client-safe, type-only `./types` exports so both faces read the same declaration. `$on` listener parameters, result, and `next()` derive from `Events[Event]`; verbatim correspondence holds by construction.
|
||||
- Only Cordis's type declarations are shared. Delivery semantics, registration, and failure handling belong to Typert.
|
||||
|
||||
When an `Events` entry's signature reaches a Host-only symbol (a Service, `Agent`, a Context), the answer is to **split the code until the entry lands cleanly in `./types`** — never a declaration half-left in `index.ts`, and never a structurally equivalent shadow type in `./types`. None of the five packages needs that here: their entries reach only `SettingsNamespace`, `SettingsUpdateSource`, `CredentialRef`, and `SessionId`, all pure types. The agent-presets package renames its previous vocabulary module to `preset.ts`, leaving the exported `types.ts` dedicated to the client-safe event declaration.
|
||||
When an `Events` member reaches a Host-only symbol such as a Service, `Agent`, or Context, the code is split until the declaration can live cleanly in `./types`. A declaration is never split between `index.ts` and `types.ts`, and `types.ts` does not invent a structurally equivalent shadow type. Every current owner exposes its selected event declaration from a Client-safe type export.
|
||||
|
||||
All five events ride this path, and their dedicated `HostFrame` variants or Client aliases are gone. Model consumers subscribe directly to both owner inputs, `llm/adapters-updated` and `settings/document-updated`; preset-derived consumers subscribe to `agent-preset/selected`. Frames that actually project or deduplicate data stay dedicated: `host/workspace-changed`/`-removed`/`host/archived-sessions-changed` (view derivation plus per-connection dedup state), and `host/session-added`/`-removed`/`host/session-status`/`host/agent-error` (live-object projection or frame-time derived fields).
|
||||
All allowlisted events use this path, and dedicated frames and Client aliases are removed. Model consumers subscribe directly to `llm/adapters-updated` and `settings/document-updated`; preset consumers subscribe to `agent-preset/selected`; stateless Session and dynamic-Cordis notifications use `emit`; Approval and Question use Agent-scoped `waterfall`. Data that needs a baseline, projection, or deduplication retains a dedicated Remote stream.
|
||||
|
||||
`skills/change`, `tools/change`, and `system-prompt/change` have the same shape but **no shipped consumer**; under "require a current owner and need" they stay out of the allowlist and are recorded here only as the extension seat.
|
||||
`skills/change`, `tools/change`, and `system-prompt/change` have the same pure invalidation form but no shipped consumer. The rule that every abstraction needs a current owner and need keeps them outside the allowlist; they remain only an extension point recorded here.
|
||||
|
||||
### Consumer contract (dsh-typert-protocol)
|
||||
### Consumer contract (`dsh-typert-protocol`)
|
||||
|
||||
type-meta gains one **shape predicate**, one **selection seat**, and **one** member on `TypertClientRemote`. No runtime code:
|
||||
Type metadata adds event-form predicates, mode entries, a selection seat, and one member of `TypertClientRemote`, with no runtime code:
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
import type { Events } from '@deepseek-ai/cordis'
|
||||
|
||||
/** Cordis events shaped for one-way remote delivery: no Scope binding, void return. */
|
||||
type TypertForwardingMode<Event extends keyof Events> =
|
||||
unknown extends ThisParameterType<Events[Event]>
|
||||
? TypertEventResult<Event> extends void ? 'emit' : never
|
||||
: TypertWaterfallEvent<Event> extends never ? never : 'waterfall'
|
||||
|
||||
/** Cordis event names that can cross the Remote Event carrier without a second signature. */
|
||||
export type TypertForwardableEvent = {
|
||||
[Event in keyof Events]: unknown extends ThisParameterType<Events[Event]>
|
||||
? ReturnType<Events[Event]> extends void ? Event : never
|
||||
[Event in keyof Events]: TypertForwardingMode<Event> extends never ? never : Event
|
||||
}[keyof Events]
|
||||
|
||||
/** Event and dispatch mode accepted by the Remote Event source. */
|
||||
export type TypertForwardableEventEntry = {
|
||||
[Event in keyof Events]: TypertForwardingMode<Event> extends infer Mode
|
||||
? Mode extends 'emit' | 'waterfall'
|
||||
? { readonly event: Event; readonly mode: Mode }
|
||||
: never
|
||||
: never
|
||||
}[keyof Events]
|
||||
|
||||
@@ -51,126 +63,136 @@ export type TypertRemoteEvent = Extract<keyof Events, keyof TypertRemoteEventSel
|
||||
|
||||
```ts ignore-check
|
||||
/** Subscribe to one forwarded Host event; the returned disposer belongs to the calling fiber. */
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventListener<Event>): () => void
|
||||
```
|
||||
|
||||
`Events` resolves per program: the full Host vocabulary in the Host program, whatever the Client face can see in the Client program. The same predicate therefore holds on both sides without dragging Host declarations into the Client.
|
||||
`Events` resolves per program: the complete Host event vocabulary in a Host program and only declarations visible to the Client compilation face in a Client program. The same predicate therefore holds on both sides without bringing Host declarations into the Client.
|
||||
|
||||
**The surface separates the consumer verb from the carrier handoff**: consumers subscribe with `$on`, and whoever owns the Host frame sink hands each decoded frame over with `$dispatch`. It cannot be a module-level function reaching across Client plugins — the client bundle purity gate (`packages/client/tsdown.client.ts`) admits value imports only from the implicit `PLATFORM_MODULES` plus `PRELOADED_CLIENT_EXTERNALS` baseline, the package's `dsh.client.external` requests, the `INLINE_SAFE` wire layer, and generated `/remote` contributions. Inlining around it would copy `ClientRemoteService` into the runtime bundle, making `instanceof` permanently false. A cordis service method is the collaboration shape that gate prescribes:
|
||||
**The contract exposes only the consumer verb.** `ClientRemoteService` registers the one internal `$events` pump as a Connection generation source when it activates, independently of whether any `$on` subscription exists. Browsers open `$events` through the shared Remote mux; in-process compositions open the same logical stream through `connection.rpc.open`. Decoding, exact item validation, and Cordis dispatch are private Gateway Client implementation. `TypertClientRemote` exposes no producer operation, so a business plugin cannot synthesize a Host event.
|
||||
|
||||
Each time the Host opens `$events`, the API Remotes source factory installs every allowlist listener synchronously. Gateway then yields the opening `{ type: 'ready' }` before iterating the event source. `ConnectionController` waits for that ready item and `host.describe` in parallel and publishes `connected` only after both succeed, so baseline reads cannot race ahead of incremental listeners.
|
||||
|
||||
A physical mux disconnect ends the logical stream with `RemoteStreamCarrierError`. A Host Remote stream error, unexpected normal completion, non-ready opening item, or malformed event item also ends the current generation. Connection withdraws that generation's `hostDescription` and reopens `$events` and `host.describe` after backoff; Gateway mux only rebuilds the physical WebSocket. Ordinary events are not replayed. State whose correctness requires recovery must provide a query, cursor, or opening baseline and cannot treat `$on` as a reliable journal.
|
||||
|
||||
The Client dispatches on a Cordis key private to each Remote instance. Ordinary `emit` uses `parallel()` and contains listener failures; Agent-scoped `waterfall` uses `waterfall()` on the resolved Agent Context and allows a result, rejection, or `next()` delegation. Both registration kinds belong to the calling fiber, and Host events do not trigger same-named Client-local events.
|
||||
|
||||
### The allowlist: one declaration read by both faces
|
||||
|
||||
`packages/api/remotes/src/remote-events.ts` appears in both `tsconfig.host.json` and `tsconfig.client.json` and is the allowlist's sole home. `src/types.ts` derives the type face:
|
||||
|
||||
```ts ignore-check
|
||||
$dispatch(event: string, args: readonly unknown[]): void
|
||||
```
|
||||
|
||||
client/runtime — the owner of the host frame sink — calls it directly, so the frame reaches the subscription table without an intermediate event to relay it. The `event` parameter is `string`, not `TypertRemoteEvent`: this is a wire boundary, and a name nobody subscribed to is dropped silently.
|
||||
|
||||
Delivery shares no implementation with the cordis event system: one-way only, no waterfall/bail/parallel/serial modes and no `@mode` concept (`ReturnType extends void` is the static expression of that rule), no `this` binding, no `EventOptions`, `prepend`, or priority. Listeners run in registration order, and one that throws is contained and logged — it must never take down the frame pump (the same posture `ConnectionController` already applies to its sinks).
|
||||
|
||||
### The allowlist: one declaration both faces read
|
||||
|
||||
`packages/api/remotes/src/remote-events.ts` is listed in the `files` of both `tsconfig.host.json` and `tsconfig.client.json`, and is the allowlist's single home; `src/types.ts` derives its type face:
|
||||
|
||||
```ts
|
||||
// remote-events.ts — the value
|
||||
export const API_REMOTE_FORWARDED_EVENTS = [
|
||||
'agent-preset/selected',
|
||||
'commands/change',
|
||||
'credentials/reference-updated',
|
||||
'llm/adapters-updated',
|
||||
'settings/document-updated',
|
||||
] as const
|
||||
{ event: 'agent-preset/selected', mode: 'emit' },
|
||||
{ event: 'approval/request', mode: 'waterfall' },
|
||||
...SESSION_CONTROLLER_REMOTE_EVENTS.map(event => ({ event, mode: 'emit' as const })),
|
||||
{ event: 'commands/change', mode: 'emit' },
|
||||
{ event: 'credentials/reference-updated', mode: 'emit' },
|
||||
{ event: 'cordis/request-run', mode: 'emit' },
|
||||
{ event: 'cordis/request-run-resolved', mode: 'emit' },
|
||||
{ event: 'cordis/dynamic-package', mode: 'emit' },
|
||||
{ event: 'cordis/dynamic-retract', mode: 'emit' },
|
||||
{ event: 'cordis/inspect-query', mode: 'emit' },
|
||||
{ event: 'cordis/inspect-query-resolved', mode: 'emit' },
|
||||
{ event: 'llm/adapters-updated', mode: 'emit' },
|
||||
{ event: 'settings/document-updated', mode: 'emit' },
|
||||
{ event: 'user-questions/request', mode: 'waterfall' },
|
||||
] as const satisfies readonly TypertForwardableEventEntry[]
|
||||
|
||||
// types.ts — the type face, derived
|
||||
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]
|
||||
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]['event']
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface TypertRemoteEventSelection extends Record<ApiRemoteForwardedEvent, true> {}
|
||||
}
|
||||
```
|
||||
|
||||
Forwarding one more event is therefore **one line in that array**: the type projection, `$on`'s key surface, and the Host forwarding loop all derive from it. `ctx.remote.$on('slots/changed', …)` (a Client-local event) and `$on('skills/change', …)` (declared but unselected) are both **compile errors**.
|
||||
Adding an event is therefore one array entry: type projection, the `$on` key set, Host dispatch mode, and the forwarding loop all derive from it. `ctx.remote.$on('slots/changed', …)` for a Client-local event and `$on('skills/change', …)` for a declared but unselected event are compile errors.
|
||||
|
||||
The Host face adds one shape assertion, binding the Host event vocabulary to that same array:
|
||||
The declaration's trailing `satisfies` applies Host event-vocabulary and mode constraints to the same allowlist:
|
||||
|
||||
```ts ignore-check
|
||||
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEvent[]
|
||||
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEventEntry[]
|
||||
```
|
||||
|
||||
It is an expression statement rather than a named constant, which `noUnusedLocals` would reject (the underscore prefix exempts parameters only). It enforces three things: the **name is real** (the predicate is keyed on `keyof Events`), the event **binds no Scope** (`goal/changed` and kin have a `ThisParameterType` other than `unknown` and drop out — the static expression of "no AgentScope dependency"), and the event is **one-way** (a non-`void` return, i.e. a waterfall/bail shape, drops out).
|
||||
It enforces three properties: the name exists because the predicate is keyed by `keyof Events`; the selected mode matches the signature; and the signature is either an unscoped `void` notification or a waterfall with top-level Agent scope, a same-result `next()`, and a Promise return. Other Scope, bail, parallel, and serial forms are excluded.
|
||||
|
||||
**"Verbatim" is proved nowhere because it holds by construction**: `$on`'s listener type comes from the one cordis `Events` declaration in the owner package's `./types`, and Host forwarding reads that same declaration. There is no second declaration that could drift.
|
||||
Verbatim correspondence is not proved separately because it holds by construction. `$on`'s listener type and Host forwarding both read the owner package's one Cordis `Events` declaration, so no second declaration can drift.
|
||||
|
||||
JSON-safety is a runtime concern: before forwarding, apiproxy validates each argument with `dsh-session`'s `isJsonValue` and **throws loudly** when one fails, because that is an allowlist composition mistake rather than untrusted input.
|
||||
JSON safety remains a runtime concern. Before queueing, the API Remotes Host source checks every argument with `dsh-session`'s `isJsonValue` and fails loudly when one is invalid, because this is an allowlist composition error rather than untrusted input.
|
||||
|
||||
### Wire contract (apiproxy)
|
||||
### Wire protocol (API Gateway Remote mux)
|
||||
|
||||
```ts ignore-check
|
||||
| { type: 'host/remote-event'; event: string; args: JsonValue[] }
|
||||
ready { type, clientId }
|
||||
emit { type, event, args }
|
||||
waterfall { type, event, eventId, agentId, request }
|
||||
cancel { type, eventId }
|
||||
```
|
||||
|
||||
The zod branch keeps `args: z.array(z.unknown())`: the frame arrives from `JSON.parse`, so every element is already a JSON value, and the structural contract belongs to the owner package's `Events` declaration — the same posture the existing `session/projection` frame takes with its `value`.
|
||||
The Client opens internal logical stream `$events` with payload `{ args: {} }`. Gateway rejects extra parameters, a missing Host source, and duplicate source registration. Withdrawing a source aborts every stream opened by that registration. Each Client stream owns an independent queue and allowlist listener set in `api/remotes`, so disconnecting one Client neither consumes nor withdraws another Client's events.
|
||||
|
||||
`events.host()` subscribes by allowlist when the stream opens. Each stream owns its disposers, so no broadcast set or derived invalidation listener is needed.
|
||||
The Client requires an opening `ready` item with a non-empty `clientId`; every later item is checked for exact fields by discriminant. An ordinary `emit` with an unknown but structurally valid event name is dropped when there is no subscriber. Waterfalls use `eventId` to correlate `$events/result` and `agentId` to select a Client Agent Context. The Client returns only values representable as lossless JSON; transport does not reinterpret business fields.
|
||||
|
||||
`api/events.ts` is a wire contract file the browser side also compiles, so every type it references must come from an owner package's **client-safe, type-only subpath**, never the package root. Evidence: importing one type from `@deepseek-ai/dsh-session` root drags the root's `declare module 'cordis' { interface Context { sessions: SessionStore } }` into the Client compilation face and overrides the Client's `ctx.sessions: ISessions`, producing 18 errors in the unrelated `ui-input-trigger` and `ui-conversation`. `JsonValue` therefore needs a re-export from `dsh-session/src/types.ts`.
|
||||
`$events` is an internal Gateway endpoint. It does not enter a generated Typert Remote descriptor or become `ctx.remote.<namespace>`. Application selection exists only in the API Remotes allowlist and Host source; Gateway owns registration, payload validation, and physical transport only.
|
||||
|
||||
### The apps/web browser e2e belong to the Host face
|
||||
### The `apps/web` browser e2e belongs to the Host face
|
||||
|
||||
The `apps/web/tests/**` e2e type-check in the root **`tsconfig.host.json`**: they boot a real harness in-process and read `ctx.apiProxy`, the Host `SessionStore`'s `get`/`create`/`flush`, and `ctx.sessionProjectionCache`. **Driving a browser at runtime does not make a file part of the Client program** — moving them into the Client aggregate immediately produces 21 errors, because one program cannot hold both faces' merges for the same Context key.
|
||||
The `apps/web/tests/**` e2e files typecheck in root `tsconfig.host.json`: they boot a real harness in process and directly access `ctx.apiProxy`, Host `SessionStore.get/create/flush`, and `ctx.sessionProjectionCache`. Driving a browser at runtime does not place a file in the Client TypeScript program. Moving these tests to the Client aggregate produces 21 errors because one program cannot hold both faces' merges for the same Context key.
|
||||
|
||||
That yields a discipline this design depends on: **when those tests import a value or a type from a Client package, they pull that package's whole project — and every project it references — into the Host build graph**. Four consumers (`ui-settings-general`, `ui-settings-models`, `ui-permission`, `ui-commands`) reference `api/remotes`' Client face, and that face cannot compile until Host tsdown has generated `@deepseek-ai/dsh-goal/remote`. The result is a build-order deadlock: Host tsc needs the Client face, which needs the generated artifact, which Host tsdown produces after Host tsc.
|
||||
This implies one build rule needed by the design: importing a value or type from a Client package in those tests brings that package's whole project and all its project references into the Host build graph. Four consumers (`ui-settings-general`, `ui-settings-models`, `ui-permission`, and `ui-commands`) reference API Remotes' Client face, which cannot compile until Host tsdown generates `@deepseek-ai/dsh-goal/remote`. That forms a build-order cycle: Host tsc needs API Remotes Client, which needs generated `goal/remote`, which Host tsdown emits after Host tsc.
|
||||
|
||||
The few Client-owned symbols are therefore **mirrored** on the test side (`scaffold.ts` exports the mirrored welcome-notice constants; the two chat e2e keep importing `dsh-client-runtime/client` because the `runtime` project is already in the Host graph), which lets those four consumers leave the Host graph. The 15 Client project references in `apps/cli/tsconfig.json` lost their owner-map role and are gone. Each mirrored value matches its source verbatim; a drift shows up as a missed selector or an unsuppressed notice, both loud failures.
|
||||
The few required Client symbols are mirrored on the test side: `scaffold.ts` exports the mirrored welcome-notice constants, while the two chat e2e files import `dsh-client-runtime/client` directly because the Runtime project already belongs to the Host graph. This removes those four consumers from the Host graph, and the 15 Client project references in `apps/cli/tsconfig.json` no longer serve an owner-map role. Each mirror is byte-identical to its source; drift produces a selector mismatch or an unsuppressed notice and fails loudly.
|
||||
|
||||
### Change inventory
|
||||
|
||||
| Location | Change |
|
||||
|---|---|
|
||||
| `dsh-typert-protocol` | `src/types.ts` gains `TypertForwardableEvent`, `TypertRemoteEventSelection`, and `TypertRemoteEvent`; `TypertClientRemote` gains `$on` and `$dispatch`. Types only, no runtime |
|
||||
| `api/gateway` Client half | `ClientRemoteService` implements `$on` (subscriptions addressed by registration, `ctx.effect` ownership for the calling fiber) and `$dispatch` (snapshot delivery in registration order, containing a listener that throws or rejects) |
|
||||
| `api/remotes` | New `src/remote-events.ts` (the allowlist value) and `src/types.ts` (type projection, selection seat), both listed in both faces' `files`; a `./types` export with `lib/types/**/*.js` added to `files`; the Host face adds the shape assertion and `import type {}` for the five owner `./types`; the Client half re-exports those five plus `@deepseek-ai/dsh-api-gateway/client` |
|
||||
| Root `tsconfig.base.json` | Client-safe `paths` entries for settings, credentials, llm, agent-presets, and api-remotes types point at the **source** plane |
|
||||
| `dsh-commands` / `dsh-settings` / `dsh-credentials` / `dsh-llm` / `dsh-agent-presets` | Each forwarded `interface Events` member lives in the owner's client-safe `./types`; agent-presets moves its previous domain vocabulary to `preset.ts` so the exported file itself remains `types.ts` |
|
||||
| `host/apiproxy` | `HostFrame` gains `host/remote-event` and loses the five dedicated passthrough or invalidation variants with their zod branches; `events.host()` subscribes by allowlist and validates through `assertJsonArgs` |
|
||||
| `dsh-session` | `src/types.ts` re-exports `JsonValue` so wire contract files can use the client-safe subpath |
|
||||
| `client/runtime` | The five Client-event bridge branches collapse into `ctx.remote.$dispatch(frame.event, frame.args)`, adding a `remote` injection and deleting their duplicated `Events` declarations |
|
||||
| Seven consumers | ui-commands / ui-model-selection / ui-settings-models / ui-settings-general / ui-permission / ui-agent-preset / ui-skill subscribe through `ctx.remote.$on(...)`, following `ui-goal`'s precedent for the type-only facade import and the `'remote'` injection |
|
||||
| `client/connection` | The fixture's `emitHost` produces `host/remote-event` |
|
||||
| `apps/web/tests` + `apps/cli` | Client symbols mirrored on the test side (see above); `apps/cli/tsconfig.json` drops its 15 Client project references |
|
||||
| `dsh-typert-protocol` | `src/types.ts` provides forwardable-mode derivation, selection, and Client-listener projection; `TypertClientRemote` exposes only `$on`. Types only, no runtime |
|
||||
| `api/gateway` | Host provides one Remote event source, `$events`, pending-waterfall coordination, and `$events/result`; Client registers the private pump as the Connection generation source and owns frame validation and Cordis dispatch |
|
||||
| `api/remotes` | `src/remote-events.ts` (mode-bearing allowlist value) and `src/types.ts` (key projection and selection) belong to both faces; Host registers each Client source and validates JSON before queueing; Client continues to compose generated Remote contributions |
|
||||
| Root `tsconfig.base.json` | Adds source-plane `paths` entries for `dsh-settings/types`, `dsh-credentials/types`, and `dsh-api-remotes/types` |
|
||||
| `dsh-commands` / `dsh-settings` / `dsh-credentials` | Moves each `interface Events` member to the owner's Client-safe `./types`; settings and credentials add that export, move brands and pure types with it, retain constructors in index, and include `lib/types/**/*.js` in published files |
|
||||
| `host/apiproxy` | Contains no `HostFrame`, `events.host()`, or other Host downlink carrier; API Proxy does not participate in Host events or Connection generation |
|
||||
| `dsh-session` | Exposes `isJsonValue` for validation of every event argument by the API Remotes Host source |
|
||||
| `client/runtime` | Removes the bridge from Host frames to the Remote subscription table; it only publishes `connection/reset` after a Connection generation is established |
|
||||
| Consumers | Client plugins subscribe directly through `ctx.remote.$on(...)`, import owner event declarations type-only, and inject `'remote'` |
|
||||
| `client/connection` | Provides the one generation-source registration point; `ConnectionController` combines `$events` ready with `host.describe`, and the fixture emits events from the same source |
|
||||
| `apps/web/tests` + `apps/cli` | Mirrors Client symbols on the test side as described above and removes 15 Client project references from `apps/cli/tsconfig.json` |
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Open a general downlink channel for Remote events** (the push counterpart of `ctx.connection.rpc`, a third WebSocket). This best matches "Connection owns the carrier, the Gateway never touches transport", but it means a new stream in the Host downlink, `WebApiClient`, `ConnectionController`, the fixture, and the web e2e — a cost out of proportion to this change. Reusing the host stream costs a temporary tenancy inside a legacy frame union; when that stream moves, the wrapper moves with it and the consumer contract does not change.
|
||||
**Continue using API Proxy's Host downlink.** This reuses Connection generation and `connection/reset` but leaves the Remote event allowlist, queue, schema, and Client Runtime bridge in API Proxy and prevents domain transports from sharing the lifecycle of other Remote streams. With API Gateway's resident `/api/remote.mux`, `$events` adds only one internal logical stream and belongs naturally in Gateway.
|
||||
|
||||
**Declare a separate `TypertRemoteEventMap` in type-meta and let owner packages merge into it.** The consumer key set would equal exactly "events declared remotely deliverable", but every signature would be written a second time outside cordis `Events`, requiring a bidirectional `extends` proof to stop the two from drifting, plus a new type-meta dependency for three owner packages. Sharing the one `Events` declaration makes that equivalence structural, so the table is not created.
|
||||
**Open a third physical WebSocket or duplex stream for Remote events.** An independent channel could own connection state but would duplicate authenticated upgrade, multiplexing, cancellation, error mapping, and reconnect backoff already provided by Gateway mux. Internal `$events` retains an independent logical stream, while waterfall results reuse HTTP unary calls.
|
||||
|
||||
**Have the typert generator project Host `Events` declarations** (codec, `.d.ts`, declaration map, like `/remote`). The generator already analyzes Host events, but it cannot see projection or redaction intent, and it would change the generator and the build surface. Verbatim forwarding needs no projection.
|
||||
**Declare a separate `TypertRemoteEventMap` in type metadata and let owner packages declaration-merge into it.** The consumer key set would exactly equal remotely deliverable events, but every signature would be written again outside Cordis `Events`, requiring a bidirectional equivalence proof and new type-metadata dependencies for owner packages. Sharing one `Events` declaration makes equivalence structural, so the second map is not created.
|
||||
|
||||
**Give forwardable events a payload projection function** (a `{ name, project, zod }` forwarding table). This could fold the two model-directory inputs into one derived invalidation and also cover workspace view derivation, at the cost of hand-aligning projection logic with payload types — the central table the method side just removed.
|
||||
**Have the Typert generator project Host `Events` declarations.** The generator already analyzes Host events, but it cannot infer projection or redaction intent and would expand the generator and build surface. Verbatim forwarding needs no projection.
|
||||
|
||||
**Move the apps/web browser e2e into the Client aggregate.** "Client tests belong to the Client face" looks right and fails immediately with 21 errors: those tests use Host services, and in the Client program `ctx.sessions` is `ISessions`.
|
||||
**Give forwardable events a payload projection function.** A `{ event, project, zod }` table could combine model-directory inputs and derive Workspace views, but would manually align projection logic with payload types and recreate the central table removed from Remote methods.
|
||||
|
||||
**Split `directory-picker-browse`/`-native` into Host and Client faces** so no Client package reaches the Host graph. The direction is right — they are genuinely unsplit dual-half packages — but the change lands in another owner's packages and buys only a cleaner build graph; once this design mirrors the Client symbols on the test side, it no longer needs the split. **Assessed and declined.**
|
||||
**Move the `apps/web` browser e2e into the Client aggregate.** The intuition that browser tests belong to the Client face fails with 21 errors because the tests use Host services while the Client program's `ctx.sessions` is `ISessions`.
|
||||
|
||||
**Split `directory-picker-browse`/`-native` into Host and Client faces.** This would remove Client packages from the Host graph, but changes another owner's packages for only a cleaner build graph. Mirroring the required Client symbols on the test side removes the need for that split.
|
||||
|
||||
## Verification
|
||||
|
||||
What pins this behavior:
|
||||
|
||||
- A real composition test puts one `host/remote-event` frame on the real host stream per Host emit, with `event` the Host name and `args` equal element for element.
|
||||
- Type-level negatives reject three candidate classes: a name that is not an event, a Scope-bound event (`goal/changed`), and an event whose return is not `void`. `$on('slots/changed', …)` (Client-local) and `$on('skills/change', …)` (declared but unselected) both fail to compile, so `$on`'s key surface equals the allowlist.
|
||||
- On the consumer side, `$on('settings/document-updated', …)` resolves `ns` as `SettingsNamespace`: the brand survives the wire.
|
||||
- `$on`'s disposer belongs to the calling fiber, and two registrations of one function object retire independently — a table keyed on listener identity would collapse them, so subscriptions are addressed by registration.
|
||||
- Delivery contains a listener that throws AND one that rejects a returned promise: the declared return is `void`, so nobody awaits an async listener, and its rejection would otherwise escape this containment entirely. Delivery iterates a snapshot, so subscribing or disposing mid-frame cannot change who receives that frame.
|
||||
- `assertJsonArgs` is unit-tested directly rather than by driving a malformed emit through the event bus: a typed `ctx.emit` cannot construct one, since every allowlisted event has a statically JSON-safe payload.
|
||||
- The five dedicated `HostFrame` variants, five Client-side aliases, and their bridge branches are absent. The model directories observe both owner inputs, while command, skill, and session-row consumers observe the preset owner's committed-selection event.
|
||||
- A real Host-source composition test proves that two Client streams each receive `{ event, args }`, disconnecting one does not affect the other, and non-JSON arguments fail loudly without poisoning later valid delivery.
|
||||
- Type negatives reject unselected events, non-`void` unscoped events, non-Agent-scoped waterfalls, and allowlist modes that disagree with signatures. `$on('slots/changed', …)` and `$on('skills/change', …)` both fail to compile, so `$on`'s key set equals the allowlist.
|
||||
- Consumer `$on('settings/document-updated', …)` resolves `ns` as `SettingsNamespace`, preserving the brand across the wire.
|
||||
- A `$on` disposer belongs to the calling fiber, and registering the same function object twice produces independently removable registrations; subscriptions are addressed by registration rather than listener identity.
|
||||
- Ordinary notifications contain both a throwing listener and a listener returning a rejected Promise. Waterfall tests pin Client result, `next()`, rejection, cancellation, first claim across multiple Clients, and reconnect replay of a pending request.
|
||||
- Gateway tests cover missing, duplicate, and withdrawn sources; payload rejection; ready-before-event ordering; and browser and in-process carriers. Client tests cover generation-source registration, description/increment readiness order, reopen after physical failure, Host errors and unexpected completion, non-ready opening items, malformed event items, `$events/result` failure, and disposal quiescence.
|
||||
- `host/remote-event`, public `$dispatch`, the Client Runtime bridge, and API Proxy's allowlist dependency are absent; consumers observe owner events directly.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Tenancy inside a legacy frame union.** The contract lives in apiproxy's `HostFrame`, so a reader may assume apiproxy owns Remote events. The frame's JSDoc names `api-remotes` as the allowlist owner, and apiproxy's README records the tenancy under known limitations. When the host stream moves off that package, the wrapper moves with it and the consumer contract does not change.
|
||||
- **Two files break api/remotes' face-disjointness contract.** `src/remote-events.ts` and `src/types.ts` belong to both projects, so each emits an identical declaration into the shared `lib/types`. Content is byte-identical and the `.tsbuildinfo` files stay separate, so this is harmless in practice; the README's build-boundary section states the exception and its cause (the `paths` entry points at source).
|
||||
- **The carrier handoff is developer-visible.** Any Client plugin holding `ctx.remote` can call `$dispatch` and synthesize a forwarded event. That exposure predates the verb — `ctx.emit` was equally reachable while an internal event relayed the frame — and matches what `connection/reset` already allows for a fabricated reconnect; the Client is one trust domain. Tests pin the handoff-to-`$on` conversion and do not pretend the port authenticates its caller.
|
||||
- **A malformed argument fails in the emitter's containment, not at load.** `assertJsonArgs` throws inside the forwarding listener, so the emitting seam's listener containment logs it and drops that frame: loud in the Host log rather than at load or at the emit point.
|
||||
- **Mirrored test values can drift.** Nothing mechanically checks the Client constants mirrored in `apps/web/tests` against their source; the safety net is only that a drift misses a selector. The rule lives in `apps/web/tests/README.md` and is held by review — a grep-level gate was considered and deliberately skipped.
|
||||
- **Capabilities given up.** No projected or redacted payloads, no Scope-bound events (`agentCtx.remote.$on`), and no replay on reconnect — these are pure invalidation signals, and `connection/reset` already covers refetching after a reconnect. The mux stream's session events, answerable frames, and snapshot baselines stay out of scope.
|
||||
- **Client packages remain in the Host graph.** Twelve projects (`connection`, `runtime`, `ui-slots`, and kin) still reach it through the unsplit `directory-picker-browse`/`-native` pair and `api/gateway → client/connection`. They compile and no longer implicate api/remotes' Client face, so they did not block this change; splitting those packages would remove a few but was assessed and declined. The two chat e2e importing `dsh-client-runtime/client` rely on `runtime` already being in that graph — incidental, not a guarantee.
|
||||
- **The invariant companion holds no runtime check.** An earlier revision asserted the dispatch shape (`thisArg === null`, `mode === 'emit'`) over the live event bus, which coupled the companion to the allowlist value and made rolldown hoist it into a third bundle chunk the mechanical publication list does not carry. The Host face's `TypertForwardableEvent` assertion already refuses both deviations at compile time, so the companion is an explained empty installer.
|
||||
- **Gateway has one non-generated endpoint.** `$events` has no business namespace and does not enter the Typert descriptor. It is the internal connection point between Gateway and API Remotes and defines the Client Connection generation lifetime. Strict empty-payload validation, opening-ready validation, and single-source registration prevent it from becoming another handwritten business API.
|
||||
- **Two files break API Remotes' face-disjointness rule.** `src/remote-events.ts` and `src/types.ts` belong to both projects and emit identical declarations into shared `lib/types`. Their content is byte-identical and `.tsbuildinfo` files remain separate, so this is safe in practice; the README records why source-plane `paths` require the exception.
|
||||
- **Producer operations remain private.** Business plugins can call only `$on`. Host-source registration and Client dispatch are absent from `TypertClientRemote`; test doubles drive subscriptions through their own `emit` operations rather than impersonating a production API.
|
||||
- **Malformed arguments fail at emit.** An API Remotes listener throws before queueing, so Host `ctx.emit` immediately observes an allowlist composition error and the queue can still deliver subsequent valid events.
|
||||
- **Test-side mirrors can drift.** No mechanism compares mirrored Client constants under `apps/web/tests` with their source. Drift instead produces a selector mismatch. `apps/web/tests/README.md` records the review rule; a grep-level gate is deliberately omitted.
|
||||
- **Capabilities deliberately omitted.** Payload projection and redaction are unsupported, scopes other than Agent are unsupported, and ordinary notifications are not replayed. Recoverable state needs a query, cursor, or opening baseline; a waterfall is replayed only while its original Host invocation remains pending.
|
||||
- **Some Client packages remain in the Host graph.** Twelve projects, including `connection`, `runtime`, and `ui-slots`, remain reachable through unsplit `directory-picker-browse`/`-native` and `api/gateway → client/connection`. They compile and no longer pull in API Remotes' Client face, so this change does not split them. Direct `dsh-client-runtime/client` imports in two chat e2e files rely on Runtime's current presence in that graph rather than a general guarantee.
|
||||
- **The invariant companion intentionally has no runtime check.** A prior revision asserted delivery form on the live event bus, coupling the companion to the allowlist and causing Rolldown to emit a third bundle chunk omitted by the mechanically derived publication list. The Host-face `TypertForwardableEventEntry` assertion already rejects those mismatches at compile time, so the companion is an explained empty installer.
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
[Typert Remote 方法调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md)最初只覆盖「一次请求一个结果」的定向调用,明确把 Session 事件流与有状态交互留在别处;Host 向消费端的**单向事件推送**需要一个不归 API Proxy 领域所有的投递机制。
|
||||
[Typert Remote 方法调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.zh.md)最初只覆盖「一次请求一个结果」的定向调用,明确把 Session 事件流与有状态交互留在别处;Host 向消费端的事件需要一个不归 API Proxy 领域所有的投递机制。
|
||||
|
||||
Host 拥有 `agent-preset/selected`、`commands/change`、`credentials/reference-updated`、`llm/adapters-updated`、`settings/document-updated` 等单向事件;它们既不依赖 AgentScope,载荷也本来就是 JSON。若每条事件都要穿过 API Proxy 手写帧、Client Runtime 手写桥和 Client 事件别名才能抵达 UI,这些层不会陈述 owner 事件之外的新事实。
|
||||
|
||||
@@ -14,31 +14,43 @@ Host 拥有 `agent-preset/selected`、`commands/change`、`credentials/reference
|
||||
|
||||
## 决策
|
||||
|
||||
消费端 Remote 面持有一个单向事件订阅动词 `ctx.remote.$on(event, listener)`;**名单驱动、原样转发**:
|
||||
消费端 Remote 面持有一个事件订阅动词 `ctx.remote.$on(event, listener)`;**名单驱动、原样转发**:
|
||||
|
||||
- `packages/api/remotes/src/remote-events.ts` 持有一份可转发 host 事件名单,它同时是「消费端能订阅什么」的唯一控制点。旁边的 `src/types.ts` 由它派生类型投影并填充 selection 座位,按包约定保持纯类型。两个文件**都同时列进本包 host 与 client 两个 face 的 `files`**,两侧读同一份。
|
||||
- `packages/api/remotes/src/remote-events.ts` 持有一份带 `emit`/`waterfall` mode 的可转发 Host 事件名单,它同时是「消费端能订阅什么」的唯一控制点。旁边的 `src/types.ts` 由它派生类型投影并填充 selection 座位,按包约定保持纯类型。两个文件**都同时列进本包 Host 与 Client 两个 face 的 `files`**,两侧读同一份。
|
||||
- wire 上的事件名 **就是 host cordis 事件原名**(`settings/document-updated`),不加 `host/` 前缀;载荷 **就是 host 的实参列表**,逐元素原样过 JSON,无投影、无脱敏、无改名。
|
||||
- Host source 由 `api/remotes` 注册到 API Gateway;Gateway 在既有 `/api/remote.mux` 上保留内部 logical endpoint `$events`,不增加物理连接,也不让 API Proxy 解释事件。
|
||||
- 事件**签名**不另立表:owner 包把自己的 cordis `Events` 声明搬进 client-safe 的 `./types` 纯类型出口,两侧读**同一份**——`$on` 的 listener 类型就是 `Events[Event]` 本身。「原样」不需要证明,是构造性成立的。
|
||||
- Host source 由 `api/remotes` 注册到 API Gateway;Gateway 在既有 `/api/remote.mux` 上保留内部 logical endpoint `$events`,不增加物理连接,也不让 API Proxy 解释事件。waterfall 结果通过 HTTP 一元 endpoint `$events/result` 返回。
|
||||
- 事件**签名**不另立表:owner 包把自己的 cordis `Events` 声明搬进 client-safe 的 `./types` 纯类型出口,两侧读**同一份**——`$on` 的 listener 参数、结果和 `next()` 都由 `Events[Event]` 推导。「原样」不需要证明,是构造性成立的。
|
||||
- 但**只借 cordis 的类型形状,不接 cordis 的事件系统**:投递语义、注册表、异常处置全归 Typert 自己。
|
||||
|
||||
一条 `Events` 条目若签名里够到了 host-only 符号(Service、`Agent`、Context 等),处理方式是**把代码拆到能干净落进 `./types` 为止**;不接受「一半留 index、一半搬走」的分裂声明,也不接受在 `./types` 里造结构等价的影子类型。当前名单内各 owner 都从 client-safe 类型出口提供同一份事件声明。
|
||||
|
||||
名单内事件全部走这条路径,专用帧与 Client 别名都已删除。模型消费方直接订阅 `llm/adapters-updated` 和 `settings/document-updated`;preset 消费方订阅 `agent-preset/selected`;Session 与动态 Cordis 的无状态通知使用同一机制。真正需要 baseline、投影或去重的数据仍保留专用 Remote stream。
|
||||
名单内事件全部走这条路径,专用帧与 Client 别名都已删除。模型消费方直接订阅 `llm/adapters-updated` 和 `settings/document-updated`;preset 消费方订阅 `agent-preset/selected`;Session 与动态 Cordis 的无状态通知使用 `emit`;Approval 与 Question 使用 Agent-scoped `waterfall`。真正需要 baseline、投影或去重的数据仍保留专用 Remote stream。
|
||||
|
||||
`skills/change`、`tools/change`、`system-prompt/change` 是同形状的纯失效事件但**没有任何已交付消费者**,按「每个抽象都要有当前 owner 与需求」不进名单,只作为扩展位记录在此。
|
||||
|
||||
### 消费端契约(dsh-typert-protocol)
|
||||
|
||||
type-meta 加一个**形状谓词**、一个**选择座位**和 `TypertClientRemote` 的**一个**成员;零运行时代码:
|
||||
type-meta 加事件形状谓词、mode 条目、选择座位和 `TypertClientRemote` 的一个成员;零运行时代码:
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
import type { Events } from '@deepseek-ai/cordis'
|
||||
|
||||
/** Cordis events shaped for one-way remote delivery: no Scope binding, void return. */
|
||||
type TypertForwardingMode<Event extends keyof Events> =
|
||||
unknown extends ThisParameterType<Events[Event]>
|
||||
? TypertEventResult<Event> extends void ? 'emit' : never
|
||||
: TypertWaterfallEvent<Event> extends never ? never : 'waterfall'
|
||||
|
||||
/** Cordis event names that can cross the Remote Event carrier without a second signature. */
|
||||
export type TypertForwardableEvent = {
|
||||
[Event in keyof Events]: unknown extends ThisParameterType<Events[Event]>
|
||||
? ReturnType<Events[Event]> extends void ? Event : never
|
||||
[Event in keyof Events]: TypertForwardingMode<Event> extends never ? never : Event
|
||||
}[keyof Events]
|
||||
|
||||
/** Event and dispatch mode accepted by the Remote Event source. */
|
||||
export type TypertForwardableEventEntry = {
|
||||
[Event in keyof Events]: TypertForwardingMode<Event> extends infer Mode
|
||||
? Mode extends 'emit' | 'waterfall'
|
||||
? { readonly event: Event; readonly mode: Mode }
|
||||
: never
|
||||
: never
|
||||
}[keyof Events]
|
||||
|
||||
@@ -51,7 +63,7 @@ export type TypertRemoteEvent = Extract<keyof Events, keyof TypertRemoteEventSel
|
||||
|
||||
```ts ignore-check
|
||||
/** Subscribe to one forwarded Host event; the returned disposer belongs to the calling fiber. */
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventListener<Event>): () => void
|
||||
```
|
||||
|
||||
`Events` 按程序解析:host 程序里是 host 事件全集,client 程序里是 client 编译面看得见的那些——同一个谓词在两侧各自成立,不需要把 host 声明拖进 client。
|
||||
@@ -62,50 +74,48 @@ $on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): ()
|
||||
|
||||
物理 mux 断开会让 logical stream 以 `RemoteStreamCarrierError` 结束;Host 返回的 Remote stream error、意外正常结束、非 ready 首项或畸形事件项也会结束当前 generation。Connection 撤回该 generation 的 `hostDescription`,在退避后重开 `$events` 和 `host.describe`;Gateway mux 只负责重建物理 WebSocket。转发事件不重放;凡正确性依赖恢复的状态,owner 必须另有查询、cursor 或 opening baseline,不能把 `$on` 当作可靠日志。
|
||||
|
||||
投递语义与 cordis 事件系统不共用实现:只有单向投递,没有 waterfall / bail / parallel / serial 模式,也没有 `@mode` 概念(`ReturnType extends void` 是这条纪律的静态表达);不绑 `this`;没有 `EventOptions`、`prepend`、优先级;按注册顺序逐个调用,单个 listener 抛错或返回拒绝的 Promise 都就地隔离并记日志,不能拖垮事件投递或 Connection generation。
|
||||
Client 以 Remote 实例私有 Cordis key 分发。普通 `emit` 使用 `parallel()` 并隔离 listener 失败;Agent-scoped `waterfall` 在解析出的 Agent Context 上使用 `waterfall()`,允许结果、拒绝或 `next()` 委托。两类注册都归属调用方 fiber,且 Host 事件不会触发 Client 本地同名事件。
|
||||
|
||||
### 名单:两个 face 共读的同一份声明
|
||||
|
||||
`packages/api/remotes/src/remote-events.ts` 同时列进 `tsconfig.host.json` 与 `tsconfig.client.json` 的 `files`,是名单的**唯一家**;`src/types.ts` 由它派生类型面:
|
||||
|
||||
```ts
|
||||
```ts ignore-check
|
||||
// remote-events.ts — the value
|
||||
export const API_REMOTE_FORWARDED_EVENTS = [
|
||||
'agent-preset/selected',
|
||||
'api-session/activity',
|
||||
'api-session/added',
|
||||
'api-session/error',
|
||||
'api-session/removed',
|
||||
'api-session/status',
|
||||
'commands/change',
|
||||
'credentials/reference-updated',
|
||||
'cordis/request-run',
|
||||
'cordis/request-run-resolved',
|
||||
'cordis/dynamic-package',
|
||||
'cordis/dynamic-retract',
|
||||
'cordis/inspect-query',
|
||||
'cordis/inspect-query-resolved',
|
||||
'llm/adapters-updated',
|
||||
'settings/document-updated',
|
||||
] as const
|
||||
{ event: 'agent-preset/selected', mode: 'emit' },
|
||||
{ event: 'approval/request', mode: 'waterfall' },
|
||||
...SESSION_CONTROLLER_REMOTE_EVENTS.map(event => ({ event, mode: 'emit' as const })),
|
||||
{ event: 'commands/change', mode: 'emit' },
|
||||
{ event: 'credentials/reference-updated', mode: 'emit' },
|
||||
{ event: 'cordis/request-run', mode: 'emit' },
|
||||
{ event: 'cordis/request-run-resolved', mode: 'emit' },
|
||||
{ event: 'cordis/dynamic-package', mode: 'emit' },
|
||||
{ event: 'cordis/dynamic-retract', mode: 'emit' },
|
||||
{ event: 'cordis/inspect-query', mode: 'emit' },
|
||||
{ event: 'cordis/inspect-query-resolved', mode: 'emit' },
|
||||
{ event: 'llm/adapters-updated', mode: 'emit' },
|
||||
{ event: 'settings/document-updated', mode: 'emit' },
|
||||
{ event: 'user-questions/request', mode: 'waterfall' },
|
||||
] as const satisfies readonly TypertForwardableEventEntry[]
|
||||
|
||||
// types.ts — the type face, derived
|
||||
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]
|
||||
export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number]['event']
|
||||
|
||||
declare module '@deepseek-ai/dsh-typert-protocol' {
|
||||
interface TypertRemoteEventSelection extends Record<ApiRemoteForwardedEvent, true> {}
|
||||
}
|
||||
```
|
||||
|
||||
于是**加一个事件只改这一行数组**:类型投影、`$on` 的键面、host 的转发循环全部从它派生。`ctx.remote.$on('slots/changed', …)`(client 本地事件)或 `$on('skills/change', …)`(名单没开)都是**编译错误**。
|
||||
于是**加一个事件只改这一行数组**:类型投影、`$on` 的键面、Host dispatch mode 与转发循环全部从它派生。`ctx.remote.$on('slots/changed', …)`(Client 本地事件)或 `$on('skills/change', …)`(名单没开)都是**编译错误**。
|
||||
|
||||
host 半再加一处形状断言,把 host 事件词汇的约束落到同一份名单上:
|
||||
数组声明末尾的 `satisfies` 把 Host 事件词汇与 mode 约束落到同一份名单上:
|
||||
|
||||
```ts ignore-check
|
||||
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEvent[]
|
||||
API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEventEntry[]
|
||||
```
|
||||
|
||||
写成表达式语句而不是命名常量:后者会被 `noUnusedLocals` 判为未使用(下划线前缀只豁免参数)。它卡住三件事:**名字合法**(谓词以 `keyof Events` 为基)、**不绑 Scope**(`goal/changed` 那族的 `ThisParameterType` 不是 `unknown`,被排除——「不依赖 AgentScope」的静态表达)、**单向**(非 `void` 返回的 waterfall/bail 形状被排除)。
|
||||
它卡住三件事:**名字合法**(谓词以 `keyof Events` 为基)、**mode 匹配签名**,以及只接受无 scope 的 `void` 通知或带一级 Agent scope、同结果 `next()` 和 Promise 返回的 waterfall。其他 Scope、bail、parallel 与 serial 形状都被排除。
|
||||
|
||||
**「原样」不在任何地方证明,而是构造性成立**:`$on` 的 listener 类型取自 owner 包 `./types` 里那一份 cordis `Events` 声明,host 转发读的是同一份,不存在可以彼此偏离的第二份声明。
|
||||
|
||||
@@ -114,13 +124,15 @@ API_REMOTE_FORWARDED_EVENTS satisfies readonly TypertForwardableEvent[]
|
||||
### 线协议(API Gateway Remote mux)
|
||||
|
||||
```ts ignore-check
|
||||
{ type: 'ready' }
|
||||
{ event: string; args: JsonValue[] }
|
||||
ready { type, clientId }
|
||||
emit { type, event, args }
|
||||
waterfall { type, event, eventId, agentId, request }
|
||||
cancel { type, eventId }
|
||||
```
|
||||
|
||||
Client 以 endpoint `$events` 和 payload `{ args: {} }` 打开 internal logical stream。Gateway 拒绝额外参数、缺失 Host source 和重复 source 注册;source 被撤回时会中止所有由该注册打开的 stream。每个 Client stream 在 `api/remotes` 中拥有独立队列与一组 allowlist listener,因此一个 Client 断开不会消费或撤销另一个 Client 的事件。
|
||||
|
||||
Client 要求首项恰好是只含 `type: 'ready'` 的对象,后续每个 item 则恰好包含非空 `event` 与数组 `args` 两个字段。浏览器 wire 的 JSON 解码保证元素是 JSON 值;进程内载体则读取同一个已经过 `isJsonValue` 校验的 Host source。未知但结构合法的事件名会在没有订阅者时静默丢弃。
|
||||
Client 要求首项是带非空 `clientId` 的 `ready`;后续 item 按 discriminant 精确校验字段。普通 `emit` 的未知但结构合法事件名在没有订阅者时静默丢弃。waterfall 通过 `eventId` 关联 `$events/result`,并由 `agentId` 选择 Client Agent Context;Client 只回传可无损表示为 JSON 的结果,不在 transport 层重复解释业务字段。
|
||||
|
||||
`$events` 是 Gateway 内部 endpoint,不进入生成的 Typert Remote descriptor,也不成为 `ctx.remote.<namespace>`。应用选择仍只存在于 `api/remotes` 的 allowlist 和 Host source;Gateway 只拥有注册、payload 校验与物理传输。
|
||||
|
||||
@@ -136,9 +148,9 @@ Client 要求首项恰好是只含 `type: 'ready'` 的对象,后续每个 item
|
||||
|
||||
| 位置 | 改动 |
|
||||
|---|---|
|
||||
| `dsh-typert-protocol` | `src/types.ts` 提供 `TypertForwardableEvent`、`TypertRemoteEventSelection` 与 `TypertRemoteEvent`;`TypertClientRemote` 只公开 `$on`。纯类型,零运行时 |
|
||||
| `api/gateway` | Host 半提供唯一 Remote event source 注册位、`$events` logical stream 与 opening ready 项;Client 半把私有 pump 注册为 Connection generation source,负责 item 校验、按注册顺序派发以及 listener 异常收容 |
|
||||
| `api/remotes` | `src/remote-events.ts`(名单值)与 `src/types.ts`(类型投影 + 选择座位)双列进两个 face;Host 半注册每 Client 独立的 allowlist source,并在入队前校验 JSON;Client 半继续组合生成的 Remote contribution |
|
||||
| `dsh-typert-protocol` | `src/types.ts` 提供 forwardable mode 推导、selection 与 Client listener 投影;`TypertClientRemote` 只公开 `$on`。纯类型,零运行时 |
|
||||
| `api/gateway` | Host 半提供唯一 Remote event source、`$events` stream、pending waterfall 协调和 `$events/result`;Client 半把私有 pump 注册为 Connection generation source,负责 frame 校验和 Cordis 分发 |
|
||||
| `api/remotes` | `src/remote-events.ts`(带 mode 的名单值)与 `src/types.ts`(键投影 + selection)双列进两个 face;Host 半注册每 Client source,并在入队前校验 JSON;Client 半继续组合生成的 Remote contribution |
|
||||
| 根 `tsconfig.base.json` | 加 `dsh-settings/types`、`dsh-credentials/types`、`dsh-api-remotes/types` 三条 `paths`,全部指向**源**平面 |
|
||||
| `dsh-commands` / `dsh-settings` / `dsh-credentials` | `interface Events` 子块移入各自 client-safe 的 `./types`(settings/credentials 新建该出口,brand 与纯类型一并移入,index 继续 re-export 并留住构造器;`files` 补 `lib/types/**/*.js`) |
|
||||
| `host/apiproxy` | 不包含 `HostFrame`、`events.host()` 或其他 Host 下行 carrier;API Proxy 不参与 Host 事件或 Connection generation |
|
||||
@@ -152,7 +164,7 @@ Client 要求首项恰好是只含 `type: 'ready'` 的对象,后续每个 item
|
||||
|
||||
**继续寄生 API Proxy 的 Host downlink。**这样可以复用 Connection generation 和 `connection/reset`,但会让 API Proxy 保留 Remote 事件 allowlist、队列、schema 和 Client Runtime bridge,领域传输也无法随其他 Remote stream 共用生命周期。API Gateway 已有常驻 `/api/remote.mux` 后,`$events` 只增加一个 internal logical stream,不需要第三条 WebSocket,因此转移到 Gateway 的成本和所有权都更合理。
|
||||
|
||||
**给 Remote 事件另开第三条物理 WebSocket。**独立通道能拥有自己的连接状态,但会重复 Gateway mux 已经提供的认证升级、复用、取消、错误映射和退避重连。内部 `$events` endpoint 保留独立 logical stream,同时复用一条物理连接。
|
||||
**给 Remote 事件另开第三条物理 WebSocket 或 duplex stream。**独立通道能拥有自己的连接状态,但会重复 Gateway mux 已经提供的认证升级、复用、取消、错误映射和退避重连。内部 `$events` endpoint 保留独立 logical stream,waterfall 结果复用 HTTP 一元调用。
|
||||
|
||||
**在 type-meta 立一张独立的 `TypertRemoteEventMap`,让 owner 包 declare-merge 进去**。消费端键集会精确等于「被声明为可远程投递的事件」;代价是每条事件的签名要在 cordis `Events` 之外**再写一遍**,于是需要一条双向 `extends` 的等价性证明来防漂移,还要给三个 owner 包新增 type-meta 依赖。共用同一份 `Events` 声明让等价性变成构造性成立,这张表因此不立。
|
||||
|
||||
@@ -169,12 +181,11 @@ Client 要求首项恰好是只含 `type: 'ready'` 的对象,后续每个 item
|
||||
钉住该行为的东西:
|
||||
|
||||
- Host source 真组合测试:两个 Client stream 各自收到 host emit 的 `{ event, args }`,其中一个断开不会影响另一个;非 JSON 实参会响亮拒绝且不会毒化后续合法事件。
|
||||
- 类型层负例拒绝三类候选:不是事件的名字、绑 Scope 的事件(`goal/changed`)、返回值非 `void` 的事件。`$on('slots/changed', …)`(client 本地事件)与 `$on('skills/change', …)`(已声明但未选中)都编译失败——因此 `$on` 的键面恰好等于名单。
|
||||
- 类型层负例拒绝未选择事件、非 `void` 的无 scope 事件、非 Agent-scoped waterfall,以及声明 mode 与签名不符的条目。`$on('slots/changed', …)`(Client 本地事件)与 `$on('skills/change', …)`(已声明但未选中)都编译失败——因此 `$on` 的键面恰好等于名单。
|
||||
- 消费端 `$on('settings/document-updated', …)` 把 `ns` 解析为 `SettingsNamespace`:brand 穿过 wire 存活。
|
||||
- `$on` 的 disposer 归属调用方 fiber;同一个函数对象订阅两次时两条注册各自独立退订——按 listener 身份做键的表会把它们合并,所以订阅按注册项寻址。
|
||||
- 投递同时收容抛出的 listener 与拒绝所返回 promise 的 listener:声明返回值是 `void`,没人 await 异步 listener,其拒绝否则会完全逃出这层收容。投递遍历快照,因此派发中订阅或退订都不会改变本帧的接收者集合。
|
||||
- Gateway 测试覆盖 source 缺失、重复注册、撤销中止、payload 拒绝、ready 先于事件,以及浏览器与进程内两种 carrier;Client 测试覆盖 generation source 注册边界、描述与增量就绪顺序、物理失败后重开、Host 错误与意外结束、非 ready 首项、畸形事件项和 dispose quiescence。
|
||||
- JSON 参数校验直接在 Host source 上覆盖:类型化的 `ctx.emit` 通常造不出畸形值,但 runtime allowlist 配置错误仍必须响亮失败。
|
||||
- 普通通知同时收容抛出的 listener 与拒绝所返回 Promise 的 listener;waterfall 测试固定 Client result、`next()`、拒绝、取消、多 Client 首个 claim 和重连重放 pending request。
|
||||
- Gateway 测试覆盖 source 缺失、重复注册、撤销中止、payload 拒绝、ready 先于事件,以及浏览器与进程内两种 carrier;Client 测试覆盖 generation source 注册边界、描述与增量就绪顺序、物理失败后重开、Host 错误与意外结束、非 ready 首项、畸形事件项、`$events/result` 失败和 dispose quiescence。
|
||||
- `host/remote-event`、公开 `$dispatch`、Client Runtime bridge 和 API Proxy 的 allowlist 依赖都不存在;各消费方直接观察 owner 事件。
|
||||
|
||||
## 后果
|
||||
@@ -184,6 +195,6 @@ Client 要求首项恰好是只含 `type: 'ready'` 的对象,后续每个 item
|
||||
- **生产方保持私有**:业务插件只能调用 `$on`;Host source 注册和 Client 派发都不在 `TypertClientRemote` 上暴露,测试 double 以自己的 `emit` 方法驱动订阅,不伪装成生产接口。
|
||||
- **畸形实参在 emit 点失败**:`api/remotes` listener 在入队前抛出,因此调用 Host `ctx.emit` 的操作立即看到名单配置错误;队列仍可继续投递后续合法事件。
|
||||
- **测试侧镜像值可能漂移**:没有任何机制核对 `apps/web/tests` 中镜像的 client 常量与其源;安全网只是漂移会让选择器失配。规则写在 `apps/web/tests/README.md`,由 review 守;grep 级门禁经评估后刻意不做。
|
||||
- **放弃的能力**:不支持投影或脱敏载荷、不支持 Scope 化事件(`agentCtx.remote.$on`)、重连不重放。需要可靠恢复的状态必须拥有查询、cursor 或 opening baseline;可应答交互与快照状态不应进入 `$on`。
|
||||
- **放弃的能力**:不支持投影或脱敏载荷,不支持 Agent 以外的 Scope,也不为普通通知提供重放。需要可靠恢复的状态必须拥有查询、cursor 或 opening baseline;waterfall 只重放仍处于同一次 Host 调用生命周期内的 pending request。
|
||||
- **仍有 client 包留在 host 图里**:12 个工程(`connection`、`runtime`、`ui-slots` 等)经未拆分的 `directory-picker-browse`/`-native` 与 `api/gateway → client/connection` 仍可达 host 图。它们都能编译且不再牵连 api/remotes 的 client face,因此没有阻塞本次改动;拆分那些包能减少几个,但经评估后不做。两个 chat e2e 直接引 `dsh-client-runtime/client` 依赖 `runtime` 本来就在图里——属偶然而非保证。
|
||||
- **invariant companion 不做运行期检查**:早先的修订曾在活事件总线上断言投递形状(`thisArg === null`、`mode === 'emit'`),这让 companion 与名单值耦合,并使 rolldown 把它提成第三个 bundle chunk——而机械推导的发布文件清单并不携带它。host 面的 `TypertForwardableEvent` 断言在编译期已拒绝这两种偏离,因此该 companion 是一个带说明的空 installer。
|
||||
|
||||
+2
-2
@@ -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 .agents/notes/implemented/architecture/2026-08-18-session-history-and-event-transport.md
|
||||
2026-08-18-session-history-and-event-transport.md: 976189c815d1980790cc534ee7cdfc3da47e89fe
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 06987f34100096a53647510af6a2ac2d3c5cdfa4
|
||||
2026-08-18-session-history-and-event-transport.md: 3d2b6737bcff1a4f929fc5b878f76e1be804a69d
|
||||
2026-08-18-session-history-and-event-transport.zh.md: 95d271256468e05800b67b3b6989d8909be1ac84
|
||||
|
||||
+322
-39
@@ -1,4 +1,4 @@
|
||||
# Agent Note: Session history and event transport
|
||||
# Agent Note: Session history, control state, and Remote event transport
|
||||
|
||||
Status: implemented
|
||||
|
||||
@@ -6,81 +6,364 @@ English | [中文](2026-08-18-session-history-and-event-transport.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The browser Session consumes two data categories with different lifecycles. A durable Session log and its projections must support cold reads while no Agent is attached; queue, approval, question, and jobs state is process-local and authoritative only while the Agent or corresponding wait still exists. The legacy API Proxy mixed both categories in one all-Session mux, where `session/subscribed`, history refetches, and several baselines jointly handled reconnects, so the interface could not reveal whether an observation was allowed to resume an Agent.
|
||||
The browser consumes three kinds of data with different lifecycles: persistable, paginated Session logs; process-local state that needs an opening baseline to converge after reconnect; and immediate notifications that need no replay.
|
||||
|
||||
Typert's generic `Agent` and `Session` lookups resume an ordinary cold Session. If history, projection, or state subscriptions use those parameters directly, opening a page can resume an Agent; if every operation instead remains cold, prompt, create, and fork cannot perform the activation they explicitly require. Activation policy must belong to each operation rather than arise implicitly from a carrier or parameter type.
|
||||
These kinds of data cannot share one recovery rule. Session logs have stable sequence numbers and persistence, so a cursor can fill gaps; queue, jobs, and Workspace lists need a complete snapshot to replace an old mirror; ordinary notifications only promise delivery within the current Connection generation.
|
||||
|
||||
Removing the aggregate `session/event` path also creates a list-consistency problem: the old client updated activity ordering from every event it received, while a per-Session `follow` does not cover Sessions that are not open. The list must obtain the latest user-prompt time from a cold-readable domain projection instead of depending on whether one browser follows that log.
|
||||
Observing Session history, lists, and projections must allow cold reads. If transport performs a general Typert lookup whenever an argument contains a Session or Agent, opening a page, switching tabs, or reconnecting the network implicitly resumes an Agent, so observation gains execution side effects.
|
||||
|
||||
Commands such as prompt, create, fork, and model selection do need to create or resume an Agent according to their own semantics. Activation authority must belong to each Remote method, not be decided implicitly by the carrier, parameter types, or a shared lookup.
|
||||
|
||||
The legacy API Proxy all-Session mux, `HostFrame`, and Workspace notifications encode domain data, baselines, errors, and connection lifecycle in one handwritten protocol. Each additional state duplicates frame declarations, a Client bridge, reconnect handling, and cleanup logic, while API Proxy cannot return to owning only business methods that have not yet migrated.
|
||||
|
||||
Host-to-Client Cordis events also have two invocation modes. Ordinary notifications only need broadcast delivery; Agent-scoped waterfalls such as Approval and Question must let a Client claim, delegate through `next()`, return a result, or reject while preserving one Host invocation identity across multiple Clients, disconnects, and cancellation.
|
||||
|
||||
These requirements need one general transport lifecycle without making Gateway understand Session, Workspace, Approval, or Question business data.
|
||||
|
||||
## Decision
|
||||
|
||||
`packages/api/session-controller` provides `@deepseek-ai/dsh-api-session-controller`. Its Host service mounts as `ctx.sessionController` and generates `ctx.remote.session`; its Client entry consumes unary and stream methods through the API Gateway's shared Remote WebSocket mux. One owner handles Session cold reads, live control, interaction responses, and explicit business commands, while internal agent, commands, control, history, and list controllers retain implementation-level separation.
|
||||
API Gateway owns Remote transport, stream lifecycles, and Remote Event coordination. Session Controller and Workspace Controller own their Host APIs, wire types, and Client domain adapters. Client Runtime only composes and consumes these objects; it does not implement another carrier state machine.
|
||||
|
||||
The API Gateway Client plugin opens `/api/remote.mux` as soon as it activates and keeps the physical WebSocket connected even with no logical streams. The mux recreates the physical connection with capped jittered backoff after an initial connection failure or an established connection loss; logical streams waiting to open share that reconnect loop, while an already-open generated stream terminates with `RemoteStreamCarrierError`. Gateway's `$stream` supervisor reopens only after that carrier failure: it permits one isolated retry against an available Host or waits for the next Host generation, while the Session consumer supplies the latest sequence for follow or requires a replacement baseline for control. Business and protocol failures remain terminal. Client disposal stops backoff, closes candidate and active sockets, and awaits the background loop. In-process `connection.rpc.open` continues to bypass the browser mux.
|
||||
Current ownership is:
|
||||
|
||||
### Activation policy
|
||||
```text
|
||||
[client/connection]
|
||||
|-- Host description
|
||||
|-- Connection generation
|
||||
`-- unary RPC transport
|
||||
|
||||
Session Remote methods pass a `SessionId` or `SessionAddress` without triggering a generic Typert lookup through the parameter type. `SessionController` distinguishes cold inspection, live-only lookup, and resume-permitted resolution so every endpoint's activation behavior is visible and independently testable. The generic `Agent` and `Session` lookups it configures for other Remote namespaces reuse the same preset, concurrent-resume, and subagent-ownership policy.
|
||||
[api/gateway/client]
|
||||
|-- RemoteStream
|
||||
|-- RemoteSnapshotStream
|
||||
|-- RemoteJournalStream
|
||||
`-- ctx.remote.$on + $events pump
|
||||
|
||||
[api/session-controller]
|
||||
|-- ctx.remote.session unary commands
|
||||
|-- session.control snapshot stream
|
||||
|-- session.page + session.follow journal
|
||||
`-- Session Client adapters
|
||||
|
||||
[api/workspace-controller]
|
||||
|-- ctx.remote.workspace unary commands
|
||||
|-- workspace.follow snapshot stream
|
||||
`-- Workspace Client model and adapter
|
||||
|
||||
[api/remotes]
|
||||
`-- application Remote Event allowlist and Host Cordis source
|
||||
|
||||
[client/runtime]
|
||||
`-- compose Session and Workspace domain state for consumers
|
||||
```
|
||||
|
||||
API Proxy owns neither the Session or Workspace Remote namespace nor the Host downlink event carrier. `/api/events.host`, `HostFrame`, `stream/error`, `ServerRequest`, and their WebSocket/SSE branches do not participate in this data path.
|
||||
|
||||
### Connection generation and physical connections
|
||||
|
||||
The browser's Client Remote plugin starts `RemoteStreamMuxClient` idempotently on activation and connects to `/api/remote.mux` immediately. The physical WebSocket remains resident even when there is no business logical stream.
|
||||
|
||||
After an initial connection failure or the loss of a connected socket, the mux rebuilds the physical connection with capped jittered backoff. Logical streams not yet opened share that reconnect loop; streams already open end their current physical generation with `RemoteStreamCarrierError`.
|
||||
|
||||
In-process `connection.rpc.open` uses the same logical endpoint semantics while bypassing the browser WebSocket mux.
|
||||
|
||||
The Gateway-internal `$events` logical stream is the sole generation source for `ConnectionHandle`. It does not depend on whether any business `$on` subscription exists, so connection health does not vary with the number of UI listeners.
|
||||
|
||||
The Host event source installs incremental listeners synchronously before returning its first frame. Gateway then sends `{ type: 'ready' }` with a `clientId`; this frame proves that the current generation can receive increments.
|
||||
|
||||
`ConnectionController` waits for `$events` readiness and `host.describe` in parallel. It publishes `connected` only after both complete, so a Session or Workspace baseline cannot be read before Host incremental listeners are ready.
|
||||
|
||||
Unexpected normal completion of `$events`, a Host error, a malformed opening frame, or a carrier failure ends the current Connection generation. Connection withdraws `hostDescription`, then re-establishes `$events` and `host.describe` after backoff.
|
||||
|
||||
Gateway stream generation, Connection generation, and a Session business open epoch are three independent counters: the first identifies physical replacement of one logical stream, the second identifies a Host-availability handshake, and the last prevents an obsolete Session open from writing into current state.
|
||||
|
||||
Plugin disposal stops backoff, cancels candidate and active sockets, ends logical streams, and awaits quiescence of background loops and consumers.
|
||||
|
||||
### General Remote stream model
|
||||
|
||||
Gateway Client provides three React-independent, single-consumer lifecycle objects:
|
||||
|
||||
```text
|
||||
RemoteStream<Item>
|
||||
|-- RemoteSnapshotStream<Snapshot, Delta>
|
||||
`-- RemoteJournalStream<Page, Entry, Cursor>
|
||||
```
|
||||
|
||||
Domain Controllers use them through composition or thin adapters; Session and Workspace do not inherit a common Controller base class that knows domain frames.
|
||||
|
||||
#### `RemoteStream`
|
||||
|
||||
`ctx.remote.$stream(options)` returns a `RemoteStream<Item>` responsible for reopening, cancellation, and disposal of one logical stream across physical generations.
|
||||
|
||||
Each item carries a monotonic generation, that generation's `AbortSignal`, and `accept()`. A domain consumer calls `accept()` only after validating the opening cursor or baseline.
|
||||
|
||||
Only `RemoteStreamCarrierError` permits retry. When the Host remains available, one independent reopen is allowed; otherwise the stream waits for a new Connection generation. Business errors, protocol errors, and opening failures terminate immediately.
|
||||
|
||||
`restart()` replaces only the current physical generation and preserves the logical stream. `dispose()` permanently ends the logical stream, pending retry, and iterator, then waits for quiescence.
|
||||
|
||||
`RemoteStream` does not understand baselines, deltas, pages, cursors, sequence numbers, or any domain frame.
|
||||
|
||||
#### `RemoteSnapshotStream`
|
||||
|
||||
`RemoteSnapshotStream<Snapshot, Delta>` requires each generation to start with exactly one complete snapshot, followed only by deltas.
|
||||
|
||||
An update before the snapshot or a second snapshot in the same generation is a terminal protocol error.
|
||||
|
||||
The generation is accepted only after its snapshot has been applied successfully. The previously published state remains readable while the carrier reconnects, and the new generation's snapshot replaces the old mirror atomically.
|
||||
|
||||
The domain adapter supplies frame discrimination, snapshot replacement, a delta reducer, carrier state, and a terminal failure sink. The general layer parses no Session or Workspace fields.
|
||||
|
||||
Session control and Workspace state each use an independent `RemoteSnapshotStream`.
|
||||
|
||||
#### `RemoteJournalStream`
|
||||
|
||||
`RemoteJournalStream<Page, Entry, Cursor>` combines one live follow with a page method in the same namespace. It applies to an append-only journal with a stable order, paginated history, and a live tail.
|
||||
|
||||
Initial opening establishes follow and obtains its opening cursor before reading the initial page. Live entries produced while the page request is pending already enter the follow queue, closing the race between reading history and subscribing afterward.
|
||||
|
||||
The general layer removes overlap between the page and queued entries by cursor, verifies continuity, and publishes one complete window after the page covers the opening cursor.
|
||||
|
||||
Contiguous live entries publish `append`; older history pages publish `prepend`. Reconnect, cursor jumps, or continuity that cannot be proven trigger a tail-page repair.
|
||||
|
||||
The old window remains readable during repair. The page and live entries accumulated during that read form a continuous window and publish one `replace`, never exposing a half-repaired state.
|
||||
|
||||
If a page request is canceled with its physical carrier generation, the journal waits for the next generation's opening cursor and rereads the page at that cursor. This cancellation does not leak to the domain object as a terminal page failure.
|
||||
|
||||
`RemoteJournalStream` owns the opening cursor, resume cursor, pagination, reconnect catch-up, overlap removal, and gap repair. A domain Session object does not copy these state machines.
|
||||
|
||||
### Session Controller
|
||||
|
||||
`packages/api/session-controller` provides Host `ctx.sessionController` and the generated `ctx.remote.session` namespace.
|
||||
|
||||
It owns Session list, search, create, models, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, and control.
|
||||
|
||||
The package separates agent, commands, control, history, and list controllers internally, but Session identity resolution, activation policy, subagent ownership, and Remote error projection have one public owner.
|
||||
|
||||
Other Host Remote namespaces reuse the same identity rules through `ctx.sessionController.inspect()` or `resolveAgent()`; they do not retain a second Session resolver.
|
||||
|
||||
#### Activation policy
|
||||
|
||||
Session Remote methods pass `SessionId` or `SessionAddress`; parameter types do not trigger a general Typert Session lookup.
|
||||
|
||||
Each method explicitly selects a cold inspection, live-only lookup, or resume-capable resolution:
|
||||
|
||||
| Operation | Source or result without a live Agent | Activation rule |
|
||||
|---|---|---|
|
||||
| `session.page(address)` | Read the header and log from persistence | Never resumes an Agent |
|
||||
| `session.follow(address)` | Inspect persistence, replay the missing suffix, then wait for future commits | Connecting and waiting never resume an Agent; events can appear only after another explicit command activates the Session |
|
||||
| Projection and Session-list baseline | Recover from durable events or the projection cache | Never resumes an Agent; reading a title does not require an Agent |
|
||||
| Queue, approval, question, jobs, and live projection in `session.control()` | Observe only attached Agents, pending registries, and process-local registries; absence means empty or unavailable | Subscription, reconnect, and baseline generation never resume an Agent |
|
||||
| `session.respond`, `updateQueue`, and `cancel` | Reach only a pending item or live Agent that still exists; stale operations return an explicit failure | Never resumes an Agent for live state that has already disappeared |
|
||||
| Session list, search, attachment, and fork-source reads | Inspect persistence or an attached Session | The read itself never resumes an Agent |
|
||||
| Explicit Session commands such as prompt, rename, and model changes | Resolve or resume the target according to the command's own policy | Resumes only when the command contract explicitly permits it |
|
||||
| Create and the fork target | Create a new Session and Agent | The explicit user command authorizes creation; reading the fork source remains cold |
|
||||
| `session.list`, `search` | persistence, projection cache, or cold log | Never resumes an Agent |
|
||||
| `session.page(address)` | attached Session or persistence log | Never resumes an Agent |
|
||||
| `session.follow(address)` | cold-read current cursor, then wait for future appends | Neither opening nor waiting resumes an Agent |
|
||||
| `session.control()` | current attached Agents, pending registry, and process-local registries | Baseline and reconnect do not resume an Agent |
|
||||
| `session.attachment`, fork source read | authorized durable Session data | A read does not resume an Agent |
|
||||
| `session.updateQueue`, `cancel` | only the current live Agent | Does not resume vanished state |
|
||||
| `models`, `selectModel`, `rename`, `prompt` | command resolves the target Session | Resumes only when the method explicitly permits it |
|
||||
| `create` and fork target | new Session/Agent | The user command supplies creation authority |
|
||||
|
||||
`follow` installs its `session/event` listener before inspecting an attached Session or persistence. It returns the cursor at open time; a reconnect carrying `afterSeq` first replays the missing suffix from the authoritative log, then drains commits buffered during the read in sequence order. A cold Session can therefore open history and follow immediately and remain waiting without attaching an Agent. A physical WebSocket loss resumes from the last applied sequence; Host business and persistence failures arrive as terminal Remote Stream errors and publish as the Session's `openError`, rather than being misclassified as an indefinitely retryable carrier loss.
|
||||
Reading titles, lists, and projections does not require an Agent. An observation operation cannot inherit resume authority merely because another Remote endpoint uses Agent lookup.
|
||||
|
||||
### Live control stream
|
||||
#### Session journal
|
||||
|
||||
`control()` is one Host-wide shared Remote stream that preserves the value of aggregate observation: a browser receives interaction and transient state for every currently live Session without activating those Sessions by opening their transcripts. The Host installs queue, pending-interaction, jobs, projection, and Agent-lifecycle listeners before producing a complete baseline, then drains changes buffered during baseline construction. Every physical reconnect replaces the Client's transient mirror with a new baseline instead of inventing durable sequences for process-local values.
|
||||
`session.page` returns a history window clipped on message boundaries with contiguous internal sequence numbers. Every request must carry an explicit `throughSeq`; this value comes from the corresponding `session.follow` generation's opening cursor and fixes the read at the same log cut. A tail page without `beforeSeq` must end exactly at `throughSeq`, where `-1` denotes an empty log. `beforeSeq` only selects an older page before that cut and cannot replace the synchronization cursor. `maxMessages` limits user/assistant message count without dropping chunks, tools, or state events between those messages.
|
||||
|
||||
Queue and jobs use complete snapshots with last-wins application. Agent attach, detach, and owner disposal produce a baseline or empty snapshot capable of clearing stale values. Approval and question control frames carry a stable `interactionId`; the opening baseline replays requests that remain pending, resolved frames withdraw requests, and the `respond` Remote unary uses the same identity with the existing outcome or answer semantics. The mechanism preserves first-responder-wins and explicit stale-response failure without the old `RpcRequest<MuxFrame>` envelope.
|
||||
The tail page also carries a projection baseline no later than `throughSeq`; older pages carry only historical entries. The Client merges pages and subsequent live control updates by projection watermark.
|
||||
|
||||
The projection baseline still accompanies the tail `page` log cut. `control()` pushes only later complete projection values with their watermarks, and the Client merges both sources by retaining the higher sequence. A cold title and other log-derived projections recover through `page` or list reads; subscribing to live projections never starts an Agent to obtain a value. The opened cursor from `follow` replaces `session/subscribed` for the durable log, while the control baseline replaces its responsibility for clearing queue, jobs, and pending-interaction mirrors. The legacy `session/event`, `session/subscribed`, and aggregate event mux consequently have no remaining responsibility.
|
||||
Ordinary Sessions and direct subagents use one `SessionAddress` protocol. A direct-subagent address carries parent Session, child Session, and mode; a cold Host read verifies durable ownership and descriptor rather than authorizing access from the child id alone.
|
||||
|
||||
Session added and removed notifications and Agent running status can recover from a Session-list baseline, while an Agent error without a turn position is an immediate notification that needs neither a response nor replay. These do not enter the stateful control stream; `@deepseek-ai/dsh-api-session-controller` exposes them as client-safe events under the [`ctx.remote.$on`](2026-08-10-remote-event-delivery.md) delivery rules. Observing these events also never resumes an Agent.
|
||||
`session.follow` installs `session/event` and `session/created` listeners before checking an attached Session or persistence, then reads the current cursor.
|
||||
|
||||
### Unified Session Controller ownership
|
||||
The first follow response is `{ type: 'opened', cursor }`. A generation with `afterSeq` first replays the missing suffix from the authoritative log, then emits commits buffered during the read in sequence order.
|
||||
|
||||
`SessionController` owns the Session BFF formerly housed in API Proxy: list, search, create, models, selectModel, rename, fork, prompt, attachment, updateQueue, cancel, page, follow, control, and respond. It owns preset-aware creation and resumption, the subagent ownership fence, Workspace association, model selection, history reads, and endpoint-specific error projection. Remaining API Proxy domains reuse this identity policy through `ctx.sessionController.inspect()` and `resolveAgent()` instead of retaining a second resolver.
|
||||
A cold Session can open history immediately and keep follow waiting. Future events appear only after another explicit command resumes the Agent.
|
||||
|
||||
The service selects cold inspection, live-only `ctx.agents.get`, or explicit ensure/resume per endpoint. Queue mutation, cancel, and interaction response can operate only on authoritative objects in the current process even when the user initiates the command; prompt, rename, and model changes explicitly resume according to their own contracts. Internal controllers keep data-channel and command implementations separate, while public ownership and activation policy have one home.
|
||||
Client `SessionEventStream` extends `RemoteJournalStream` and supplies only `session.follow`, `session.page`, the Session sequence algorithm, and repair requests. The general layer first obtains opening cursor `C`, then calls `session.page({ throughSeq: C })`; entries `C + 1...` received during the read remain in the follow queue, and the page must cover exactly through `C` before the layer merges and publishes a continuous sequence.
|
||||
|
||||
Session create and fork may still call the Workspace registry to establish ownership, while Workspace Remote methods and `host/workspace-*` notifications remain in API Proxy. Workspace migration is not a prerequisite for completing the Session data channel.
|
||||
```text
|
||||
ctx.remote.session.follow(address, afterSeq?) --------|
|
||||
|[]> SessionEventStream
|
||||
ctx.remote.session.page(address, throughSeq, pageArgs) -| |-- replace(window)
|
||||
|-- prepend(history)
|
||||
`-- append(live entry)
|
||||
```
|
||||
|
||||
### List timing and projections
|
||||
Each Client Session owns only one current `events: SessionEventStream | undefined`. The read-only `SessionEventSource` gives the materialized event window to Conversation consumers.
|
||||
|
||||
`@deepseek-ai/dsh-api-session-controller` owns the `sessionListMetadata` projection and the list projection built from it. The state changes `blank` to false at the first `turn/start` and records `lastPromptAt` for each `user/message` whose source is the user; a Session row's `updatedAt` is always `max(header.createdAt, lastPromptAt)`. A cold list recovers this value from the projection cache or durable log, and a live change updates the list through a client-safe `$on` notification, so a Session whose transcript is closed still moves after a new prompt.
|
||||
A Session's `openGeneration` only prevents an asynchronous result retired by resync, address replacement, or disposal from writing into current state. It does not participate in transport retry.
|
||||
|
||||
`updatedAt` is a derived field of the API Session list. It is neither written to the Session header nor borrowed from the Workspace's own `updatedAt`; Workspace ordering and update times remain owned by the Workspace registry.
|
||||
A terminal failure from the initial page, repair page, or follow enters the current Session's `openError`. A stale business epoch or stale stream cannot overwrite newer state.
|
||||
|
||||
#### Session live control
|
||||
|
||||
`session.control()` is a Host-wide snapshot stream. One browser can observe transient state for all current live Sessions without opening a journal for every transcript.
|
||||
|
||||
Each generation emits a complete baseline first, followed by queue, jobs, and projection deltas. The baseline reads attached Agents and process-local registries without resuming cold Agents.
|
||||
|
||||
Queue and jobs use complete replacement values and apply last-wins. Agent attach, detach, Session disposal, and owner disposal can all clear a stale mirror through an empty value or a new baseline.
|
||||
|
||||
The original `approval/request` and `user-questions/request` events are forwardable waterfalls. If an Agent-scoped Client listener claims a request, it returns directly. If all delivered Clients call `next()`, the original Cordis waterfall continues to later Host listeners. Session control neither stores nor replays these requests.
|
||||
|
||||
The projection baseline and a tail page's log cut are produced independently. The Client always retains the value with the higher sequence number. Subscribing to live projection does not start an Agent merely to obtain a value.
|
||||
|
||||
Session added, removed, activity, running status, and Agent error without a turn position do not enter the stateful control stream; they are `ctx.remote.$on` notifications that are either repairable from a list baseline or need no replay.
|
||||
|
||||
Session-list `updatedAt` is `max(header.createdAt, sessionListMetadata.lastPromptAt)`. Only a user-originated `user/message` updates `lastPromptAt`; it can be recovered from a cold projection and does not depend on whether a browser follows that Session.
|
||||
|
||||
### Workspace Controller
|
||||
|
||||
`packages/api/workspace-controller` provides Host `ctx.workspaceController` and the generated `ctx.remote.workspace` namespace.
|
||||
|
||||
It owns create, rename, delete, insertBefore, insertSessionBefore, archiveSession, and `follow`. Workspace registry remains the durable source of truth; the Controller owns Remote commands, projection, and error mapping.
|
||||
|
||||
`WorkspaceFeed` synchronously observes storage `domain/changed`, and each follow generation emits a complete baseline before `upsert`, `remove`, `order`, and `archived` deltas.
|
||||
|
||||
A complete `order` frame is authoritative for Workspace ordering. It avoids having the Client infer display order from upsert arrival order and converges after a reconnect baseline.
|
||||
|
||||
`createWorkspaceStateStream()` assembles `workspace.follow` as a `RemoteSnapshotStream`. Client Runtime only starts and owns that stream.
|
||||
|
||||
`ClientWorkspaceModel` lives on Workspace Controller's Client face. It owns baseline/increment parsing, the materialized list, the archived set, command-result echo, and merge rules for races between unary and stream arrivals.
|
||||
|
||||
A successful unary command can update the local model immediately; a later stream commit still corrects state with the Host projection and complete order. Deleted Workspace ids are recorded so a delayed result cannot reinsert them.
|
||||
|
||||
```text
|
||||
ctx.remote.workspace.follow() -|[]> RemoteSnapshotStream
|
||||
|-- replace(baseline)
|
||||
|-- upsert/remove(view)
|
||||
|-- replace(order)
|
||||
`-- replace(archived ids)
|
||||
```
|
||||
|
||||
Workspace Remote methods, state feed, and Client data model do not pass through API Proxy or depend on `host/workspace-*` notifications.
|
||||
|
||||
### Remote Event
|
||||
|
||||
Remote Event reuses owner packages' Cordis `Events` declarations. The original Host event is the sole business signature, and Client `ctx.remote.$on(event, listener)` derives its parameters, waterfall result, and `next()` from that declaration.
|
||||
|
||||
The allowlist in `packages/api/remotes` is the sole source of application selection. Each entry explicitly marks `emit` or `waterfall`; this mode determines Host listening, the legal Client key set, and the wire frame type together.
|
||||
|
||||
The system declares no `RemoteInvocationMap`, requires no second Client `@Remote`, and does not infer invocation mode by checking whether the final runtime argument is a function.
|
||||
|
||||
Remote Event downlink frames form an explicit discriminated union:
|
||||
|
||||
```text
|
||||
ready { type, clientId }
|
||||
emit { type, event, args }
|
||||
waterfall { type, event, eventId, agentId, request }
|
||||
cancel { type, eventId }
|
||||
```
|
||||
|
||||
Both WebSocket JSON and in-process carrier entry points start from `unknown` and validate the discriminant plus exact fields. Dispatch after validation accepts only the typed union. TypeScript static types do not replace wire validation.
|
||||
|
||||
Ordinary `emit` arguments must be lossless JSON. The Client calls `parallel()` on a Cordis key private to each Remote instance, preserving registration order, calling-fiber ownership, and listener-error isolation.
|
||||
|
||||
The private key prevents Host events and same-named Client-local Cordis events from triggering one another. Client Remote maintains neither its own subscription registry nor a handwritten listener chain.
|
||||
|
||||
Returning waterfalls currently support Agent scope only. The event signature must contain one request with a direct `agent` field followed by a `next()` returning the same result type, and the whole event returns a Promise.
|
||||
|
||||
The Host projects only top-level `agent` and `signal` fields from the request: `agent` becomes top-level `agentId` in the frame, `signal` becomes the delivery lifetime, and all remaining fields must be lossless JSON as a whole.
|
||||
|
||||
The Client synchronously resolves `agentId` to an existing Agent Context, restores the current delivery signal into the request's direct `signal` field, and invokes Cordis `waterfall()` on the target Context's private key.
|
||||
|
||||
The system does not scan arbitrarily deep objects, transmit path arrays or placeholders, deep-clone/restore Context and AbortSignal, or wait for a future Agent Context.
|
||||
|
||||
When no Client adapter is registered, the Agent Context is absent, or that Context has been disposed, that Client immediately returns `next`. It does not subscribe to a registry, recheck races after resolution, or create a temporary Fiber for one delivery.
|
||||
|
||||
Gateway Host retains `eventId`, the Host continuation, and delivered Client generations for every unfinished waterfall. A new Client generation receives a replay of the same pending event.
|
||||
|
||||
Each generation's queue guarantees one delivery, so the Client stores no `seen` set. `clientId + eventId` binds a result to the current generation; a reply from an old connection cannot complete delivery on a new one.
|
||||
|
||||
When several Clients receive a waterfall, the first result or rejection completes the Host invocation and sends `cancel` to the other Clients. Gateway continues the original Cordis chain only after every delivered Client returns `next`.
|
||||
|
||||
Host caller-signal cancellation, Agent Context disposal, Client-generation completion, and losing-Client cancellation all terminate their corresponding waits.
|
||||
|
||||
The Client returns `next`, result, or rejection through the existing HTTP unary RPC `$events/result`; downlink events continue to share the Remote WebSocket mux, with no duplex WebSocket for responses.
|
||||
|
||||
Gateway only verifies that a waterfall return value has a lossless JSON representation; it does not interpret business fields. Semantics such as whether a Question answer belongs to an offered option remain owned by the requester or UI domain and are not revalidated by transport.
|
||||
|
||||
When `UserQuestionService` observes that the caller's `AbortSignal` was canceled during a request and the provider threw an ordinary error, it normalizes that failure to `UserQuestionError` with `ASK_ABORTED` while retaining the original error as `cause`. A domain error already supplied by the provider preserves its identity.
|
||||
|
||||
A failure of `$events/result` fails the current Connection generation. Host withdraws that Client's delivery with the generation, the pending event is replayed in the next generation, and Client maintains no second result-retry queue.
|
||||
|
||||
Ordinary `$on` notifications are not replayed after disconnect. State whose correctness depends on recovery must have a query, cursor, or opening baseline and cannot rely on eventual Remote Event delivery.
|
||||
|
||||
An event is not replayed when its Client listener registers after arrival. HMR has no dedicated redelivery semantics.
|
||||
|
||||
### API Proxy's remaining boundary
|
||||
|
||||
Session Controller and Workspace Controller provide generated Remote namespaces directly; API Remotes and API Gateway provide Host-to-Client events directly.
|
||||
|
||||
Client Connection maintains only Host generation, description, and generic RPC. It does not parse domain frames.
|
||||
|
||||
Client Runtime only receives domain changes produced by Controller adapters. It recognizes no `HostFrame`, `session/subscribed`, `session/event` mux frame, or `host/workspace-*` frame.
|
||||
|
||||
API Proxy carries only independent business APIs it owns. Session, Workspace, Remote Event, and Connection generation do not depend on it.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Resume an Agent whenever any Session stream opens.** Viewing history, reading a title, reconnecting a tab, or observing background state would then have execution side effects, and several browsers could trigger redundant resumes. Cold logs and recoverable projections already have persistence sources, so observation has no authority to activate execution.
|
||||
**Resume an Agent whenever any Session stream opens.** Viewing history, reading a title, reconnecting a tab, or observing background state would gain execution side effects, and multiple browsers could trigger duplicate resumes. Cold logs and projections already have persistence sources.
|
||||
|
||||
**Allow `follow` only for a live Agent.** This would force the transcript's first screen to resume an Agent or return to the race between unary history and a separate live stream. Subscribing by identity before a cold read covers both history and events from later explicit activation without activating the Agent itself.
|
||||
**Permit `session.follow` only for live Agents.** The first transcript render would have to resume an Agent or reintroduce the race between unary history and live subscription. Following by identity before a cold read covers both history and future explicit activation.
|
||||
|
||||
**Publish separate `session-transport` and `api/session` packages.** The data channel and command API are conceptually distinct, but both depend on Session addresses, Agent activation policy, interaction responses, and Client mount order. Splitting them would create cross-package coordination without independently replaceable capabilities. One `SessionController` provides unified public ownership while internal controllers preserve implementation separation and each endpoint declares whether activation is permitted.
|
||||
**Split Session transport and Session commands into two public packages.** Both depend on Session address, Agent activation policy, subagent ownership, error mapping, and Client mount ordering. One public Controller preserves unified ownership while internal classes can evolve independently.
|
||||
|
||||
**Convert queue, approval, question, jobs, and projection entirely to ordinary `$on` events.** Ordinary events provide no reconnect baseline and cannot express a stable response identity for pending interactions; one lost push would leave state permanently stale. The shared control stream establishes one complete baseline for stateful live data, while lifecycle notifications recoverable by query continue to use `$on`.
|
||||
**Move queue, jobs, projection, Workspace, and logs to ordinary `$on`.** Ordinary events have no reconnect baseline, cursor, or gap repair, so one missed delivery leaves permanently stale state. Only notifications that need no recovery, can be repaired by an independent query, or carry their own lifetime as a waterfall fit `$on`.
|
||||
|
||||
**Retain the API Proxy mux.** This avoids migrating existing frames but preserves a hand-written union, schema, response envelope, and second stream lifecycle, preventing API Proxy from leaving the Session data plane.
|
||||
**Make every domain Controller inherit a page/follow/retry base class.** Session journals and Workspace snapshots have different opening, recovery, and ordering rules. Gateway's three compositional stream objects reuse transport lifecycle while domain adapters declare only their own frame semantics.
|
||||
|
||||
**Keep deriving list activity from aggregate `session/event` delivery.** List correctness would depend on which Sessions a browser happens to consume and would treat arbitrary plugin events as user activity. `sessionListMetadata.lastPromptAt` directly represents the ordering fact the product needs and can be recovered from cold durable state.
|
||||
**Declare a separate Client invocation map for Remote Event.** A second map or Client `@Remote` would copy owner Cordis event signatures and create a drift point. Deriving `$on` listeners and results from the same `Events` declaration preserves equivalence by construction.
|
||||
|
||||
**Project Agent scope through arbitrary object depth.** Recursive Context and AbortSignal scans need path, placeholder, clone, and restore protocols and turn incidental object structure into a wire promise. Top-level `agent` and `signal` cover current waterfalls.
|
||||
|
||||
**Wait for a Client Agent Context or adapter before dispatching.** Registry waiters, post-resolution race checks, and temporary delivery Fibers add lifecycle to a Client that can delegate immediately. Returning `next` when the target is absent preserves Cordis waterfall semantics.
|
||||
|
||||
**Use an independent physical WebSocket or duplex stream for Remote Event.** Gateway mux already provides authenticated upgrade, multiplexing, cancellation, error mapping, and reconnect. Downlink `$events` plus HTTP `$events/result` expresses request/response without a third connection.
|
||||
|
||||
**Retain API Proxy's Host mux.** This keeps the handwritten union, schema, response envelope, and second stream lifecycle, and prevents Session and Workspace Controllers from owning their data protocols independently.
|
||||
|
||||
**Update Session list time from aggregate `session/event`.** List correctness would depend on which Sessions a browser consumes and would mistake arbitrary plugin events for user activity. The durable `lastPromptAt` projection expresses the ordering fact directly.
|
||||
|
||||
## Verification
|
||||
|
||||
Host tests pin that cold `page` and cold `follow` do not add an attached Agent, a cold follow receives contiguous events after an explicit prompt resumes the Session, reconnect replays only missing sequences, and persistence or business failures retain their category and message as terminal errors. Control tests pin listener-before-baseline ordering, no cold-Session resumption, attach and detach cleanup, complete queue and jobs snapshots, stable pending-interaction identities with first-responder-wins, and higher-sequence projection watermarks winning.
|
||||
Gateway mux tests pin connection without logical streams, idle residency, initial-failure and disconnect recovery, active-stream carrier failure, cancellation, and no reconnect after disposal.
|
||||
|
||||
Session Controller tests separately pin cold reads, live-only commands, and explicit-resume commands, proving they do not share one implicit activation policy; create and fork cover presets, ownership, and Workspace association. List tests cover one `lastPromptAt → updatedAt` calculation for attached and cold Sessions and prove that a prompt reorders a Session whose transcript is closed. Client tests cover independent follow and control cancellation, replacement of transient mirrors after a control reconnect, and the absence of legacy mux frames from the Session data flow.
|
||||
Connection tests pin missing, duplicate, and withdrawn generation sources; the race between `$events` ready and `host.describe`; and description withdrawal and rebuilding after generation failure.
|
||||
|
||||
`RemoteStream` tests pin single consumption, retry reset after opening acceptance, generation-only `restart()`, no retry for terminal errors, and disposal quiescence.
|
||||
|
||||
`RemoteSnapshotStream` tests pin exactly one opening snapshot per generation, rejection of an update before a snapshot, rejection of duplicate snapshots, and reconnect replacement.
|
||||
|
||||
`RemoteJournalStream` tests pin follow-before-page, opening-overlap removal, contiguous append, historical prepend, reconnect catch-up, gap repair, and one atomic replacement.
|
||||
|
||||
Session Host tests pin cold page/follow without increasing attached Agents, contiguous events reaching a cold follow after an explicit prompt, direct-subagent ownership, message-aligned pagination, and terminal-error projection.
|
||||
|
||||
Session control tests pin baseline-first delivery, no cold-Session resume, attach/detach cleanup, queue and jobs replacement, and the projection watermark.
|
||||
|
||||
Session Client tests pin one journal owner per Session, no writeback from stale open epochs, independent cancellation of control and journal, and retaining the published window during carrier retry.
|
||||
|
||||
Workspace Host tests pin baseline-first delivery, upsert/remove, authoritative order, archived set, and follower disposal.
|
||||
|
||||
Workspace Client tests pin snapshot replacement, unary/stream races, no resurrection after delete, stable ordering, and terminal failure.
|
||||
|
||||
Remote Event type tests reject unselected events, non-void unscoped events, non-Agent-scoped waterfalls, and modes that disagree with signatures.
|
||||
|
||||
Remote Event Host tests pin listener-before-ready, payload validation, pending replay, first result across multiple Clients, all-next delegation, rejection, Host cancellation, Context release, and losing-Client cancellation.
|
||||
|
||||
Remote Event Client tests pin instance-private keys, Cordis registration order, Agent Context resolution, `next`, result, rejection, cancellation, rejection of stale-generation replies, and Connection-generation failure when `$events/result` fails. User Question tests pin normalization of in-progress signal cancellation and preservation of its cause.
|
||||
|
||||
Missing, duplicate, and withdrawn sources; non-ready first items; unknown discriminants; extra fields; and non-JSON values all fail loudly at their respective wire entries.
|
||||
|
||||
Static checks pin that API Proxy exports no Session/Workspace Host-frame carrier and Client Runtime contains no corresponding bridge.
|
||||
|
||||
## Consequences
|
||||
|
||||
The browser can read and follow a durable Session while its Agent is stopped. Observation never implicitly resumes execution; only explicit Session commands activate or create an Agent according to their own contracts. Durable logs repair missing suffixes by sequence, while process-local control state converges from a complete baseline, so the two reconnect strategies no longer imitate each other.
|
||||
The browser can read and follow a durable Session while its Agent is stopped. Observation does not implicitly resume execution; only explicitly authorized Session commands create or resume Agents according to their own rules.
|
||||
|
||||
This decision takes ownership of the Session lifecycle, transcript, input control, and stateful streams deferred by [unary API Proxy migration](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md), and replaces that proposal's direct delegation of `session.rename` to the title service with one `api/session-controller` owner; its other business migrations remain independent. It replaces only the API Proxy carrier from [web background-job display](../feature/2026-08-08-web-background-job-display.md), retaining complete job snapshots, process-local lifecycles, and the rule that observation never resumes an Agent. Workspace remains an explicitly deferred boundary.
|
||||
Durable logs repair a missing suffix by sequence number and page; Session control and Workspace state converge through opening snapshots; ordinary Remote Events promise no replay. Recovery semantics follow the data kind instead of imitating one another.
|
||||
|
||||
Gateway owns only transport, generation, pending waterfalls, and strict wire validation, not Session or Workspace business fields. A domain Controller supplies only openers, cursor rules, baseline reducers, and error presentation.
|
||||
|
||||
Session and Workspace Host APIs, stream adapters, and Client data models each have an explicit owner. API Proxy is no longer their intermediary.
|
||||
|
||||
The general stream objects add three explicit layers while deleting the retry, cancellation, generation, baseline, and gap-repair shells previously duplicated by each Controller.
|
||||
|
||||
Remote waterfalls preserve first claim across multiple Clients, continuation of the Host chain after every Client calls `next`, reconnect replay of pending calls, and end-to-end cancellation. The current protocol supports only top-level Agent scope and lossless-JSON requests and results.
|
||||
|
||||
This decision extends the allowlist and single Cordis-signature design from [Remote event delivery](2026-08-10-remote-event-delivery.md): ordinary notifications use `emit`, while Agent-scoped async waterfalls use the same `ctx.remote.$on` surface with explicit `waterfall` mode. It creates no second invocation map.
|
||||
|
||||
This decision takes over the Session, Workspace, and Host-event carriers retained by [simple unary API Proxy migration](../../proposed/architecture/2026-08-10-unary-apiproxy-remote-migration.md) while preserving the complete jobs snapshot, process-local lifecycle, and “observation does not resume an Agent” semantics required by [background job display](../feature/2026-08-08-web-background-job-display.md).
|
||||
|
||||
+13
-9
@@ -126,13 +126,15 @@ Session control 与 Workspace state 各使用一个独立的 `RemoteSnapshotStre
|
||||
|
||||
repair 期间旧 window 保持可读;page 与期间积累的 live entries 拼成连续窗口后只发布一次 `replace`,不会把半修复状态暴露给消费者。
|
||||
|
||||
若 page 请求随物理 carrier generation 一起取消,journal 等待下一 generation 的 opening cursor,再以新 cursor 重读 page;该取消不会作为 terminal page failure 泄漏给领域对象。
|
||||
|
||||
`RemoteJournalStream` 拥有 opening cursor、resume cursor、分页、重连 catch-up、重叠去重和 gap repair。领域 Session 对象不复制这些状态机。
|
||||
|
||||
### Session Controller
|
||||
|
||||
`packages/api/session-controller` 提供 Host `ctx.sessionController` 与生成的 `ctx.remote.session` namespace。
|
||||
|
||||
它拥有 Session list、search、create、models、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow、control 与 respond。
|
||||
它拥有 Session list、search、create、models、selectModel、rename、fork、prompt、attachment、updateQueue、cancel、page、follow 与 control。
|
||||
|
||||
包内的 agent、commands、control、history 与 list controller 分开实现,但 Session 身份解析、激活策略、subagent ownership 和 Remote 错误投影只有一个公开 owner。
|
||||
|
||||
@@ -151,7 +153,7 @@ Session Remote 方法传递 `SessionId` 或 `SessionAddress`,不靠参数类
|
||||
| `session.follow(address)` | 冷读当前 cursor,等待将来的 append | 建联和等待都不恢复 Agent |
|
||||
| `session.control()` | 当前 attached Agent、pending registry 与进程内 registry | baseline 与重连不恢复 Agent |
|
||||
| `session.attachment`、fork 源读取 | 已授权的持久 Session 数据 | 读取不恢复 Agent |
|
||||
| `session.updateQueue`、`cancel`、`respond` | 仅命中当前 live 或 pending 对象 | 不为已消失状态恢复 Agent |
|
||||
| `session.updateQueue`、`cancel` | 仅命中当前 live Agent | 不为已消失状态恢复 Agent |
|
||||
| `models`、`selectModel`、`rename`、`prompt` | 命令解析目标 Session | 仅按方法约定显式恢复 |
|
||||
| `create` 与 fork 目标 | 新 Session/Agent | 用户命令提供创建授权 |
|
||||
|
||||
@@ -191,13 +193,11 @@ initial page、repair page 或 follow 的 terminal failure 进入当前 Session
|
||||
|
||||
`session.control()` 是 Host 范围的 snapshot stream,一个浏览器可观察所有当前 live Session 的瞬态状态,而不必为每个 transcript 打开 journal。
|
||||
|
||||
每个 generation 先发完整 baseline,再发 queue、jobs、projection、approval 与 question 的增量帧。baseline 读取 attached Agent 和进程内 registry,不恢复冷 Agent。
|
||||
每个 generation 先发完整 baseline,再发 queue、jobs 与 projection 增量帧。baseline 读取 attached Agent 和进程内 registry,不恢复冷 Agent。
|
||||
|
||||
queue 与 jobs 使用完整 replacement 值并按 last-wins 应用。Agent attach、detach、Session disposal 与 owner disposal 都能用空值或新 baseline 清除陈旧镜像。
|
||||
|
||||
pending approval 与 question 使用稳定 `interactionId`。opening baseline 包含仍待处理的请求,resolved 帧撤销请求,`session.respond` 使用同一 id,保留首个有效应答者获胜与过期应答明确失败的语义。
|
||||
|
||||
原始 `approval/request` 与 `user-questions/request` 同时是可转发 waterfall。若某个 Agent-scoped Client listener claim,请求直接返回;若所有已投递 Client 都调用 `next()`,原 Cordis waterfall 继续到后续 Host listener,因此 control provider 仍能提供可重连的 pending 镜像。
|
||||
原始 `approval/request` 与 `user-questions/request` 是可转发 waterfall。若某个 Agent-scoped Client listener claim,请求直接返回;若所有已投递 Client 都调用 `next()`,原 Cordis waterfall 继续到后续 Host listener。Session control 不保存或重放这些请求。
|
||||
|
||||
projection baseline 与 tail page 的日志切点独立产生,Client 总是保留较高 seq 的值。订阅 live projection 不会为取得值而启动 Agent。
|
||||
|
||||
@@ -274,6 +274,10 @@ Host caller signal 取消、Agent Context 释放、Client generation 结束和 l
|
||||
|
||||
Client 通过现有 HTTP unary RPC `$events/result` 回送 `next`、result 或 rejection;下行事件仍复用 Remote WebSocket mux,不为应答建立 duplex WebSocket。
|
||||
|
||||
Gateway 只验证 waterfall 返回值能无损表示为 JSON,不解释业务字段。Question 回答的 option 归属等语义由请求方或 UI 领域承担,transport 不重复校验。
|
||||
|
||||
`UserQuestionService` 在请求期间观察到调用方 `AbortSignal` 已取消、且 provider 抛出普通错误时,将其归一为 `UserQuestionError` 的 `ASK_ABORTED`,并把原错误保留为 `cause`;provider 已给出的领域错误保持不变。
|
||||
|
||||
`$events/result` 失败会令当前 Connection generation 失败。Host 随 generation 撤销该 Client 的 delivery,pending event 在下一 generation 重放,Client 不维护第二套结果重试队列。
|
||||
|
||||
普通 `$on` 通知在断线后不重放。凡正确性依赖恢复的数据必须有 query、cursor 或 opening baseline,不能依赖 Remote Event 恰好送达。
|
||||
@@ -298,7 +302,7 @@ API Proxy 只承接自身拥有的独立业务 API,不是 Session、Workspace
|
||||
|
||||
**把 Session transport 与 Session commands 拆成两个公开包。** 两者共同依赖 Session address、Agent 激活策略、subagent ownership、错误映射和 Client 挂载顺序;一个公开 Controller 保持统一所有权,内部 class 仍可独立演化。
|
||||
|
||||
**把 queue、jobs、projection、Workspace 与日志都改成普通 `$on`。** 普通事件没有 reconnect baseline、cursor 或 gap repair,漏掉一次推送就会留下永久陈旧状态;只有无需恢复或可由独立查询修复的通知适合 `$on`。
|
||||
**把 queue、jobs、projection、Workspace 与日志都改成普通 `$on`。** 普通事件没有 reconnect baseline、cursor 或 gap repair,漏掉一次推送就会留下永久陈旧状态;只有无需恢复、可由独立查询修复,或以 waterfall 本身持有请求生命周期的通知适合 `$on`。
|
||||
|
||||
**让每个领域 Controller 继承一个 page/follow/retry 基类。** Session journal 与 Workspace snapshot 的 opening、恢复和排序规则不同;Gateway 的三个组合式 stream 对象复用 transport 生命周期,同时让领域 adapter 只声明自己的 frame 语义。
|
||||
|
||||
@@ -328,7 +332,7 @@ Connection 测试固定 generation source 缺失、重复注册、撤回、`$eve
|
||||
|
||||
Session Host 测试固定 cold page/follow 不增加 attached Agent、显式 prompt 后 cold follow 收到连续事件、direct subagent ownership、message-aligned pagination 和终止错误投影。
|
||||
|
||||
Session control 测试固定 baseline-first、冷 Session 不恢复、attach/detach 清理、queue 与 jobs replacement、projection watermark,以及 pending interaction 的稳定 id 与首个应答者获胜。
|
||||
Session control 测试固定 baseline-first、冷 Session 不恢复、attach/detach 清理、queue 与 jobs replacement,以及 projection watermark。
|
||||
|
||||
Session Client 测试固定每 Session 单一 journal owner、旧 open epoch 不写回、control 与 journal 独立取消,以及 carrier retry 期间保留已发布窗口。
|
||||
|
||||
@@ -340,7 +344,7 @@ Remote Event 类型测试拒绝未选择事件、非 void 的 unscoped 事件、
|
||||
|
||||
Remote Event Host 测试固定 listener-before-ready、payload 校验、pending replay、多 Client first-result、all-next delegation、rejection、Host cancellation、Context release 和 losing-client cancel。
|
||||
|
||||
Remote Event Client 测试固定实例私有 key、Cordis 注册顺序、Agent Context 解析、`next`、result、rejection、cancel、旧 generation 回包拒绝和 `$events/result` 失败导致 generation 结束。
|
||||
Remote Event Client 测试固定实例私有 key、Cordis 注册顺序、Agent Context 解析、`next`、result、rejection、cancel、旧 generation 回包拒绝和 `$events/result` 失败导致 generation 结束;User Question 测试固定进行中 signal 取消的错误归一化及 cause 保留。
|
||||
|
||||
缺失 source、重复 source、撤回 source、非 ready 首项、未知 discriminant、额外字段与非 JSON 值都在各自 wire 入口响亮失败。
|
||||
|
||||
|
||||
@@ -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/capability-seams.md
|
||||
capability-seams.md: b1faa5d4dce37eb338921c7117d451aae9ad252e
|
||||
capability-seams.zh.md: 52c63e1491f184e7578b2eb6647fa5ff63a9e60b
|
||||
capability-seams.md: 75f050f329e709e5c88bffbe0d3bc2072d4286de
|
||||
capability-seams.zh.md: 25fa48c67e406b03677debba44eff5d49fd3c626
|
||||
|
||||
@@ -38,6 +38,8 @@ flowchart LR
|
||||
pkg_api_session_controller["api-session-controller"]
|
||||
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
|
||||
pkg_apiproxy["apiproxy"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
|
||||
svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
|
||||
pkg_scope["scope"]
|
||||
pkg_typert_registry["typert-registry"]
|
||||
@@ -218,6 +220,7 @@ flowchart LR
|
||||
pkg_agent_team --> svc_agentTeams
|
||||
pkg_api_gateway --> svc_typertGateway
|
||||
pkg_api_session_controller --> svc_sessionController
|
||||
pkg_api_workspace_controller --> svc_workspaceController
|
||||
pkg_apiproxy --> svc_apiProxy
|
||||
pkg_approval --> svc_approval
|
||||
pkg_attachment --> svc_attachments
|
||||
@@ -449,6 +452,7 @@ flowchart LR
|
||||
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | Rewrites oversized current tool results through replayable single-node surface replacements before summary compaction. |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | Owns append-only Session instances and emits the durable session event feed. |
|
||||
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | `apiproxy` | - | Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains. |
|
||||
| `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace. |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | Companion subpaths register owner-local checks; the service owns selection, uniqueness, child fibers, and package-attributed failures. |
|
||||
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | Plugins register live zod contributions directly or through dsh-typert-loader; the API gateway consumes invocation descriptors and providers, while other runtime consumers query schemas and reflection metadata at their own edges. |
|
||||
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | Associates generated Remote descriptors with live Cordis services, resolves registered identities, and exposes unary calls through the shared Connection RPC carrier. |
|
||||
|
||||
@@ -40,6 +40,8 @@ flowchart LR
|
||||
pkg_api_session_controller["api-session-controller"]
|
||||
svc_sessionController["ctx.sessionController<br/>Host Session Remote controller"]
|
||||
pkg_apiproxy["apiproxy"]
|
||||
pkg_api_workspace_controller["api-workspace-controller"]
|
||||
svc_workspaceController["ctx.workspaceController<br/>Host Workspace Remote controller"]
|
||||
svc_invariants["ctx.invariants<br/>Package-owned invariant registry"]
|
||||
pkg_scope["scope"]
|
||||
pkg_typert_registry["typert-registry"]
|
||||
@@ -220,6 +222,7 @@ flowchart LR
|
||||
pkg_agent_team --> svc_agentTeams
|
||||
pkg_api_gateway --> svc_typertGateway
|
||||
pkg_api_session_controller --> svc_sessionController
|
||||
pkg_api_workspace_controller --> svc_workspaceController
|
||||
pkg_apiproxy --> svc_apiProxy
|
||||
pkg_approval --> svc_approval
|
||||
pkg_attachment --> svc_attachments
|
||||
@@ -451,6 +454,7 @@ flowchart LR
|
||||
| `ctx.toolResultPruner` | `core` | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | - | [`compaction-basic`](../packages/compaction/compaction-basic) | - | 在摘要压缩前,通过可回放的单节点表层替换来改写过大的当前工具结果。 |
|
||||
| `ctx.sessions` | `core` | [`session`](../packages/core/session) | - | [`agent-loop`](../packages/core/agent-loop), [`agent`](../packages/core/agent), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), `subagent-inprocess`, [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback) | - | 拥有仅追加的 Session 实例,并发出持久的会话事件流。 |
|
||||
| `ctx.sessionController` | `core` | [`api-session-controller`](../packages/api/session-controller) | - | `apiproxy` | - | 负责 Session 命令、冷读取、持久事件跟随、实时控制状态与 Agent 激活策略;apiProxy 在需要 Session 上下文的领域中复用其检查和 Agent 解析操作。 |
|
||||
| `ctx.workspaceController` | `core` | [`api-workspace-controller`](../packages/api/workspace-controller) | - | - | - | 通过生成的 Remote namespace 负责 Workspace 命令和可在重连后收敛的 Workspace 状态投递。 |
|
||||
| `ctx.invariants` | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) | - | [`session`](../packages/core/session), [`agent`](../packages/core/agent), [`scope`](../packages/core/scope), [`agent-loop`](../packages/core/agent-loop) | - | 配套子路径注册所属包本地的检查;该服务负责选择、唯一性、子 fiber,以及标明所属包的失败。 |
|
||||
| `ctx.typert` | `core` | [`typert-registry`](../packages/typert/registry) | - | [`typert-loader`](../packages/typert/loader), [`api-gateway`](../packages/api/gateway) | - | 插件直接或通过 dsh-typert-loader 注册实时 zod 贡献;API 网关消费调用描述符和提供方,其他运行时消费方则在各自边界查询 schema 与反射元数据。 |
|
||||
| `ctx.typertGateway` | `core` | [`api-gateway`](../packages/api/gateway) | - | - | - | 将生成的 Remote 描述符与实时 Cordis 服务关联,解析已注册的身份,并通过共享的 Connection RPC 载体提供一元调用。 |
|
||||
|
||||
@@ -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/config-catalog.md
|
||||
config-catalog.md: 31f73a905afb1fb94274b309f77b4ba41b697166
|
||||
config-catalog.zh.md: f09888603a9e77db1ea6ebf2a73eb2918b5bc62f
|
||||
config-catalog.md: e83d794302e7fbcf84cb5c1672821e59842e2b28
|
||||
config-catalog.zh.md: 406dbb3c77527317332b48cf513909c56a9b8a3b
|
||||
|
||||
@@ -269,7 +269,7 @@ Source: [`packages/core/agent-tool-presentation/src/index.ts:38`](../packages/co
|
||||
|
||||
## `@deepseek-ai/dsh-api-session-controller`
|
||||
|
||||
Requires: `agentDefaultModel` · `agents` · `attachments` · `llm` · `sessions` · `sessionQuery` · `tools` · `typert` · `userQuestions` · `workspaceRegistry`
|
||||
Requires: `agentDefaultModel` · `agents` · `attachments` · `llm` · `sessions` · `sessionQuery` · `tools` · `typert` · `workspaceRegistry`
|
||||
|
||||
```ts config-catalog
|
||||
/** Session Controller deployment policy. */
|
||||
@@ -279,7 +279,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/api/session-controller/src/index.ts:60`](../packages/api/session-controller/src/index.ts)
|
||||
Source: [`packages/api/session-controller/src/index.ts:58`](../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-attachment-local"></a>
|
||||
|
||||
@@ -381,7 +381,7 @@ export interface ConnectionConfig {
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/client/connection/src/index.ts:53`](../packages/client/connection/src/index.ts)
|
||||
Source: [`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-hmr"></a>
|
||||
|
||||
@@ -758,7 +758,7 @@ Source: [`packages/hooks/hooks-codex/src/index.ts:44`](../packages/hooks/hooks-c
|
||||
|
||||
## `@deepseek-ai/dsh-host-apiproxy`
|
||||
|
||||
Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `sessionController` · `workspaceRegistry`
|
||||
Requires: `agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `sessionController`
|
||||
|
||||
```ts config-catalog
|
||||
/** Gateway plugin configuration. */
|
||||
@@ -3028,7 +3028,7 @@ export interface Config {
|
||||
export type ApprovalPolicy = 'ask' | 'never'
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:142`](../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-web"></a>
|
||||
|
||||
@@ -3239,7 +3239,8 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
|
||||
- `@deepseek-ai/dsh-acp-app` — requires `cmdlineArgs` ([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts))
|
||||
- `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-gateway` — requires `typert` ([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes` — requires `typertGateway` ([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-workspace-controller` — requires `typert` · `workspaceRegistry` ([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-authorization` — requires `credentials` ([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-locale` ([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-modules` — requires `webServer` · `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
|
||||
|
||||
@@ -271,7 +271,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-api-session-controller`
|
||||
|
||||
需要:`agentDefaultModel` · `agents` · `attachments` · `llm` · `sessions` · `sessionQuery` · `tools` · `typert` · `userQuestions` · `workspaceRegistry`
|
||||
需要:`agentDefaultModel` · `agents` · `attachments` · `llm` · `sessions` · `sessionQuery` · `tools` · `typert` · `workspaceRegistry`
|
||||
|
||||
```ts config-catalog
|
||||
/** Session Controller deployment policy. */
|
||||
@@ -281,7 +281,7 @@ export interface Config {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/api/session-controller/src/index.ts:60`](../packages/api/session-controller/src/index.ts)
|
||||
来源:[`packages/api/session-controller/src/index.ts:58`](../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-attachment-local"></a>
|
||||
|
||||
@@ -383,7 +383,7 @@ export interface ConnectionConfig {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/client/connection/src/index.ts:53`](../packages/client/connection/src/index.ts)
|
||||
来源:[`packages/client/connection/src/index.ts:52`](../packages/client/connection/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-client-hmr"></a>
|
||||
|
||||
@@ -760,7 +760,7 @@ export interface Config {
|
||||
|
||||
## `@deepseek-ai/dsh-host-apiproxy`
|
||||
|
||||
需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `sessionController` · `workspaceRegistry`
|
||||
需要:`agentDefaultModel` · `agents` · `attachments` · `directoryPicker` · `llm` · `sessions` · `subagents` · `sessionQuery` · `sessionController`
|
||||
|
||||
```ts config-catalog
|
||||
/** Gateway plugin configuration. */
|
||||
@@ -3030,7 +3030,7 @@ export interface Config {
|
||||
export type ApprovalPolicy = 'ask' | 'never'
|
||||
```
|
||||
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:177`](../packages/interaction/user-approval/src/index.ts)
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:142`](../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
<a id="deepseek-aidsh-web"></a>
|
||||
|
||||
@@ -3241,7 +3241,8 @@ export interface Config {
|
||||
- `@deepseek-ai/dsh-acp-app` — 需要 `cmdlineArgs`([`packages/bundle/acp-app/src/index.ts`](../packages/bundle/acp-app/src/index.ts))
|
||||
- `@deepseek-ai/dsh-agent`([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-gateway` — 需要 `typert`([`packages/api/gateway/src/index.ts`](../packages/api/gateway/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-remotes` — 需要 `typertGateway`([`packages/api/remotes/src/index.ts`](../packages/api/remotes/src/index.ts))
|
||||
- `@deepseek-ai/dsh-api-workspace-controller` — 需要 `typert` · `workspaceRegistry`([`packages/api/workspace-controller/src/index.ts`](../packages/api/workspace-controller/src/index.ts))
|
||||
- `@deepseek-ai/dsh-authorization` — 需要 `credentials`([`packages/credentials/authorization/src/index.ts`](../packages/credentials/authorization/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-locale`([`packages/client/locale/src/index.ts`](../packages/client/locale/src/index.ts))
|
||||
- `@deepseek-ai/dsh-client-modules` — 需要 `webServer` · `loader`([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
|
||||
event-producer-consumer.md: a9cfc1201c7d851405c99fce2e8177297f0cfe5f
|
||||
event-producer-consumer.zh.md: ecccc50fc9b3cd2fd730e30ec2636c773334b00d
|
||||
event-producer-consumer.md: 8cf11d8be322686c89f8bc57c9f0bc2c4a3aeb74
|
||||
event-producer-consumer.zh.md: 6b79a0fded3b5fa6956e9d7b047e78b68ded1f48
|
||||
|
||||
@@ -8,10 +8,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| Event | Mode | Declared in | Dispatchers | Listeners |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team` |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team` |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
@@ -19,32 +19,37 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:462`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:442`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:469`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:448`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:455`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
|
||||
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:379`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:385`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:391`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:379`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:385`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:391`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -59,6 +64,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:152`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:197`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:85`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `modules` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
| 事件 | 模式 | 声明位置 | 派发方 | 监听方 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `agent-loop/config-start-failed` | `emit` | [`packages/core/agent-loop/src/index.ts:183`](../packages/core/agent-loop/src/index.ts) | [`agent-loop`](../packages/core/agent-loop) (`events.dispatch`) | - |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `apiproxy` |
|
||||
| `agent-preset/selected` | `emit` | [`packages/preset/agent-presets/src/types.ts:13`](../packages/preset/agent-presets/src/types.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`) | `remotes` |
|
||||
| `agent/created` | `emit` | [`packages/core/agent/src/runtime-types.ts:159`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-presets`](../packages/preset/agent-presets), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `tool-agent-team` |
|
||||
| `agent/disposed` | `emit` | [`packages/core/agent/src/runtime-types.ts:168`](../packages/core/agent/src/runtime-types.ts) | [`agent`](../packages/core/agent) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), [`file-reference-local`](../packages/context/file-reference-local), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), `tool-agent-team` |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), `apiproxy`, [`goal-round-driver`](../packages/goal/goal-round-driver), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/error` | `emit` | [`packages/core/agent/src/runtime-types.ts:290`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), `session-controller`, [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:197`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:205`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
|
||||
| `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:186`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
@@ -21,32 +21,37 @@
|
||||
| `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:244`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
|
||||
| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:260`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
|
||||
| `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:217`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, `apiproxy`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server` |
|
||||
| `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
|
||||
| `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` |
|
||||
| `api-session/activity` | `emit` | [`packages/api/session-controller/src/types.ts:462`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/added` | `emit` | [`packages/api/session-controller/src/types.ts:442`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/error` | `emit` | [`packages/api/session-controller/src/types.ts:469`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/removed` | `emit` | [`packages/api/session-controller/src/types.ts:448`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `api-session/status` | `emit` | [`packages/api/session-controller/src/types.ts:455`](../packages/api/session-controller/src/types.ts) | `session-controller` (`emit`) | `remotes` |
|
||||
| `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/types.ts:85`](../packages/interaction/user-approval/src/types.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `remotes` |
|
||||
| `authorization/settled` | `emit` | [`packages/credentials/authorization/src/index.ts:57`](../packages/credentials/authorization/src/index.ts) | [`authorization`](../packages/credentials/authorization) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` |
|
||||
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:379`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:385`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:391`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `apiproxy` |
|
||||
| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:80`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `remotes` |
|
||||
| `cordis/dynamic-package` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:379`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/dynamic-retract` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:385`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/inspect-query` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:391`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/inspect-query-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:397`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:367`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `cordis/request-run-resolved` | `emit` | [`packages/extensions/cordis-host-runner/src/types.ts:373`](../packages/extensions/cordis-host-runner/src/types.ts) | [`cordis-host-runner`](../packages/extensions/cordis-host-runner) (`emit`) | `remotes` |
|
||||
| `credentials/record-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:87`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`authorization`](../packages/credentials/authorization) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) |
|
||||
| `credentials/reference-updated` | `emit` | [`packages/credentials/credentials/src/types.ts:75`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | [`credentials`](../packages/credentials/credentials), `remotes` |
|
||||
| `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace), `workspace-controller` |
|
||||
| `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy), [`skill-filesystem`](../packages/skill/skill-filesystem) |
|
||||
| `fs/write-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:58`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-observation-policy`](../packages/fs/fs-observation-policy) |
|
||||
| `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), `apiproxy`, [`llm`](../packages/llm/llm) |
|
||||
| `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
|
||||
| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:65`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
|
||||
| `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | `apiproxy`, [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, `apiproxy`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/created` | `emit` | [`packages/core/session/src/index.ts:54`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
|
||||
| `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`agent-presets`](../packages/preset/agent-presets), `agent-team`, [`compaction`](../packages/compaction/compaction), [`compaction-basic`](../packages/compaction/compaction-basic), [`file-reference-local`](../packages/context/file-reference-local), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/test-support/loader-smoke), `server`, [`session`](../packages/core/session), `session-controller`, [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
|
||||
| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` |
|
||||
| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
|
||||
| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
|
||||
| `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
|
||||
| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:164`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
|
||||
@@ -61,6 +66,7 @@
|
||||
| `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:175`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) |
|
||||
| `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:152`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-jobs`](../packages/jobs/tool-jobs) |
|
||||
| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:197`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`agent-instructions`](../packages/context/agent-instructions), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
|
||||
| `user-questions/request` | `waterfall` | [`packages/interaction/user-questions/src/types.ts:85`](../packages/interaction/user-questions/src/types.ts) | [`user-questions`](../packages/interaction/user-questions) (`waterfall`) | `remotes` |
|
||||
| `webserver/index-inject` | `emit` | [`packages/host/webserver/src/index.ts:34`](../packages/host/webserver/src/index.ts) | `webserver` (`emit`) | `modules` |
|
||||
| `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
| `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
|
||||
|
||||
@@ -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/persistence-catalog.md
|
||||
persistence-catalog.md: 8b1206ba18a6f49a0eb809a1a3a12828fd94f827
|
||||
persistence-catalog.zh.md: 9a085ece1fd2df331f34e311c48bfe0c8ca92c91
|
||||
persistence-catalog.md: 5155968af0886a389d0b01d9332af927cff95a55
|
||||
persistence-catalog.zh.md: abd4ae767a5cfca45be76b54d392815244070869
|
||||
|
||||
@@ -115,7 +115,7 @@ Sources: [`packages/core/session/src/types.ts:321`](../packages/core/session/src
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types.ts)
|
||||
Source: [`packages/core/agent/src/types.ts:38`](../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent-preset/*`
|
||||
|
||||
@@ -160,7 +160,7 @@ Source: [`packages/preset/agent-presets/src/session.ts:26`](../packages/preset/a
|
||||
|
||||
Types: [CallId](subsystems/core.md)
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/types.ts:44`](../packages/interaction/user-approval/src/types.ts)
|
||||
|
||||
<a id="approvaldecided--log-only"></a>
|
||||
|
||||
@@ -178,7 +178,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:44`](../packages/inter
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/types.ts:55`](../packages/interaction/user-approval/src/types.ts)
|
||||
|
||||
<a id="approvalpolicy--log-only"></a>
|
||||
|
||||
@@ -200,7 +200,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:55`](../packages/inter
|
||||
}
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/index.ts:32`](../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
### `assistant/*`
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/core/agent/src/types.ts:19`](../packages/core/agent/src/types.ts)
|
||||
来源:[`packages/core/agent/src/types.ts:38`](../packages/core/agent/src/types.ts)
|
||||
|
||||
### `agent-preset/*`
|
||||
|
||||
@@ -162,7 +162,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
|
||||
类型:[CallId](subsystems/core.zh.md)
|
||||
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:44`](../packages/interaction/user-approval/src/index.ts)
|
||||
来源:[`packages/interaction/user-approval/src/types.ts:44`](../packages/interaction/user-approval/src/types.ts)
|
||||
|
||||
<a id="approvaldecided--log-only"></a>
|
||||
|
||||
@@ -180,7 +180,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:55`](../packages/interaction/user-approval/src/index.ts)
|
||||
来源:[`packages/interaction/user-approval/src/types.ts:55`](../packages/interaction/user-approval/src/types.ts)
|
||||
|
||||
<a id="approvalpolicy--log-only"></a>
|
||||
|
||||
@@ -202,7 +202,7 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
||||
}
|
||||
```
|
||||
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:67`](../packages/interaction/user-approval/src/index.ts)
|
||||
来源:[`packages/interaction/user-approval/src/index.ts:32`](../packages/interaction/user-approval/src/index.ts)
|
||||
|
||||
### `assistant/*`
|
||||
|
||||
|
||||
@@ -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: 7d3d09314f8fbb151cc8a00dbbee5e2cc14e2c9a
|
||||
approval.zh.md: 22d0b0ec4242fcb2ad6fb82c29893a5914369238
|
||||
approval.md: 7b12e7f766555fda09b5b2ac405129b8bfe17daf
|
||||
approval.zh.md: 7596f28d51ef6dfd4e883eaff8c155111e1d2f1c
|
||||
|
||||
@@ -57,7 +57,7 @@ Both policies contribute their complete current meaning to the cache-safe runtim
|
||||
* Readonly same-process permission question. `callId` links to an already
|
||||
* presented tool call, so arguments are not duplicated here.
|
||||
*/
|
||||
interface ApprovalRequest {
|
||||
interface ApprovalRequest extends ApprovalRequestEvent {
|
||||
/**
|
||||
* The agent on whose behalf the question is asked. Routes the question (a
|
||||
* UI answerer only answers for agents it owns) and receives the audit
|
||||
@@ -151,20 +151,20 @@ Source: [`packages/interaction/user-approval/src/index.ts`](../../packages/inter
|
||||
|
||||
#### `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.
|
||||
Ask composed answerers for one decision. Return an outcome to claim the request or call `next()` to delegate. 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).
|
||||
* request or call `next()` to delegate. Scope-filtered dispatch
|
||||
* (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param req - pending approval request.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>
|
||||
'approval/request'( this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>, ): Promise<ApprovalOutcome>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.md)
|
||||
Types: [Agent](core.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts`](../../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/types.ts`](../../packages/interaction/user-approval/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -57,7 +57,7 @@ type ApprovalPolicy = 'ask' | 'never'
|
||||
* Readonly same-process permission question. `callId` links to an already
|
||||
* presented tool call, so arguments are not duplicated here.
|
||||
*/
|
||||
interface ApprovalRequest {
|
||||
interface ApprovalRequest extends ApprovalRequestEvent {
|
||||
/**
|
||||
* The agent on whose behalf the question is asked. Routes the question (a
|
||||
* UI answerer only answers for agents it owns) and receives the audit
|
||||
@@ -151,20 +151,20 @@ Source: [`packages/interaction/user-approval/src/index.ts`](../../packages/inter
|
||||
|
||||
#### `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.
|
||||
Ask composed answerers for one decision. Return an outcome to claim the request or call `next()` to delegate. 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).
|
||||
* request or call `next()` to delegate. Scope-filtered dispatch
|
||||
* (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param req - pending approval request.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'approval/request'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>
|
||||
'approval/request'( this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>, ): Promise<ApprovalOutcome>
|
||||
```
|
||||
|
||||
Types: [Scoped](scope.zh.md)
|
||||
Types: [Agent](core.zh.md) · [Scoped](scope.zh.md)
|
||||
|
||||
Source: [`packages/interaction/user-approval/src/index.ts`](../../packages/interaction/user-approval/src/index.ts)
|
||||
Source: [`packages/interaction/user-approval/src/types.ts`](../../packages/interaction/user-approval/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/core.md
|
||||
core.md: 73c724756e6a8dfdf8723871db646d50fab089d4
|
||||
core.zh.md: a3c2cbaed4a019afaf4aab442191a60b392303c8
|
||||
core.md: 3417560539f41009a290f4c31b255f338624d56d
|
||||
core.zh.md: 75f3c0ca16e1235cb2155f6e54e86e414e61b498
|
||||
|
||||
@@ -57,9 +57,9 @@ interface AgentHandle {
|
||||
Source: [`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
```ts type-equiv
|
||||
/** Public live-agent handle. */
|
||||
/** Public live-agent handle; the runtime face augments its live capabilities. */
|
||||
interface Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
/** Session-backed Agent identity. */
|
||||
readonly id: SessionId
|
||||
/** The provider route and model this agent's requests use. */
|
||||
readonly options: AgentOptions
|
||||
|
||||
@@ -61,9 +61,9 @@ interface AgentHandle {
|
||||
源码:[`packages/core/agent/src/types.ts`](../../packages/core/agent/src/types.ts)
|
||||
|
||||
```ts type-equiv
|
||||
/** Public live-agent handle. */
|
||||
/** Public live-agent handle; the runtime face augments its live capabilities. */
|
||||
interface Agent {
|
||||
/** The single identity shared with {@link session}. */
|
||||
/** Session-backed Agent identity. */
|
||||
readonly id: SessionId
|
||||
/** The provider route and model this agent's requests use. */
|
||||
readonly options: AgentOptions
|
||||
|
||||
@@ -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: e9a8d81cbec84b427972d33ffa7902031d310a52
|
||||
session.zh.md: ee77131546f3b35bd7c0033348aec0207eb99e03
|
||||
session.md: 7d80adfc25e3ebb9f482a3e1c84c16163dba318e
|
||||
session.zh.md: 6337a54bd221a3908793c2231bcde4af8a879bb9
|
||||
|
||||
@@ -715,18 +715,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
|
||||
* @returns one complete baseline followed by live replacement frames.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
|
||||
|
||||
/**
|
||||
* Settle one still-pending approval or structured question.
|
||||
* @param request - interaction identity and caller response.
|
||||
* @returns whether a matching pending interaction accepted the response.
|
||||
*/
|
||||
@Remote('respond') respond(request: SessionRespondRequest): SessionRespondReceipt
|
||||
```
|
||||
|
||||
Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/index.ts:66`](../../packages/api/session-controller/src/index.ts)
|
||||
Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="ctxsessions--sessionstore"></a>
|
||||
|
||||
@@ -886,7 +879,7 @@ One user-authored durable message advanced Session list activity.
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:529`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionadded--emit"></a>
|
||||
|
||||
@@ -903,7 +896,7 @@ A Session became visible to Session list consumers.
|
||||
'api-session/added'(summary: SessionSummary): void
|
||||
```
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:509`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionerror--emit"></a>
|
||||
|
||||
@@ -923,7 +916,7 @@ One Agent failed outside a durable turn position.
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:536`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionremoved--emit"></a>
|
||||
|
||||
@@ -942,7 +935,7 @@ A Session left the live Host registry.
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:515`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionstatus--emit"></a>
|
||||
|
||||
@@ -962,7 +955,7 @@ One Agent changed running state.
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:522`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="session-events"></a>
|
||||
|
||||
|
||||
@@ -719,18 +719,11 @@ inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionH
|
||||
* @returns one complete baseline followed by live replacement frames.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>
|
||||
|
||||
/**
|
||||
* Settle one still-pending approval or structured question.
|
||||
* @param request - interaction identity and caller response.
|
||||
* @returns whether a matching pending interaction accepted the response.
|
||||
*/
|
||||
@Remote('respond') respond(request: SessionRespondRequest): SessionRespondReceipt
|
||||
```
|
||||
|
||||
Types: [SessionHeader](persistence.md) · [SessionId](core.md) · [SessionSearchRequest](session-query.md)
|
||||
Types: [SessionHeader](persistence.zh.md) · [SessionId](core.zh.md) · [SessionSearchRequest](session-query.zh.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/index.ts:66`](../../packages/api/session-controller/src/index.ts)
|
||||
Source: [`packages/api/session-controller/src/index.ts`](../../packages/api/session-controller/src/index.ts)
|
||||
|
||||
<a id="ctxsessions--sessionstore"></a>
|
||||
|
||||
@@ -888,9 +881,9 @@ One user-authored durable message advanced Session list activity.
|
||||
'api-session/activity'(sessionId: SessionId, updatedAt: number): void
|
||||
```
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
Types: [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:529`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionadded--emit"></a>
|
||||
|
||||
@@ -907,7 +900,7 @@ A Session became visible to Session list consumers.
|
||||
'api-session/added'(summary: SessionSummary): void
|
||||
```
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:509`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionerror--emit"></a>
|
||||
|
||||
@@ -925,9 +918,9 @@ One Agent failed outside a durable turn position.
|
||||
'api-session/error'(sessionId: SessionId, message: string): void
|
||||
```
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
Types: [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:536`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionremoved--emit"></a>
|
||||
|
||||
@@ -944,9 +937,9 @@ A Session left the live Host registry.
|
||||
'api-session/removed'(sessionId: SessionId): void
|
||||
```
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
Types: [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:515`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="api-sessionstatus--emit"></a>
|
||||
|
||||
@@ -964,9 +957,9 @@ One Agent changed running state.
|
||||
'api-session/status'(sessionId: SessionId, running: boolean): void
|
||||
```
|
||||
|
||||
Types: [SessionId](core.md)
|
||||
Types: [SessionId](core.zh.md)
|
||||
|
||||
Source: [`packages/api/session-controller/src/types.ts:522`](../../packages/api/session-controller/src/types.ts)
|
||||
Source: [`packages/api/session-controller/src/types.ts`](../../packages/api/session-controller/src/types.ts)
|
||||
|
||||
<a id="session-events"></a>
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/typert.md
|
||||
typert.md: 47c3f4bfcea566a4f2eff480f8b0c7e80ed5ae36
|
||||
typert.zh.md: 3feaee7d132c5c7c8b9b602b002bcd82d68e84c6
|
||||
typert.md: 5939834962114318b724181692a546f4057bd10a
|
||||
typert.zh.md: 80e0af5dfb82b0d099c96caca9df64666fec5b94
|
||||
|
||||
+25
-21
@@ -97,7 +97,7 @@ interface InvocationDescriptor {
|
||||
}
|
||||
/** Optional consuming-Context projection for one direct lookup parameter. */
|
||||
readonly scope?: {
|
||||
/** Context kind whose Client binder supplies the identity. */
|
||||
/** Context kind whose Client adapter supplies the identity. */
|
||||
readonly context: string
|
||||
/** Lookup parameter wire field replaced by the Context identity. */
|
||||
readonly wire: string
|
||||
@@ -180,6 +180,14 @@ type TypertGatewayErrorCode =
|
||||
```ts type-equiv
|
||||
/** Host dispatcher consumed by Connection adapters. */
|
||||
interface TypertGateway {
|
||||
/** Carrier adapter shared by WebSocket and in-process transports. */
|
||||
readonly wireStream: TypertGatewayWireStream
|
||||
/**
|
||||
* Register the application-selected forwarded-event source.
|
||||
* @param source - stream factory installed by the Remote assembly.
|
||||
* @returns disposer removing this exact source and cancelling its active streams.
|
||||
*/
|
||||
registerRemoteEvents(source: TypertRemoteEventSource): () => Promise<void>
|
||||
/**
|
||||
* Invoke one live Remote method without assuming a carrier or response envelope.
|
||||
* @param request - decoded endpoint and named wire arguments.
|
||||
@@ -210,26 +218,15 @@ interface TypertClientRemote extends TypertRemoteNamespaceMap {
|
||||
*/
|
||||
$mount(contribution: TypertRemoteContribution): Promise<TypertDisposer>
|
||||
/**
|
||||
* Subscribe to one forwarded Host event; delivery is one-way, in registration
|
||||
* order, and isolates a throwing listener from the rest.
|
||||
* Subscribe to one forwarded Host event. Notifications run in registration
|
||||
* order and isolate failures; scoped waterfalls return, delegate through
|
||||
* `next()`, or reject the Host dispatch.
|
||||
* @template Event - forwarded event name selected by the Host assembly.
|
||||
* @param event - forwarded Host event name, unchanged on the wire.
|
||||
* @param listener - receives the Host's argument list as declared by Cordis `Events`.
|
||||
* @param listener - receives the Client projection of the Cordis `Events` declaration.
|
||||
* @returns disposer owned by the calling fiber.
|
||||
*/
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
/**
|
||||
* Hand one decoded forwarded frame to the subscription table. The carrier
|
||||
* owning the Host frame sink calls this; a consumer subscribes with
|
||||
* {@link TypertClientRemote.$on} and never calls it.
|
||||
*
|
||||
* `event` is a plain string because this is the wire boundary: the name is
|
||||
* whatever the Host assembly's allowlist selected, and one nobody subscribed
|
||||
* to is dropped silently.
|
||||
* @param event - forwarded Host event name, exactly as the Host emitted it.
|
||||
* @param args - the Host argument list, already JSON-decoded.
|
||||
*/
|
||||
$dispatch(event: string, args: readonly unknown[]): void
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventListener<Event>): () => void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -239,7 +236,7 @@ interface TypertClientRemote extends TypertRemoteNamespaceMap {
|
||||
|
||||
## Cordis API
|
||||
|
||||
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` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
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`) — the language sides differ only in locale-specific paired document paths. 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` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxapiproxy--apiproxy"></a>
|
||||
|
||||
@@ -247,7 +244,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row.
|
||||
|
||||
Source: [`packages/host/apiproxy/src/api/index.ts:20`](../../packages/host/apiproxy/src/api/index.ts)
|
||||
Source: [`packages/host/apiproxy/src/api/index.ts`](../../packages/host/apiproxy/src/api/index.ts)
|
||||
|
||||
<a id="ctxtypert--typertregistry"></a>
|
||||
|
||||
@@ -313,7 +310,7 @@ toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema
|
||||
|
||||
Types: [TypertContribution](invariants.md) · [TypertFace](invariants.md) · [TypertPackageFilter](invariants.md) · [TypertPackageRecord](invariants.md) · [TypertSchemaFilter](invariants.md) · [TypertSchemaRecord](invariants.md)
|
||||
|
||||
Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/registry/src/service.ts)
|
||||
Source: [`packages/typert/registry/src/service.ts`](../../packages/typert/registry/src/service.ts)
|
||||
|
||||
<a id="ctxtypertgateway--typertgatewayservice"></a>
|
||||
|
||||
@@ -322,6 +319,13 @@ Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/re
|
||||
Resolve strict generated definitions or conservative SRC markers against current Cordis Services and Typert providers.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register the sole application-selected forwarded-event source.
|
||||
* @param source - stream factory installed by the Remote assembly.
|
||||
* @returns disposer removing this source and cancelling its active streams.
|
||||
*/
|
||||
registerRemoteEvents(source: TypertRemoteEventSource): () => Promise<void>
|
||||
|
||||
/**
|
||||
* Invoke one live Remote method through strict generated reflection or SRC markers.
|
||||
* @param request - decoded endpoint and exact named wire arguments.
|
||||
@@ -338,5 +342,5 @@ async invoke(request: InvokeRemoteRequest): Promise<unknown>
|
||||
async stream(request: InvokeRemoteRequest): Promise<AsyncIterable<unknown>>
|
||||
```
|
||||
|
||||
Source: [`packages/api/gateway/src/index.ts:109`](../../packages/api/gateway/src/index.ts)
|
||||
Source: [`packages/api/gateway/src/index.ts`](../../packages/api/gateway/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -97,7 +97,7 @@ interface InvocationDescriptor {
|
||||
}
|
||||
/** Optional consuming-Context projection for one direct lookup parameter. */
|
||||
readonly scope?: {
|
||||
/** Context kind whose Client binder supplies the identity. */
|
||||
/** Context kind whose Client adapter supplies the identity. */
|
||||
readonly context: string
|
||||
/** Lookup parameter wire field replaced by the Context identity. */
|
||||
readonly wire: string
|
||||
@@ -180,6 +180,14 @@ type TypertGatewayErrorCode =
|
||||
```ts type-equiv
|
||||
/** Host dispatcher consumed by Connection adapters. */
|
||||
interface TypertGateway {
|
||||
/** Carrier adapter shared by WebSocket and in-process transports. */
|
||||
readonly wireStream: TypertGatewayWireStream
|
||||
/**
|
||||
* Register the application-selected forwarded-event source.
|
||||
* @param source - stream factory installed by the Remote assembly.
|
||||
* @returns disposer removing this exact source and cancelling its active streams.
|
||||
*/
|
||||
registerRemoteEvents(source: TypertRemoteEventSource): () => Promise<void>
|
||||
/**
|
||||
* Invoke one live Remote method without assuming a carrier or response envelope.
|
||||
* @param request - decoded endpoint and named wire arguments.
|
||||
@@ -210,26 +218,15 @@ interface TypertClientRemote extends TypertRemoteNamespaceMap {
|
||||
*/
|
||||
$mount(contribution: TypertRemoteContribution): Promise<TypertDisposer>
|
||||
/**
|
||||
* Subscribe to one forwarded Host event; delivery is one-way, in registration
|
||||
* order, and isolates a throwing listener from the rest.
|
||||
* Subscribe to one forwarded Host event. Notifications run in registration
|
||||
* order and isolate failures; scoped waterfalls return, delegate through
|
||||
* `next()`, or reject the Host dispatch.
|
||||
* @template Event - forwarded event name selected by the Host assembly.
|
||||
* @param event - forwarded Host event name, unchanged on the wire.
|
||||
* @param listener - receives the Host's argument list as declared by Cordis `Events`.
|
||||
* @param listener - receives the Client projection of the Cordis `Events` declaration.
|
||||
* @returns disposer owned by the calling fiber.
|
||||
*/
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): () => void
|
||||
/**
|
||||
* Hand one decoded forwarded frame to the subscription table. The carrier
|
||||
* owning the Host frame sink calls this; a consumer subscribes with
|
||||
* {@link TypertClientRemote.$on} and never calls it.
|
||||
*
|
||||
* `event` is a plain string because this is the wire boundary: the name is
|
||||
* whatever the Host assembly's allowlist selected, and one nobody subscribed
|
||||
* to is dropped silently.
|
||||
* @param event - forwarded Host event name, exactly as the Host emitted it.
|
||||
* @param args - the Host argument list, already JSON-decoded.
|
||||
*/
|
||||
$dispatch(event: string, args: readonly unknown[]): void
|
||||
$on<Event extends TypertRemoteEvent>(event: Event, listener: TypertClientEventListener<Event>): () => void
|
||||
}
|
||||
```
|
||||
|
||||
@@ -239,7 +236,7 @@ interface TypertClientRemote extends TypertRemoteNamespaceMap {
|
||||
|
||||
## Cordis API
|
||||
|
||||
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` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
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`) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.zh.md#dispatch-modes), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
|
||||
|
||||
<a id="ctxapiproxy--apiproxy"></a>
|
||||
|
||||
@@ -247,7 +244,7 @@ Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnp
|
||||
|
||||
Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row.
|
||||
|
||||
Source: [`packages/host/apiproxy/src/api/index.ts:20`](../../packages/host/apiproxy/src/api/index.ts)
|
||||
Source: [`packages/host/apiproxy/src/api/index.ts`](../../packages/host/apiproxy/src/api/index.ts)
|
||||
|
||||
<a id="ctxtypert--typertregistry"></a>
|
||||
|
||||
@@ -311,9 +308,9 @@ listPackages(filter: TypertPackageFilter = {}): TypertPackageRecord[]
|
||||
toJSONSchema(key: string, params?: z.core.ToJSONSchemaParams): z.core.JSONSchema.BaseSchema
|
||||
```
|
||||
|
||||
Types: [TypertContribution](invariants.md) · [TypertFace](invariants.md) · [TypertPackageFilter](invariants.md) · [TypertPackageRecord](invariants.md) · [TypertSchemaFilter](invariants.md) · [TypertSchemaRecord](invariants.md)
|
||||
Types: [TypertContribution](invariants.zh.md) · [TypertFace](invariants.zh.md) · [TypertPackageFilter](invariants.zh.md) · [TypertPackageRecord](invariants.zh.md) · [TypertSchemaFilter](invariants.zh.md) · [TypertSchemaRecord](invariants.zh.md)
|
||||
|
||||
Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/registry/src/service.ts)
|
||||
Source: [`packages/typert/registry/src/service.ts`](../../packages/typert/registry/src/service.ts)
|
||||
|
||||
<a id="ctxtypertgateway--typertgatewayservice"></a>
|
||||
|
||||
@@ -322,6 +319,13 @@ Source: [`packages/typert/registry/src/service.ts:446`](../../packages/typert/re
|
||||
Resolve strict generated definitions or conservative SRC markers against current Cordis Services and Typert providers.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Register the sole application-selected forwarded-event source.
|
||||
* @param source - stream factory installed by the Remote assembly.
|
||||
* @returns disposer removing this source and cancelling its active streams.
|
||||
*/
|
||||
registerRemoteEvents(source: TypertRemoteEventSource): () => Promise<void>
|
||||
|
||||
/**
|
||||
* Invoke one live Remote method through strict generated reflection or SRC markers.
|
||||
* @param request - decoded endpoint and exact named wire arguments.
|
||||
@@ -338,5 +342,5 @@ async invoke(request: InvokeRemoteRequest): Promise<unknown>
|
||||
async stream(request: InvokeRemoteRequest): Promise<AsyncIterable<unknown>>
|
||||
```
|
||||
|
||||
Source: [`packages/api/gateway/src/index.ts:109`](../../packages/api/gateway/src/index.ts)
|
||||
Source: [`packages/api/gateway/src/index.ts`](../../packages/api/gateway/src/index.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/user-questions.md
|
||||
user-questions.md: f6a8fe611caf9f51c9a233a96be25d69a839e3f3
|
||||
user-questions.zh.md: c7fc7f911261b8a7c3c3490a2ca059a2ecdca120
|
||||
user-questions.md: 93b1bc52580da16fe8ad0de4bc16ec309aaacc1f
|
||||
user-questions.zh.md: 50098f6e0f6b1b38a1035566b04e24e0b7a80ff7
|
||||
|
||||
@@ -167,12 +167,38 @@ registerProvider(provider: UserQuestionProvider): () => void
|
||||
*
|
||||
* @param request Questions, owner agent, and abort signal.
|
||||
* @returns The answer chosen or typed by the human.
|
||||
* @throws {UserQuestionError} code `CALLER_NOT_LIVE` when a supplied
|
||||
* agent is not the registry's exact live instance, or `DELEGATED_CALLER`
|
||||
* when that live agent is owned by another agent.
|
||||
* @throws {UserQuestionError} code `ASK_ABORTED` when the supplied signal
|
||||
* is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent
|
||||
* is not the registry's exact live instance, or `DELEGATED_CALLER` when
|
||||
* that live agent is owned by another agent.
|
||||
*/
|
||||
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/index.ts`](../../packages/interaction/user-questions/src/index.ts)
|
||||
|
||||
<a id="user-questions-events"></a>
|
||||
|
||||
### `user-questions/*` events
|
||||
|
||||
<a id="user-questionsrequest--waterfall"></a>
|
||||
|
||||
#### `user-questions/request` — waterfall
|
||||
|
||||
Ask composed answerers for structured user input. Return an answer to claim the request or call `next()` to delegate. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Ask composed answerers for structured user input. Return an answer to
|
||||
* claim the request or call `next()` to delegate. Scope-filtered dispatch
|
||||
* (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param request - pending user-question request.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'user-questions/request'( this: Scoped<Agent>, request: AskUserQuestionRequestEvent, next: () => Promise<AskUserQuestionAnswer>, ): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Types: [Agent](core.md) · [Scoped](scope.md)
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/types.ts`](../../packages/interaction/user-questions/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -167,12 +167,38 @@ registerProvider(provider: UserQuestionProvider): () => void
|
||||
*
|
||||
* @param request Questions, owner agent, and abort signal.
|
||||
* @returns The answer chosen or typed by the human.
|
||||
* @throws {UserQuestionError} code `CALLER_NOT_LIVE` when a supplied
|
||||
* agent is not the registry's exact live instance, or `DELEGATED_CALLER`
|
||||
* when that live agent is owned by another agent.
|
||||
* @throws {UserQuestionError} code `ASK_ABORTED` when the supplied signal
|
||||
* is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent
|
||||
* is not the registry's exact live instance, or `DELEGATED_CALLER` when
|
||||
* that live agent is owned by another agent.
|
||||
*/
|
||||
async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/index.ts`](../../packages/interaction/user-questions/src/index.ts)
|
||||
|
||||
<a id="user-questions-events"></a>
|
||||
|
||||
### `user-questions/*` events
|
||||
|
||||
<a id="user-questionsrequest--waterfall"></a>
|
||||
|
||||
#### `user-questions/request` — waterfall
|
||||
|
||||
Ask composed answerers for structured user input. Return an answer to claim the request or call `next()` to delegate. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Ask composed answerers for structured user input. Return an answer to
|
||||
* claim the request or call `next()` to delegate. Scope-filtered dispatch
|
||||
* (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.
|
||||
* @param request - pending user-question request.
|
||||
* @mode waterfall
|
||||
*/
|
||||
'user-questions/request'( this: Scoped<Agent>, request: AskUserQuestionRequestEvent, next: () => Promise<AskUserQuestionAnswer>, ): Promise<AskUserQuestionAnswer>
|
||||
```
|
||||
|
||||
Types: [Agent](core.zh.md) · [Scoped](scope.zh.md)
|
||||
|
||||
Source: [`packages/interaction/user-questions/src/types.ts`](../../packages/interaction/user-questions/src/types.ts)
|
||||
<!-- END GENERATED cordis-surface -->
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write docs/subsystems/workspace.md
|
||||
workspace.md: 7706a326154c375afe316b640d48003345f4bef5
|
||||
workspace.zh.md: d621baf15487138aa837a4ad366a8d451f0886cc
|
||||
workspace.md: bf2ba88b84b65cdd289f4bd603354dbd7027b239
|
||||
workspace.zh.md: a1a190c2c4c006d067620ab6d8494cba947b0368
|
||||
|
||||
@@ -149,6 +149,65 @@ abstract capability(): DirectoryPickerCapability
|
||||
|
||||
Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
|
||||
|
||||
<a id="ctxworkspacecontroller--workspacecontroller"></a>
|
||||
|
||||
### `ctx.workspaceController` — `WorkspaceController`
|
||||
|
||||
Host service backing the generated `ctx.remote.workspace` namespace.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create or idempotently resolve one Workspace over an existing directory.
|
||||
* @param request - directory path to register.
|
||||
* @returns the Workspace and whether this call created it.
|
||||
*/
|
||||
@Remote('create') create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue>
|
||||
|
||||
/**
|
||||
* Rename one Workspace to a unique non-blank title.
|
||||
* @param request - Workspace identity and proposed title.
|
||||
* @returns the updated Workspace projection.
|
||||
*/
|
||||
@Remote('rename') rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue>
|
||||
|
||||
/**
|
||||
* Remove one Workspace registration while retaining files and Sessions.
|
||||
* @param request - Workspace identity to remove.
|
||||
* @returns deletion confirmation.
|
||||
*/
|
||||
@Remote('delete') delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue>
|
||||
|
||||
/**
|
||||
* Move one Workspace within the registry display order.
|
||||
* @param request - moved Workspace and optional anchor.
|
||||
* @returns the complete resulting Workspace order.
|
||||
*/
|
||||
@Remote('insertBefore') insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue>
|
||||
|
||||
/**
|
||||
* Move one accounted Session within a Workspace.
|
||||
* @param request - Workspace, Session, and optional anchor identities.
|
||||
* @returns the updated Workspace projection.
|
||||
*/
|
||||
@Remote('insertSessionBefore') insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue>
|
||||
|
||||
/**
|
||||
* Hide one known Session from Workspace grouping surfaces.
|
||||
* @param request - Session identity to archive.
|
||||
* @returns the complete resulting archive set.
|
||||
*/
|
||||
@Remote('archiveSession') archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue>
|
||||
|
||||
/**
|
||||
* Stream a complete Workspace baseline followed by ordered increments.
|
||||
* @param signal - generation cancellation.
|
||||
* @returns baseline followed by ordered Workspace increments.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) follow(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame>
|
||||
```
|
||||
|
||||
Source: [`packages/api/workspace-controller/src/index.ts`](../../packages/api/workspace-controller/src/index.ts)
|
||||
|
||||
<a id="ctxworkspaceregistry--workspaceregistry"></a>
|
||||
|
||||
### `ctx.workspaceRegistry` — `WorkspaceRegistry`
|
||||
|
||||
@@ -149,6 +149,65 @@ abstract capability(): DirectoryPickerCapability
|
||||
|
||||
Source: [`packages/host/directory-picker/src/index.ts`](../../packages/host/directory-picker/src/index.ts)
|
||||
|
||||
<a id="ctxworkspacecontroller--workspacecontroller"></a>
|
||||
|
||||
### `ctx.workspaceController` — `WorkspaceController`
|
||||
|
||||
Host service backing the generated `ctx.remote.workspace` namespace.
|
||||
|
||||
```ts cordis-catalog
|
||||
/**
|
||||
* Create or idempotently resolve one Workspace over an existing directory.
|
||||
* @param request - directory path to register.
|
||||
* @returns the Workspace and whether this call created it.
|
||||
*/
|
||||
@Remote('create') create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue>
|
||||
|
||||
/**
|
||||
* Rename one Workspace to a unique non-blank title.
|
||||
* @param request - Workspace identity and proposed title.
|
||||
* @returns the updated Workspace projection.
|
||||
*/
|
||||
@Remote('rename') rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue>
|
||||
|
||||
/**
|
||||
* Remove one Workspace registration while retaining files and Sessions.
|
||||
* @param request - Workspace identity to remove.
|
||||
* @returns deletion confirmation.
|
||||
*/
|
||||
@Remote('delete') delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue>
|
||||
|
||||
/**
|
||||
* Move one Workspace within the registry display order.
|
||||
* @param request - moved Workspace and optional anchor.
|
||||
* @returns the complete resulting Workspace order.
|
||||
*/
|
||||
@Remote('insertBefore') insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue>
|
||||
|
||||
/**
|
||||
* Move one accounted Session within a Workspace.
|
||||
* @param request - Workspace, Session, and optional anchor identities.
|
||||
* @returns the updated Workspace projection.
|
||||
*/
|
||||
@Remote('insertSessionBefore') insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue>
|
||||
|
||||
/**
|
||||
* Hide one known Session from Workspace grouping surfaces.
|
||||
* @param request - Session identity to archive.
|
||||
* @returns the complete resulting archive set.
|
||||
*/
|
||||
@Remote('archiveSession') archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue>
|
||||
|
||||
/**
|
||||
* Stream a complete Workspace baseline followed by ordered increments.
|
||||
* @param signal - generation cancellation.
|
||||
* @returns baseline followed by ordered Workspace increments.
|
||||
*/
|
||||
@Remote({ mode: 'stream' }) follow(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame>
|
||||
```
|
||||
|
||||
Source: [`packages/api/workspace-controller/src/index.ts`](../../packages/api/workspace-controller/src/index.ts)
|
||||
|
||||
<a id="ctxworkspaceregistry--workspaceregistry"></a>
|
||||
|
||||
### `ctx.workspaceRegistry` — `WorkspaceRegistry`
|
||||
|
||||
@@ -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 packages/api/gateway/README.md
|
||||
README.md: ac40c89f314941a7a0fa6fc62ed784961e39dc8d
|
||||
README.zh.md: ff242369de5f92c67f3e0237b440ab2d114bf0d7
|
||||
README.md: fe4834b4ca36590bc289be9b6863ffb082e8ee71
|
||||
README.zh.md: dff5a58599f0d3bf97996ceb36b134bf62da8ca0
|
||||
|
||||
@@ -16,6 +16,8 @@ A cancellation-aware Remote method declares `signal: AbortSignal` as its final H
|
||||
|
||||
A stream Remote uses `@Remote({ mode: 'stream' })` and returns an `Iterable` or `AsyncIterable`. `ctx.typertGateway.stream()` applies the same endpoint, argument, lookup, and cancellation checks as unary invocation, then validates each yielded item with the generated result codec. The Client opens the Gateway-owned `/api/remote.mux` WebSocket when its plugin activates, keeps it connected while idle, and retries physical connection failures with capped backoff. Independently cancellable logical streams share that socket; an in-process Connection carrier provides equivalent streams directly without opening it.
|
||||
|
||||
Host composition can register one application event source through `registerRemoteEvents()`. Gateway reserves the internal `$events` logical endpoint for that source, accepts only empty `args`, and aborts streams opened by the registration when the source is withdrawn. API Remotes owns the event selection, argument validation, and per-Client queues. Its source factory attaches incremental listeners synchronously; Gateway then yields `{ type: 'ready' }` before iterating the source, so the Client starts baseline reads only after incremental delivery is ready.
|
||||
|
||||
## Client service: `ClientRemote` (ctx key: `remote`)
|
||||
|
||||
`ctx.remote.$mount()` validates and registers a generated Host-for-Client contribution, then installs concrete direct and scoped methods for the calling Cordis fiber. Each namespace is a traced `remote.<namespace>` child Service and unloads after its last method is withdrawn. Duplicate endpoints, namespace collisions, and descriptors without strict generated codecs fail before methods become callable.
|
||||
@@ -24,7 +26,7 @@ Each unary call validates positional inputs, constructs the descriptor's exact n
|
||||
|
||||
`ctx.remote.$stream()` returns a single-consumer `RemoteStream` spanning physical carrier generations. It permits one immediate retry while the Host remains available, otherwise waits for the next connected Host generation, and annotates each item with its physical generation. The domain consumer validates and accepts each generation's opening value; business and protocol failures remain terminal. `RemoteSnapshotStream` adds one opening snapshot followed by deltas, while `RemoteJournalStream` adds follow-before-page opening, cursor deduplication, pagination, reconnect catch-up, and gap repair. Disposing any stream cancels its requests and resolves after the active iterator is fully stopped.
|
||||
|
||||
`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. Delivery is one-way and follows registration order; a listener that throws is logged and isolated from the remaining listeners, which never affects the frame pump. `ctx.remote.$dispatch()` is the other half of that surface, and it is the carrier's: the Client half owning the Host frame sink hands each decoded frame over, and an event name nobody subscribes to is dropped, since the wire carries whatever the Host selected. A consumer subscribes and never calls it.
|
||||
`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. The Client Remote service registers the `$events` pump as a Connection generation source when it activates, whether or not any `$on` listener exists. Browsers use Remote mux, while in-process compositions use `connection.rpc.open`; the `ready` item and `host.describe` jointly establish a Connection generation. Carrier failure, Remote stream failure, unexpected normal completion, a non-ready opening item, or a malformed event item ends that generation and lets Connection reopen it after backoff. Ordinary notifications run in registration order and isolate listener failures. Agent-scoped waterfalls let a listener return a result, call `next()`, or reject; Gateway returns that outcome through the existing HTTP unary carrier.
|
||||
|
||||
Generated declaration merges provide the TypeScript API through the shared `TypertClientRemote` contract. The Client entry contains no Host Service or Host Cordis interface merge, and method lookup and invocation use ordinary objects and functions rather than a JavaScript Proxy.
|
||||
|
||||
@@ -43,4 +45,4 @@ No direct effect; invoked business Services own any model-visible result.
|
||||
- Only strict generated contributions can mount on the Client face. SRC markers have no Client codec or type projection.
|
||||
- `$stream()` supervises carrier replacement but does not infer replay semantics; each domain owns its resume cursor or replacement-baseline validation and normal-end classification.
|
||||
- Lookup resolvers are configured per key; an individual Remote parameter or endpoint cannot currently select a live-only policy under the same `agent`/`session` key.
|
||||
- Forwarded events reach `$on` exactly as the Host emitted them: no payload projection or redaction, no Scope-bound subscription, and no replay after a reconnect.
|
||||
- Forwarded events reach `$on` without business-payload projection or redaction. Ordinary notifications are not replayed after reconnect; Agent-scoped waterfalls project only the top-level Agent identity needed to select the Client Context and carry their own pending lifetime.
|
||||
|
||||
@@ -16,7 +16,7 @@ Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandle
|
||||
|
||||
流式 Remote 使用 `@Remote({ mode: 'stream' })` 并返回 `Iterable` 或 `AsyncIterable`。`ctx.typertGateway.stream()` 执行与一元调用相同的 endpoint、参数、lookup 和取消校验,再用生成的 result codec 校验每个产出项。Client 插件激活时打开 Gateway 自有的 `/api/remote.mux` WebSocket,使其在空闲时保持连接,并以有上限的退避重试物理连接失败。可独立取消的逻辑流共享这条连接;进程内 Connection 载体直接提供等价的流,不打开该 WebSocket。
|
||||
|
||||
Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的 stream;事件名单、参数 JSON 校验和每 Client 队列由 API Remotes 拥有,不进入生成的业务 descriptor。source factory 必须在返回 iterable 前同步挂好增量 listener;Gateway 紧接着先产出 `{ type: 'ready' }`,再迭代 source,让 Client 能在增量通道就绪后才开始 baseline 读取。
|
||||
Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source。Gateway 为它保留内部 `$events` logical endpoint,只接受空 `args`,并在 source 撤回时中止该注册打开的 stream。事件名单、参数校验和每 Client 队列由 API Remotes 拥有。source factory 在返回 iterable 前同步挂好增量 listener;Gateway 随后先产出 `{ type: 'ready' }`,再迭代 source,让 Client 只在增量投递就绪后开始 baseline 读取。
|
||||
|
||||
## Client 服务:`ClientRemote`(ctx key:`remote`)
|
||||
|
||||
@@ -26,7 +26,7 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
|
||||
|
||||
`ctx.remote.$stream()` 返回跨越多个物理载体代次的单消费方 `RemoteStream`。Host 仍在线时,它允许一次立即重试;Host 离线时,它等待下一代连接,并为每个流项标注物理代次。领域消费方校验并接受各代次的 opening value;业务与协议错误仍然终止流。`RemoteSnapshotStream` 在此之上规定每代由一个 opening snapshot 和后续 delta 组成,`RemoteJournalStream` 则提供 follow-before-page、cursor 去重、分页、重连追赶与缺口修复。dispose 任一种 stream 都会取消其请求,并在活动 iterator 完全停止后完成。
|
||||
|
||||
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,因此即使当前无 `$on` 订阅,它也会在 Connection 循环启动时打开。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;`ready` 项将该逻辑流与 `host.describe` 共同组成一个 Connection generation。物理 carrier 失败、Remote stream error、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 退避后重开。投递按注册顺序进行;抛错或返回拒绝 Promise 的 listener 会被记录并与其余 listener 隔离。生产方交接不在 `TypertClientRemote` 上公开。
|
||||
`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。Client Remote 服务激活时就把 `$events` pump 注册为 Connection generation source,因此即使当前无 `$on` 订阅,它也会在 Connection 循环启动时打开。浏览器使用 Remote mux,进程内组合使用 `connection.rpc.open`;`ready` 项与 `host.describe` 共同建立一个 Connection generation。物理 carrier 失败、Remote stream error、意外正常结束、非 ready 首项或畸形事件项都会终止该 generation,由 Connection 退避后重开。普通通知按注册顺序运行并隔离 listener 失败;Agent-scoped waterfall 允许 listener 返回结果、调用 `next()` 或拒绝,Gateway 再通过现有 HTTP 一元载体回送该结果。
|
||||
|
||||
生成的声明合并通过共享的 `TypertClientRemote` 约定提供 TypeScript API。Client 入口不包含 Host 服务或 Host Cordis 接口合并;方法查找和调用使用普通对象与函数,而不使用 JavaScript Proxy。
|
||||
|
||||
@@ -45,4 +45,4 @@ Host 组合可通过 `registerRemoteEvents()` 注册唯一的应用事件 source
|
||||
- Client 侧只能挂载严格模式生成的贡献项。SRC 标记不具备 Client 编解码器或类型投影。
|
||||
- `$stream()` 监督载体替换,但不推断回放语义;各领域自行拥有恢复 cursor 或替换 baseline 的校验,以及正常结束的分类。Connection generation 会重开内部 `$events`,但不会重放断线期间的事件。
|
||||
- lookup resolver 按 key 配置;当前无法让单个 Remote 参数或 endpoint 在同一 `agent`/`session` key 下选择 live-only 策略。
|
||||
- 被转发的事件原样到达 `$on`:没有载荷投影或脱敏,不支持 Scope 化订阅,重连后也不重放。
|
||||
- 被转发的事件到达 `$on` 时不做业务载荷投影或脱敏。普通通知在重连后不重放;Agent-scoped waterfall 只投影选择 Client Context 所需的顶层 Agent 身份,并自行携带 pending 生命周期。
|
||||
|
||||
@@ -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 packages/api/remotes/README.md
|
||||
README.md: 2a70d47d863d6858528e8f4fda9ec2faa8c408fa
|
||||
README.zh.md: cab5333c8b40432e5bb624d77482d9daf4279a1d
|
||||
README.md: 0c4f0fcab4a741f457f1ffbdd9a4ba7688b9d32c
|
||||
README.zh.md: b381751cec5285c34bb310b330fc55d34033cbf2
|
||||
|
||||
@@ -2,19 +2,21 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Two-sided BFF for Host Remote capabilities selected by this application. The Host entry owns the forwarded-event selection and its Host compilation face; the Client entry imports generated `/remote` artifacts as runtime values, mounts each contribution through `ctx.remote.$mount()`, and re-exports their declaration merges. Client business packages depend on this facade rather than the Gateway implementation or individual Remote runtime entries.
|
||||
Two-sided BFF for Host Remote capabilities selected by this application. The Host entry owns the forwarded-event selection and registers its application event source with API Gateway; the Client entry imports generated `/remote` artifacts as runtime values, mounts each contribution through `ctx.remote.$mount()`, and re-exports their declaration merges. Client business packages depend on this facade rather than the Gateway implementation or individual Remote runtime entries.
|
||||
|
||||
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.md) owns Agent and Session identity policy, including the Typert lookup resolvers used by other namespaces. This package only selects and mounts that generated Session contribution; it does not duplicate activation policy.
|
||||
|
||||
The current Client assembly mounts Commands, Goal, dynamic Cordis, read-only Host plugin inventory, message feedback, and Session contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
|
||||
The current Client assembly mounts Commands, Goal, dynamic Cordis, file and Session references, read-only Host plugin inventory, message feedback, Session Controller, and Workspace Controller contributions. Cordis effect ownership withdraws every contribution when this assembly unloads, while `@deepseek-ai/dsh-api-gateway/client` owns descriptor validation, traced namespace Services, direct and scoped methods, invocation, streams, and cancellation. The Client entry consumes the shared `TypertClientRemote` interface through Cordis and does not import the concrete Gateway. It re-exports the Gateway Client face's declaration merges type-only, so a consumer reaching the forwarded-event vocabulary through this facade gains no runtime edge to the Gateway implementation.
|
||||
|
||||
This package contains no transport or Host service discovery logic. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
|
||||
This package owns no physical transport or Host service discovery. It projects the application selection into generated Remote contributions and an independent Host event source per Client; API Gateway owns endpoints, carriers, cancellation, and reconnection. Its Client face can be reused by Web or a future TUI that provides the same React-free `ctx.remote` contract.
|
||||
|
||||
## Forwarded Host events
|
||||
|
||||
`src/remote-events.ts` holds `API_REMOTE_FORWARDED_EVENTS`, the allowlist of Host cordis events this application forwards to consumers verbatim — no projection, no redaction, no renaming — and therefore the legal key set of `ctx.remote.$on`; the type-only `src/types.ts` derives its selection face. Forwarding one more event is an entry in that array and nothing else: the type projection, the consumer key face, and the Host forwarding loop all derive from it.
|
||||
`src/remote-events.ts` holds `API_REMOTE_FORWARDED_EVENTS`, the allowlist of Host Cordis events this application forwards without renaming, and therefore the legal key set of `ctx.remote.$on`; each entry also selects ordinary emission or Agent-scoped waterfall delivery. The type-only `src/types.ts` derives its selection face. Forwarding one more event requires one entry in that array: the type projection, consumer key face, and Host forwarding loop all derive from it.
|
||||
|
||||
The listener signature is not restated here. Each allowlisted event's cordis `Events` declaration lives in its owner package's client-safe `./types` export (`dsh-agent-presets`, `dsh-commands`, `dsh-credentials`, `dsh-llm`, `dsh-settings`), and both faces of this package pull those declarations in, so "forwarded verbatim" holds by construction rather than by proof. The Host face additionally asserts the list against `TypertForwardableEvent`, which rejects a name that is not a declared event, one that binds an AgentScope, and one whose shape is not one-way.
|
||||
The listener signature is not restated here. Each allowlisted event's Cordis `Events` declaration lives in its owner package's client-safe `./types` export, and both faces of this package pull those declarations in. The Host face additionally asserts every entry against `TypertForwardableEventEntry`: an `emit` entry must be a declared one-way event, while a `waterfall` entry must be a declared Agent-scoped waterfall whose final parameter is its same-result `next()` callback.
|
||||
|
||||
The Host entry registers an independent allowlist listener set and queue for each Client stream. It rejects non-JSON ordinary-event arguments before enqueueing. For a waterfall, it projects only the top-level Agent identity and JSON request fields; a Client result must also be lossless JSON, while `next()` delegates to the following Host listener. The source attaches all listeners synchronously before `ctx.typertGateway.registerRemoteEvents()` exposes Gateway's internal `$events` logical stream, so its first `ready` item proves that incremental delivery is active. Withdrawing the registration aborts active streams; API Proxy does not participate in event forwarding or Connection generation.
|
||||
|
||||
## Build boundary
|
||||
|
||||
@@ -38,3 +40,4 @@ No direct effect; mounted Host capabilities own any model-visible behavior they
|
||||
|
||||
- The capability set is fixed by explicit build-time value imports; the Client does not discover the Host's active Services or Remote definitions at runtime.
|
||||
- Additional capabilities require an explicit `/remote` value import and mount in this assembly.
|
||||
- Ordinary forwarded events are not replayed; state that requires reliable recovery needs an owner-provided query, cursor, or opening baseline.
|
||||
|
||||
@@ -4,19 +4,19 @@
|
||||
|
||||
为本应用选定的 Host Remote 能力提供双侧 BFF。Host 入口拥有转发事件名单并向 API Gateway 注册应用事件 source;Client 入口以运行时值形式导入生成的 `/remote` 产物,通过 `ctx.remote.$mount()` 挂载每项贡献,并重新导出对应的声明合并。Client 业务包依赖该外观,而不依赖 Gateway 实现或单独的 Remote 运行时入口。
|
||||
|
||||
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
|
||||
[`@deepseek-ai/dsh-api-session-controller`](../session-controller/README.zh.md) 拥有 Agent 与 Session 身份策略,包括供其他 namespace 使用的 Typert lookup resolver。本包只选择并挂载生成的 Session contribution,不复制激活策略。
|
||||
|
||||
当前 Client 组合挂载 Commands、Goal、动态 Cordis、只读 Host 插件清单、消息反馈和 Session contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
|
||||
当前 Client 组合挂载 Commands、Goal、动态 Cordis、文件与 Session 引用、只读 Host 插件清单、消息反馈、Session Controller 和 Workspace Controller contribution。该组合卸载时,Cordis effect 的所有权机制会撤回所有贡献;`@deepseek-ai/dsh-api-gateway/client` 负责描述符校验、可追踪 namespace Service、直接与作用域方法、调用、流与取消。Client 入口通过 Cordis 消费共享的 `TypertClientRemote` 接口,不导入具体 Gateway;它只以 type-only 形式重新导出 Gateway Client face 的声明合并,因此消费端经由本外观取到转发事件词汇时,运行时不会多出一条通往 Gateway 实现的边。
|
||||
|
||||
本包不拥有物理传输或 Host 服务发现。它只把应用选择投影为生成的 Remote contribution 和每 Client 独立的 Host event source;API Gateway 负责 endpoint、carrier、取消与重连。Web 或未来的 TUI 只要提供同一份不依赖 React 的 `ctx.remote` 约定,均可复用其 Client face。
|
||||
|
||||
## 转发的 Host 事件
|
||||
|
||||
`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`——本应用原样转发给消费端的 Host cordis 事件名单(无投影、无脱敏、无改名),它同时就是 `ctx.remote.$on` 的合法键集;只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一行:类型投影、消费端键面与 Host 转发循环全部由它派生。
|
||||
`src/remote-events.ts` 持有 `API_REMOTE_FORWARDED_EVENTS`,即本应用不改名转发给消费端的 Host Cordis 事件名单;每个条目还会选择普通发送或 Agent-scoped waterfall 投递。该名单同时就是 `ctx.remote.$on` 的合法键集,只含类型的 `src/types.ts` 派生其选择面。多转发一个事件只需在该数组里加一项:类型投影、消费端键面与 Host 转发循环全部由它派生。
|
||||
|
||||
监听器签名不在此处重写。名单内每条事件的 cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口,本包两个 face 都把那些声明纳入编译面,因此「原样转发」是构造性成立的,不需要另立证明。Host face 还额外把名单断言给 `TypertForwardableEvent`:未声明的事件名、绑定 AgentScope 的事件、以及形状不是单向的事件都会在此被拒绝。
|
||||
监听器签名不在此处重写。名单内每条事件的 Cordis `Events` 声明都住在其 owner 包 client-safe 的 `./types` 出口,本包两个 face 都把那些声明纳入编译面。Host face 还会把每个条目断言给 `TypertForwardableEventEntry`:`emit` 条目必须是已声明的单向事件,`waterfall` 条目则必须是已声明的 Agent-scoped waterfall,且其最后一个参数是返回相同结果类型的 `next()` 回调。
|
||||
|
||||
Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,并在事件入队前逐参数拒绝非 JSON 值。该 source 在 factory 返回前同步挂好所有 listener,再通过 `ctx.typertGateway.registerRemoteEvents()` 接到 Gateway 内部的 `$events` logical stream;这个顺序让 Gateway 的首个 `ready` 项能够作为增量投递已就绪的证明。撤回注册会中止仍在活动的 stream;API Proxy 不参与事件转发或 Connection generation。
|
||||
Host entry 为每条 Client stream 独立注册 allowlist listener 和队列,并在普通事件入队前拒绝非 JSON 参数。对于 waterfall,它只投影顶层 Agent 身份与 JSON 请求字段;Client 结果也必须能无损表示为 JSON,而 `next()` 会委托给后续 Host listener。该 source 在 `ctx.typertGateway.registerRemoteEvents()` 暴露 Gateway 内部的 `$events` logical stream 前同步挂好所有 listener,因此首个 `ready` 项能证明增量投递已就绪。撤回注册会中止活动 stream;API Proxy 不参与事件转发或 Connection generation。
|
||||
|
||||
## 构建边界
|
||||
|
||||
|
||||
@@ -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 packages/api/session-controller/README.md
|
||||
README.md: 55e5894ad1e8d2ab1f6c4c8373e87c3fc4fe397f
|
||||
README.zh.md: 2c308902088d8c445fb6f421111d514bf84e9de3
|
||||
README.md: cda9349e432472a0ed9fd623afef0b689ff72f73
|
||||
README.zh.md: 2aaee8f968cf7373110e291c197adbeca21f490e
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `ctx.remote.session` namespace. It serves Session list, search, creation, model selection, rename, fork, prompt, attachment, queue, cancellation, message-aligned history, live log following, Host-wide control state, and pending-interaction responses.
|
||||
`@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `ctx.remote.session` namespace. It serves Session list, search, creation, model selection, rename, fork, prompt, attachment, queue, cancellation, message-aligned history, live log following, and Host-wide control state.
|
||||
|
||||
Each endpoint states its activation policy. List, search, attachment, history pages, and log following can inspect persistence without activating an Agent; queue mutation, cancellation, and interaction responses require the corresponding live state; model, rename, and prompt commands may explicitly resume an ordinary Session. Create and fork are the only operations that create a new Agent. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces.
|
||||
Each endpoint states its activation policy. List, search, attachment, history pages, and log following can inspect persistence without activating an Agent; queue mutation and cancellation require the corresponding live state; model, rename, and prompt commands may explicitly resume an ordinary Session. Create and fork are the only operations that create a new Agent. The service applies one preset-aware resume policy and subagent ownership fence to its own methods and to the Typert Agent and Session lookups used by other Remote namespaces.
|
||||
|
||||
The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, and `append` changes, and repairs reconnect or sequence gaps through a tail page. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces queue, jobs, projection, approval, and question state instead of treating transient values as durable events.
|
||||
The Client adapter exposes `SessionEventStream`, a Gateway `RemoteJournalStream` bound to one ordinary or direct-subagent address. It opens follow before the initial page, publishes only contiguous `replace`, `prepend`, and `append` changes, and repairs reconnect or sequence gaps through a tail page. A business, persistence, or unresolved continuity failure terminates the stream, while only physical carrier loss selects automatic resumption. `SessionControlStream` is a Gateway `RemoteSnapshotStream`; every generation opens with a complete process-local baseline, so reconnect replaces queue, jobs, and projection state instead of treating transient values as durable events.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -18,5 +18,5 @@ No direct effect; model requests remain owned by the Agent and LLM packages.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- Control baselines represent process-local state and therefore cannot reconstruct pending interactions or jobs after a Host restart.
|
||||
- Control baselines represent process-local state and therefore cannot reconstruct jobs after a Host restart.
|
||||
- A failed follow resumption remains visible to the caller instead of retrying indefinitely.
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务和生成的 Client `ctx.remote.session` namespace。它提供 Session 列表、搜索、创建、模型选择、重命名、fork、prompt、附件、queue、取消、按消息对齐的历史、live 日志跟随、Host 范围 control 状态和 pending interaction 响应。
|
||||
`@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务和生成的 Client `ctx.remote.session` namespace。它提供 Session 列表、搜索、创建、模型选择、重命名、fork、prompt、附件、queue、取消、按消息对齐的历史、live 日志跟随和 Host 范围 control 状态。
|
||||
|
||||
每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页和日志跟随可以在不激活 Agent 的情况下检查 persistence;queue 变更、取消和 interaction 响应要求对应 live 状态仍然存在;模型、重命名和 prompt 命令可以显式恢复普通 Session。只有 create 和 fork 会创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。
|
||||
每个 endpoint 都声明自己的激活策略。列表、搜索、附件、历史页和日志跟随可以在不激活 Agent 的情况下检查 persistence;queue 变更和取消要求对应 live 状态仍然存在;模型、重命名和 prompt 命令可以显式恢复普通 Session。只有 create 和 fork 会创建新 Agent。该服务把同一套感知 preset 的恢复策略和 subagent ownership fence 同时用于自身方法,以及其他 Remote namespace 使用的 Typert Agent 与 Session lookup。
|
||||
|
||||
Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq 缺口。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs、projection、approval 和 question 状态,而不会把瞬态值当作 durable event。
|
||||
Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session 或 direct subagent address 的 Gateway `RemoteJournalStream`。它在读取首个 page 前打开 follow,只发布连续的 `replace`、`prepend` 和 `append` 变更,并通过 tail page 修复重连或 seq 缺口。业务、persistence 或无法恢复的连续性错误会终止 stream,只有物理载体断开才触发自动恢复。`SessionControlStream` 是 Gateway `RemoteSnapshotStream`;每代都以完整的进程本地 baseline 开始,因此重连会替换 queue、jobs 和 projection 状态,而不会把瞬态值当作 durable event。
|
||||
|
||||
## 模型体验
|
||||
|
||||
@@ -18,5 +18,5 @@ Client adapter 提供 `SessionEventStream`,即绑定到一个普通 Session
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- Control baseline 表示进程本地状态,因此 Host 重启后无法重建 pending interaction 或 jobs。
|
||||
- Control baseline 表示进程本地状态,因此 Host 重启后无法重建 jobs。
|
||||
- follow 恢复失败会对调用方可见,而不会无限重试。
|
||||
|
||||
@@ -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 packages/api/workspace-controller/README.md
|
||||
README.md: 0e126f1a0cc52353cb479f42a592e8997207e2e5
|
||||
README.zh.md: 2a1c56d8c02ffe7ac6a797b2c620ce3be42a3cc4
|
||||
@@ -0,0 +1,22 @@
|
||||
# Workspace Controller
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
`@deepseek-ai/dsh-api-workspace-controller` owns the Host `ctx.workspaceController` service and the generated Client `ctx.remote.workspace` namespace. Its Remote methods create, rename, remove, and reorder Workspaces, reorder Sessions within a Workspace, archive Sessions from Workspace navigation, and follow the complete Workspace projection.
|
||||
|
||||
The Host controller serializes mutations whose correctness depends on current registry state and returns stable `WorkspaceError` values for expected failures. Its `follow()` stream synchronously attaches to durable Workspace changes, emits one complete baseline first, then emits ordered `upsert`, `remove`, `order`, and `archived` increments. A reconnect starts another generation with a replacement baseline, so consumers do not depend on receiving every increment while disconnected.
|
||||
|
||||
The Client entry provides `ClientWorkspaceModel` and `createWorkspaceStateStream()`. The model owns Workspace rows, registry order, archived Session ids, unary mutation echoes, and stream/unary race resolution. A newer Host row wins by `updatedAt`; a committed stream order outranks an older unary response; a removed Workspace id cannot be resurrected by delayed data. The package exposes framework-neutral snapshots and subscriptions, leaving navigation policy and React hooks to the UI owner.
|
||||
|
||||
## Model Experience
|
||||
|
||||
None, as Workspace organization is browser and Host control state and registers no prompt, tool, or session event.
|
||||
|
||||
#### KV Cache effect
|
||||
|
||||
No direct effect; Workspace mutations do not alter model requests.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- `follow()` replaces the whole projection after reconnect and has no durable cursor or incremental catch-up protocol.
|
||||
- Process-local deletion markers prevent delayed data from reviving a removed Workspace only for the lifetime of the Client model.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Workspace Controller
|
||||
|
||||
[English](README.md) | 中文
|
||||
|
||||
`@deepseek-ai/dsh-api-workspace-controller` 拥有 Host 的 `ctx.workspaceController` 服务和生成的 Client `ctx.remote.workspace` namespace。它的 Remote 方法负责创建、重命名、移除和重排 Workspace,在 Workspace 内重排 Session,从 Workspace 导航中归档 Session,以及跟随完整的 Workspace 投影。
|
||||
|
||||
Host 控制器会串行执行正确性取决于当前 registry 状态的变更,并为预期失败返回稳定的 `WorkspaceError` 值。它的 `follow()` 流会同步订阅持久 Workspace 变更,先发出一份完整 baseline,再按顺序发出 `upsert`、`remove`、`order` 和 `archived` 增量。重连会以替换 baseline 开始新一代,因此消费方不依赖收到断线期间的每个增量。
|
||||
|
||||
Client 入口提供 `ClientWorkspaceModel` 和 `createWorkspaceStateStream()`。该模型拥有 Workspace 行、registry 顺序、已归档 Session id、一元变更回声,以及流与一元调用的竞态处理。较新的 Host 行按 `updatedAt` 获胜;已提交的流顺序优先于较旧的一元响应;已经移除的 Workspace id 不会被延迟数据复活。该包公开与框架无关的快照和订阅,把导航策略与 React hook 留给 UI owner。
|
||||
|
||||
## 模型体验
|
||||
|
||||
无,因为 Workspace 组织属于浏览器与 Host 控制状态,并且不注册提示词、工具或会话事件。
|
||||
|
||||
#### KV Cache 影响
|
||||
|
||||
无直接影响;Workspace 变更不会改变模型请求。
|
||||
|
||||
## 已知限制与延期工作
|
||||
|
||||
- `follow()` 在重连后替换完整投影,不提供持久 cursor 或增量追赶协议。
|
||||
- 进程本地删除标记只会在 Client 模型生命周期内阻止延迟数据复活已移除的 Workspace。
|
||||
@@ -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 packages/client/connection/README.md
|
||||
README.md: 71ef204a589bb67c15ccab58d3cac5a13782ce27
|
||||
README.zh.md: 6d33ac3c13cdfceeba6e7472b618084267d09bbc
|
||||
README.md: d2614515744ee69ca11443a7bc440a589d3f26b3
|
||||
README.zh.md: 13df74ddf7bb21455bb5528119bd7c3d5d149b87
|
||||
|
||||
@@ -2,15 +2,21 @@
|
||||
|
||||
English | [中文](README.zh.md)
|
||||
|
||||
Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared api client + current-page loopback state + observable generation-scoped `hostDescription` + single-consumer stream-loop starter); the export face carries the wire contract types, the `AbstractApiClient` abstraction, and the loop's sink/config types. Each successful readiness handshake publishes the exact `host.describe` value before `onConnected`; generation loss and explicit stop clear it, so native-capability consumers never retain a disconnected answer. The browser carrier uses HTTP POST for unary and respond operations and opens one downlink-only WebSocket each for `events.mux` and `events.host`; the in-process carrier satisfies the same two-stream abstraction. The exported `ClientTransportHooks` names the page global `__DSH_TRANSPORT__` that replaces the browser carrier wholesale: the served web app leaves it unset and gets HTTP + WebSocket, while a shell owning a different physical transport (the worker preview's postMessage tunnel) provides `createApiClient` and `fetch` — plus `loadBundle` when it also owns bundle bytes — instead of forking the plugin. The Host half owns the single `/api` route and its Fetch bridge; a registered Typert interceptor claims its Remote endpoints before the API Proxy fallback. Loopback hostname classification stays package-internal: the `/api` Host fence and WebSocket upgrades use it directly, while other client plugins consume the derived `ctx.connection.isLoopback` state. The node half's `/api` route pins the privileged method set (`host.pickDirectory`, `host.openPath`, and the whole configuration plane — `settings.describe`/`openDocument`/`update`/`replace`/`mutate` and `credentials.describe`/`set`/`unset`; reads and native actions included, since describing returns the exposed configuration, opening acts on the Host desktop, and probing an arbitrary reference reports where a credential comes from — and the agent-preset authoring plane, `agentPreset.read`/`copy`/`openDocument`/`remove`, since a composition names the plugins a session runs, so reading one is reconnaissance, and copy/remove/openDocument manage the roster and drive the host desktop (authoring is copy-only, so none of them accepts composition text or a path); `agentPreset.list` and `agentPreset.select` stay out — the roster carries only ids and trust, and choosing a preset grants nothing `session.create`'s own `agentPreset` did not, over a default that already carries bash) to loopback by passing the trust fence with an empty trust list — a declared `trustedHosts` authority reaches every other method, while these stay loopback-local until a real authentication layer exists. The platform carriers and ConnectionController loop are package-internal; apply selects and drives them. The downlink boundary is documented in the [WebSocket downlink carrier Agent Note](../../../.agents/notes/implemented/architecture/2026-08-04-websocket-downlink-carrier.md).
|
||||
Protocol and connection-generation layer. The Client plugin mounts `ctx.connection`, containing the shared API client, current-page loopback state, generation-scoped observable `hostDescription`, a generic RPC carrier, and the registration point for one generation source and the connection loop. A generation publishes `hostDescription` and calls `onConnected` only after its source is ready and `host.describe` succeeds; source completion, failure, withdrawal, or an explicit stop clears that value before `ConnectionController` reconnects with backoff.
|
||||
|
||||
The browser uses HTTP POST for API Proxy and generic Remote unary calls. API Gateway owns the `/api/remote.mux` WebSocket and its logical streams; in-process compositions provide equivalent Remote streams through `connection.rpc.open` without opening a WebSocket. The Host half owns the sole `/api` route, Fetch bridge, and trust checks. Typert Gateway claims its Remote endpoints first, and unclaimed requests fall through to API Proxy. Loopback hostname classification remains package-internal: the Host fence and WebSocket upgrade use it directly, while other Client plugins consume `ctx.connection.isLoopback`.
|
||||
|
||||
The Node half keeps privileged methods (`host.pickDirectory`, `host.openPath`, the settings and credentials configuration planes, `llm.discoverModels`, and `agentPreset.read`/`copy`/`openDocument`/`remove`) loopback-only by passing an empty trust list to the fence. `agentPreset.list` and `agentPreset.select` are excluded: the roster carries only ids and trust levels, while `session.create` already selects a preset. Declared `trustedHosts` authorities can reach other methods; privileged operations remain loopback-only until a real authentication layer exists.
|
||||
|
||||
## /api browser-trust fence
|
||||
|
||||
The node half guards every entry under `/api` before bridging or upgrading (`src/api-request-trust.ts`). Every request — browser-marked or not — must present a `Host` that is a loopback authority or matches a `trustedHosts` entry: exact on `host:port` entries, any port on port-less entries, both sides compared through WHATWG normalization (DNS-rebinding defense). There is deliberately no shortcut for unmarked HTTP requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to image and navigation reads, so an unmarked request may still be a rebound browser read with a readable response, and Host is the one header rebinding cannot forge; a browser WebSocket handshake carries `Origin` and passes the same comparison. Non-browser clients pass the same fence via loopback, deployment-derived LAN IP literals, or a declared authority. When markers are present, an attached `Origin` must equal the Host authority, and an explicit `sec-fetch-site: cross-site` marker is refused. A `trustedHosts` entry that is not a bare, canonical `host[:port]` authority — one WHATWG parsing reads back exactly as written — fails the plugin load loudly: parsing would otherwise quietly authorize the hostname inside `harness.internal/path`, or broaden a dangling-colon or zero-padded port to an any-port grant. HTTP failures answer plain 403 before any RPC dispatch; upgrade failures reject the handshake before any event stream starts. Non-loopback compositions must trust their serving authorities explicitly: the Web runtime derives LAN IP literals from an all-interfaces server config, while `trustedHosts` in cordis.yml and the CLI's `--trusted-host` flag declare named authorities. `dsh web --host 0.0.0.0` is intentionally unsupported until remote access has an authentication layer. The fence is a reachability policy, not authentication; the Web carrier provides no authentication layer. Decision record: [the api browser-trust boundary Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md).
|
||||
|
||||
## `/api` WebSocket downlinks
|
||||
## Connection generation
|
||||
|
||||
`/api/events.mux` and `/api/events.host` each accept a WebSocket upgrade and send only the corresponding `ServerRequest` text messages to the browser; the client sends no application data over these sockets. If either socket ends, the current connection generation fails and rebuilds both streams; readiness still requires both sockets to be open and the `host.describe` HTTP call to succeed. Host teardown terminates both sockets, aborts their sources, and waits for source cleanup before returning. Ordinary network GETs to these paths return 426 with no SSE fallback; `toFetchHandler`'s SSE codec serves only the isomorphic in-process carrier.
|
||||
API Gateway Client registers the internal `$events` logical stream as the sole generation source, independently of whether any `$on` listener exists. The Host attaches all incremental listeners in the API Remotes source factory, then sends one `{ type: 'ready' }` item before events. `ConnectionController` waits for that item and `host.describe` in parallel; `onConnected` cannot start baseline reads until both succeed, so baseline acquisition cannot race ahead of incremental observation.
|
||||
|
||||
An ended `$events` stream, a Remote stream error, a non-ready opening item, or a malformed event item invalidates the current generation. The controller immediately withdraws `hostDescription`, publishes `reconnecting`, and rebuilds the `$events` plus `host.describe` handshake after backoff. Gateway mux reconnects the physical WebSocket; Connection generation reopens the logical stream and establishes the next baseline starting point.
|
||||
|
||||
## Model Experience
|
||||
|
||||
@@ -22,5 +28,4 @@ None; this package neither assembles nor sends a provider request.
|
||||
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **History resumes an unattached session** — opening history may create the host-side agent and add latency to the first open; there is no persistence-only read path.
|
||||
- **The `/api` bridge buffers each request body in memory** — `maxRequestBodyBytes` (default 300 MiB, sized for the default 200 MiB aggregate image limit after base64 expansion plus envelope headroom) is therefore also the per-request resident bound; a streaming body path would be needed to lower it without shrinking the image limits.
|
||||
|
||||
@@ -28,5 +28,4 @@ API Gateway Client 把内部 `$events` logical stream 注册为唯一 generation
|
||||
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **History 会恢复未附加的会话**:打开 history 可能创建宿主侧 agent,并增加首次打开的延迟;没有仅从持久化读取的路径。
|
||||
- **`/api` 桥把每个请求体整体缓冲在内存里**:`maxRequestBodyBytes`(默认 300 MiB,按默认 200 MiB 图片总量上限经 base64 膨胀加信封余量得出)因此同时是单请求的驻留内存上界;要降低它而不缩小图片限额,需要流式请求体路径。
|
||||
|
||||
@@ -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 packages/client/runtime/README.md
|
||||
README.md: 5d9536b52720841a10c731e77077d0dd2262d625
|
||||
README.zh.md: aec7a5fa96b030db4c519f8faf5156c2112e3f07
|
||||
README.md: adeeca51ef2948da09c2906c803adcd49dcc4b74
|
||||
README.zh.md: 8235593bf00a6634efa7c98a1b3c3bea9eafa564
|
||||
|
||||
@@ -62,7 +62,6 @@ export type RequestErrorAction = { kind: 'retry' } | undefined
|
||||
export type SessionStartSource = 'startup' | 'resume' | 'clear' | 'compact'
|
||||
|
||||
declare module './types.ts' {
|
||||
/** Public live-agent handle. */
|
||||
interface Agent {
|
||||
/** The provider route and model this agent's requests use. */
|
||||
readonly options: AgentOptions
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { UserMessage } from '@deepseek-ai/dsh-llm/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session/types'
|
||||
import type { TypertContext, TypertLookup } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
/** Minimum Agent identity visible to cross-process event declarations. */
|
||||
/** Public live-agent handle; the runtime face augments its live capabilities. */
|
||||
export interface Agent {
|
||||
/** Session-backed Agent identity. */
|
||||
readonly id: SessionId
|
||||
|
||||
@@ -387,12 +387,6 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
description: 'Host-only download surfaces (GET, no wire envelope); absent from IApiClient.',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
signature: 'respond(message: ClientResponse): Promise<RpcReceipt>',
|
||||
description: 'Response entry for server requests; not a domain method.',
|
||||
parameters: [{ name: 'message', description: 'Client response carrying the server request\'s rpcId.' }],
|
||||
returns: 'Transport receipt for the response delivery.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1195,6 +1189,109 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'sessionController',
|
||||
summary: 'Host service backing the generated `ctx.remote.session` namespace.',
|
||||
description: 'Host service backing the generated `ctx.remote.session` namespace.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult>',
|
||||
description: 'Resolve or resume one ordinary Session for another Host API domain.',
|
||||
parameters: [{ name: 'sessionId', description: 'Session identity whose Agent owns the operation.' }],
|
||||
returns: 'the live Agent or the stable Session-domain failure.',
|
||||
},
|
||||
{
|
||||
signature: 'inspect( sessionId: SessionId, signal?: AbortSignal, ): Promise<{ meta: SessionHeader; events: SessionEvent[] }>',
|
||||
description: 'Inspect one attached or persisted Session without activating its Agent.',
|
||||
parameters: [{ name: 'sessionId', description: 'durable Session identity.' }, { name: 'signal', description: 'optional caller cancellation for persistence reads.' }],
|
||||
returns: 'the current attached state or persisted header and event prefix.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'list\') async list(_request: SessionListRequest, signal: AbortSignal): Promise<SessionListValue>',
|
||||
description: 'Read all visible Session rows without resuming an Agent.',
|
||||
parameters: [{ name: '_request', description: 'reserved empty list request.' }, { name: 'signal', description: 'cancellation for persistence reads.' }],
|
||||
returns: 'visible Session summaries ordered by activity.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'search\') search(request: SessionSearchRequest, signal: AbortSignal): Promise<SessionSearchValue>',
|
||||
description: 'Search visible Session content without resuming an Agent.',
|
||||
parameters: [{ name: 'request', description: 'literal message-content query.' }, { name: 'signal', description: 'cancellation for list and search reads.' }],
|
||||
returns: 'authorized bounded Session search results.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'create\') create(request: SessionCreateRequest): Promise<SessionCreateValue>',
|
||||
description: 'Create or idempotently adopt one ordinary Session.',
|
||||
parameters: [{ name: 'request', description: 'requested identity, location, and Agent preset.' }],
|
||||
returns: 'the Session identity and resolved preset when configured.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'models\') models(request: SessionModelsRequest): Promise<SessionModels>',
|
||||
description: 'Read model choices after explicitly resuming the addressed Session.',
|
||||
parameters: [{ name: 'request', description: 'Session whose model state is requested.' }],
|
||||
returns: 'the current selection and available model groups.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'selectModel\') selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue>',
|
||||
description: 'Select one Session-local model after explicitly resuming the Session.',
|
||||
parameters: [{ name: 'request', description: 'Session identity and requested model selection.' }],
|
||||
returns: 'the normalized selection installed for the Session.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'rename\') rename(request: SessionRenameRequest): Promise<SessionRenameValue>',
|
||||
description: 'Rename one Session after explicitly resuming it.',
|
||||
parameters: [{ name: 'request', description: 'Session identity and proposed title.' }],
|
||||
returns: 'the accepted title and durable event sequence.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'fork\') fork(request: SessionForkRequest): Promise<SessionForkValue>',
|
||||
description: 'Fork one cold-readable completed-turn prefix into a new Session.',
|
||||
parameters: [{ name: 'request', description: 'source Session and optional event anchor.' }],
|
||||
returns: 'the new Session identity.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'prompt\') prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue>',
|
||||
description: 'Admit one prompt after explicitly resuming its Session.',
|
||||
parameters: [{ name: 'request', description: 'Session identity, prompt content, source metadata, and delivery mode.' }, { name: 'signal', description: 'caller cancellation before prompt admission begins.' }],
|
||||
returns: 'acknowledgement that the Agent accepted the prompt.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'attachment\') attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>',
|
||||
description: 'Read one image proven reachable from the addressed Session log.',
|
||||
parameters: [{ name: 'request', description: 'Session and attachment identities used for authorization.' }],
|
||||
returns: 'the durable attachment reference and base64-encoded bytes.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'updateQueue\') updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue',
|
||||
description: 'Mutate one still-pending queue occurrence on a live Agent.',
|
||||
parameters: [{ name: 'request', description: 'Session, queue item, and requested mutation.' }],
|
||||
returns: 'acknowledgement that the queue mutation was applied.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'cancel\') cancel(request: SessionCancelRequest): SessionCancelValue',
|
||||
description: 'Cancel one active Agent turn without dropping its pending inbox.',
|
||||
parameters: [{ name: 'request', description: 'Session whose active Agent turn is cancelled.' }],
|
||||
returns: 'acknowledgement that cancellation was requested.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'page\') page(request: SessionPageRequest, signal: AbortSignal): Promise<SessionPage>',
|
||||
description: 'Read one cold-safe, message-aligned Session history page.',
|
||||
parameters: [{ name: 'request', description: 'durable address, backward cursor, and page budget.' }, { name: 'signal', description: 'cancellation for persistence and presentation reads.' }],
|
||||
returns: 'one chronological page and optional latest projections.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote({ mode: \'stream\' }) follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>',
|
||||
description: 'Follow one Session log from its opening or resume cursor.',
|
||||
parameters: [{ name: 'request', description: 'durable address and last committed sequence already held by the caller.' }, { name: 'signal', description: 'cancellation owned by the Remote stream carrier.' }],
|
||||
returns: 'an opened cursor followed by gap-free event frames.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote({ mode: \'stream\' }) control(signal: AbortSignal): AsyncIterable<SessionControlFrame>',
|
||||
description: 'Stream a complete live-control baseline followed by replacement frames.',
|
||||
parameters: [{ name: 'signal', description: 'cancellation owned by the Remote stream carrier.' }],
|
||||
returns: 'one complete baseline followed by live replacement frames.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'sessionPersistence',
|
||||
summary: 'Durable append-only session storage.',
|
||||
@@ -2214,6 +2311,17 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
summary: 'Resolve strict generated definitions or conservative SRC markers against current Cordis Services and Typert providers.',
|
||||
description: 'Resolve strict generated definitions or conservative SRC markers against current Cordis Services and Typert providers.',
|
||||
methods: [
|
||||
{
|
||||
signature: 'readonly wireStream: TypertGatewayWireStream = { open: (endpoint, payload, signal) => this.openWireStream(endpoint, payload, signal), failure: error => rpcError(error), }',
|
||||
description: 'Carrier adapter shared by the WebSocket mux and local Host transports.',
|
||||
parameters: [],
|
||||
},
|
||||
{
|
||||
signature: 'registerRemoteEvents(source: TypertRemoteEventSource): () => Promise<void>',
|
||||
description: 'Register the sole application-selected forwarded-event source.',
|
||||
parameters: [{ name: 'source', description: 'stream factory installed by the Remote assembly.' }],
|
||||
returns: 'disposer removing this source and cancelling its active streams.',
|
||||
},
|
||||
{
|
||||
signature: 'async invoke(request: InvokeRemoteRequest): Promise<unknown>',
|
||||
description: 'Invoke one live Remote method through strict generated reflection or SRC markers.',
|
||||
@@ -2221,6 +2329,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
returns: 'the validated business result.',
|
||||
throws: ['{@link TypertGatewayError} for dispatch, provider, or boundary failures; lookup-policy and business errors retain identity.'],
|
||||
},
|
||||
{
|
||||
signature: 'async stream(request: InvokeRemoteRequest): Promise<AsyncIterable<unknown>>',
|
||||
description: 'Open one live stream Remote method without assuming a physical carrier.',
|
||||
parameters: [{ name: 'request', description: 'decoded endpoint and named wire arguments.' }],
|
||||
returns: 'an iterable whose items have passed the generated result codec.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -2239,7 +2353,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
description: 'Ask the active UI provider and wait for the user\'s answer.\n\nWhen a caller supplies an agent, human interaction is valid only for the exact live runtime root. Runtime ownership, not durable session lineage, decides this boundary: an owned child has no human answerer and would block forever, while a lineage-bearing session resumed as a new runtime root may ask normally.',
|
||||
parameters: [{ name: 'request', description: 'Questions, owner agent, and abort signal.' }],
|
||||
returns: 'The answer chosen or typed by the human.',
|
||||
throws: ['{UserQuestionError} code `CALLER_NOT_LIVE` when a supplied agent is not the registry\'s exact live instance, or `DELEGATED_CALLER` when that live agent is owned by another agent.'],
|
||||
throws: ['{UserQuestionError} code `ASK_ABORTED` when the supplied signal is already or becomes aborted, `CALLER_NOT_LIVE` when a supplied agent is not the registry\'s exact live instance, or `DELEGATED_CALLER` when that live agent is owned by another agent.'],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -2355,6 +2469,55 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'workspaceController',
|
||||
summary: 'Host service backing the generated `ctx.remote.workspace` namespace.',
|
||||
description: 'Host service backing the generated `ctx.remote.workspace` namespace.',
|
||||
methods: [
|
||||
{
|
||||
signature: '@Remote(\'create\') create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue>',
|
||||
description: 'Create or idempotently resolve one Workspace over an existing directory.',
|
||||
parameters: [{ name: 'request', description: 'directory path to register.' }],
|
||||
returns: 'the Workspace and whether this call created it.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'rename\') rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue>',
|
||||
description: 'Rename one Workspace to a unique non-blank title.',
|
||||
parameters: [{ name: 'request', description: 'Workspace identity and proposed title.' }],
|
||||
returns: 'the updated Workspace projection.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'delete\') delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue>',
|
||||
description: 'Remove one Workspace registration while retaining files and Sessions.',
|
||||
parameters: [{ name: 'request', description: 'Workspace identity to remove.' }],
|
||||
returns: 'deletion confirmation.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'insertBefore\') insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue>',
|
||||
description: 'Move one Workspace within the registry display order.',
|
||||
parameters: [{ name: 'request', description: 'moved Workspace and optional anchor.' }],
|
||||
returns: 'the complete resulting Workspace order.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'insertSessionBefore\') insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue>',
|
||||
description: 'Move one accounted Session within a Workspace.',
|
||||
parameters: [{ name: 'request', description: 'Workspace, Session, and optional anchor identities.' }],
|
||||
returns: 'the updated Workspace projection.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote(\'archiveSession\') archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue>',
|
||||
description: 'Hide one known Session from Workspace grouping surfaces.',
|
||||
parameters: [{ name: 'request', description: 'Session identity to archive.' }],
|
||||
returns: 'the complete resulting archive set.',
|
||||
},
|
||||
{
|
||||
signature: '@Remote({ mode: \'stream\' }) follow(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame>',
|
||||
description: 'Stream a complete Workspace baseline followed by ordered increments.',
|
||||
parameters: [{ name: 'signal', description: 'generation cancellation.' }],
|
||||
returns: 'baseline followed by ordered Workspace increments.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'workspaceRegistry',
|
||||
summary: 'Durable workspace registry.',
|
||||
@@ -2520,13 +2683,53 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
description: '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.',
|
||||
parameters: [{ name: 'payload', description: '.signal - the current turn\'s explicit abort signal. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.' }],
|
||||
},
|
||||
{
|
||||
name: 'api-session/activity',
|
||||
mode: 'emit',
|
||||
signature: '\'api-session/activity\'(sessionId: SessionId, updatedAt: number): void',
|
||||
summary: 'One user-authored durable message advanced Session list activity.',
|
||||
description: 'One user-authored durable message advanced Session list activity.',
|
||||
parameters: [{ name: 'sessionId', description: 'addressed Session identity.' }, { name: 'updatedAt', description: 'durable message time used for list ordering.' }],
|
||||
},
|
||||
{
|
||||
name: 'api-session/added',
|
||||
mode: 'emit',
|
||||
signature: '\'api-session/added\'(summary: SessionSummary): void',
|
||||
summary: 'A Session became visible to Session list consumers.',
|
||||
description: 'A Session became visible to Session list consumers.',
|
||||
parameters: [{ name: 'summary', description: 'initial list row for the Session.' }],
|
||||
},
|
||||
{
|
||||
name: 'api-session/error',
|
||||
mode: 'emit',
|
||||
signature: '\'api-session/error\'(sessionId: SessionId, message: string): void',
|
||||
summary: 'One Agent failed outside a durable turn position.',
|
||||
description: 'One Agent failed outside a durable turn position.',
|
||||
parameters: [{ name: 'sessionId', description: 'Agent and Session identity.' }, { name: 'message', description: 'user-safe failure chain.' }],
|
||||
},
|
||||
{
|
||||
name: 'api-session/removed',
|
||||
mode: 'emit',
|
||||
signature: '\'api-session/removed\'(sessionId: SessionId): void',
|
||||
summary: 'A Session left the live Host registry.',
|
||||
description: 'A Session left the live Host registry.',
|
||||
parameters: [{ name: 'sessionId', description: 'removed Session identity.' }],
|
||||
},
|
||||
{
|
||||
name: 'api-session/status',
|
||||
mode: 'emit',
|
||||
signature: '\'api-session/status\'(sessionId: SessionId, running: boolean): void',
|
||||
summary: 'One Agent changed running state.',
|
||||
description: 'One Agent changed running state.',
|
||||
parameters: [{ name: 'sessionId', description: 'Agent and Session identity.' }, { name: 'running', description: 'whether the Agent is running.' }],
|
||||
},
|
||||
{
|
||||
name: 'approval/request',
|
||||
mode: 'waterfall',
|
||||
signature: '\'approval/request\'(this: Scoped<ApprovalService>, req: ApprovalRequest, next: () => Promise<ApprovalOutcome>): Promise<ApprovalOutcome>',
|
||||
signature: '\'approval/request\'( this: Scoped<Agent>, req: ApprovalRequestEvent, next: () => Promise<ApprovalOutcome>, ): Promise<ApprovalOutcome>',
|
||||
summary: 'Ask composed answerers for one decision.',
|
||||
description: '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.',
|
||||
parameters: [{ name: 'req', description: 'the pending decision (agent, tool identity, reason, signal).' }],
|
||||
description: 'Ask composed answerers for one decision. Return an outcome to claim the request or call `next()` to delegate. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.',
|
||||
parameters: [{ name: 'req', description: 'pending approval request.' }],
|
||||
},
|
||||
{
|
||||
name: 'authorization/settled',
|
||||
@@ -2824,6 +3027,14 @@ export const EVENT_API: readonly EventApiEntry[] = [
|
||||
description: 'Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`.',
|
||||
parameters: [{ name: 'exec', description: 'the execution object that traversed the pipeline.' }, { name: 'result', description: 'a deep-frozen snapshot of the final returned result.' }],
|
||||
},
|
||||
{
|
||||
name: 'user-questions/request',
|
||||
mode: 'waterfall',
|
||||
signature: '\'user-questions/request\'( this: Scoped<Agent>, request: AskUserQuestionRequestEvent, next: () => Promise<AskUserQuestionAnswer>, ): Promise<AskUserQuestionAnswer>',
|
||||
summary: 'Ask composed answerers for structured user input.',
|
||||
description: 'Ask composed answerers for structured user input. Return an answer to claim the request or call `next()` to delegate. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent.',
|
||||
parameters: [{ name: 'request', description: 'pending user-question request.' }],
|
||||
},
|
||||
{
|
||||
name: 'webserver/index-inject',
|
||||
mode: 'emit',
|
||||
@@ -2890,7 +3101,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly inbox: Inbox;\n readonly status: AgentStatus;\n readonly ctx: Context;\n cancel(cause: AgentCancelCause, options?: CancelOptions): void;\n whenIdle(): Promise<void>;\n runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T>;\n send(message: UserMessage, target: InboxTarget, wakeup: boolean): void;\n followup(message: UserMessage): void;\n steer(message: UserMessage): void;\n inject(message: UserMessage): void;\n}',
|
||||
declaration: 'export interface Agent {\n readonly id: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AgentCancelCause',
|
||||
@@ -2928,6 +3139,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ApiKeyRecord',
|
||||
declaration: 'export interface ApiKeyRecord {\n readonly kind: \'api-key\';\n readonly key?: string;\n readonly env?: Readonly<Record<string, string>>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ApiSessionAgentError',
|
||||
declaration: 'export type ApiSessionAgentError = Extract<SessionError, {\n readonly code: \'session-not-found\' | \'agent-busy\' | \'internal\';\n}>;',
|
||||
},
|
||||
{
|
||||
name: 'ApiSessionAgentResult',
|
||||
declaration: 'export type ApiSessionAgentResult = {\n readonly agent: Agent;\n} | {\n readonly error: ApiSessionAgentError;\n};',
|
||||
},
|
||||
{
|
||||
name: 'ApprovalOutcome',
|
||||
declaration: 'export type ApprovalOutcome = \'allowed-once\' | \'rejected\' | \'cancelled\' | \'unavailable\';',
|
||||
@@ -2938,11 +3157,11 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'ApprovalRequest',
|
||||
declaration: 'export interface ApprovalRequest {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: CallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}',
|
||||
declaration: 'export interface ApprovalRequest extends ApprovalRequestEvent {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: CallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ApprovalService',
|
||||
declaration: 'export class ApprovalService extends Service {\n static Config: z<Config>;\n constructor(ctx: Context, public config: Config);\n setPolicy(agent: Agent, policy: ApprovalPolicy): void;\n async request(req: ApprovalRequest): Promise<ApprovalOutcome>;\n overrideOf(session: Session): ApprovalPolicy | undefined;\n}',
|
||||
name: 'ApprovalRequestEvent',
|
||||
declaration: 'export interface ApprovalRequestEvent {\n readonly agent: Agent;\n readonly toolName: string;\n readonly callId?: CallId;\n readonly reason?: string;\n readonly signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AskUserQuestionAnswer',
|
||||
@@ -2968,6 +3187,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'AskUserQuestionRequest',
|
||||
declaration: 'export interface AskUserQuestionRequest {\n questions: AskUserQuestionItem[];\n agent?: Agent;\n signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AskUserQuestionRequestEvent',
|
||||
declaration: 'export interface AskUserQuestionRequestEvent {\n questions: AskUserQuestionItem[];\n agent: Agent;\n signal?: AbortSignal;\n}',
|
||||
},
|
||||
{
|
||||
name: 'AssembleContext',
|
||||
declaration: 'export interface AssembleContext {\n scope?: ScopeKey;\n signal?: AbortSignal;\n}',
|
||||
@@ -3060,14 +3283,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'Branded',
|
||||
declaration: 'export type Branded<B extends string> = string & {\n readonly [BRAND]: B;\n};',
|
||||
},
|
||||
{
|
||||
name: 'CancelOptions',
|
||||
declaration: 'export interface CancelOptions {\n keepInbox?: boolean | undefined;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ClientResponse',
|
||||
declaration: 'export interface ClientResponse {\n type: \'client-response\';\n rpcId: RpcId;\n result: RpcResult<unknown>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'CodeBindingErrorClass',
|
||||
declaration: 'export interface CodeBindingErrorClass {\n name: string;\n memberNameProperty: string;\n}',
|
||||
@@ -3528,18 +3743,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ImageVariantId',
|
||||
declaration: 'export type ImageVariantId = Branded<\'ImageVariantId\'>;',
|
||||
},
|
||||
{
|
||||
name: 'Inbox',
|
||||
declaration: 'export class Inbox {\n constructor(private readonly session: Session, private readonly notifications: InboxNotifications);\n get nextTurn(): readonly UserMessage[];\n get nextStep(): readonly UserMessage[];\n get hasPending(): boolean;\n clear(): void;\n claim(target: InboxTarget, turn: number): UserMessage[];\n append(target: InboxTarget, message: UserMessage): void;\n prepend(target: InboxTarget, message: UserMessage): void;\n replace(messageId: MessageId, newMessage: UserMessage): boolean;\n remove(messageId: MessageId): boolean;\n splice(target: InboxTarget, start: number, deleteCount: number, inserted: UserMessage[]): UserMessage[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'InboxNotifications',
|
||||
declaration: 'export interface InboxNotifications {\n inserted(message: UserMessage): void;\n discarded(message: UserMessage): void;\n claimed(message: UserMessage, turn: number): void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'InboxTarget',
|
||||
declaration: 'export type InboxTarget = \'next-turn\' | \'next-step\';',
|
||||
},
|
||||
{
|
||||
name: 'IndexInjection',
|
||||
declaration: 'export type IndexInjection = {\n kind: \'global\';\n name: string;\n value: unknown;\n} | {\n kind: \'script\';\n placement: IndexInjectionPlacement;\n text: string;\n} | {\n kind: \'script-src\';\n placement: IndexInjectionPlacement;\n src: string;\n} | {\n kind: \'style\';\n text: string;\n} | {\n kind: \'html\';\n placement: IndexInjectionPlacement;\n html: string;\n};',
|
||||
@@ -3558,7 +3761,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'InvocationDescriptor',
|
||||
declaration: 'export interface InvocationDescriptor {\n readonly id: string;\n readonly service: string;\n readonly namespace: string;\n readonly method: string;\n readonly implementation?: string;\n readonly invocation: {\n readonly kind: \'direct\';\n } | {\n readonly kind: \'context\';\n readonly context: string;\n readonly wire: string;\n readonly codec: TypertCodec;\n };\n readonly scope?: {\n readonly context: string;\n readonly wire: string;\n };\n readonly parameters: readonly InvocationParameterDescriptor[];\n readonly cancellation?: {\n readonly parameter: \'signal\';\n };\n readonly result: TypertCodec;\n readonly sourceLocation?: InvocationSourceLocation;\n}',
|
||||
declaration: 'export interface InvocationDescriptor {\n readonly id: string;\n readonly service: string;\n readonly namespace: string;\n readonly method: string;\n readonly implementation?: string;\n readonly mode?: \'stream\';\n readonly invocation: {\n readonly kind: \'direct\';\n } | {\n readonly kind: \'context\';\n readonly context: string;\n readonly wire: string;\n readonly codec: TypertCodec;\n };\n readonly scope?: {\n readonly context: string;\n readonly wire: string;\n };\n readonly parameters: readonly InvocationParameterDescriptor[];\n readonly cancellation?: {\n readonly parameter: \'signal\';\n };\n readonly result: TypertCodec;\n readonly sourceLocation?: InvocationSourceLocation;\n}',
|
||||
},
|
||||
{
|
||||
name: 'InvocationParameterDescriptor',
|
||||
@@ -3844,6 +4047,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'MessageSourceMap',
|
||||
declaration: 'export interface MessageSourceMap {\n user: {\n kind: \'user\';\n };\n plugin: {\n kind: \'plugin\';\n plugin: string;\n } & ContextFormed;\n model: ModelMessageSource;\n tool: ToolMessageSource;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelCatalogFailure',
|
||||
declaration: 'export interface ModelCatalogFailure {\n readonly id: string;\n readonly name: string;\n readonly message: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelCatalogModel',
|
||||
declaration: 'export interface ModelCatalogModel {\n readonly id: string;\n readonly name: string;\n readonly description?: string;\n readonly reasoning?: ModelReasoning;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelMessageSource',
|
||||
declaration: 'export interface ModelMessageSource extends AssistantProvenance {\n kind: \'model\';\n}',
|
||||
@@ -3856,6 +4067,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ModelModalityMap',
|
||||
declaration: 'export interface ModelModalityMap {\n text: \'text\';\n image: \'image\';\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelProviderGroup',
|
||||
declaration: 'export interface ModelProviderGroup {\n readonly id: string;\n readonly name: string;\n readonly models: readonly ModelCatalogModel[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelReasoning',
|
||||
declaration: 'export interface ModelReasoning {\n readonly efforts: readonly ModelReasoningEffort[];\n readonly defaultEffort?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ModelReasoningEffort',
|
||||
declaration: 'export interface ModelReasoningEffort {\n readonly id: string;\n readonly name: string;\n readonly description?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'ObjectJsonSchema',
|
||||
declaration: 'export type ObjectJsonSchema = JsonSchemaNode & {\n type: \'object\';\n};',
|
||||
@@ -3940,6 +4163,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'PromptAssembly',
|
||||
declaration: 'export interface PromptAssembly {\n sections: AssembledSection[];\n contexts: AssembledContext[];\n tools: ToolSchema[];\n variables: Record<string, string | undefined>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'PromptContentPart',
|
||||
declaration: 'export type PromptContentPart = {\n readonly type: \'text\';\n readonly text: string;\n} | {\n readonly type: \'image\';\n readonly mediaType: ImageMediaType;\n readonly data: string;\n readonly name?: string;\n};',
|
||||
},
|
||||
{
|
||||
name: 'PromptContext',
|
||||
declaration: 'export interface PromptContext {\n readonly name: string;\n readonly order: number;\n readonly text: string | ((context: AssembleContext) => string);\n}',
|
||||
@@ -4046,16 +4273,12 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
},
|
||||
{
|
||||
name: 'RpcErrorDetailsMap',
|
||||
declaration: 'export interface RpcErrorDetailsMap {\n \'bad-request\': {\n issues: ZodIssue[];\n };\n \'cancelled\': {};\n \'session-not-found\': {\n sessionId: SessionId;\n };\n \'model-unavailable\': {\n provider: string;\n model: string;\n };\n \'session-conflict\': {\n sessionId: SessionId;\n requestedCwd: string;\n existingCwd?: string;\n };\n \'invalid-time-zone\': {\n value: string;\n };\n \'workspace-attach-failed\': {\n sessionId: SessionId;\n workspaceId: string;\n };\n \'workspace-not-found\': {\n workspaceId: string;\n };\n \'workspace-invalid-path\': {\n path: string;\n };\n \'workspace-name-conflict\': {\n name: string;\n };\n \'workspace-move-invalid\': {\n workspaceId: string;\n sessionId: SessionId;\n beforeSessionId?: SessionId;\n };\n \'directory-unreadable\': {\n path: string;\n };\n \'directory-exists\': {\n path: string;\n };\n \'directory-create-failed\': {\n path: string;\n };\n \'directory-picker-unavailable\': {\n capability: string;\n };\n \'agent-preset-read-only\': {\n agentPreset: string;\n reason: string;\n };\n \'agent-preset-locked\': {\n sessionId: SessionId;\n agentPreset: string;\n };\n \'agent-preset-conflict\': {\n sessionId: SessionId;\n requestedPreset: string;\n existingPreset?: string;\n };\n \'agent-preset-not-found\': {\n agentPreset: string;\n /* …truncated — full shape in source */',
|
||||
declaration: 'export interface RpcErrorDetailsMap {\n \'bad-request\': {\n issues: ZodIssue[];\n };\n \'cancelled\': {};\n \'session-not-found\': {\n sessionId: SessionId;\n };\n \'invalid-time-zone\': {\n value: string;\n };\n \'directory-unreadable\': {\n path: string;\n };\n \'directory-exists\': {\n path: string;\n };\n \'directory-create-failed\': {\n path: string;\n };\n \'directory-picker-unavailable\': {\n capability: string;\n };\n \'agent-preset-read-only\': {\n agentPreset: string;\n reason: string;\n };\n \'agent-preset-locked\': {\n sessionId: SessionId;\n agentPreset: string;\n };\n \'agent-preset-not-found\': {\n agentPreset: string;\n available: readonly string[];\n };\n \'agent-preset-invalid\': {\n agentPreset: string;\n reason: string;\n };\n \'agent-busy\': {\n reason: string;\n };\n \'settings-rejected\': {\n ns: string;\n };\n \'settings-conflict\': {\n ns: string;\n expected: number;\n actual: number;\n };\n \'credential-rejected\': {\n ref: string;\n };\n \'model-discovery-failed\': {\n settingsNs: string;\n baseURL?: string;\n };\n \'subagent-parent-unavailable\': {\n parentSessionId: SessionId;\n };\n \'subagent-not-found\': {\n parentSessionId: SessionId;\n childSessionId: SessionId;\n };\n \'subagent-catalog-diagnostic\': {\n parentSessionId: SessionId;\n childS /* …truncated — full shape in source */',
|
||||
},
|
||||
{
|
||||
name: 'RpcId',
|
||||
declaration: 'export type RpcId = Branded<\'rpc-id\'>;',
|
||||
},
|
||||
{
|
||||
name: 'RpcReceipt',
|
||||
declaration: 'export type RpcReceipt = {\n accepted: true;\n} | {\n accepted: false;\n reason: \'not-pending\' | \'bad-response\';\n};',
|
||||
},
|
||||
{
|
||||
name: 'RpcResult',
|
||||
declaration: 'export type RpcResult<T> = {\n ok: true;\n value: T;\n} | {\n ok: false;\n error: RpcError;\n};',
|
||||
@@ -4140,14 +4363,62 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'ServerResponse',
|
||||
declaration: 'export interface ServerResponse {\n type: \'server-response\';\n rpcId: RpcId;\n result: RpcResult<unknown>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionAddress',
|
||||
declaration: 'export type SessionAddress = {\n readonly kind: \'session\';\n readonly sessionId: SessionId;\n} | {\n readonly kind: \'subagent\';\n readonly parentSessionId: SessionId;\n readonly childSessionId: SessionId;\n readonly mode: \'one-shot\' | \'continuable\';\n};',
|
||||
},
|
||||
{
|
||||
name: 'SessionAttachmentRequest',
|
||||
declaration: 'export interface SessionAttachmentRequest {\n readonly sessionId: SessionId;\n readonly attachmentId: AttachmentIdType;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionAttachmentValue',
|
||||
declaration: 'export interface SessionAttachmentValue {\n readonly attachment: ImageAttachmentRef;\n readonly data: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionAvailability',
|
||||
declaration: 'export type SessionAvailability = \'live\' | \'persisted\';',
|
||||
},
|
||||
{
|
||||
name: 'SessionCancelRequest',
|
||||
declaration: 'export interface SessionCancelRequest {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionCancelValue',
|
||||
declaration: 'export interface SessionCancelValue {\n readonly accepted: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionControlBaseline',
|
||||
declaration: 'export interface SessionControlBaseline {\n readonly queues: Readonly<Record<SessionId, readonly SessionQueuedItem[]>>;\n readonly jobs: Readonly<Record<SessionId, readonly SessionJob[]>>;\n readonly projections: Readonly<Record<SessionId, SessionProjectionsBlock>>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionControlFrame',
|
||||
declaration: 'export type SessionControlFrame = {\n readonly type: \'baseline\';\n readonly value: SessionControlBaseline;\n} | {\n readonly type: \'queue\';\n readonly sessionId: SessionId;\n readonly items: readonly SessionQueuedItem[];\n} | {\n readonly type: \'jobs\';\n readonly sessionId: SessionId;\n readonly jobs: readonly SessionJob[];\n} | ({\n readonly type: \'projection\';\n} & SessionProjectionUpdate);',
|
||||
},
|
||||
{
|
||||
name: 'SessionCreateRequest',
|
||||
declaration: 'export interface SessionCreateRequest {\n readonly workspaceId?: WorkspaceId;\n readonly cwd?: string;\n readonly sessionId?: SessionId;\n readonly agentPreset?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionCreateValue',
|
||||
declaration: 'export interface SessionCreateValue {\n readonly sessionId: SessionId;\n readonly agentPreset?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionError',
|
||||
declaration: 'export type SessionError = {\n [Code in keyof SessionErrorDetailsMap]: {\n readonly code: Code;\n readonly message: string;\n readonly details: SessionErrorDetailsMap[Code];\n };\n}[keyof SessionErrorDetailsMap];',
|
||||
},
|
||||
{
|
||||
name: 'SessionErrorDetailsMap',
|
||||
declaration: 'export interface SessionErrorDetailsMap {\n \'bad-request\': Record<never, never>;\n cancelled: Record<never, never>;\n \'session-not-found\': {\n readonly sessionId: SessionId;\n };\n \'model-unavailable\': {\n readonly provider: string;\n readonly model: string;\n };\n \'session-conflict\': {\n readonly sessionId: SessionId;\n readonly requestedCwd: string;\n readonly existingCwd?: string;\n };\n \'invalid-time-zone\': {\n readonly value: string;\n };\n \'workspace-attach-failed\': {\n readonly sessionId: SessionId;\n readonly workspaceId: string;\n };\n \'workspace-not-found\': {\n readonly workspaceId: string;\n };\n \'agent-preset-conflict\': {\n readonly sessionId: SessionId;\n readonly requestedPreset: string;\n readonly existingPreset?: string;\n };\n \'agent-preset-not-found\': {\n readonly agentPreset: string;\n readonly available: readonly string[];\n };\n \'agent-preset-invalid\': {\n readonly agentPreset: string;\n readonly reason: string;\n };\n \'agent-busy\': {\n readonly reason: string;\n };\n \'attachment-error\': {\n readonly reason: string;\n };\n \'queue-item-not-found\': {\n readonly itemId: MessageId;\n };\n \'steer-unavailable\': {\n readonly itemId: MessageId;\n };\n \'title-invalid\': {\n readonly sessionId: SessionId;\n };\n \'fork-unavailable\': {\n readonly sessionId: SessionId;\n /* …truncated — full shape in source */',
|
||||
},
|
||||
{
|
||||
name: 'SessionEvent',
|
||||
declaration: 'export type SessionEvent<T extends SessionEventType = SessionEventType> = {\n [K in SessionEventType]: {\n type: K;\n seq: number;\n time: number;\n data: SessionEventMap[K];\n ignorable?: true;\n } & (K extends SurfaceEventType ? {\n sourceEventSeqs?: number[];\n surfaceOp?: SurfaceOp;\n } : object);\n}[T];',
|
||||
},
|
||||
{
|
||||
name: 'SessionEventEntry',
|
||||
declaration: 'export interface SessionEventEntry {\n readonly event: SessionWireEvent;\n readonly view?: SessionToolView;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionEventMap',
|
||||
declaration: 'export interface SessionEventMap {\n \'turn/start\': {\n turn: number;\n };\n \'turn/end\': {\n turn: number;\n reason: TurnEndReason;\n };\n \'step/start\': {\n turn: number;\n step: number;\n };\n \'step/end\': {\n turn: number;\n step: number;\n };\n \'user/message\': UserMessage;\n \'assistant/chunk\': {\n turn: number;\n step: number;\n chunk: StreamChunk;\n };\n \'assistant/message\': {\n turn: number;\n step: number;\n message: AssistantMessage;\n usage?: TokenUsage;\n interrupted?: true;\n };\n \'tool/call\': {\n turn: number;\n step: number;\n callId: CallId;\n name: string;\n arguments: string;\n };\n \'tool/result\': {\n turn: number;\n step: number;\n message: ToolResultMessage;\n error?: {\n name: string;\n code: string;\n };\n meta?: JsonValue;\n };\n \'request/header\': {\n header: EpochHeader;\n reason: RequestHeaderReason;\n };\n \'request/context\': RequestContext;\n \'session/end-seed\': Record<string, never>;\n}',
|
||||
@@ -4208,10 +4479,26 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionEventWindow',
|
||||
declaration: 'export interface SessionEventWindow {\n session: SessionHeader;\n target: SessionEvent;\n events: SessionEvent[];\n startSeq: number;\n endSeq: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionFollowFrame',
|
||||
declaration: 'export type SessionFollowFrame = {\n readonly type: \'opened\';\n readonly cursor: number;\n} | ({\n readonly type: \'event\';\n} & SessionEventEntry);',
|
||||
},
|
||||
{
|
||||
name: 'SessionFollowRequest',
|
||||
declaration: 'export interface SessionFollowRequest {\n readonly address: SessionAddress;\n readonly afterSeq?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionForkRequest',
|
||||
declaration: 'export interface SessionForkRequest {\n readonly sessionId: SessionId;\n readonly atSeq?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionForkSource',
|
||||
declaration: 'export type SessionForkSource = Session | SessionId;',
|
||||
},
|
||||
{
|
||||
name: 'SessionForkValue',
|
||||
declaration: 'export interface SessionForkValue {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionHeader',
|
||||
declaration: 'export interface SessionHeader {\n readonly version: number;\n readonly id: SessionId;\n readonly createdAt: number;\n readonly cwd?: string;\n readonly parentSession?: SessionId;\n readonly seedLength?: number;\n readonly origin?: \'subagent\';\n readonly delegationDepth?: number;\n readonly agentPreset?: string;\n}',
|
||||
@@ -4224,6 +4511,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionInspection',
|
||||
declaration: 'export interface SessionInspection {\n readonly meta: SessionHeader;\n readonly events: readonly SessionEvent[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionJob',
|
||||
declaration: 'export interface SessionJob {\n readonly id: JobId;\n readonly kind: string;\n readonly label: string;\n readonly status: \'running\' | \'stopping\' | \'completed\' | \'killed\' | \'failed\';\n readonly detail?: string;\n readonly startedAt: number;\n readonly finishedAt?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionLineageNode',
|
||||
declaration: 'export interface SessionLineageNode {\n session: SessionRecord;\n descendants: SessionLineageNode[];\n}',
|
||||
@@ -4232,6 +4523,14 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionLineageTrace',
|
||||
declaration: 'export type SessionLineageTrace = {\n target: SessionRecord;\n ancestors: SessionRecord[];\n descendants: SessionLineageNode[];\n} & ({\n complete: true;\n root: SessionRecord;\n} | {\n complete: false;\n unresolvedParentId: SessionId;\n});',
|
||||
},
|
||||
{
|
||||
name: 'SessionListRequest',
|
||||
declaration: 'export interface SessionListRequest {\n readonly cursor?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionListValue',
|
||||
declaration: 'export interface SessionListValue {\n readonly items: readonly SessionSummary[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionLocation',
|
||||
declaration: 'export interface SessionLocation {\n readonly kind: string;\n readonly path: string;\n}',
|
||||
@@ -4240,6 +4539,22 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionLogSnapshot',
|
||||
declaration: 'export interface SessionLogSnapshot {\n session: SessionHeader;\n events: SessionEvent[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionModels',
|
||||
declaration: 'export interface SessionModels {\n readonly current: ModelSelection;\n readonly routable: boolean;\n readonly groups: readonly ModelProviderGroup[];\n readonly failures: readonly ModelCatalogFailure[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionModelsRequest',
|
||||
declaration: 'export interface SessionModelsRequest {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionPage',
|
||||
declaration: 'export interface SessionPage {\n readonly events: readonly SessionEventEntry[];\n readonly hasMore: boolean;\n readonly projections?: SessionProjectionsBlock;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionPageRequest',
|
||||
declaration: 'export interface SessionPageRequest {\n readonly address: SessionAddress;\n readonly throughSeq: number;\n readonly beforeSeq?: number;\n readonly maxMessages?: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionPersistenceRevision',
|
||||
declaration: 'export type SessionPersistenceRevision = Branded<\'SessionPersistenceRevision\'>;',
|
||||
@@ -4260,10 +4575,38 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionProjectionMap',
|
||||
declaration: 'export interface SessionProjectionMap {\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionsBlock',
|
||||
declaration: 'export interface SessionProjectionsBlock {\n readonly asOfSeq: number;\n readonly values: SessionProjectionValues;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionStateMap',
|
||||
declaration: 'export interface SessionProjectionStateMap {\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionUpdate',
|
||||
declaration: 'export interface SessionProjectionUpdate {\n readonly sessionId: SessionId;\n readonly key: string;\n readonly value: JsonValue;\n readonly seq: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionValue',
|
||||
declaration: 'export type SessionProjectionValue = JsonValue;',
|
||||
},
|
||||
{
|
||||
name: 'SessionProjectionValues',
|
||||
declaration: 'export type SessionProjectionValues = Partial<SessionProjectionMap> & Readonly<Record<string, SessionProjectionValue>>;',
|
||||
},
|
||||
{
|
||||
name: 'SessionPromptRequest',
|
||||
declaration: 'export interface SessionPromptRequest {\n readonly requestId: SessionRequestId;\n readonly sessionId: SessionId;\n readonly mode: \'queue\' | \'steer\';\n readonly content: readonly PromptContentPart[];\n readonly clientTimeZone?: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionPromptValue',
|
||||
declaration: 'export interface SessionPromptValue {\n readonly accepted: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionQueuedItem',
|
||||
declaration: 'export interface SessionQueuedItem {\n readonly id: MessageId;\n readonly placement: \'queued\' | \'steering\' | \'context\';\n readonly message: {\n readonly id: MessageId;\n readonly content: readonly JsonValue[];\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionRawArtifact',
|
||||
declaration: 'export interface SessionRawArtifact {\n readonly meta: SessionHeader;\n readonly filename: string;\n readonly content: string;\n}',
|
||||
@@ -4284,6 +4627,18 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionReferenceMentionCandidate',
|
||||
declaration: 'export interface SessionReferenceMentionCandidate extends SessionReferenceCandidate {\n mention: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionRenameRequest',
|
||||
declaration: 'export interface SessionRenameRequest {\n readonly sessionId: SessionId;\n readonly title: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionRenameValue',
|
||||
declaration: 'export interface SessionRenameValue {\n readonly title: string;\n readonly seq: number;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionRequestId',
|
||||
declaration: 'export type SessionRequestId = Branded<\'session-request-id\'>;',
|
||||
},
|
||||
{
|
||||
name: 'SessionResultFilter',
|
||||
declaration: 'export type SessionResultFilter = {\n kind: \'id\';\n values: readonly SessionId[];\n} | {\n kind: \'cwd\';\n values: readonly (string | null)[];\n} | ({\n kind: \'created-at\';\n} & SessionResultRange) | {\n kind: \'parent\';\n values: readonly (SessionId | null)[];\n} | {\n kind: \'availability\';\n values: readonly SessionAvailability[];\n};',
|
||||
@@ -4304,13 +4659,25 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionSearchHit',
|
||||
declaration: 'export interface SessionSearchHit extends SessionRecord {\n bestMatch: SessionEventSearchHit;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionSearchItem',
|
||||
declaration: 'export interface SessionSearchItem {\n readonly sessionId: SessionId;\n readonly snippet: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionSearchPage',
|
||||
declaration: 'export interface SessionSearchPage<T> {\n items: readonly T[];\n nextCursor?: SessionSearchCursor;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionSearchRequest',
|
||||
declaration: 'export interface SessionSearchRequest {\n query: string;\n sessionFilters?: readonly SessionResultFilter[];\n eventFilters?: readonly SessionEventMetadataFilter[];\n limit?: number;\n cursor?: SessionSearchCursor;\n}',
|
||||
name: 'SessionSearchValue',
|
||||
declaration: 'export interface SessionSearchValue {\n readonly items: readonly SessionSearchItem[];\n readonly hasMore: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionSelectModelRequest',
|
||||
declaration: 'export interface SessionSelectModelRequest extends ModelSelection {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionSelectModelValue',
|
||||
declaration: 'export interface SessionSelectModelValue {\n readonly selected: ModelSelection;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionStartSource',
|
||||
@@ -4380,6 +4747,26 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'SessionTitleUserMessage',
|
||||
declaration: 'export interface SessionTitleUserMessage {\n readonly seq: number;\n readonly text: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionToolCallView',
|
||||
declaration: 'export type SessionToolCallView = (Omit<GenericCallView, \'rawInput\'> & {\n readonly rawInput?: JsonValue;\n}) | TerminalCallView | DiffCallView;',
|
||||
},
|
||||
{
|
||||
name: 'SessionToolView',
|
||||
declaration: 'export type SessionToolView = {\n readonly for: \'call\';\n readonly view: SessionToolCallView;\n} | {\n readonly for: \'result\';\n readonly view: ToolResultView;\n};',
|
||||
},
|
||||
{
|
||||
name: 'SessionUpdateQueueRequest',
|
||||
declaration: 'export interface SessionUpdateQueueRequest {\n readonly sessionId: SessionId;\n readonly itemId: MessageId;\n readonly action: QueueAction;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionUpdateQueueValue',
|
||||
declaration: 'export interface SessionUpdateQueueValue {\n readonly accepted: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SessionWireEvent',
|
||||
declaration: 'export interface SessionWireEvent {\n readonly type: string;\n readonly seq: number;\n readonly time: number;\n readonly data: JsonValue;\n readonly ignorable?: true;\n readonly sourceEventSeqs?: number[];\n readonly surfaceOp?: SurfaceOp;\n}',
|
||||
},
|
||||
{
|
||||
name: 'SettingsApplies',
|
||||
declaration: 'export type SettingsApplies = \'live\' | \'restart\';',
|
||||
@@ -4952,6 +5339,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'TypertEventModel',
|
||||
declaration: 'export interface TypertEventModel extends TypertDocumentation {\n readonly name: string;\n readonly mode?: string;\n readonly signature: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertGatewayWireStream',
|
||||
declaration: 'export interface TypertGatewayWireStream {\n readonly open: (endpoint: string, payload: unknown, signal: AbortSignal) => Promise<AsyncIterable<unknown>>;\n readonly failure: (error: unknown) => {\n readonly code: string;\n readonly message: string;\n readonly details: object;\n };\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertMemberModel',
|
||||
declaration: 'export interface TypertMemberModel {\n readonly kind: \'property\' | \'method\' | \'getter\' | \'setter\' | \'call\' | \'construct\' | \'index\';\n readonly name: string;\n readonly signature: string;\n readonly summary?: string;\n readonly jsDoc?: string;\n}',
|
||||
@@ -4972,6 +5363,30 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'TypertPackageRecord',
|
||||
declaration: 'export interface TypertPackageRecord {\n readonly package: string;\n readonly face: TypertFace;\n readonly key: string;\n readonly model: TypertPackageModel;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventContext',
|
||||
declaration: 'export interface TypertRemoteEventContext {\n readonly value: Context;\n readonly subject: object;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventDispatch',
|
||||
declaration: 'export type TypertRemoteEventDispatch = TypertRemoteEventFrame | TypertRemoteEventInvocation;',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventFrame',
|
||||
declaration: 'export interface TypertRemoteEventFrame {\n readonly event: string;\n readonly args: readonly unknown[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventInvocation',
|
||||
declaration: 'export interface TypertRemoteEventInvocation {\n readonly event: string;\n readonly request: object;\n readonly context: TypertRemoteEventContext;\n readonly resolve: (outcome: TypertRemoteEventOutcome) => void;\n readonly reject: (reason: unknown) => void;\n}',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventOutcome',
|
||||
declaration: 'export type TypertRemoteEventOutcome = {\n readonly kind: \'result\';\n readonly value: unknown;\n} | {\n readonly kind: \'next\';\n};',
|
||||
},
|
||||
{
|
||||
name: 'TypertRemoteEventSource',
|
||||
declaration: 'export type TypertRemoteEventSource = (signal: AbortSignal) => AsyncIterable<TypertRemoteEventDispatch>;',
|
||||
},
|
||||
{
|
||||
name: 'TypertSchemaFilter',
|
||||
declaration: 'export interface TypertSchemaFilter {\n readonly package?: string;\n readonly face?: TypertFace;\n}',
|
||||
@@ -5152,6 +5567,70 @@ export const TYPE_API: readonly TypeApiEntry[] = [
|
||||
name: 'WorkflowStopReason',
|
||||
declaration: 'export type WorkflowStopReason = \'completed\' | \'cancelled\' | \'error\';',
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
declaration: 'export interface Workspace {\n readonly id: WorkspaceId;\n readonly path: string;\n readonly title: string;\n readonly createdAt: string;\n readonly updatedAt: string;\n readonly sessionIds: readonly SessionId[];\n setTitle(title: string): Promise<void>;\n attachSession(sessionId: SessionId): Promise<void>;\n insertSessionBefore(sessionId: SessionId, beforeSessionId?: SessionId): Promise<void>;\n detachSession(sessionId: SessionId): Promise<void>;\n status(): Promise<\'ok\' | \'missing-dir\'>;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceArchiveSessionRequest',
|
||||
declaration: 'export interface WorkspaceArchiveSessionRequest {\n readonly sessionId: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceArchiveValue',
|
||||
declaration: 'export interface WorkspaceArchiveValue {\n readonly archivedSessionIds: readonly SessionId[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceBaseline',
|
||||
declaration: 'export interface WorkspaceBaseline {\n readonly items: readonly WorkspaceView[];\n readonly archivedSessionIds: readonly SessionId[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceCreateRequest',
|
||||
declaration: 'export interface WorkspaceCreateRequest {\n readonly path: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceCreateValue',
|
||||
declaration: 'export interface WorkspaceCreateValue {\n readonly workspace: WorkspaceView;\n readonly created: boolean;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceDeleteRequest',
|
||||
declaration: 'export interface WorkspaceDeleteRequest {\n readonly workspaceId: WorkspaceId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceDeleteValue',
|
||||
declaration: 'export interface WorkspaceDeleteValue {\n readonly deleted: true;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceFollowFrame',
|
||||
declaration: 'export type WorkspaceFollowFrame = {\n readonly type: \'baseline\';\n readonly value: WorkspaceBaseline;\n} | WorkspaceFollowIncrement;',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceFollowIncrement',
|
||||
declaration: 'export type WorkspaceFollowIncrement = {\n readonly type: \'upsert\';\n readonly workspace: WorkspaceView;\n} | {\n readonly type: \'remove\';\n readonly workspaceId: WorkspaceId;\n} | {\n readonly type: \'order\';\n readonly workspaceIds: readonly WorkspaceId[];\n} | {\n readonly type: \'archived\';\n readonly archivedSessionIds: readonly SessionId[];\n};',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceInsertBeforeRequest',
|
||||
declaration: 'export interface WorkspaceInsertBeforeRequest {\n readonly workspaceId: WorkspaceId;\n readonly beforeWorkspaceId?: WorkspaceId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceInsertSessionBeforeRequest',
|
||||
declaration: 'export interface WorkspaceInsertSessionBeforeRequest {\n readonly workspaceId: WorkspaceId;\n readonly sessionId: SessionId;\n readonly beforeSessionId?: SessionId;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceOrderValue',
|
||||
declaration: 'export interface WorkspaceOrderValue {\n readonly workspaceIds: readonly WorkspaceId[];\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceRenameRequest',
|
||||
declaration: 'export interface WorkspaceRenameRequest {\n readonly workspaceId: WorkspaceId;\n readonly title: string;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceValue',
|
||||
declaration: 'export interface WorkspaceValue {\n readonly workspace: WorkspaceView;\n}',
|
||||
},
|
||||
{
|
||||
name: 'WorkspaceView',
|
||||
declaration: 'export interface WorkspaceView {\n readonly workspaceId: WorkspaceId;\n readonly path: string;\n readonly title: string;\n readonly sessionIds: readonly SessionId[];\n readonly createdAt: string;\n readonly updatedAt: string;\n}',
|
||||
},
|
||||
]
|
||||
|
||||
/** The inherited `ctx` API (cordis core + loader/hmr/timer), in curated order. */
|
||||
|
||||
@@ -116,6 +116,7 @@ export const SERVICE_PAGE: Record<string, string> = {
|
||||
workflowEngine: 'workflow.md',
|
||||
webhookRuntime: 'webhook.md',
|
||||
workspaceRegistry: 'workspace.md',
|
||||
workspaceController: 'workspace.md',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,6 +196,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
|
||||
'system-prompt': 'system-prompt.md',
|
||||
'session-telemetry': 'session-telemetry.md',
|
||||
'tools': 'tools.md',
|
||||
'user-questions': 'user-questions.md',
|
||||
'webserver': 'web-server.md',
|
||||
'workflow': 'workflow.md',
|
||||
}
|
||||
@@ -328,7 +330,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ApprovalOutcome: 'approval.md',
|
||||
ApprovalPolicy: 'approval.md',
|
||||
ApprovalRequest: 'approval.md',
|
||||
ApprovalRequestEvent: 'approval.md',
|
||||
ApprovalService: 'approval.md',
|
||||
AskUserQuestionRequestEvent: 'user-questions.md',
|
||||
EncodedImageAttachment: 'attachment.md',
|
||||
ImageAttachmentRef: 'attachment.md',
|
||||
ImageRequestPolicy: 'attachment.md',
|
||||
@@ -553,7 +557,19 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
DomainChanged: 'storage.md',
|
||||
DomainFacility: 'storage.md',
|
||||
Workspace: 'workspace.md',
|
||||
WorkspaceArchiveSessionRequest: 'workspace.md',
|
||||
WorkspaceArchiveValue: 'workspace.md',
|
||||
WorkspaceCreateRequest: 'workspace.md',
|
||||
WorkspaceCreateValue: 'workspace.md',
|
||||
WorkspaceDeleteRequest: 'workspace.md',
|
||||
WorkspaceDeleteValue: 'workspace.md',
|
||||
WorkspaceFollowFrame: 'workspace.md',
|
||||
WorkspaceId: 'workspace.md',
|
||||
WorkspaceInsertBeforeRequest: 'workspace.md',
|
||||
WorkspaceInsertSessionBeforeRequest: 'workspace.md',
|
||||
WorkspaceOrderValue: 'workspace.md',
|
||||
WorkspaceRenameRequest: 'workspace.md',
|
||||
WorkspaceValue: 'workspace.md',
|
||||
WebBootGraph: 'client-modules.md',
|
||||
SessionTelemetryRecord: 'session-telemetry.md',
|
||||
WorkflowRunInfo: 'workflow.md',
|
||||
@@ -566,6 +582,7 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
|
||||
ProjectionCheckpoint: 'session-projection.md',
|
||||
DirectoryPickerCapability: 'workspace.md',
|
||||
TypertContribution: 'invariants.md',
|
||||
TypertRemoteEventSource: 'typert.md',
|
||||
TypertFace: 'invariants.md',
|
||||
TypertPackageFilter: 'invariants.md',
|
||||
TypertPackageRecord: 'invariants.md',
|
||||
|
||||
@@ -157,6 +157,13 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['apiproxy'],
|
||||
note: 'Owns Session commands, cold reads, durable-event following, live control state, and Agent activation policy; apiProxy reuses its inspection and Agent-resolution operations for Session-aware domains.',
|
||||
},
|
||||
{
|
||||
key: 'workspaceController',
|
||||
pkg: 'api-workspace-controller',
|
||||
title: 'Host Workspace Remote controller',
|
||||
mode: 'core',
|
||||
note: 'Owns Workspace commands and reconnect-safe Workspace state delivery through the generated Remote namespace.',
|
||||
},
|
||||
{
|
||||
key: 'invariants',
|
||||
pkg: 'invariants',
|
||||
|
||||
@@ -129,7 +129,13 @@
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
"symbol": "Agent",
|
||||
"source": "packages/core/agent/src/runtime-types.ts"
|
||||
"source": "packages/core/agent/src/types.ts",
|
||||
"augmentations": [
|
||||
{
|
||||
"source": "packages/core/agent/src/runtime-types.ts",
|
||||
"module": "./types.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"doc": "docs/subsystems/core.md",
|
||||
|
||||
@@ -156,6 +156,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/test-support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/api/gateway': { kind: 'none', reason: 'Remote dispatch infrastructure; invoked business methods own any model-visible effect.' },
|
||||
'packages/api/session-controller': { kind: 'none', reason: 'Session API and transport owner; invoked Agent commands own any model-visible effect.' },
|
||||
'packages/api/workspace-controller': { kind: 'none', reason: 'Workspace API and state projection owner; it registers no prompt, tool, or session event.' },
|
||||
'packages/typert/protocol': { kind: 'none', reason: 'Compiler-independent Remote protocol declarations; registers nothing model-facing.' },
|
||||
'packages/typert/generator': { kind: 'none', reason: 'The build-time generator runs outside any agent runtime and touches no model request.' },
|
||||
'packages/jobs/jobs': { kind: 'indirect', reason: 'Producer and controller plugins own all model rendering over the job registry.' },
|
||||
|
||||
@@ -28,6 +28,13 @@ interface ManifestEntry {
|
||||
symbol: string
|
||||
/** Source file (repo-relative) that exports the symbol. */
|
||||
source: string
|
||||
/** Explicit module augmentations whose members complete an interface. */
|
||||
augmentations?: Array<{
|
||||
/** Source file (repo-relative) containing the augmentation. */
|
||||
source: string
|
||||
/** String-literal module specifier containing the merged interface. */
|
||||
module: string
|
||||
}>
|
||||
/** Complete declaration (default), or a body-stripped public class API. */
|
||||
projection?: 'public-api'
|
||||
}
|
||||
@@ -133,6 +140,67 @@ function sourceDeclaration(sourceRel: string, symbol: string): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
interface InterfacePart {
|
||||
text: string
|
||||
sourceFile: ts.SourceFile
|
||||
declaration: ts.InterfaceDeclaration
|
||||
}
|
||||
|
||||
/** Find one top-level interface declaration in a source file. */
|
||||
function sourceInterface(sourceRel: string, symbol: string): InterfacePart | null {
|
||||
const abs = resolve(root, sourceRel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
|
||||
const declaration = sourceFile.statements.find((statement): statement is ts.InterfaceDeclaration =>
|
||||
ts.isInterfaceDeclaration(statement) && statement.name.text === symbol,
|
||||
)
|
||||
return declaration === undefined ? null : { text, sourceFile, declaration }
|
||||
}
|
||||
|
||||
/** Find one interface declaration inside an explicit string-literal module augmentation. */
|
||||
function augmentedInterface(sourceRel: string, moduleName: string, symbol: string): InterfacePart | null {
|
||||
const abs = resolve(root, sourceRel)
|
||||
const text = readFileSync(abs, 'utf8')
|
||||
const sourceFile = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, /* setParentNodes */ true)
|
||||
for (const statement of sourceFile.statements) {
|
||||
if (!ts.isModuleDeclaration(statement) || !ts.isStringLiteral(statement.name)
|
||||
|| statement.name.text !== moduleName || !statement.body || !ts.isModuleBlock(statement.body)) continue
|
||||
const declaration = statement.body.statements.find((member): member is ts.InterfaceDeclaration =>
|
||||
ts.isInterfaceDeclaration(member) && member.name.text === symbol,
|
||||
)
|
||||
if (declaration !== undefined) return { text, sourceFile, declaration }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Render one interface plus explicitly named module augmentations as its merged declaration. */
|
||||
function mergedInterfaceDeclaration(entry: ManifestEntry): string | null {
|
||||
const base = sourceInterface(entry.source, entry.symbol)
|
||||
if (base === null) return null
|
||||
const additions: InterfacePart[] = []
|
||||
for (const augmentation of entry.augmentations ?? []) {
|
||||
const part = augmentedInterface(augmentation.source, augmentation.module, entry.symbol)
|
||||
if (part === null) return null
|
||||
additions.push(part)
|
||||
}
|
||||
|
||||
const parts = [base, ...additions]
|
||||
const docs = parts.map(part => sourceJSDoc(part.text, part.declaration)).filter(Boolean)
|
||||
const typeParameters = base.declaration.typeParameters
|
||||
?.map(parameter => parameter.getText(base.sourceFile)).join(', ')
|
||||
const heritage = parts.flatMap(part =>
|
||||
part.declaration.heritageClauses?.map(clause => clause.getText(part.sourceFile)) ?? [],
|
||||
).join(' ')
|
||||
const header = `interface ${entry.symbol}${typeParameters ? `<${typeParameters}>` : ''}${heritage ? ` ${heritage}` : ''} {`
|
||||
const members = parts.flatMap(part => part.declaration.members.map((member) => {
|
||||
const jsDoc = sourceJSDoc(part.text, member)
|
||||
const declaration = part.text.slice(member.getStart(part.sourceFile), member.getEnd())
|
||||
return jsDoc === '' ? declaration : `${jsDoc}\n${declaration}`
|
||||
}))
|
||||
const declaration = [header, ...members.map(member => member.split('\n').map(line => ` ${line}`).join('\n')), '}'].join('\n')
|
||||
return docs.length === 0 ? declaration : `${docs.join('\n')}\n${declaration}`
|
||||
}
|
||||
|
||||
/** Leading source JSDoc attached to one declaration or member. */
|
||||
function sourceJSDoc(text: string, node: ts.Node): string {
|
||||
return ts.getJSDocCommentsAndTags(node)
|
||||
@@ -270,7 +338,9 @@ let verified = 0
|
||||
for (const e of entries) {
|
||||
const b = blockByKey.get(keyOf(e))
|
||||
if (!b) continue // already reported as an orphan entry
|
||||
const decl = e.projection === 'public-api'
|
||||
const decl = e.augmentations !== undefined
|
||||
? mergedInterfaceDeclaration(e)
|
||||
: e.projection === 'public-api'
|
||||
? sourcePublicApi(e.source, e.symbol)
|
||||
: sourceDeclaration(e.source, e.symbol)
|
||||
if (decl === null) {
|
||||
|
||||
Reference in New Issue
Block a user