@orkestrel/scaffold 0.0.104 → 0.0.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/bin/main.js +8 -8
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/orchestration.md +3 -3
  4. package/dist/host/agents/skills/orkestrel-parallel/SKILL.md +72 -0
  5. package/dist/host/agents/skills/orkestrel-parallel/agents/openai.yaml +4 -0
  6. package/dist/host/agents/skills/orkestrel-parallel/references/harnesses.md +12 -0
  7. package/dist/host/claude/agents/orkestrel.md +7 -7
  8. package/dist/host/claude/agents/verifier.md +1 -1
  9. package/dist/host/claude/skills/orkestrel-parallel/SKILL.md +11 -0
  10. package/dist/host/codex/agents/verifier.toml +3 -1
  11. package/dist/host/guides/agent.md +102 -57
  12. package/dist/host/guides/form.md +8 -8
  13. package/dist/host/guides/ollama.md +275 -18
  14. package/dist/host/guides/scaffold.md +38 -38
  15. package/dist/host/guides/study.md +589 -39
  16. package/dist/host/guides/table.md +14 -14
  17. package/dist/host/guides/test.md +94 -67
  18. package/dist/host/manifest.json +42 -14
  19. package/dist/host/tests/policy.test.ts +2 -2
  20. package/dist/host/tests/setupPolicy.ts +1 -1
  21. package/dist/src/core/index.cjs +13 -12
  22. package/dist/src/core/index.cjs.map +1 -1
  23. package/dist/src/core/index.d.cts +6 -6
  24. package/dist/src/core/index.d.ts +6 -6
  25. package/dist/src/core/index.js +13 -12
  26. package/dist/src/core/index.js.map +1 -1
  27. package/dist/src/server/index.cjs +7 -7
  28. package/dist/src/server/index.cjs.map +1 -1
  29. package/dist/src/server/index.d.cts +10 -10
  30. package/dist/src/server/index.d.ts +10 -10
  31. package/dist/src/server/index.js +7 -7
  32. package/dist/src/server/index.js.map +1 -1
  33. package/package.json +8 -8
@@ -3,15 +3,15 @@
3
3
  > The Ollama chat provider wire on the `/api/chat` path and the raw judge wire for Mica
4
4
  > on the `/api/generate` path, with shared engines from the `@orkestrel/agent` package.
5
5
 
6
- The package supplies a chat provider wire on the `/api/chat` path and a raw judge wire for Mica on the `/api/generate` path. The `OllamaProvider` class extends the `AgentProvider` engine for chat generation and streaming. The `OllamaJudge` class extends the `AgentJudge` engine for calibrated candidate probabilities. The shared engines own deadlines, transport, headers, and result assembly; see [Agent](agent.md) for their contracts.
6
+ The package supplies a chat provider wire on the `/api/chat` path and a raw judge wire for Mica on the `/api/generate` path. The `OllamaProvider` class extends the `AgentProvider` engine for chat generation and streaming. The `OllamaJudge` class extends the `AgentJudge` engine for calibrated candidate probabilities. The shared engines own deadlines, transport, headers, and result assembly; see [Agent](agent.md) for their contracts. The server face, `@orkestrel/ollama/server`, owns a dedicated daemon session in Node: it starts a declared Ollama executable on a dedicated loopback port, verifies the executable's version and every declared model digest, unloads the declared models, and stops the process tree it started; see [Daemon session](#daemon-session).
7
7
 
8
8
  The design is deliberately spare — an external boundary kept honest. Every `unknown` wire value is narrowed through the `@orkestrel/contract` guards (`isRecord`, `isString`, `isNumber`) rather than a type assertion, and a missing or malformed field degrades to a sensible default (empty content, no usage, `{}` arguments) rather than a throw. Every provider call streams: the request always carries `stream: true`, and `generate` drains the same NDJSON path `stream` exposes, so neither call can report content the other would not. The wire `think` flag is configurable through `OllamaOptions.think` (default `false`) and overrideable per call through `ProviderStreamOptions.think`; with `think: true` the daemon returns reasoning on the separate `message.thinking` channel, streamed live as `thinking` deltas. Either way the base's splitter separates any `<think>` span the daemon inlines anyway, so the assembled `content` is clean and the reasoning surfaces as `ProviderResult.thinking`. The agent records that reasoning on the assistant message. The provider's `replay` policy filters stored reasoning at the provider input boundary; the Ollama request mapper omits that reasoning from the wire. A per-call `ProviderStreamOptions.schema` (a JSON schema object, from `@orkestrel/agent`) forwards verbatim as the wire's structured-output `format` field, and is omitted from the request when no schema is supplied. `OllamaOptions.truncate` and `OllamaOptions.shift` are opt-in context controls: each is sent as a top-level wire key only when set, so omission delegates to the daemon default and leaves the request body unchanged, and `false` requests refusal instead of truncation or shifting. A fixture test proves the encoding and makes no claim about daemon support. Token usage reuses the `TokenUsage` shape rather than minting its own.
9
9
 
10
- The dependency is strictly one-way: this surface imports the base and the contract types from `@orkestrel/agent` and reaches errors through that base, the parser factory from `@orkestrel/ndjson`, tool-call shapes from `@orkestrel/tool`, the usage shape from `@orkestrel/budget`, and the guards from `@orkestrel/contract` — those packages never import from here. The published face is core, so `src/core` reaches no `node:*` module and no DOM global and the same build serves a server process and a browser page. It is tested live against `qwen3.5:2b-q4_K_M` in a dedicated `service` test project that requires the daemon and warms the model first, with no `skipIf`, while the `src:core` project stays hermetic on canned-transport and recording-proxy assertions that pass with the daemon down. Source: [`src/core`](../src/core). Surfaced through the `@orkestrel/ollama` barrel, aliased `@src/core` inside this repo.
10
+ The dependency is strictly one-way: this surface imports the base and the contract types from `@orkestrel/agent` and reaches errors through that base, the parser factory from `@orkestrel/ndjson`, tool-call shapes from `@orkestrel/tool`, the usage shape from `@orkestrel/budget`, and the guards from `@orkestrel/contract` — those packages never import from here. The package publishes two faces. The core face, `@orkestrel/ollama`, is built from `src/core`, which reaches no `node:*` module and no DOM global, so the same build serves a server process and a browser page. The server face, `@orkestrel/ollama/server`, is built from `src/server` and runs in Node only: it imports Node modules, the process session from `@orkestrel/process`, the emitter from `@orkestrel/emitter`, the guards from `@orkestrel/contract`, and the terminal timing reader from the core face, and nothing in `src/core` imports it. The core face is tested live against `qwen3.5:2b-q4_K_M` in a dedicated `service` test project that requires the daemon and warms the model first, with no `skipIf`, while the `src:core` project stays hermetic on canned-transport and recording-proxy assertions that pass with the daemon down. Source: [`src/core`](../src/core), surfaced through the `@orkestrel/ollama` barrel and aliased `@src/core` inside this repo, and [`src/server`](../src/server), surfaced through the `@orkestrel/ollama/server` barrel and aliased `@src/server`.
11
11
 
12
12
  ## Surface
13
13
 
14
- The 80% case: create a provider once, then `generate` a turn against a conversation, bounding the call with an `AbortSignal` (from `@orkestrel/abort`). Messages are the abstract `Message` shape from `@orkestrel/agent` (`{ id, role, content }`), so the same conversation drives any provider.
14
+ The 80% case: create a provider, then `generate` a turn against a conversation, bounding the call with an `AbortSignal` (from `@orkestrel/abort`). Messages are the abstract `Message` shape from `@orkestrel/agent` (`{ id, role, content }`), so the same conversation drives any provider.
15
15
 
16
16
  ```ts
17
17
  import { createAbort } from '@orkestrel/abort'
@@ -124,9 +124,60 @@ The raw judge exports the following declarations.
124
124
  | `MICA_NOUL_LABELS` | const | `readonly string[]` | Lists Mica's false and true labels in readout order. |
125
125
  | `MICA_SPECIAL_TOKENS` | const | `readonly string[]` | Lists the control tokens escaped by Mica's native renderer. |
126
126
 
127
+ The server face, `@orkestrel/ollama/server`, exports the following daemon session declarations. It runs in Node only; [Daemon session](#daemon-session) states the lifecycle these declarations carry.
128
+
129
+ | API | Kind | Shape | Summary |
130
+ | ------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
131
+ | `createOllamaDaemon` | function | `(options: OllamaDaemonOptions) => OllamaDaemonInterface` | Creates an unstarted daemon with a snapshot of the caller's declaration and lifecycle hooks. |
132
+ | `OllamaDaemon` | class | `OllamaDaemonInterface` | Owns a dedicated daemon through verified startup, declared maintenance, and permanent cleanup. |
133
+ | `OllamaDaemonInterface` | interface | `{ url, identity, maintenance, emitter } plus start, unload, stop, destroy` | Owns a dedicated daemon from validated startup through declared-model maintenance and stop. |
134
+ | `OllamaDaemonOptions` | interface | `Pick<ProviderOptions, 'fetch'> plus { on?, error?, executable, url, models, workspace, environment?, store?, readiness?, metadata?, termination?, signal? }` | Configures a dedicated daemon with explicit executable, endpoint, and model declarations. |
135
+ | `OllamaExecutable` | interface | `{ path, digest, version, arguments? }` | Declares the executable identity required before a daemon starts. |
136
+ | `OllamaModel` | interface | `{ name, digest }` | Declares a model name and the digest its inventory entry must report. |
137
+ | `OllamaMetadataOptions` | interface | `{ timeout?, bytes? }` | Bounds each metadata or unload response independently of the startup deadline. |
138
+ | `OllamaDaemonIdentity` | interface | `{ executable, url, pid, models }` | Records the executable, endpoint, root process, and model identities verified at startup. |
139
+ | `OllamaBytes` | interface | `{ content, size, digest, complete }` | Preserves a bounded HTTP body as base64 with its byte count and SHA-256 digest. |
140
+ | `OllamaMaintenance` | interface | `{ sequence, model, url, request, response?, status?, timing?, error? }` | Records one declared model's unload request and the response observed for that attempt. |
141
+ | `OllamaDaemonEventMap` | type | `{ start: [identity: OllamaDaemonIdentity], unload: [maintenance: OllamaMaintenance], stop: [confirmed: boolean], error: [error: unknown] }` | Reports verified startup, recorded unloads, permanent stop, and daemon failures. |
142
+ | `OLLAMA_READINESS` | const | `number` | Bounds startup through version and model verification to 60000 milliseconds by default. |
143
+ | `OLLAMA_METADATA_TIMEOUT` | const | `number` | Bounds each metadata or unload request and body read to 10000 milliseconds by default. |
144
+ | `OLLAMA_METADATA_BYTES` | const | `number` | Bounds each retained metadata or unload response to 1048576 bytes by default. |
145
+
146
+ The server face reports every refusal as one branded error class.
147
+
148
+ | API | Kind | Shape | Summary |
149
+ | -------------------- | --------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
150
+ | `OllamaError` | class | `(message: string, options: OllamaErrorOptions)` | Carries a daemon refusal with a machine-readable category, observations, and original cause. |
151
+ | `OllamaErrorOptions` | interface | `{ code, context?, cause? }` | Configures a daemon refusal with its category, observations, and original cause. |
152
+ | `OllamaErrorContext` | interface | `{ operation?, path?, url?, model?, expected?, received?, evidence? }` | Carries the operation and identity observations accompanying a daemon refusal. |
153
+ | `OllamaErrorCode` | type | `(typeof OLLAMA_ERROR_CODES)[number]` | Defines the machine-readable refusal categories of the daemon lifecycle. |
154
+ | `OLLAMA_ERROR_CODES` | const | `readonly OllamaErrorCode[]` | Lists every machine-readable daemon refusal category. |
155
+ | `OLLAMA_ERROR_BRAND` | const | `unique symbol` | Identifies daemon errors across installations and module formats through the global symbol registry. |
156
+ | `isOllamaError` | function | `OllamaError` | Checks whether an unknown value carries the daemon error brand and declared members. |
157
+
158
+ The session composes the following validators and protocol helpers, each exported so a caller can check a declaration or a retained body without starting a daemon.
159
+
160
+ | API | Kind | Shape | Summary |
161
+ | --------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
162
+ | `isOllamaDaemonOptions` | function | `OllamaDaemonOptions` | Checks whether an unknown declaration can safely reach daemon preflight. |
163
+ | `isOllamaExecutable` | function | `OllamaExecutable` | Checks whether a value declares an absolute executable and its offline identity. |
164
+ | `isOllamaModel` | function | `OllamaModel` | Checks whether a value declares an exact model name and inventory digest. |
165
+ | `isOllamaOrigin` | function | `string` | Checks whether a string selects an explicit dedicated HTTP loopback origin. |
166
+ | `buildOllamaEnvironment` | function | `(inherited: Readonly<Record<string, string \| undefined>>, environment: Readonly<Record<string, string \| undefined>> \| undefined, url: string, store?: string) => Readonly<Record<string, string \| undefined>>` | Builds the environment with dedicated values overriding every inherited or caller casing. |
167
+ | `matchesOllamaAnnouncement` | function | `(line: string, url: string) => boolean` | Checks whether stderr announces the dedicated loopback listener. |
168
+ | `verifyOllamaVersion` | function | `(value: unknown, expected: string) => void` | Verifies a metadata reply against the declared executable version. |
169
+ | `verifyOllamaModels` | function | `(value: unknown, models: readonly OllamaModel[]) => void` | Verifies every declared model against a complete metadata inventory. |
170
+ | `retainOllamaBytes` | function | `(bytes: Uint8Array, complete: boolean) => OllamaBytes` | Retains exact body bytes with an independently reproducible digest. |
171
+ | `readOllamaJSON` | function | `(bytes: OllamaBytes) => Readonly<Record<string, unknown>>` | Reads a complete retained JSON metadata body without reserializing it. |
172
+ | `readOllamaTerminal` | function | `(bytes: OllamaBytes, model: OllamaModel) => OllamaTiming \| undefined` | Reads unload timing only after a complete terminal reply names the declared model. |
173
+ | `readOllamaBody` | function | `(response: Response, limit: number) => Promise<Pick<OllamaMaintenance, 'response' \| 'status' \| 'error'>>` | Reads a bounded response and preserves its partial bytes on a transport refusal. |
174
+ | `readOllamaReply` | function | `(origin: string, path: string, options?: OllamaMetadataOptions, signal?: AbortSignal, request?: OllamaBytes, transport?: OllamaDaemonOptions['fetch']) => Promise<Pick<OllamaMaintenance, 'response' \| 'status' \| 'error'>>` | Exchanges metadata or unload bytes with a dedicated origin under one complete-request deadline. |
175
+
176
+ `OllamaDaemonInterface` keeps its data members in its row: `url` is the declared origin, `identity` is the verified startup receipt (`undefined` before verification and kept after stop), `maintenance` is every unload attempt in sequence order, and `emitter` is the typed lifecycle surface. Its lifecycle methods are documented under [Methods](#methods). `SessionOptions` is owned by `@orkestrel/process`, and `EmitterInterface`, `EmitterHooks`, and `EmitterErrorHandler` by `@orkestrel/emitter`; the server face reuses them and redefines none of them.
177
+
127
178
  ## Methods
128
179
 
129
- The wire seams `OllamaProvider` declares — exactly the members of `AgentProviderInterface` that the base leaves open. The runtime never calls them directly: a caller drives `generate` or `stream`, and the engine calls these to frame, project, decode, and drain the Ollama wire. Those inherited calls, the `id` / `name` / `format` data members, and the `ProviderParserInterface` the seam returns are documented in [`agent.md`](agent.md).
180
+ Each following table lists the methods one class or interface declares. The wire seams `OllamaProvider` declares are exactly the members of `AgentProviderInterface` that the base leaves open. The runtime never calls them directly: a caller drives `generate` or `stream`, and the engine calls these to frame, project, decode, and drain the Ollama wire. Those inherited calls, the `id` / `name` / `format` data members, and the `ProviderParserInterface` the seam returns are documented in [`agent.md`](agent.md).
130
181
 
131
182
  #### `OllamaProvider`
132
183
 
@@ -148,11 +199,22 @@ The engine calls these seams for each question. Call the inherited `ask` method
148
199
  | `encode` | `WireGenerateRequest` | Projects a single question onto Mica's raw generate request. |
149
200
  | `read` | `JudgeResult` | Decodes a completed raw response into an answer or a missing-label refusal. |
150
201
 
202
+ #### `OllamaDaemonInterface`
203
+
204
+ A caller drives these methods directly, in the order [Daemon session](#daemon-session) states. `OllamaDaemon` implements them and exposes no other public method.
205
+
206
+ | Method | Returns | Summary |
207
+ | --------- | --------------------------------------- | ---------------------------------------------------------------------------------- |
208
+ | `start` | `Promise<OllamaDaemonIdentity>` | Starts the declared executable and verifies its endpoint and models. |
209
+ | `unload` | `Promise<readonly OllamaMaintenance[]>` | Unloads the declared models in declaration order and retains each attempt's bytes. |
210
+ | `stop` | `Promise<boolean>` | Permanently stops the owned tree, joins its session, and re-hashes the executable. |
211
+ | `destroy` | `Promise<void>` | Stops the daemon and releases its observation emitter after cleanup settles. |
212
+
151
213
  ## Contract
152
214
 
153
- These invariants hold across `src/core` ↔ `ollama.md`. The engine contract itself — `ProviderInterface`, `AgentProvider`, `ProviderResult`, `ProviderError`, `ProviderAbortError` — is in `@orkestrel/agent`, and [`agent.md`](agent.md) states it.
215
+ These invariants hold across `src/core`, `src/server`, and `ollama.md`. The doc ↔ source bijection and the method bijection cover both faces, and the rest state the core face; [Daemon session](#daemon-session) states the server face's lifecycle, detection forms, and limits. The engine contract itself — `ProviderInterface`, `AgentProvider`, `ProviderResult`, `ProviderError`, `ProviderAbortError` — is in `@orkestrel/agent`, and [`agent.md`](agent.md) states it.
154
216
 
155
- 1. **Doc ↔ source bijection.** Every `function` / `class` / `const` / `interface` / `type` row in the `## Surface` table is a real export of the `src/core` surface, and every export appears as a Surface row — exhaustive, both directions.
217
+ 1. **Doc ↔ source bijection.** Every `function` / `class` / `const` / `interface` / `type` row in the `## Surface` tables is a real export of the `src/core` or `src/server` surface, and every export of either barrel appears as a Surface row — exhaustive, both directions.
156
218
  2. **Imports each boundary from its owner, no cycle.** `OllamaProvider` extends `AgentProvider` and implements `AgentProviderInterface`; that base, `ProviderRequest`, `ProviderIncrement`, `ProviderParserInterface`, `ProviderInterface`, `ProviderOptions`, and `Message` come from `@orkestrel/agent`. `createNDJSONParser` comes from `@orkestrel/ndjson`, `ToolCall` from `@orkestrel/tool`, `TokenUsage` from `@orkestrel/budget`, and the guards (`isRecord` / `isString` / `isNumber` / `isNonNegativeInteger` / `parseJSONAs`) from `@orkestrel/contract`. The provider errors are owned by `@orkestrel/agent` and `ToolDefinition` by `@orkestrel/tool`: `src/core` imports neither, and both reach a caller through the base — the base throws the errors this wire's failures become, and it accepts the `ToolDefinition[]` a call advertises. Each dependency is one-way and never imports from `@src/core`. No module under `src/` imports `@orkestrel/timeout` — the base arms the deadline — and the manifest declares it as a development dependency for the guide's bounding pattern, which imports it as a consumer would.
157
219
  3. **The `/api/chat` wire body.** `OllamaProvider` POSTs to `${url}${OLLAMA_CHAT_PATH}`, defaulting `url` to `DEFAULT_OLLAMA_URL` and `keep_alive` to `DEFAULT_KEEP_ALIVE`. The body carries `model`, the mapped `messages`, `stream: true` on every provider call, `keep_alive`, and `think` (the per-call `ProviderStreamOptions.think` override when present, else `OllamaOptions.think`, default `false`), adding `options` only when configured, `format` only when the call supplies a `ProviderStreamOptions.schema`, and `tools` only when a non-empty `ToolDefinition[]` is passed. The top-level `truncate` and `shift` keys follow `tools`, each present only when its `OllamaOptions` member is set, with `false` carried verbatim and neither nested in `options`; `generate` and `stream` encode them identically. `false` requires daemon support to take effect. Each tool maps to `{ type: 'function', function: { name } }`, with `description` and `parameters` added only when the corresponding `ToolDefinition` fields are supplied. `title` and `annotations` are **never sent**: the `/api/chat` tool function object carries no field for either. Messages map to the wire's minimal `{ role, content }` turn, with `tool_calls` added only on a turn that replays them and `images` only on a multimodal turn. A non-OK status is the base's failure, not this package's: it throws `ProviderError` with code `'HTTP'`, the response `status`, and a message bounded to an excerpt of the response body.
158
220
  4. **Configurable `think` on the wire; the base's splitter keeps the assembled content clean.** For a thinking-capable model (for example `qwen3`) the per-request `think` flag is the only wire-level reasoning control (its native renderer honours neither the qwen3 `/no_think` token nor a Modelfile `PARAMETER think false`). It is configurable through `OllamaOptions.think` (default `false` — so the general-purpose provider stays immediate for non-thinking models and tests fast) and overrideable per call through `ProviderStreamOptions.think`. Set `think: true` when the caller displays reasoning separately from the answer: the daemon then separates reasoning natively, returning it on the distinct `message.thinking` channel, which `read` reports and the engine yields as `ProviderDelta` `{ channel: 'thinking', text }` and accumulates onto `ProviderResult.thinking`. Either way the base's `split` behaviour stays armed, because a daemon may ignore `think: false` for a thinking model and render reasoning inline as `<think>…</think>` content; `OllamaProvider` passes no `split` to `super`, so the default separation applies and the assembled `content` is the splitter's clean accumulation. The agent records the reasoning on the assistant message. `OllamaOptions.replay` filters its return to the provider input, defaulting to `'none'`; the Ollama request mapper omits stored reasoning from the wire.
@@ -161,19 +223,87 @@ These invariants hold across `src/core` ↔ `ollama.md`. The engine contract its
161
223
  7. **Usage and truncation from the `done` line (reuses `TokenUsage`).** `read` reports `usage` only for a record whose `done` is `true` and whose `prompt_eval_count` and `eval_count` are both numbers — mapped to `{ prompt, completion, total: prompt + completion }`, the `TokenUsage` shape, imported rather than redefined. A delta line carries no counts and a `done: false` line's counts are ignored, so neither contributes usage, and `ProviderResult.usage` is absent when the turn reported none. `read` projects the same record's `done_reason` through `extractTruncated`: `'length'` (the `num_predict` cap or a full context window) becomes `truncated: true` on the increment and so on `ProviderResult.truncated`, and any other reason, a missing one, or a `done_reason` on a record that is not `done` leaves `truncated` absent. The projection does not read the request's `truncate` setting, so a prompt the daemon truncates under its default returns normally and reports nothing.
162
224
  8. **Tool-call extraction with id-generation (no `as`).** `result.tools` is the model's `message.tool_calls`, each entry narrowed to `{ id, name, arguments }`: the entry and its `function` must be records and `name` a string (else the entry is dropped); `arguments` is the wire object as-is, a JSON string parsed to a record, or `{}` when neither; `id` is the wire's `id` when a string, else a freshly minted `crypto.randomUUID()`. An empty result `tools` is never surfaced — its absence means "no calls".
163
225
  9. **Boundary narrowing — all wire `unknown` through guards, never `as`.** Every value read off the wire (each NDJSON record, `message`, `content`, `thinking`, the usage counts, `tool_calls`, `arguments`) arrives as `unknown` and is narrowed through the `@orkestrel/contract` guards — never a type assertion. A missing or malformed provider field degrades to a sensible default (empty content, no usage, `{}` arguments), never a throw — the one external boundary kept honest.
164
- 10. **The base owns everything that is not the wire.** The per-call deadline (`OllamaOptions.timeout`, `120_000`ms when omitted, folded with the caller's signal so either cancels the request and the timer is always cleared), the cancellation rule (a `stream` cancelled mid-flight throws `ProviderAbortError` carrying the partial, and the reader and parser are released in a `finally`), the transport seam (`OllamaOptions.fetch` and the per-request `OllamaOptions.headers` hook, awaited inside the deadline and raced against the combined signal), and the bounded error read all live in `AgentProvider`. `OllamaOptions` extends `ProviderOptions`, so those keys are the base's and behave identically for every provider built on it; [`agent.md`](agent.md) states each rule once.
226
+ 10. **The base owns everything that is not the wire.** The per-call deadline (`OllamaOptions.timeout`, `120_000`ms when omitted, folded with the caller's signal so either cancels the request and the timer is always cleared), the cancellation rule (a `stream` cancelled mid-flight throws `ProviderAbortError` carrying the partial, and the reader and parser are released in a `finally`), the transport seam (`OllamaOptions.fetch` and the per-request `OllamaOptions.headers` hook, awaited inside the deadline and raced against the combined signal), and the bounded error read all live in `AgentProvider`. `OllamaOptions` extends `ProviderOptions`, so those keys are the base's and behave identically for every provider built on it; [`agent.md`](agent.md) defines those rules.
165
227
  11. **Context framing.** An instruction manager accepts a `ContextSectionFormat<InstructionInterface>` through `createInstructionManager({ format })`. The agent uses that manager to build the instruction text. Ollama structured output uses the separate per-call `ProviderStreamOptions.schema` value, forwarded as the wire `format` field.
166
- 12. **Event-free.** A pure functional boundary — no Emitter, no events. Each call is a function of its arguments.
167
- 13. **A core face that runs in a browser.** The package publishes the `.` entry alone, built from `src/core`, which imports no `node:*` module and no DOM global; the scoped core typecheck compiles it with the `ESNext` and `WebWorker` libraries and no ambient `@types`, so neither Node's globals nor the DOM's are in scope, and the base binds `globalThis.fetch` to its own receiver so a browser does not throw `Illegal invocation` on a bare transport reference. [`tests/service/page.test.ts`](../tests/service/page.test.ts) gates that claim rather than recording it: it launches the host's own Chrome or Edge through `@orkestrel/browser`, serves the built `@orkestrel/ollama` core entry and its whole `@orkestrel` import closure from an import map on a `127.0.0.1` fixture, and reads the page's requests from the browser's own request log. The page loads with no uncaught error and no console error, on the recorders a deliberate page fault then makes report; an `Agent` holding a page-defined DOM-writing `Tool` completes a tool loop against the live daemon through a same-origin `createRelay` server, where the model's recorded call reaches the page, the tool's minted receipt is read back out of the real DOM and found again as the `role: 'tool'` message of the relay request and the `/api/chat` request that follow the turn the call was dispatched in, and the run settles with `partial: false`; the page issues exactly one `POST /inference` per turn and nothing else across the whole operation window, with a deliberate `/control` request proving such a window can report one; a wrong bearer arrives as `ProviderError` with code `'HTTP'` and status `401` with no daemon request made; and `createOllama` in the page answers a prompt over `/api/chat` on the daemon's own origin after `requireDaemonOrigin` confirms the daemon permits the page's origin. Each attempt ends within its allowance plus its release share, so nothing the attempt schedules outlasts the case that holds it. The attempt's signal cancels the part of the connection the installed `BrowserOptions.signal` races — discovery, the port-free check, the launch, and `client.connect()` — and the attempt's own race is what bounds every phase after that, the target listing the connection ends with included; [`tests/service/page.test.ts`](../tests/service/page.test.ts) and the `PAGE_BOUNDS` table it spends state the mechanism. The model's choice to call a tool is retried at most 3 times, and that choice is the only reading another attempt buys: every attempt retains its observations before any page read can throw, and only a run that completed an answer without dispatching the tool spends another launch. An acquisition, read, release, or run failure ends the retry, and so does a run that deadline interrupted before it settled — an interrupted run reports no tool call for a reason another launch cannot clear, so retrying it would report the exhaustion as the model never choosing the tool. The request accounting is asserted over each retained attempt, so what the gate establishes is that the model called the tool within those attempts and that no attempt's traffic went unread. The daemon is the remaining limit — a page reaches `/api/chat` directly only where the daemon, or a proxy in front of it, answers the page's origin; otherwise relay the call through your own server ([Relaying through your own server](#relaying-through-your-own-server)).
228
+ 12. **Event-free.** The provider and the judge are a pure functional boundary — no Emitter, no events. Each call is a function of its arguments. The server face's daemon session is the one emitter in the package, through `OllamaDaemonInterface.emitter`.
229
+ 13. **A core face that runs in a browser.** The `.` entry is built from `src/core`, which imports no `node:*` module and no DOM global; the scoped core typecheck compiles it with the `ESNext` and `WebWorker` libraries and no ambient `@types`, so neither Node's globals nor the DOM's are in scope, and the base binds `globalThis.fetch` to its own receiver so a browser does not throw `Illegal invocation` on a bare transport reference. The `./server` entry, built from `src/server`, imports Node modules and the process session, so a browser bundle takes the `.` entry alone. [`tests/service/page.test.ts`](../tests/service/page.test.ts) gates that claim rather than recording it: it launches the host's own Chrome or Edge through `@orkestrel/browser`, serves the built `@orkestrel/ollama` core entry and its whole `@orkestrel` import closure from an import map on a `127.0.0.1` fixture, and reads the page's requests from the browser's own request log. The page loads with no uncaught error and no console error, on the recorders a deliberate page fault then makes report; an `Agent` holding a page-defined DOM-writing `Tool` completes a tool loop against the live daemon through a same-origin `createRelay` server, where the model's recorded call reaches the page, the tool's minted receipt is read back out of the real DOM and found again as the `role: 'tool'` message of the relay request and the `/api/chat` request that follow the turn the call was dispatched in, and the run settles with `partial: false`; the page issues exactly one `POST /inference` per turn and nothing else across the whole operation window, with a deliberate `/control` request proving such a window can report one; a wrong bearer arrives as `ProviderError` with code `'HTTP'` and status `401` with no daemon request made; and `createOllama` in the page answers a prompt over `/api/chat` on the daemon's own origin after `requireDaemonOrigin` confirms the daemon permits the page's origin. Each attempt ends within its allowance plus its release share, so nothing the attempt schedules outlasts the case that holds it. The attempt's signal cancels the part of the connection the installed `BrowserOptions.signal` races — discovery, the port-free check, the launch, and `client.connect()` — and the attempt's own race is what bounds every phase after that, the target listing the connection ends with included; [`tests/service/page.test.ts`](../tests/service/page.test.ts) and the `PAGE_BOUNDS` table it spends state the mechanism. The model's choice to call a tool is retried at most 3 times, and that choice is the only reading another attempt buys: every attempt retains its observations before any page read can throw, and only a run that completed an answer without dispatching the tool spends another launch. An acquisition, read, release, or run failure ends the retry, and so does a run that deadline interrupted before it settled — an interrupted run reports no tool call for a reason another launch cannot clear, so retrying it would report the exhaustion as the model never choosing the tool. The request accounting is asserted over each retained attempt, so what the gate establishes is that the model called the tool within those attempts and that no attempt's traffic went unread. The daemon is the remaining limit — a page reaches `/api/chat` directly only where the daemon, or a proxy in front of it, answers the page's origin; otherwise relay the call through your own server ([Relaying through your own server](#relaying-through-your-own-server)).
168
230
  14. **Tested live against a real local Ollama (no `skipIf`).** The live tests run against a real Ollama daemon — no mocks, only genuine third-party calls — model `qwen3.5:2b-q4_K_M`, with `OLLAMA_HOST` / `OLLAMA_MODEL` overridable. Unlike the other surfaces, the dedicated `service` project requires the daemon: `tests/setupService.ts` throws a clear error if it is unreachable and warms the model (a `num_predict: 1` chat) before the suite, so the live tests run unconditionally (no `describe.skipIf`). The project runs serially (`fileParallelism: false`) with a 120s test/hook timeout so a cold load cannot flake it; `keep_alive` keeps the model resident across files, and `bash scripts/ollama.sh` brings the daemon and model up before the battery in CI. Assertions are structural — they hold whatever wording a small model produces — and never pin exact output. The hermetic half sits in `tests/src/core/`, and the relay round trip is proved hermetically in [`tests/src/core/integration.test.ts`](../tests/src/core/integration.test.ts) and live in [`tests/service/relay.test.ts`](../tests/service/relay.test.ts).
169
- 15. **Doc ↔ source method bijection.** Each `## Methods` table lists the methods its class declares: `frame`, `encode`, `read`, and `finish` for `OllamaProvider`; `encode` and `read` for `OllamaJudge`. The inherited `generate`, `stream`, and `ask` methods are declared by the agent package's engines.
231
+ 15. **Doc ↔ source method bijection.** Each `## Methods` table lists the methods its class or interface declares: `frame`, `encode`, `read`, and `finish` for `OllamaProvider`; `encode` and `read` for `OllamaJudge`; `start`, `unload`, `stop`, and `destroy` for `OllamaDaemonInterface`, which `OllamaDaemon` implements with no other public method. The inherited `generate`, `stream`, and `ask` methods are declared by the agent package's engines.
170
232
 
171
233
  16. **Render.** The raw prompt mirrors the TypeSafe adapter path of Mica's server: the `rows_from_request` function in the `typesafe_server.py` file followed by the `prompt_for` function in the `native.py` file. Structured state uses JSON with a one-space indent and JavaScript numeric spelling: JavaScript cannot distinguish 100.0 from 100 and spells an exponent as 1e-7 where Python spells 1e-07. Parity is byte-exact for string states and structured states whose numbers spell the same in both runtimes. The wire preserves the system prompt and escapes control tokens in state, instructions, option names, and descriptions with U+200B after the opening angle bracket. Noul criteria render false before true; an absent or empty side renders its key. Choice names render in brackets unless empty, positional (including Unicode decimal digits), or equal to the wire label. A null description renders the `None` text. Score levels omit names. The assistant suffix disables thinking with the `<think>\n\n</think>\n\n` text.
172
234
  17. **Readout.** The wire sends each question in its own `POST` request to the `OLLAMA_GENERATE_PATH` endpoint. Each body carries the `raw: true`, `stream: false`, `logprobs: true`, and `top_logprobs: 20` settings. The fixed `num_predict: 1` and `temperature: 1` settings override the caller's sampling settings. Calibration divides candidate logprob gaps by its finite positive temperature before a softmax over candidates alone. The `extractTopLogprobs` function reads only the `logprobs[0].top_logprobs` list and preserves token text. Malformed or duplicate entries raise a `JudgeError` error with the `PROTOCOL` code. The wire reads usage from a completed record through the `extractUsage` function and reports usage only when the `isTokenUsage` guard admits it.
173
235
  18. **Refusal.** A label outside the top list refuses that question. The `missing` collection names the caller's option, level index, or the `false` or `true` key. The wire still answers the other questions and the result keeps the spent usage. A missing label never receives an invented probability.
174
236
  19. **Limits.** A choice admits 2 to 20 options and a score 2 to 10 levels. A request outside those limits raises a `JudgeError` error with the `QUESTION` code. Structured instructions or criteria raise the same error before inference because the Mica server flattens them with Python's `str()` function. The engine validates every body before making a transport call. The `keepAlive` setting accepts a duration string or a number and defaults to the `DEFAULT_KEEP_ALIVE` value. The daemon enforces its context window. The caller selects the wire.
175
237
  20. **Identity and cancellation.** The `judge.model` property and every `JudgeResult.model` property carry a JSON tuple of the model tag, system prompt, effective calibration temperature, effective options with sorted keys, and the `MICA_RENDER_REVISION` value. The effective options include the fixed `num_predict: 1` and `temperature: 1` settings. Equal option bags yield one identity regardless of key order; a changed effective option changes it. Each call has the inherited deadline and header hook. Cancellation raises a `JudgeAbortError` error carrying the completed calls' answers, refusals, and usage.
176
- 21. **Terminal timing.** The `extractTiming` function reads the daemon's own measurements from one parsed record and changes no request, scheduling, usage extraction, or inference count. It returns `undefined` unless the record owns `done: true`. The `OllamaDurations` members map `total_duration`, `load_duration`, `prompt_eval_duration`, and `eval_duration` to `total`, `load`, `prompt`, and `decode`; they are raw integer nanoseconds that the function keeps verbatim and never converts. The `OllamaCounts` members map `prompt_eval_count`, `eval_count`, and `prompt_eval_cached_count` to `prompt`, `decode`, and `cached`; they are tokens. A field the record does not own is unknown: its member is `undefined` and its key is absent from `invalid`, and no member is ever defaulted to zero or inferred. A field the record owns whose value is not a non-negative safe integer, including an own `undefined`, negative zero, a string, a fraction, or an integer above `Number.MAX_SAFE_INTEGER`, also reads `undefined` and its wire key enters `invalid`, in the order the wire fields are listed above. The function neither mutates the record nor throws for an ordinary hostile record, and it reads own fields only. Whether a pinned daemon sends each field is a live measurement, not a property of this function.
238
+ 21. **Terminal timing.** The `extractTiming` function reads the daemon's own measurements from one parsed record and changes no request, scheduling, usage extraction, or inference count. It returns `undefined` unless the record owns `done: true`. The `OllamaDurations` members map `total_duration`, `load_duration`, `prompt_eval_duration`, and `eval_duration` to `total`, `load`, `prompt`, and `decode`; they are raw integer nanoseconds that the function keeps verbatim and never converts. The `OllamaCounts` members map `prompt_eval_count`, `eval_count`, and `prompt_eval_cached_count` to `prompt`, `decode`, and `cached`; they are tokens. A field the record does not own is unknown: its member is `undefined` and its key is absent from `invalid`, and no member is ever defaulted to zero or inferred. A field the record owns whose value is not a non-negative safe integer, including an own `undefined`, negative zero, a string, a fraction, or an integer above `Number.MAX_SAFE_INTEGER`, also reads `undefined` and its wire key enters `invalid`, in the preceding wire-field order. The function neither mutates the record nor throws for an ordinary unexpected record, and it reads own fields only. Whether a pinned daemon sends each field is a live measurement, not a property of this function.
239
+
240
+ ## Daemon session
241
+
242
+ The server face owns one dedicated Ollama daemon, from a declaration you record before the run, through startup verification and maintenance, to a permanent stop. It runs in Node only, spawns through the process session `@orkestrel/process` publishes, and reports its lifecycle through an `@orkestrel/emitter` emitter. Its declaration refuses the default port 11434, so a session never addresses the daemon other programs share on that port.
243
+
244
+ The optional `fetch` collaborator carries every `/api/version` probe, `/api/tags` verification, and `/api/generate` unload, with the same transport type as the core provider and judge. Supply an instrumented sender to record an attempt before sending it and observe its response headers and stream chunks. The daemon reads the returned body under its existing bounds and retains its maintenance bytes and digests without decoding or reserializing them. Omission uses the global `fetch` bound to `globalThis`.
245
+
246
+ ### Lifecycle
247
+
248
+ A session moves through these steps in order. `PORT` stands for the dedicated port you choose and `NAME` for a declared model name.
249
+
250
+ 1. **Declare.** Record the executable's absolute path, its lowercase hexadecimal SHA-256 digest, and the version its `/api/version` route reports, all read before the run. Add the dedicated origin `http://127.0.0.1:PORT` with an explicit port other than 11434 and 0, an absolute working directory, and each model with the digest its `/api/tags` entry reports. `createOllamaDaemon` and the `OllamaDaemon` constructor validate the declaration and copy it before any file, socket, or process operation, and a malformed declaration throws `OllamaError` with code `invalid`.
251
+ 2. **Start.** `start` checks that the workspace and the optional `store` are existing directories, that the executable is a regular file — a `.exe` or `.com` file on Windows and an executable file elsewhere — and not a `#!` script, and that its bytes hash to the declared digest. It then binds the declared port and releases it, refusing with `occupied` when another process holds the port, and spawns the executable through a process session with the declared `arguments` (default `serve`) and the selected environment. Concurrent and repeated calls share the first startup result.
252
+ 3. **Verify.** Readiness wakes on the child's own stderr. `start` requires the child's listen announcement for the declared port, then a `/api/version` reply equal to the declared version, then a `/api/tags` inventory that reports every declared model with its declared digest, and it checks that the owned child is live at each of those replies. Only after every check passes does `start` emit `start` and resolve the frozen `OllamaDaemonIdentity` receipt. `readiness` bounds the whole sequence, `OLLAMA_READINESS` (60000 ms) by default. A failed startup stops and joins the owned session before it rejects, and a cleanup that isn't confirmed rejects with `cleanup` and the startup failure as its `cause`.
253
+ 4. **Use.** Point the core face at `daemon.url`, for example `createOllama({ model: NAME, url: daemon.url })`. The session admits no unload before verification and doesn't observe the requests you send.
254
+ 5. **Unload.** `unload` posts `{"model":NAME,"keep_alive":0}` to `/api/generate` for each declared model in declaration order, with no prompt, and retains each request and response as `OllamaBytes`: the base64 content, its byte count, its SHA-256 digest, and whether the body ended within its bound. An attempt succeeds only when the complete terminal reply carries `done: true` and names the declared model. The first refused attempt ends the call, and every attempt, refused ones included, stays in `maintenance` in sequence order. A transport or collaborator failure, or a refused reply, rejects `unload` and leaves the daemon running until `stop`; a rejection during `start` stops and joins the child before rejecting. A call that overlaps another refuses with `state`.
255
+ 6. **Stop.** `stop` is permanent. It aborts pending startup and maintenance, stops the owned process tree, joins the session's exit barrier, and hashes the executable again. It resolves `true` when the root exit is confirmed and the digest is unchanged, or when no child ever started; otherwise it emits `error` and resolves `false`. It never rejects, every call shares one barrier, and a stopped session refuses to start with `state`. `destroy` stops the session and then releases its emitter; `identity` and `maintenance` stay readable after it.
256
+
257
+ The declaration carries the following settings, each of which you can omit.
258
+
259
+ | Option | Behavior |
260
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
261
+ | `fetch` | Carries every session HTTP request. Default: the global `fetch` bound to `globalThis`. The collaborator must honor the supplied signal and manual redirect policy and return an unread standard response body. The declaration snapshot retains the function reference. |
262
+ | `environment` | Overlays inherited variables by case-folded name on every host. An `undefined` value removes a variable. The declaration's `OLLAMA_HOST` and selected `OLLAMA_MODELS` override every inherited or caller casing. |
263
+ | `store` | Selects an absolute existing model directory through `OLLAMA_MODELS`. Omission leaves the inherited or overlaid model directory in force. |
264
+ | `readiness` | Bounds startup through verification. Default: `OLLAMA_READINESS` (60000 ms). |
265
+ | `metadata` | Bounds each metadata or unload request and its body read through `timeout` (default `OLLAMA_METADATA_TIMEOUT`, 10000 ms) and retained bytes through `bytes` (default `OLLAMA_METADATA_BYTES`, 1048576 bytes). Redirects are refused. |
266
+ | `termination` | Passes `grace` and `drain` to the process session, retaining its defaults when omitted. |
267
+ | `signal` | Aborts startup or use and starts the same permanent stop. An already aborted signal refuses `start` with `aborted` before spawn. |
268
+ | `on` | Installs lifecycle listeners at construction. `emitter` reports `start`, each settled `unload` attempt, permanent `stop`, and domain `error` events. |
269
+ | `error` | Receives isolated listener failures with their event names. A listener failure never reaches the session. |
270
+
271
+ ### Detection forms and limits
272
+
273
+ The package ships no native component, so the session detects executable replacement and a foreign listener instead of preventing them. The following list names each form and the limit it leaves.
274
+
275
+ - **Executable replacement.** The session hashes the executable before spawn and again after stop. A digest that differs before spawn refuses with `digest`. A replacement during use surfaces after the fact: `stop` resolves `false`, emits an `OllamaError` with code `digest`, and keeps the startup receipt in `identity`. The session holds no lock on the file, and the digest covers the executable alone — not its arguments, its runtime libraries, or the files those arguments name.
276
+ - **A foreign listener.** `start` refuses an occupied port, wakes only on the owned child's own listen announcement for the declared port, and checks that child's liveness at each readiness reply, refusing with `owner` when the child has exited. It reads no native socket owner, and it releases its port reservation before the child binds, so another process can take the port in that gap.
277
+ - **Process cleanup.** `stop` ends the owned tree through the process session's tree stop and exit barrier. An abrupt death of the host process skips that stop. On POSIX, the process session detaches its child, so both the daemon root and its runners can survive and keep the dedicated port occupied. On Windows, the child isn't detached: the root in [libuv's kill-on-close job](https://github.com/libuv/libuv/blob/v1.51.0/src/win/process.c#L65-L91) ends with the host, but runners outside that job can survive. After the root's exit is observed, a tree stop can't recover descendants that outlived it.
278
+ - **Selection, not isolation.** The environment selects the endpoint and the model store; it isolates neither the host nor the model store from other programs.
279
+ - **Unload evidence.** A successful `unload` records that the daemon confirmed the unload for each declared model. It proves no eviction from GPU memory, no cold cache, and no change in what `/api/ps` reports.
280
+ - **Root process id.** `identity.pid` comes from the process session, never from an HTTP reply, and records an observation; the process session, not that number, owns termination.
281
+
282
+ ### Model digests
283
+
284
+ Declare each model's digest as the `digest` field its `/api/tags` entry reports, not as a SHA-256 you compute over the model's manifest file. `start` compares each declared digest with the inventory's verbatim and pulls or substitutes nothing. On the pinned Ollama 0.40.8 runner on 2026-10-23, a declaration of `qwen3.5:2b-q4_K_M` carrying its manifest file's SHA-256 was refused with `model`, and the same declaration carrying the inventory digest started, verified, unloaded with an HTTP 200 reply whose `done_reason` was `unload`, and stopped with its exit confirmed; that runner's listen announcement matched the form `matchesOllamaAnnouncement` reads. [Verify a declared model digest](#verify-a-declared-model-digest) replays that comparison without a daemon.
285
+
286
+ ### Refusals
287
+
288
+ `OllamaError` carries one of the following codes in `code`, and `OLLAMA_ERROR_CODES` lists them in this order.
289
+
290
+ | Code | Refuses or reports |
291
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
292
+ | `invalid` | a malformed declaration, a workspace or store that isn't an existing directory, or an executable that isn't a regular file, is a `#!` script, or on Windows isn't a `.exe` or `.com` file |
293
+ | `occupied` | a dedicated port another process holds |
294
+ | `digest` | an executable whose digest differs or can't be read, before spawn or after stop |
295
+ | `spawn` | another preflight or spawn failure, such as a missing executable or a child that couldn't be created |
296
+ | `readiness` | the startup deadline, which a child that never announces its listener reaches |
297
+ | `version` | a `/api/version` reply that differs from the declared version |
298
+ | `model` | a declared model the inventory omits or reports with another digest |
299
+ | `owner` | an owned child that isn't live at a readiness reply or at maintenance |
300
+ | `metadata` | a refused status, a redirect, a malformed or oversized body, or an elapsed request bound |
301
+ | `unload` | an unload reply that doesn't confirm the declared model |
302
+ | `cleanup` | a termination that wasn't confirmed |
303
+ | `aborted` | a cancellation through `signal`, or a stop during startup or maintenance |
304
+ | `state` | an operation the lifecycle doesn't admit, such as an unload before verification, an overlapping unload, or a restart |
305
+
306
+ `isOllamaError` recognizes the error by an own brand registered under `Symbol.for('@orkestrel/ollama.OllamaError')`, so an error from another installation or module format narrows too, and so does a subclass. The brand recognizes the contract and doesn't prove that this constructor ran. The error keeps `cause` exactly as supplied and freezes a copy of `context`.
177
307
 
178
308
  ## Patterns
179
309
 
@@ -301,14 +431,13 @@ judge.read(response, request).model === identity // true
301
431
  The dominant single-shot use: one prompt, one assembled result.
302
432
 
303
433
  ```ts
304
- import { createAbort } from '@orkestrel/abort'
305
434
  import type { TokenUsage } from '@orkestrel/budget'
306
435
  import { createOllama } from '@orkestrel/ollama'
307
436
 
308
437
  declare function charge(usage: TokenUsage): void // your billing integration
309
438
 
310
439
  const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', options: { temperature: 0 } })
311
- const abort = createAbort()
440
+ const abort = new AbortController()
312
441
  const messages = [
313
442
  { id: '1', role: 'user', content: 'Summarize the release notes for version 2.0.' },
314
443
  ] as const
@@ -367,7 +496,7 @@ provider.finish(parser) // [] — the parser held nothing back
367
496
 
368
497
  ### Running in the browser
369
498
 
370
- The published face is core, so the same `createOllama` call runs unchanged in a browser module. Point `url` at whatever the page can actually reach: the daemon's own origin where it answers that origin, or your own server standing in front of it. A page that reaches a daemon directly is a development arrangement — it puts the model endpoint on the network the page runs on — so prefer one of the server patterns that follow for anything a user runs. The daemon decides the page's origin before the call: a browser sends an `OPTIONS` preflight to `/api/chat` first, and a daemon that does not permit that origin refuses it, so set `OLLAMA_ORIGINS` to include the origin the page is served from. [`tests/service/page.test.ts`](../tests/service/page.test.ts) drives this arrangement for real, and gates it behind that same preflight.
499
+ The core face is host-independent, so the same `createOllama` call runs unchanged in a browser module. Point `url` at whatever the page can actually reach: the daemon's own origin where it answers that origin, or your own server standing in front of it. A page that reaches a daemon directly is a development arrangement — it puts the model endpoint on the network the page runs on — so prefer one of the server patterns that follow for anything a user runs. The daemon decides the page's origin before the call: a browser sends an `OPTIONS` preflight to `/api/chat` first, and a daemon that does not permit that origin refuses it, so set `OLLAMA_ORIGINS` to include the origin the page is served from. [`tests/service/page.test.ts`](../tests/service/page.test.ts) drives this arrangement for real, and gates it behind that same preflight.
371
500
 
372
501
  ```ts
373
502
  import { createOllama } from '@orkestrel/ollama'
@@ -451,7 +580,7 @@ const messages = [{ id: '1', role: 'user', content: 'Say hello.' }] as const
451
580
  const result = await provider.generate(messages, abort.signal)
452
581
  ```
453
582
 
454
- The `headers` hook is called once per request (async, so a token can be refreshed each call); its result merges on top of the base `Content-Type`, so it adds an authorization header without disturbing the JSON body. What this shape cannot do is hide the vendor: the page still speaks Ollama's `/api/chat`, still names the model, and still needs a proxy that speaks that same wire, so switching vendors means shipping a different provider to the browser. Relay instead when the page must stay ignorant of what answers it.
583
+ The `headers` hook is called for every request (async, so a token can be refreshed each call); its result merges on top of the base `Content-Type`, so it adds an authorization header without disturbing the JSON body. What this shape cannot do is hide the vendor: the page still speaks Ollama's `/api/chat`, still names the model, and still needs a proxy that speaks that same wire, so switching vendors means shipping a different provider to the browser. Relay instead when the page must stay ignorant of what answers it.
455
584
 
456
585
  ### Context framing
457
586
 
@@ -511,6 +640,126 @@ try {
511
640
 
512
641
  A cancel is outside that taxonomy: the caller's signal and the deadline both throw `ProviderAbortError`, which `isProviderAbortError` narrows, and its `partial` carries what had streamed.
513
642
 
643
+ ### Run a dedicated daemon
644
+
645
+ The following fence declares a session, starts it, points a provider at it, unloads the declared model, and stops. It starts a real Ollama executable, so this guide's tests never run it: `OLLAMA_PATH`, `OLLAMA_DIGEST`, `MODEL_DIGEST`, and `WORKSPACE` stand for the absolute executable path, its SHA-256 digest, the model's `/api/tags` digest, and an absolute working directory, each recorded before the run.
646
+
647
+ ```ts
648
+ import { createOllama } from '@orkestrel/ollama'
649
+ import { createOllamaDaemon, isOllamaError } from '@orkestrel/ollama/server'
650
+
651
+ declare const OLLAMA_PATH: string
652
+ declare const OLLAMA_DIGEST: string
653
+ declare const MODEL_DIGEST: string
654
+ declare const WORKSPACE: string
655
+
656
+ const daemon = createOllamaDaemon({
657
+ executable: { path: OLLAMA_PATH, digest: OLLAMA_DIGEST, version: '0.40.8' },
658
+ url: 'http://127.0.0.1:11437',
659
+ models: [{ name: 'qwen3.5:2b-q4_K_M', digest: MODEL_DIGEST }],
660
+ workspace: WORKSPACE,
661
+ })
662
+ try {
663
+ const identity = await daemon.start() // the verified receipt: executable, url, pid, and models
664
+ const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M', url: daemon.url })
665
+ const answer = await provider.generate(
666
+ [{ id: '1', role: 'user', content: 'Reply with exactly: ok' }],
667
+ new AbortController().signal,
668
+ ) // served by the dedicated daemon on port 11437
669
+ const maintenance = await daemon.unload() // one record per declared model, with its bytes
670
+ const confirmed = await daemon.stop() // true when the root exit is confirmed and the digest is unchanged
671
+ } catch (error) {
672
+ if (isOllamaError(error)) console.log(error.code, error.context) // 'model' for a digest /api/tags doesn't report
673
+ throw error
674
+ } finally {
675
+ await daemon.destroy()
676
+ }
677
+ ```
678
+
679
+ ### Refuse a declaration before any I/O
680
+
681
+ `createOllamaDaemon` validates the whole declaration before it reads a file, binds a socket, or spawns a process, so a malformed declaration fails at construction. A session that never started stops without creating a child and then refuses to start. Nothing in the following fence touches the paths it names.
682
+
683
+ ```ts
684
+ import { createOllamaDaemon, isOllamaError, isOllamaOrigin } from '@orkestrel/ollama/server'
685
+
686
+ isOllamaOrigin('http://127.0.0.1:11437') // true
687
+ isOllamaOrigin('http://127.0.0.1:11434') // false — the default port is never a dedicated one
688
+ isOllamaOrigin('http://localhost:11437') // false — the origin names 127.0.0.1 exactly
689
+
690
+ const declaration = {
691
+ executable: { path: '/opt/ollama/bin/ollama', digest: 'a'.repeat(64), version: '0.40.8' },
692
+ url: 'http://127.0.0.1:11437',
693
+ models: [{ name: 'qwen3.5:2b-q4_K_M', digest: 'b'.repeat(64) }],
694
+ workspace: '/srv/ollama',
695
+ fetch: globalThis.fetch.bind(globalThis),
696
+ }
697
+ let refusal: unknown
698
+ try {
699
+ createOllamaDaemon({ ...declaration, url: 'http://127.0.0.1:11434' })
700
+ } catch (error) {
701
+ refusal = error
702
+ }
703
+ isOllamaError(refusal) && refusal.code // 'invalid'
704
+
705
+ const daemon = createOllamaDaemon(declaration)
706
+ await daemon.stop() // true — no child was created
707
+ daemon.identity // undefined
708
+ await daemon.start().catch((error: unknown) => isOllamaError(error) && error.code) // 'state'
709
+ ```
710
+
711
+ ### Verify a declared model digest
712
+
713
+ `start` compares each declared digest with the inventory verbatim. The following fence replays the 2026-10-23 comparison on the pinned 0.40.8 runner: `TAGS_DIGEST` is the digest its `/api/tags` route reported for `qwen3.5:2b-q4_K_M`, and `MANIFEST_DIGEST` is the SHA-256 of that model's manifest file.
714
+
715
+ ```ts
716
+ import { isOllamaError, verifyOllamaModels } from '@orkestrel/ollama/server'
717
+
718
+ const TAGS_DIGEST = '78a738cc751e89cba72bb1feabf70f30c31772eb81a89a66af94ecea90dc5d11'
719
+ const MANIFEST_DIGEST = '0645cd5db74c6b4e379efe781f6eada45759366e9123e2553a2b0801f81079f3'
720
+ const inventory = { models: [{ name: 'qwen3.5:2b-q4_K_M', digest: TAGS_DIGEST }] }
721
+
722
+ verifyOllamaModels(inventory, [{ name: 'qwen3.5:2b-q4_K_M', digest: TAGS_DIGEST }]) // undefined — the digest matches
723
+ let mismatch: unknown
724
+ try {
725
+ verifyOllamaModels(inventory, [{ name: 'qwen3.5:2b-q4_K_M', digest: MANIFEST_DIGEST }])
726
+ } catch (error) {
727
+ mismatch = error
728
+ }
729
+ isOllamaError(mismatch) && mismatch.code // 'model'
730
+ isOllamaError(mismatch) && mismatch.context?.received // [TAGS_DIGEST] — the inventory's digest, not the manifest file's SHA-256
731
+ ```
732
+
733
+ ### Read a retained unload reply
734
+
735
+ Each maintenance record keeps the exact bytes it sent and received, so you can check a reply after the session ends without trusting a reparsed copy. The reply in the following fence is the one the 0.40.8 runner returned to the 2026-10-23 unload.
736
+
737
+ ```ts
738
+ import { readOllamaTerminal, retainOllamaBytes } from '@orkestrel/ollama/server'
739
+
740
+ const model = {
741
+ name: 'qwen3.5:2b-q4_K_M',
742
+ digest: '78a738cc751e89cba72bb1feabf70f30c31772eb81a89a66af94ecea90dc5d11',
743
+ }
744
+ const request = retainOllamaBytes(
745
+ Buffer.from(JSON.stringify({ model: model.name, keep_alive: 0 })),
746
+ true,
747
+ )
748
+ Buffer.from(request.content, 'base64').toString() // '{"model":"qwen3.5:2b-q4_K_M","keep_alive":0}'
749
+ request.size // 44
750
+ const reply = retainOllamaBytes(
751
+ Buffer.from(
752
+ '{"model":"qwen3.5:2b-q4_K_M","created_at":"2026-10-23T14:08:45.7014834Z","response":"","done":true,"done_reason":"unload"}',
753
+ ),
754
+ true,
755
+ )
756
+ const timing = readOllamaTerminal(reply, model)
757
+ timing?.durations.total // undefined — the unload reply carries no timing fields
758
+ timing?.invalid // []
759
+ ```
760
+
761
+ `readOllamaTerminal` throws `OllamaError` with code `unload` when the reply names another model, ends without `done: true`, carries an `error` field, or is incomplete.
762
+
514
763
  ### Practices
515
764
 
516
765
  - **Import the engine from `@orkestrel/agent`, the wire from `@orkestrel/ollama`** — `ProviderInterface` / `ProviderResult` / `ProviderError` / `isProviderError` / `isProviderAbortError` live in `@orkestrel/agent`; `createOllama` / `OllamaProvider` / `OllamaOptions` live in `@orkestrel/ollama`.
@@ -533,15 +782,22 @@ The judge proofs cover the recorded wire and the live models.
533
782
  - [`tests/src/core/helpers.test.ts`](../tests/src/core/helpers.test.ts) compares the rendered prompts byte for byte and checks calibration, candidate-only normalization, label mapping, escaping, and identity.
534
783
  - [`tests/service/judge.test.ts`](../tests/service/judge.test.ts) requires Mica and the native decision model, reads the deletion noul within 0.005 of the desk's pinned No probability 0.9884, asks the native model every form in one request, and asserts Mica's System One HTTP 400 refusal. `OLLAMA_JUDGE_MODEL`, `OLLAMA_JUDGE_TEMPERATURE`, and `OLLAMA_DECISION_MODEL` override the recorded defaults. Mica warmup admits 300000 ms because the recorded cold load took 230850 ms.
535
784
 
536
- The hermetic projects — `src:core`, `setup`, `guides`, `conformance`, `policy`, and `config` — run with no daemon and no network. The `service` project requires a warm local Ollama and, for the page proof, a Chromium-family browser and a built `dist`; `distribution` packs and installs the artifact. The import guard reads `distribution` with the hermetic projects, because it never reaches the daemon; it leaves out `probe` and `service` alone, the two projects that may.
785
+ The daemon session proofs run in the `src:server` project against `tests/src/server/fixtures/daemon.mjs`, a self-contained fake daemon that the running Node executable starts on a loopback port, and in the `service` project against a real Ollama executable.
786
+
787
+ - [`tests/src/server/OllamaDaemon.test.ts`](../tests/src/server/OllamaDaemon.test.ts) drives the lifecycle over the fake daemon: a shared startup behind a delayed version reply, verification before any model request, retained maintenance including a refused unload, an overlapping unload, and a refused restart; ordered requests through an optional transport and its global control, exact maintenance bytes across re-chunked replies, a retained transport reference after caller mutation, and transport refusals at each send boundary, with startup joining the child before rejecting and unload leaving it running until `stop`; a mismatched inventory digest refused with `model`, metadata-only requests, and the owned child joined before startup rejects; a stale executable digest and an occupied port, each refused before a child exists; silence, an exit before listening, a bind failure, and an exit before answering, each refused with the owned child gone; executable bytes replaced through the declared path during use, which `stop` reports as `false` with a `digest` error; and a stop and an abort that each end the daemon and its runner while an unrelated process survives.
788
+ - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) proves the environment merge by case-folded name, the listen announcement match, the version and model comparisons, the bounded metadata exchange and its refusals, the retained bytes and their digests, the exact unload body, and the terminal reply check, over an in-process peer that answers the metadata routes.
789
+ - [`tests/src/server/validators.test.ts`](../tests/src/server/validators.test.ts), [`tests/src/server/factories.test.ts`](../tests/src/server/factories.test.ts), and [`tests/src/server/errors.test.ts`](../tests/src/server/errors.test.ts) prove the declaration refusals before any spawn, an unstarted session's release without a spawn, and the error brand, its context copy, its cause, and the guard's refusals.
790
+ - [`tests/service/OllamaDaemon.test.ts`](../tests/service/OllamaDaemon.test.ts) starts a dedicated real daemon, verifies its receipt, unloads every declared model with the exact request body, and requires a confirmed stop without generation. An explicit `OLLAMA_DAEMON_DECLARATION` JSON value wins. Otherwise the proof resolves `OLLAMA_DAEMON_EXECUTABLE` or the `ollama` executable on `PATH` to its canonical path, computes its SHA-256, reads the provisioned service at `OLLAMA_HOST` for its version and the selected model's `/api/tags` digest, and chooses an ephemeral loopback port apart from 11434 and the provisioned service port. It runs only in the `service` project.
791
+
792
+ The hermetic projects — `src:core`, `src:server`, `setup`, `guides`, `conformance`, `policy`, and `config` — run with no Ollama daemon and no network beyond loopback. The `src:server` project starts no Ollama executable: its lifecycle proofs spawn the fake daemon and bind loopback ports only. The `service` project requires a warm local Ollama and, for the page proof, a Chromium-family browser and a built `dist`, and for the daemon session proof an executable on `PATH`, an `OLLAMA_DAEMON_EXECUTABLE` path, or an explicit `OLLAMA_DAEMON_DECLARATION` value; `distribution` packs and installs the artifact. The import guard reads `distribution` with the hermetic projects, because it never reaches the daemon; it leaves out `probe` and `service` alone, the two projects that may.
537
793
 
538
- - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` bijection (value + type exports), the `OllamaProvider` method bijection, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `createOllama + generate` fence against the `@example` block of that title (pinned so the titled pair cannot be retired silently), and the README pitch against this guide's tagline. It also runs the flagship fences and asserts the values they log. It extracts each streaming fence from the README and from this guide and executes it, with its real imports and a global `fetch` the test replaces, over the turns the fence handles: this guide's over a plain, a reclassified, a cancelled, and a refused turn, and the README's, which has no `catch` arm, over the plain and the reclassified turn. It asserts the logged values, the deltas, the fence's message list, the whole body of each request, and the error the fence throws.
794
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` and `src/server` bijection (value + type exports), the `OllamaProvider`, `OllamaJudge`, and `OllamaDaemonInterface` method bijections, and the equality gate: every `Summary` cell against its declaration's description paragraph, each titled fence — `Ask Mica a noul` and `createOllama + generate` — against the `@example` block of that title, with `Ask Mica a noul` pinned so the titled pair cannot be retired silently, and the README pitch against this guide's tagline. It also runs the flagship fences and asserts the values they log. Of the server face's fences, it runs the declaration refusal, the model digest comparison, and the retained unload reply, none of which starts a process, and never runs the dedicated daemon fence, which starts a real executable. It extracts each streaming fence from the README and from this guide and executes it, with its real imports and a global `fetch` the test replaces, over the turns the fence handles: this guide's over a plain, a reclassified, a cancelled, and a refused turn, and the README's, which has no `catch` arm, over the plain and the reclassified turn. It asserts the logged values, the deltas, the fence's message list, the whole body of each request, and the error the fence throws.
539
795
  - [`tests/src/core/OllamaProvider.test.ts`](../tests/src/core/OllamaProvider.test.ts) — hermetic provider request-shape (including the opt-in `truncate` and `shift` keys), framing, transport-seam, abort, deadline, and unreachable-upstream coverage, plus the streaming fold — deltas, tool-call accumulation, done-line usage, the `done_reason: 'length'` truncation and its stray-reason refusals, the unterminated tail flush, and think separation — driven over a canned NDJSON transport. Its recording proxy uses a deliberately unreachable default and asserts only captured requests and local provider behavior.
540
796
  - [`tests/src/core/helpers.test.ts`](../tests/src/core/helpers.test.ts) — the wire leaves: the request projection and every response extraction, including each malformed-wire degradation, the `done_reason` reader's `'length'`, other-reason, missing, and non-string cases, and the terminal timing reader's verbatim nanoseconds, zero, absent, malformed, and `done` cases.
541
797
  - [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) — hermetic `createOllama` shape, identity, defaults, passthrough, and unreachable-upstream coverage.
542
798
  - [`tests/src/core/integration.test.ts`](../tests/src/core/integration.test.ts) — the `src/core` integration scope. A real `@orkestrel/server` + `@orkestrel/router` relay in front of this provider carries ordered content and thinking deltas, tools, and usage while keeping the browser and daemon credentials apart, refuses a wrong bearer with an empty `401` before the daemon transport is entered, crosses a browser cancel and a server deadline as an abort frame with its partial, reduces a daemon stream failure to the fixed public error frame, and replays a returned tool call. Beside those, a real agent over a canned NDJSON stream reports the model, meter, and budget layers in `result.stop`, and hermetic context framing, recap, reference, cherry-pick, canonical assembly order, scope allow-lists, workspace injection, and conversation selection run through the unreachable recording proxy, with a compile-time drift gate asserting the wire request and response shapes stay compatible with the official `ollama` client's types; that gate's authoritative check is `npm run check` (root tsc), while its run under vitest is an incidental no-op (`expectTypeOf` performs no runtime assertions).
543
799
  - [`tests/setup.test.ts`](../tests/setup.test.ts) — the shared host-independent test infrastructure, over the conversation padding, the window consumers, the throwing and recording summarizers, the workspace seeder, and the `PAGE_TOOL` definition `tests/setup.ts` exports, and over the host-independent half of `tests/setupServer.ts`: its request-narrowing guards, its refusing and streaming transports, its tool fixtures, its scripted agent stream and agent-stream driver, and its environment readers.
544
- - [`tests/setupServer.test.ts`](../tests/setupServer.test.ts) — the Node-resource half of `tests/setupServer.ts`: the recording proxy and the relay server on real loopback sockets, the captured and open transports, whose fixtures answer in memory, the capture wait, the provider-stream driver, the wire tables `WEATHER_TOOL` and the insatiable tool's chunk line, and the page fixture's Node half — the request recorder the relay server and the page fixture share, the derived import map, the served document's presence guards, the contained file route, the control route, the port reservation, the membership the page bounds declare — the attempt allowance, the release share beside it, the per-call shares spent inside them, the case bound that contains the pair, and the launch count the retry spends — the ordering the live controls' own intervals hold, the attempt deadline's own expiry error, the elapsed completion a bounded attempt reports on each interleaving that can outlast it (an acquisition that never settles, one that settles inside the release share, a release that parks past that share, and a release that crossed the allowance), each of those measured against the deadlines it had to wait out plus one named scheduling allowance, so what the case proves is timer-bounded completion rather than a nominal sum, the failure each of them keeps reachable as its cause — the attempt's own failure under a composed strand, and the deadline's `TimeoutError` under a crossed release — the reading each thrown failure carries into a composed message, the decision the bounded retry's predicate makes over an inert attempt record, including the interrupted run it refuses, the release order a real fixture takes when the release after it rejects, the evaluate boundary's narrowing and the deadline it carries, the guards that narrow what the page reports back, the live page proof's browser and build gates — a host with no browser and a tree with no build — the scenarios that capture the first request a provider sends through a recording proxy, each ending the call by aborting it, awaiting it, or leaving it open, the readers and runners that measure a packed package, proved against a dependency-free fixture package and a loopback registry that answers only `ping`, and the guard that keeps every hermetic project from importing `tests/setupService.ts`, which contacts the daemon when it loads: it takes the projects from `vite.config.ts`, follows every import form to a fixed point, and refuses the module as a setup file. It compares the files that references and setup paths resolve to, never their spelling, so a backslash separator, a letter case the host folds, and a query after the path cannot hide the module, while a `?raw` or `?url` import stays an asset. It refuses a `new URL(…, import.meta.url)` built at run time as it refuses a computed `import()`, apart from `vite.config.ts`, whose computed URL is inspected and listed.
800
+ - [`tests/setupServer.test.ts`](../tests/setupServer.test.ts) — the Node-resource half of `tests/setupServer.ts`: the daemon fixtures the `src:server` proofs use — the fake daemon declarations built without starting a child, and the in-process peer that answers the metadata and unload routes and records each request's exact bytes — the recording proxy and the relay server on real loopback sockets, the captured and open transports, whose fixtures answer in memory, the capture wait, the provider-stream driver, the wire tables `WEATHER_TOOL` and the insatiable tool's chunk line, and the page fixture's Node half — the request recorder the relay server and the page fixture share, the derived import map, the served document's presence guards, the contained file route, the control route, the port reservation, the membership the page bounds declare — the attempt allowance, the release share beside it, the per-call shares spent inside them, the case bound that contains the pair, and the launch count the retry spends — the ordering the live controls' own intervals hold, the attempt deadline's own expiry error, the elapsed completion a bounded attempt reports on each interleaving that can outlast it (an acquisition that never settles, one that settles inside the release share, a release that parks past that share, and a release that crossed the allowance), each of those measured against the deadlines it had to wait out plus one named scheduling allowance, so what the case proves is timer-bounded completion rather than a nominal sum, the failure each of them keeps reachable as its cause — the attempt's own failure under a composed strand, and the deadline's `TimeoutError` under a crossed release — the reading each thrown failure carries into a composed message, the decision the bounded retry's predicate makes over an inert attempt record, including the interrupted run it refuses, the release order a real fixture takes when the release after it rejects, the evaluate boundary's narrowing and the deadline it carries, the guards that narrow what the page reports back, the live page proof's browser and build gates — a host with no browser and a tree with no build — the scenarios that capture the first request a provider sends through a recording proxy, each ending the call by aborting it, awaiting it, or leaving it open, the readers and runners that measure a packed package, proved against a dependency-free fixture package and a loopback registry that answers only `ping`, and the guard that keeps every hermetic project from importing `tests/setupService.ts`, which contacts the daemon when it loads: it takes the projects from `vite.config.ts`, follows every import form to a fixed point, and refuses the module as a setup file. It compares the files that references and setup paths resolve to, never their spelling, so a backslash separator, a letter case the host folds, and a query after the path cannot hide the module, while a `?raw` or `?url` import stays an asset. It refuses a `new URL(…, import.meta.url)` built at run time as it refuses a computed `import()`, apart from `vite.config.ts`, whose computed URL is inspected and listed.
545
801
  - [`tests/setupService.test.ts`](../tests/setupService.test.ts) — the hermetic half of the `service` project's setup: the environment readers, the sampling tables, the readiness contract, and the live page gate's refusal of an origin the daemon refuses.
546
802
  - [`tests/conformance.test.ts`](../tests/conformance.test.ts) — where this package's wire types drift from the official `ollama` client they are written against.
547
803
  - [`tests/distribution.test.ts`](../tests/distribution.test.ts) — the packed package installed into a throwaway consumer: the exports map, the shipped declarations, and the module objects a real runtime hands that consumer.
@@ -561,6 +817,7 @@ The hermetic projects — `src:core`, `setup`, `guides`, `conformance`, `policy`
561
817
  - `@orkestrel/agent` — the `AgentProvider` engine this package extends, the `ProviderInterface` contract, the errors, and the relay. Its guide is mirrored here as [`agent.md`](agent.md) and ships with that package.
562
818
  - `@orkestrel/ndjson` — the parser `frame()` returns for each call. Its guide ships with that package.
563
819
  - `@orkestrel/contract` — the `isRecord` / `isString` / `isNumber` / `parseJSONAs` helpers that narrow the wire `unknown`. Its guide ships with that package.
820
+ - `@orkestrel/process` and `@orkestrel/emitter` — the process session the daemon session spawns through and the emitter that carries its lifecycle events. Each guide ships with its own package.
564
821
  - `@orkestrel/router` and `@orkestrel/server` — the dispatcher the relay handler mounts on and the listener that serves it, mirrored here as [`router.md`](router.md) and [`server.md`](server.md). Each ships with its own package.
565
822
  - [`AGENTS.md`](../AGENTS.md) — the coding rules this package is written to.
566
823
  - [`README.md`](README.md) — the guides index.