@orkestrel/scaffold 0.0.103 → 0.0.104
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.
- package/dist/agents/skills/orkestrel-dispatch/scripts/helpers.js +84 -1
- package/dist/agents/skills/orkestrel-dispatch/scripts/launch.js +21 -6
- package/dist/agents/skills/orkestrel-harden/scripts/titles.js +338 -0
- package/dist/agents/skills/orkestrel-publish/scripts/swap.js +3 -39
- package/dist/agents/skills/orkestrel-publish/scripts/wave.js +433 -47
- package/dist/agents/skills/orkestrel-publish/scripts/window.js +2 -1
- package/dist/bin/main.js +16 -9
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +3 -3
- package/dist/host/agents/skills/enterprise-bootstrap/references/components.md +2 -2
- package/dist/host/agents/skills/enterprise-bootstrap/references/inspection.md +1 -1
- package/dist/host/agents/skills/orkestrel-build/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-debrief/references/field-testing.md +1 -1
- package/dist/host/agents/skills/orkestrel-debrief/references/instruction-audit.md +3 -3
- package/dist/host/agents/skills/orkestrel-dispatch/SKILL.md +10 -10
- package/dist/host/agents/skills/orkestrel-dispatch/references/launch.md +1 -0
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/helpers.ts +100 -1
- package/dist/host/agents/skills/orkestrel-dispatch/scripts/launch.ts +27 -6
- package/dist/host/agents/skills/orkestrel-falsify/SKILL.md +4 -4
- package/dist/host/agents/skills/orkestrel-falsify/agents/openai.yaml +1 -1
- package/dist/host/agents/skills/orkestrel-falsify/references/brief.md +8 -7
- package/dist/host/agents/skills/orkestrel-falsify/references/reconcile.md +6 -6
- package/dist/host/agents/skills/orkestrel-harden/SKILL.md +6 -1
- package/dist/host/agents/skills/orkestrel-harden/references/bindings.md +1700 -0
- package/dist/host/agents/skills/orkestrel-harden/references/contract.md +2 -2
- package/dist/host/agents/skills/orkestrel-harden/references/equivalence.md +48 -0
- package/dist/host/agents/skills/orkestrel-harden/references/hardening.md +4 -2
- package/dist/host/agents/skills/orkestrel-harden/scripts/titles.ts +425 -0
- package/dist/host/agents/skills/orkestrel-journey/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-journey/references/captures.md +9 -8
- package/dist/host/agents/skills/orkestrel-polish/SKILL.md +3 -3
- package/dist/host/agents/skills/orkestrel-publish/SKILL.md +2 -2
- package/dist/host/agents/skills/orkestrel-publish/references/release.md +1 -0
- package/dist/host/agents/skills/orkestrel-publish/references/wave.md +56 -13
- package/dist/host/agents/skills/orkestrel-publish/references/window.md +6 -6
- package/dist/host/agents/skills/orkestrel-publish/scripts/swap.ts +5 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/wave.ts +646 -49
- package/dist/host/agents/skills/orkestrel-publish/scripts/window.ts +2 -1
- package/dist/host/agents/transports/codex.md +1 -1
- package/dist/host/claude/agents/orkestrel.md +54 -53
- package/dist/host/claude/agents/reviewer.md +4 -4
- package/dist/host/claude/rules/documentation.md +2 -1
- package/dist/host/claude/rules/patterns.md +1 -1
- package/dist/host/claude/rules/quality.md +2 -2
- package/dist/host/claude/rules/styles.md +1 -1
- package/dist/host/claude/rules/tests.md +34 -8
- package/dist/host/claude/rules/typescript.md +29 -0
- package/dist/host/claude/rules/workspace.md +32 -22
- package/dist/host/claude/rules/writing.md +34 -26
- package/dist/host/claude/settings.json +2 -0
- package/dist/host/claude/skills/orkestrel-debrief/SKILL.md +1 -1
- package/dist/host/claude/skills/orkestrel-falsify/SKILL.md +1 -1
- package/dist/host/codex/agents/analyst.toml +3 -3
- package/dist/host/codex/agents/reviewer.toml +4 -4
- package/dist/host/configs/policy.ts +13 -2
- package/dist/host/dotfiles/editorconfig +0 -1
- package/dist/host/dotfiles/gitattributes +0 -1
- package/dist/host/dotfiles/gitignore +0 -1
- package/dist/host/dotfiles/oxlintrc.json +24 -0
- package/dist/host/guides/agent.md +1638 -598
- package/dist/host/guides/budget.md +89 -29
- package/dist/host/guides/form.md +477 -91
- package/dist/host/guides/mcp.md +14 -2
- package/dist/host/guides/ollama.md +97 -73
- package/dist/host/guides/probe.md +1 -1
- package/dist/host/guides/scaffold.md +158 -101
- package/dist/host/guides/study.md +2635 -0
- package/dist/host/guides/table.md +225 -15
- package/dist/host/guides/terminal.md +22 -20
- package/dist/host/guides/test.md +689 -255
- package/dist/host/manifest.json +87 -63
- package/dist/host/tests/config.test.ts +136 -82
- package/dist/host/tests/policy.test.ts +39 -1
- package/dist/host/tests/setupPolicy.ts +86 -28
- package/dist/src/core/index.cjs +1387 -611
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +35 -20
- package/dist/src/core/index.d.ts +35 -20
- package/dist/src/core/index.js +1385 -612
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +12 -12
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +12 -12
- package/dist/src/server/index.d.ts +12 -12
- package/dist/src/server/index.js +12 -12
- package/dist/src/server/index.js.map +1 -1
- package/package.json +4 -4
package/dist/host/guides/mcp.md
CHANGED
|
@@ -34,8 +34,11 @@ boundary that wraps it with `JSON.parse` / `JSON.stringify` plus the parse
|
|
|
34
34
|
the `id` it could not read. The client mirrors it: `connect` negotiates the modern revision, `tools()`
|
|
35
35
|
exposes the remote tools as local `ToolInterface`s, and `call` runs one — a
|
|
36
36
|
remote failure throws locally, so an agent's `ToolManager` isolates it exactly
|
|
37
|
-
like a local throw. A remote
|
|
38
|
-
|
|
37
|
+
like a local throw. A remote tool result with `isError: true` throws `MCPToolError`
|
|
38
|
+
(an `Error` subclass with `code` `'TOOL'`, recognized by `isMCPToolError`), so a
|
|
39
|
+
caller separates a tool refusal from a transport failure. A remote JSON-RPC error
|
|
40
|
+
rejects with `MCPError`, preserving its numeric `code` and optional `error.data`
|
|
41
|
+
as `context`.
|
|
39
42
|
|
|
40
43
|
**The wire lives one layer out.** [`src/server`](../src/server) carries the
|
|
41
44
|
Node transports and [`src/browser`](../src/browser) the browser face.
|
|
@@ -2246,6 +2249,7 @@ Passing a legacy revision to
|
|
|
2246
2249
|
| `MCPClient` | class | Connects to a remote MCP server over any injected `MCPMessageTransportInterface`, negotiates the modern revision, and exposes the server's tools as local `ToolInterface`s an agent can run. |
|
|
2247
2250
|
| `MCPTaskClient` | class | Issues the `tasks/*` methods over one correlated-request door — the client half of the stable Tasks extension, exposed as an `MCPClientInterface`'s `tasks`. |
|
|
2248
2251
|
| `HTTPClientTransport` | class | Drives a remote Streamable-HTTP MCP server over `fetch` — a client `MCPMessageTransportInterface` for the Model Context Protocol, the egress mirror of the server's `createMCPRoutes`. |
|
|
2252
|
+
| `MCPToolError` | class | Distinguishes a completed remote tool call that the peer itself reported as failed. |
|
|
2249
2253
|
| `MCPError` | class | Preserves a Model Context Protocol error's machine-readable numeric code and optional structured context. |
|
|
2250
2254
|
|
|
2251
2255
|
### Constants
|
|
@@ -2379,6 +2383,7 @@ A `Shape` cell holds the constant's declared type.
|
|
|
2379
2383
|
| `isModernRequest` | function | Determines whether a JSON-RPC invocation uses the modern per-request MCP wire shape. |
|
|
2380
2384
|
| `isMCPModernVersion` | function | Determines whether a value is a modern protocol revision accepted by a bare server. |
|
|
2381
2385
|
| `isMCPLegacyVersion` | function | Determines whether a value is a revision accepted by the optional legacy decorator. |
|
|
2386
|
+
| `isMCPToolError` | function | Determines whether an unknown value is an `MCPToolError`. |
|
|
2382
2387
|
| `isMCPError` | function | Determines whether an unknown value is an `MCPError`. |
|
|
2383
2388
|
| `parseJSONRPCMessage` | function | Narrows an already-parsed value to a `JSONRPCMessage`, or `undefined` when it is not one. |
|
|
2384
2389
|
| `parseRequestContext` | function | Parses the reserved modern request metadata into an `MCPRequestContext`. |
|
|
@@ -4502,7 +4507,9 @@ import {
|
|
|
4502
4507
|
isJSONRPCMessage,
|
|
4503
4508
|
isJSONRPCResponse,
|
|
4504
4509
|
isMCPError,
|
|
4510
|
+
isMCPToolError,
|
|
4505
4511
|
MCPError,
|
|
4512
|
+
MCPToolError,
|
|
4506
4513
|
} from '@orkestrel/mcp'
|
|
4507
4514
|
import { createToolManager } from '@orkestrel/tool'
|
|
4508
4515
|
|
|
@@ -4519,6 +4526,11 @@ const remote = new MCPError('Method not found', -32601, { method: 'missing' })
|
|
|
4519
4526
|
isMCPError(remote) // true
|
|
4520
4527
|
remote.code // -32601
|
|
4521
4528
|
remote.context // { method: 'missing' }
|
|
4529
|
+
|
|
4530
|
+
const refusal = new MCPToolError('tool exploded')
|
|
4531
|
+
isMCPToolError(refusal) // true
|
|
4532
|
+
isMCPError(refusal) // false
|
|
4533
|
+
refusal.code // 'TOOL'
|
|
4522
4534
|
```
|
|
4523
4535
|
|
|
4524
4536
|
### Route a request by era and build a modern result
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
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.
|
|
7
7
|
|
|
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
|
|
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
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.
|
|
11
11
|
|
|
@@ -29,6 +29,7 @@ result.usage // { prompt, completion, total } when the wire reported counts —
|
|
|
29
29
|
`generate` resolves the assembled `ProviderResult` (content + any tool calls + any usage). For live output, `stream` is the same call streamed: it yields `ProviderDelta`s (`content` for answer text, `thinking` for live reasoning) and returns the assembled result when the stream completes. Drive the generator and read its terminal `value`:
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
|
+
import type { ProviderDelta } from '@orkestrel/agent'
|
|
32
33
|
import { createAbort } from '@orkestrel/abort'
|
|
33
34
|
import { isProviderAbortError } from '@orkestrel/agent'
|
|
34
35
|
import { createOllama } from '@orkestrel/ollama'
|
|
@@ -36,24 +37,31 @@ import { createOllama } from '@orkestrel/ollama'
|
|
|
36
37
|
const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })
|
|
37
38
|
const abort = createAbort()
|
|
38
39
|
const messages = [{ id: '1', role: 'user', content: 'Say hello.' }] as const
|
|
39
|
-
const
|
|
40
|
-
const reasoning: string[] = []
|
|
40
|
+
const deltas: ProviderDelta[] = []
|
|
41
41
|
|
|
42
42
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (step.
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const stream = provider.stream(messages, abort.signal)
|
|
44
|
+
for (;;) {
|
|
45
|
+
const step = await stream.next()
|
|
46
|
+
if (step.done) {
|
|
47
|
+
const result = step.value // the assembled ProviderResult
|
|
48
|
+
const answer = deltas
|
|
49
|
+
.filter((delta) => delta.channel === 'content')
|
|
50
|
+
.map((delta) => delta.text)
|
|
51
|
+
const reasoning = deltas
|
|
52
|
+
.filter((delta) => delta.channel === 'thinking')
|
|
53
|
+
.map((delta) => delta.text)
|
|
54
|
+
console.log(result.content) // the answer — the settled content is the authoritative one
|
|
55
|
+
console.log(answer.join('')) // what arrived on the content channel, which a reclassified <think> span leaves longer
|
|
56
|
+
console.log(reasoning.join('')) // what arrived on the thinking channel
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
deltas.push(step.value)
|
|
49
60
|
}
|
|
50
|
-
const result = step.value // the assembled ProviderResult
|
|
51
|
-
result.content // the answer — the settled content is the authoritative one
|
|
52
|
-
answer.join('') // what arrived on the content channel, which a reclassified <think> span leaves longer
|
|
53
61
|
} catch (error) {
|
|
54
62
|
if (isProviderAbortError(error)) {
|
|
55
63
|
const recovered = error.partial.content // everything that streamed before the cancel
|
|
56
|
-
|
|
64
|
+
console.log(recovered)
|
|
57
65
|
} else throw error
|
|
58
66
|
}
|
|
59
67
|
```
|
|
@@ -66,27 +74,32 @@ Pass `tools` (a non-empty `ToolDefinition[]` from `@orkestrel/tool`) to advertis
|
|
|
66
74
|
|
|
67
75
|
A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`. An extended interface's name comes before `plus`, with the members it adds after. A function row's `Shape` cell holds its signature, and a guard row's the type it narrows to. A class row's `Shape` cell holds the interface it implements, or its constructor signature where it implements none. A `Shape` cell holds the constant's declared type.
|
|
68
76
|
|
|
69
|
-
| API | Kind | Shape
|
|
70
|
-
| -------------------- | --------- |
|
|
71
|
-
| `createOllama` | function | `(options: OllamaOptions) => ProviderInterface`
|
|
72
|
-
| `OllamaProvider` | class | `AgentProviderInterface`
|
|
73
|
-
| `OllamaOptions` | interface | `ProviderOptions plus { model, url?, keepAlive?, options?, think? }` | Represents the configuration `createOllama` accepts for the local Ollama backend. |
|
|
74
|
-
| `WireChatRequest` | interface | `{ model, messages, stream, keep_alive, think, options?, tools?, format? }` | Represents the exact `POST /api/chat` request body `OllamaProvider` sends — the internal typed wire contract. |
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
|
|
85
|
-
`
|
|
77
|
+
| API | Kind | Shape | Summary |
|
|
78
|
+
| -------------------- | --------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
79
|
+
| `createOllama` | function | `(options: OllamaOptions) => ProviderInterface` | Creates a local Ollama inference provider — a `ProviderInterface` over the daemon's `POST /api/chat`, assembling `generate` from the same NDJSON engine as `stream`. |
|
|
80
|
+
| `OllamaProvider` | class | `AgentProviderInterface` | Implements the Ollama `/api/chat` wire over the shared `AgentProvider` engine. |
|
|
81
|
+
| `OllamaOptions` | interface | `ProviderOptions plus { model, url?, keepAlive?, options?, think?, truncate?, shift? }` | Represents the configuration `createOllama` accepts for the local Ollama backend. |
|
|
82
|
+
| `WireChatRequest` | interface | `{ model, messages, stream, keep_alive, think, options?, tools?, format?, truncate?, shift? }` | Represents the exact `POST /api/chat` request body `OllamaProvider` sends — the internal typed wire contract. |
|
|
83
|
+
| `OllamaDurations` | interface | `{ total, load, prompt, decode }` | Carries the daemon durations of one terminal record in raw nanoseconds. |
|
|
84
|
+
| `OllamaCounts` | interface | `{ prompt, decode, cached }` | Carries the token counts of one terminal record. |
|
|
85
|
+
| `OllamaTiming` | interface | `{ durations, counts, invalid }` | Carries the timing observation of one terminal record. |
|
|
86
|
+
| `DEFAULT_OLLAMA_URL` | const | `string` | Names the local Ollama daemon base URL, `'http://localhost:11434'`, assumed when `OllamaOptions.url` is omitted. |
|
|
87
|
+
| `DEFAULT_KEEP_ALIVE` | const | `string` | Names how long the model stays resident after a call — `'5m'` when `OllamaOptions.keepAlive` is omitted, Ollama's own `keep_alive` default, expressed as a duration string. |
|
|
88
|
+
| `OLLAMA_CHAT_PATH` | const | `string` | Names the Ollama chat endpoint appended to the configured base URL. |
|
|
89
|
+
| `mapMessages` | function | `(messages: readonly Message[]) => WireChatRequest['messages']` | Maps conversation turns onto the `/api/chat` wire's minimal message shape. |
|
|
90
|
+
| `extractContent` | function | `(record: Readonly<Record<string, unknown>>) => string` | Extracts the assistant text of one wire record. |
|
|
91
|
+
| `extractThinking` | function | `(record: Readonly<Record<string, unknown>>) => string` | Extracts the daemon-side reasoning of one wire record. |
|
|
92
|
+
| `extractUsage` | function | `(record: Readonly<Record<string, unknown>>) => TokenUsage \| undefined` | Extracts the token usage of one wire record. |
|
|
93
|
+
| `extractTruncated` | function | `(record: Readonly<Record<string, unknown>>) => true \| undefined` | Extracts whether one terminal wire record reports that the model ended its answer at its own token limit. |
|
|
94
|
+
| `extractTiming` | function | `(record: Readonly<Record<string, unknown>>) => OllamaTiming \| undefined` | Extracts the daemon timing of one terminal wire record. |
|
|
95
|
+
| `extractTools` | function | `(record: Readonly<Record<string, unknown>>) => readonly ToolCall[]` | Extracts the tool calls of one wire record's `message.tool_calls`. |
|
|
96
|
+
| `extractArguments` | function | `(value: unknown) => Readonly<Record<string, unknown>>` | Extracts a wire `arguments` value as a record. |
|
|
97
|
+
|
|
98
|
+
`OllamaOptions` extends `ProviderOptions`, so a caller also passes the base's own keys through it: `timeout` (the per-call deadline), `fetch` (the transport), `headers` (the per-request header hook), and `replay` (the stored-thinking policy). [`agent.md`](agent.md) states each one.
|
|
86
99
|
|
|
87
100
|
The wire leaves — `mapMessages` and the `extract*` narrowing set — are the pure request projection and response extractions `OllamaProvider` composes. They are exported and unit-tested on their own so the package's highest-risk code, the boundary narrowing of every wire `unknown` through the `@orkestrel/contract` guards, is provable without a daemon. Everything they do not do lives in `@orkestrel/agent`: the deadline, the transport seam, the streaming spine, the splitter, and the assembly of a `ProviderResult`.
|
|
88
101
|
|
|
89
|
-
`OllamaProvider` declares `name` (`'ollama'`) and inherits the rest of its data members from the base: `id` is the stable per-instance trace label, and `
|
|
102
|
+
`OllamaProvider` declares `name` (`'ollama'`) and inherits the rest of its data members from the base: `id` is the stable per-instance trace label, and `replay` selects which stored assistant reasoning reaches the provider input (default `'none'`). The Ollama request mapper omits stored reasoning from the wire regardless of that policy. Its wire seams are documented under [Methods](#methods), and the `generate` / `stream` boundary it inherits under `AgentProviderInterface` in [`agent.md`](agent.md). `AgentProvider` / `AgentProviderInterface` / `ProviderInterface` / `ProviderOptions` / `ProviderRequest` / `ProviderIncrement` / `ProviderParserInterface` / `ProviderResult` / `ProviderDelta` / `ProviderStreamOptions` / `ProviderError` / `isProviderError` / `ProviderAbortError` / `isProviderAbortError` / `Message` / `ContextSectionFormat` / `InstructionInterface` are owned by `@orkestrel/agent`; `ToolDefinition` / `ToolCall` by `@orkestrel/tool`; the `TokenUsage` shape by `@orkestrel/budget`; `createNDJSONParser` by `@orkestrel/ndjson`. All are reused here, never redefined. The agent package also owns the `JudgeInterface` interface, `JudgeRequest` type, `JudgeResult` interface, `JudgeAnswer` type, `Refusal` interface, `AgentJudge` class, `JudgeError` class, `JudgeAbortError` class, and `computeReading` function.
|
|
90
103
|
|
|
91
104
|
The raw judge exports the following declarations.
|
|
92
105
|
|
|
@@ -117,49 +130,50 @@ The wire seams `OllamaProvider` declares — exactly the members of `AgentProvid
|
|
|
117
130
|
|
|
118
131
|
#### `OllamaProvider`
|
|
119
132
|
|
|
120
|
-
The `frame` method opens the call's framing state and the `finish` method drains it; the `
|
|
133
|
+
The `frame` method opens the call's framing state and the `finish` method drains it; the `encode` method writes the request and the `read` method reads one record back.
|
|
121
134
|
|
|
122
|
-
| Method | Returns | Summary
|
|
123
|
-
| -------- | -------------------------------------------------- |
|
|
124
|
-
| `frame` | `ProviderParserInterface` | Creates fresh NDJSON framing state for a call.
|
|
125
|
-
| `
|
|
126
|
-
| `read` | `ProviderIncrement` | Extracts a record's content, reasoning, tools,
|
|
127
|
-
| `finish` | `ReadonlyArray<Readonly<Record<string, unknown>>>` | Recovers a final NDJSON record that arrived without its line terminator.
|
|
135
|
+
| Method | Returns | Summary |
|
|
136
|
+
| -------- | -------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
137
|
+
| `frame` | `ProviderParserInterface` | Creates fresh NDJSON framing state for a call. |
|
|
138
|
+
| `encode` | `WireChatRequest` | Projects conversation turns and per-call options onto the Ollama request body. |
|
|
139
|
+
| `read` | `ProviderIncrement` | Extracts a record's content, reasoning, tools, completed usage report, and truncation. |
|
|
140
|
+
| `finish` | `ReadonlyArray<Readonly<Record<string, unknown>>>` | Recovers a final NDJSON record that arrived without its line terminator. |
|
|
128
141
|
|
|
129
142
|
#### `OllamaJudge`
|
|
130
143
|
|
|
131
144
|
The engine calls these seams for each question. Call the inherited `ask` method to validate the full request, issue the calls, and merge their answers and usage.
|
|
132
145
|
|
|
133
|
-
| Method
|
|
134
|
-
|
|
|
135
|
-
| `
|
|
136
|
-
| `read`
|
|
146
|
+
| Method | Returns | Summary |
|
|
147
|
+
| -------- | --------------------- | --------------------------------------------------------------------------- |
|
|
148
|
+
| `encode` | `WireGenerateRequest` | Projects a single question onto Mica's raw generate request. |
|
|
149
|
+
| `read` | `JudgeResult` | Decodes a completed raw response into an answer or a missing-label refusal. |
|
|
137
150
|
|
|
138
151
|
## Contract
|
|
139
152
|
|
|
140
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.
|
|
141
154
|
|
|
142
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.
|
|
143
|
-
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` / `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.
|
|
144
|
-
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. 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.
|
|
145
|
-
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 reasoning
|
|
156
|
+
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
|
+
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
|
+
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.
|
|
146
159
|
5. **One provider NDJSON path.** Every provider call consumes NDJSON — one JSON object per `\n`-terminated line. `frame()` returns `createNDJSONParser()`, fresh per call, so no call inherits another's half-read record, and the base pairs it with a streaming `TextDecoder` so a record split across byte reads is reassembled. At end of input the base calls `finish(parser)`, which feeds a trailing `\n` through the parser: a non-conformant proxy's final unterminated line is recovered rather than silently dropped. `OllamaProvider` passes no `strict` to `super`, so a stream that ends without a settled record still assembles its result from the deltas it carried.
|
|
147
160
|
6. **`stream` yields deltas and returns the assembled result.** The engine yields each non-empty clean content delta as `{ channel: 'content', text }` and each daemon-side reasoning delta as `{ channel: 'thinking', text }`; its return value is the assembled `ProviderResult` whose `content` is the splitter's authoritative clean accumulation — the concatenation of the yielded content deltas, except across an implicit-open reclassification, where the reasoning prefix had already streamed before a bare `</think>` revealed it and the yields cannot be recalled — plus any tool calls collected across lines, the usage from the `done` line, and the separated `thinking` when the turn produced any.
|
|
148
|
-
7. **Usage 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.
|
|
161
|
+
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.
|
|
149
162
|
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".
|
|
150
163
|
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.
|
|
151
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.
|
|
152
|
-
11. **Context
|
|
165
|
+
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.
|
|
153
166
|
12. **Event-free.** A pure functional boundary — no Emitter, no events. Each call is a function of its arguments.
|
|
154
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)).
|
|
155
168
|
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).
|
|
156
|
-
15. **Doc ↔ source method bijection.** Each `## Methods` table lists the methods its class declares: `frame`, `
|
|
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.
|
|
157
170
|
|
|
158
171
|
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.
|
|
159
172
|
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.
|
|
160
173
|
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.
|
|
161
174
|
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.
|
|
162
175
|
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.
|
|
163
177
|
|
|
164
178
|
## Patterns
|
|
165
179
|
|
|
@@ -278,7 +292,7 @@ const response = {
|
|
|
278
292
|
computeAnswer(question, extractTopLogprobs(response)) // { form: 'noul', noul: 0.5 }
|
|
279
293
|
const judge = new OllamaJudge(options)
|
|
280
294
|
const request = { state: 'Approved.', questions: { approval: question } }
|
|
281
|
-
judge.
|
|
295
|
+
judge.encode(request).prompt === prompt // true
|
|
282
296
|
judge.read(response, request).model === identity // true
|
|
283
297
|
```
|
|
284
298
|
|
|
@@ -335,7 +349,7 @@ The seam members are ordinary methods, so a test or a debugging session can read
|
|
|
335
349
|
import { OllamaProvider } from '@orkestrel/ollama'
|
|
336
350
|
|
|
337
351
|
const provider = new OllamaProvider({ model: 'qwen3.5:2b-q4_K_M', keepAlive: '9m' })
|
|
338
|
-
const request = provider.
|
|
352
|
+
const request = provider.encode({
|
|
339
353
|
messages: [{ id: '1', role: 'user', content: 'Say hello.' }],
|
|
340
354
|
options: { think: true },
|
|
341
355
|
})
|
|
@@ -441,25 +455,33 @@ The `headers` hook is called once per request (async, so a token can be refreshe
|
|
|
441
455
|
|
|
442
456
|
### Context framing
|
|
443
457
|
|
|
444
|
-
|
|
458
|
+
Configure the instruction manager's `format` option to change the prompt text the agent builds. The provider accepts the resulting messages; see [Agent](agent.md) for the manager's rendering cascade.
|
|
445
459
|
|
|
446
460
|
```ts
|
|
461
|
+
import type { ContextSectionFormat, InstructionInterface } from '@orkestrel/agent'
|
|
462
|
+
import { createAgent, createInstructionManager } from '@orkestrel/agent'
|
|
447
463
|
import { createOllama } from '@orkestrel/ollama'
|
|
448
464
|
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
465
|
+
const format: ContextSectionFormat<InstructionInterface> = {
|
|
466
|
+
open: '<instructions>',
|
|
467
|
+
render: (instruction) => `<instruction>${instruction.content}</instruction>`,
|
|
468
|
+
close: '</instructions>',
|
|
469
|
+
}
|
|
470
|
+
const instructions = createInstructionManager({ format })
|
|
471
|
+
instructions.add({ name: 'concise', content: 'Answer briefly.' })
|
|
472
|
+
const provider = createOllama({ model: 'qwen3.5:2b-q4_K_M' })
|
|
473
|
+
const agent = createAgent(provider, { instructions })
|
|
474
|
+
agent.context
|
|
475
|
+
.build()
|
|
476
|
+
.some(
|
|
477
|
+
(message) =>
|
|
478
|
+
message.content.includes('<instructions>') &&
|
|
479
|
+
message.content.includes('<instruction>Answer briefly.</instruction>') &&
|
|
480
|
+
message.content.includes('</instructions>'),
|
|
481
|
+
) // true
|
|
460
482
|
```
|
|
461
483
|
|
|
462
|
-
|
|
484
|
+
The wire `format` field carries the JSON schema from the per-call `ProviderStreamOptions.schema` option. The provider omits that field when no schema is supplied. Instruction framing renders inside message content.
|
|
463
485
|
|
|
464
486
|
### Narrowing a failed call with `isProviderError`
|
|
465
487
|
|
|
@@ -497,8 +519,10 @@ A cancel is outside that taxonomy: the caller's signal and the deadline both thr
|
|
|
497
519
|
- **Recover the stream's partial** — wrap a driven `stream` in `try`/`catch` and narrow with `isProviderAbortError` to keep the content that arrived before a cancel.
|
|
498
520
|
- **Branch on the code and the status, not the message** — narrow a caught value with `isProviderError` and read `error.code` and `error.status` (`404` for an unpulled model) instead of parsing `error.message`.
|
|
499
521
|
- **Fold usage into a budget** — `result.usage` is the `TokenUsage` shape; `consume` it per turn to enforce a token ceiling.
|
|
500
|
-
- **Give a thinking model headroom** — with `think: true`, a reasoning model (for example `qwen3.5:2b` at `temperature: 0`) can spend 200+ tokens on `message.thinking` before any `content` token arrives; a tight `num_predict` cap
|
|
522
|
+
- **Give a thinking model headroom** — with `think: true`, a reasoning model (for example `qwen3.5:2b` at `temperature: 0`) can spend 200+ tokens on `message.thinking` before any `content` token arrives; a tight `num_predict` cap ends the turn before content starts, yielding empty `result.content` with `result.truncated` set, and a tight external token budget exhausts on the usage the turn reports. Size `num_predict` and the budget with thinking overhead in mind for any model run with `think: true`.
|
|
523
|
+
- **Read the model layer** — a `num_predict` cap reached sets `result.truncated`, and an agent run ending there reports `stop.layer: 'model'`. A prompt the daemon truncates under the default `truncate: true` returns normally and is not reported.
|
|
501
524
|
- **Tune through `options`** — pass sampling parameters (`temperature`, `seed`, and `num_predict`) through `OllamaOptions.options`; they forward verbatim to the wire.
|
|
525
|
+
- **Refuse instead of truncating** — set `OllamaOptions.truncate` or `OllamaOptions.shift` to `false` to ask the daemon to refuse an oversized prompt or a window shift. Leave them unset to keep the daemon default. The provider only forwards the key; whether a given daemon honors `false` is a live measurement.
|
|
502
526
|
- **Observe at the call site** — the provider publishes no events; read `stream`'s deltas and its returned result for everything a turn produced.
|
|
503
527
|
|
|
504
528
|
## Tests
|
|
@@ -509,16 +533,16 @@ The judge proofs cover the recorded wire and the live models.
|
|
|
509
533
|
- [`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.
|
|
510
534
|
- [`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.
|
|
511
535
|
|
|
512
|
-
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.
|
|
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.
|
|
513
537
|
|
|
514
|
-
- [`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
|
|
515
|
-
- [`tests/src/core/OllamaProvider.test.ts`](../tests/src/core/OllamaProvider.test.ts) — hermetic provider request-shape, framing, transport-seam, abort, deadline, and unreachable-upstream coverage, plus the streaming fold — deltas, tool-call accumulation, done-line usage, 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.
|
|
516
|
-
- [`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.
|
|
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.
|
|
539
|
+
- [`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
|
+
- [`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.
|
|
517
541
|
- [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) — hermetic `createOllama` shape, identity, defaults, passthrough, and unreachable-upstream coverage.
|
|
518
|
-
- [`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, 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).
|
|
519
|
-
- [`tests/setup.test.ts`](../tests/setup.test.ts) — the shared host-independent test infrastructure, over the conversation padding, 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.
|
|
520
|
-
- [`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,
|
|
521
|
-
- [`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
|
|
542
|
+
- [`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
|
+
- [`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.
|
|
545
|
+
- [`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.
|
|
522
546
|
- [`tests/conformance.test.ts`](../tests/conformance.test.ts) — where this package's wire types drift from the official `ollama` client they are written against.
|
|
523
547
|
- [`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.
|
|
524
548
|
- [`tests/policy.test.ts`](../tests/policy.test.ts) and [`tests/config.test.ts`](../tests/config.test.ts) — the vendored cross-cutting proofs: the path- and text-shaped repository laws, and that the root configuration resolves its aliases, projects, and outputs.
|
|
@@ -528,7 +552,7 @@ The hermetic projects — `src:core`, `setup`, `guides`, `conformance`, `policy`
|
|
|
528
552
|
- [`tests/service/transport.test.ts`](../tests/service/transport.test.ts) — the transparent wire proxy, end-to-end. A `createRecordingProxy(OLLAMA_CONFIG.host)` — a real `@orkestrel/server` + `@orkestrel/router` HTTP server — records the inbound request, forwards it to the selected Ollama service, and streams the response back.
|
|
529
553
|
- [`tests/service/page.test.ts`](../tests/service/page.test.ts) — the live page receipt, in a real Chrome or Edge that `@orkestrel/browser` launches against a `127.0.0.1` fixture serving the page, the installed `@orkestrel` module tree, this workspace's own build, a `/control` route, and the authenticated relay from one origin. It proves that the published closure evaluates in the page with the page error recorder and the console recorder quiet, that the page parks exactly the operation table this proof evaluates by name, read back out of the running browser, that an `Agent` holding a page-defined DOM-writing `Tool` completes a tool loop against the live daemon over the same-origin relay and carries the minted receipt into the next turn's `role: 'tool'` message on the relay wire and the daemon wire, that the browser log, the page's Resource Timing drain, and the fixture's own record each report a request the page deliberately made, and the page error recorder and the console recorder each report a fault the page deliberately produced, that a wrong bearer is refused before the daemon is contacted, and that `createOllama` in the page answers a prompt on the daemon's own origin. Controls beside those drive a real browser through each interleaving the attempt bound covers, and assert the elapsed time of the whole call against the allowance plus the release share every time: an allowance too short to finish reads the attempt's own expiry error back rather than an inner browser timeout; an allowance the observation outlasts leaves the session released; an acquisition that settles after the allowance is released rather than abandoned, and is never reported as stranded; and an attempt whose release crossed the allowance reports that release rather than the answer the observation had already returned. Every precondition is a hard throw naming its fix — the daemon and model from `tests/setupService.ts`, the browser from `requirePageBrowser`, the build from `requireBuild`, and the daemon's permission for the page origin from `requireDaemonOrigin` — so a host missing one fails the project rather than skipping.
|
|
530
554
|
- [`tests/service/tools.test.ts`](../tests/service/tools.test.ts) and [`tests/service/authority.test.ts`](../tests/service/authority.test.ts) — live tool dispatch through the agent loop, and the authority gate's approval and denial paths.
|
|
531
|
-
- [`tests/service/budget.test.ts`](../tests/service/budget.test.ts) and [`tests/service/lifecycle.test.ts`](../tests/service/lifecycle.test.ts) — live token-budget enforcement mid-stream
|
|
555
|
+
- [`tests/service/budget.test.ts`](../tests/service/budget.test.ts) and [`tests/service/lifecycle.test.ts`](../tests/service/lifecycle.test.ts) — live token-budget enforcement on the reported usage, the meter layer's mid-stream trip under `allowance: budget`, the daemon's `done_reason: 'length'` at a `num_predict` cap with its `result.truncated` and model-layer `stop`, and the agent's status, event, and abort lifecycle.
|
|
532
556
|
- [`tests/service/schema.test.ts`](../tests/service/schema.test.ts) and [`tests/service/scopes.test.ts`](../tests/service/scopes.test.ts) — a live structured-output `schema` constraining the answer, and a live scope filtering what reaches the wire.
|
|
533
557
|
- [`tests/service/compaction.test.ts`](../tests/service/compaction.test.ts) and [`tests/service/conversation.test.ts`](../tests/service/conversation.test.ts) — live recap retention with manual, automatic, and repeated compaction, and live non-fatal compaction errors and long-conversation instruction behavior, with proxy calls explicitly forwarding to `OLLAMA_CONFIG.host`.
|
|
534
558
|
|
|
@@ -746,7 +746,7 @@ const claim: Claim = {
|
|
|
746
746
|
const probe = new Probe({ workspace: process.cwd() })
|
|
747
747
|
const verdict = await probe.prove(claim)
|
|
748
748
|
verdict.digest // 'bdf03e5dfd6bd413ead671c7a2940fcf'
|
|
749
|
-
verdict.receipt // 'probe:bdf03e5dfd6bd413ead671c7a2940fcf:type:typescript@6.0.3:oxlint@1.
|
|
749
|
+
verdict.receipt // 'probe:bdf03e5dfd6bd413ead671c7a2940fcf:type:typescript@6.0.3:oxlint@1.89.0:vitest@4.1.12:configs/src/tsconfig.core.json@434f59254d58cf2683d453a26bd0d837'
|
|
750
750
|
await probe.destroy()
|
|
751
751
|
```
|
|
752
752
|
|