@orkestrel/scaffold 0.0.94 → 0.0.95
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/bin/main.js +5 -5
- package/dist/bin/main.js.map +1 -1
- package/dist/host/claude/agents/orkestrel.md +52 -52
- package/dist/host/guides/agent.md +244 -38
- package/dist/host/guides/browser.md +2371 -2510
- package/dist/host/guides/codec.md +2 -2
- package/dist/host/guides/mcp.md +182 -100
- package/dist/host/guides/msg.md +14 -22
- package/dist/host/guides/ollama.md +172 -11
- package/dist/host/guides/server.md +122 -37
- package/dist/host/guides/toolbox.md +47 -40
- package/dist/host/manifest.json +10 -46
- package/dist/host/scripts/codex.sh +0 -0
- package/dist/host/scripts/cursor.sh +0 -0
- package/dist/host/scripts/deps.sh +0 -0
- package/dist/host/scripts/ollama.sh +0 -0
- package/dist/src/core/index.cjs +5 -5
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.js +5 -5
- package/dist/src/core/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
> assembly — and the bounded context → provider → tools → repeat loop that carries a turn to
|
|
7
7
|
> its end.
|
|
8
8
|
|
|
9
|
-
An agent is a conversation with a model and the loop that carries it forward. A `Conversation` holds the history — a live tail of immutable messages plus the sections older turns were compacted into. An `AgentContext` assembles that history into the next prompt, folding in the instructions and the active workspace and applying the active scope. An `Agent` drives that prompt through a provider, dispatches
|
|
9
|
+
An agent is a conversation with a model and the loop that carries it forward. A `Conversation` holds the history — a live tail of immutable messages plus the sections older turns were compacted into. An `AgentContext` assembles that history into the next prompt, folding in the instructions and the active workspace and applying the active scope. An `Agent` drives that prompt through a provider, dispatches requested tools admitted by scope and authority, feeds the results back, and repeats until the model stops. Everything else in this module either configures those nouns or observes them. Source: [`src/core`](../src/core). Published through `@orkestrel/agent`.
|
|
10
10
|
|
|
11
11
|
Hand a provider a conversation and get back one assembled `ProviderResult` (`generate`), or a live stream of channel-tagged `ProviderDelta`s that returns that same assembled result when it ends (`stream`). Reasoning separation, the authority gate, and durable jobs sit around that boundary. The model itself is the one thing this package does not supply. `ProviderInterface` stays the boundary any backend can satisfy, and `AgentProvider` is the host-independent HTTP engine a concrete provider extends rather than rewrites: it owns the deadline, the transport, the bounded error read, the framing loop, reasoning separation, and result assembly, and the subclass fills in one vendor's wire. `RelayProvider` and `createRelay` carry that same boundary across your own server, so a browser drives a model it holds no credential for. There is no hidden global state, a plugin lifecycle, a prompt-template DSL, or an implicit memory store. This is a kit of composable primitives: the loop is the convenient way to use them, not the only one, and a caller that would rather bound and drive a provider by hand can skip it entirely.
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ The loop and its tool handlers execute in the host that constructs them. See [Pl
|
|
|
18
18
|
|
|
19
19
|
## Surface
|
|
20
20
|
|
|
21
|
-
The agent-owned surface: the inference boundary and the HTTP engine behind it, the relay that carries that boundary to a browser, the conversation layer, the context and its managers, the loop, the authority gate, and the durable-job bridge. Tool and workspace entities belong to their originating packages and are consumed directly — never re-exported here — and one vendor's wire belongs to the concrete provider that extends `AgentProvider`.
|
|
21
|
+
The agent-owned surface: the inference boundary and the HTTP engine behind it, the relay that carries that boundary to a browser, the judge boundary with its HTTP engine and the System One wire, the conversation layer, the context and its managers, the loop, the authority gate, and the durable-job bridge. Tool and workspace entities belong to their originating packages and are consumed directly — never re-exported here — and one vendor's wire belongs to the concrete provider that extends `AgentProvider`.
|
|
22
22
|
|
|
23
23
|
A provider turns a conversation (plus optional tools) into a turn: `generate` resolves the assembled `ProviderResult` (content + any tool calls + any usage); `stream` yields channel-tagged `ProviderDelta`s as they arrive (`content` for answer text, `thinking` for live reasoning) and returns the same assembled result when the stream completes, so a caller can render tokens / reasoning live and still get the full outcome. Both bound the call with an `AbortSignal`:
|
|
24
24
|
|
|
@@ -46,7 +46,7 @@ const streamed = step.value // the assembled ProviderResult (content === the joi
|
|
|
46
46
|
|
|
47
47
|
Pass `tools` (a non-empty `ToolDefinition[]`) to advertise callable tools for the turn; when the model calls one, `result.tools` is a `ToolCall[]` (each with a guaranteed `id`, the tool `name`, and parsed `arguments`). Aborting a `stream` mid-flight throws a `ProviderAbortError` whose `partial` holds whatever streamed before the cancel.
|
|
48
48
|
|
|
49
|
-
A tool is a JSON-Schema-described callable from [`@orkestrel/tool`](tool.md), and the loop
|
|
49
|
+
A tool is a JSON-Schema-described callable from the [`@orkestrel/tool` package](tool.md). The registry's `definitions()` method advertises tools, and its `execute` method dispatches calls. When a turn advertises tools, the loop refuses a name excluded by the scope's tool allow-list before reaching the registry, with the scope denial. Without a tool allow-list, an unadvertised name passes through the authority gate and reaches the registry's `tool not found` failure if authority admits it. A name included in the allow-list also reaches the registry if authority admits it, even if unregistered. The returned `ToolResult` value uses the `success` discriminant: an unknown name and a throwing handler produce the failure arm, and batch execution isolates each call from its siblings. Direct registry calls behave as follows:
|
|
50
50
|
|
|
51
51
|
```ts
|
|
52
52
|
import { createTool, createToolManager } from '@orkestrel/tool'
|
|
@@ -142,7 +142,7 @@ const result = await agent.generate({
|
|
|
142
142
|
|
|
143
143
|
The turn is bounded by one cancel folded from the external `signal` + the `timeout` deadline + the `budget` signal through `AbortSignal.any`; `agent.abort(reason)` (or `stream.abort(reason)`) fires it. A cancel — external, deadline, budget, or `abort()` — commits a partial result: the `result` promise resolves with `{ partial: true, content: <what accumulated> }`, never rejects (a cancel is not an error); only a genuine provider / tool error rejects. An optional `scheduler.yield`s between turns; tool iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Exhausting `limit` while the model still holds unresolved tool intent (it requested tools on the very last allowed turn) is a distinct, non-cancel cause of `partial: true` — it fires an `exhaust` event (the turns reached) instead of `abort`. A natural finish on the last allowed turn, or `limit: 0` (which never enters the loop), stays `partial: false`. `agent.status` transitions `idle` → `running` → `done` / `error`.
|
|
144
144
|
|
|
145
|
-
Gate the model's tool calls with an optional `Authority` (`createAuthority`) — a synchronous policy gate the loop consults
|
|
145
|
+
Gate the model's tool calls with an optional `Authority` (`createAuthority`) — a synchronous policy gate the loop consults after scope admits a call, passed through `AgentOptions.authority`. It walks ordered `rules` first-match-wins (a matched rule allows unless its `allowed` is `false`), falling back to a configurable default when none match — allow-unmatched by default (a denylist), or deny-by-default when its `fallback` denies (an allowlist). A denied call is never executed: the loop synthesizes the failure arm of `ToolResult` (`error: 'denied: <reason>'`) and feeds it back as a `tool` chunk and a tool message, so no handler runs, no budget is spent, and the model still sees what happened and can choose something else. An allowed call dispatches normally, and with no `authority` set every scope-admitted call dispatches:
|
|
146
146
|
|
|
147
147
|
```ts
|
|
148
148
|
import { createAgent, createAuthority } from '@orkestrel/agent'
|
|
@@ -338,7 +338,13 @@ agent.context.apply(
|
|
|
338
338
|
const result = await agent.generate()
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
A scoped-out tool is
|
|
341
|
+
A scoped-out tool is neither described nor callable. The loop snapshots the scope's tool allow-list before advertising and checks each returned call against that list before consulting the `AgentOptions.authority` option. An `undefined` scope value, or an absent `tools` list, admits every name, including an unknown name that the registry answers with its failure arm. An empty `tools` list admits none. A scope change through the `context.apply` method takes effect on the next turn.
|
|
342
|
+
|
|
343
|
+
When no tool is advertised, including an empty registry, the provider's `content` field ends the run as the answer with a `partial: false` result. The assistant message is recorded without calls. Each dropped call emits a `deny` event with the `no tool is advertised in the active scope` reason, without a tool result or tool message.
|
|
344
|
+
|
|
345
|
+
When at least one tool is advertised, a call excluded by the tool allow-list emits a `deny` event with the `TOOL is not in the active scope` reason, where the `TOOL` placeholder is the requested name. Its denial `ToolResult` value carries the `denied: TOOL is not in the active scope` error and is fed back as a tool message. Admitted calls pass through authority and dispatch normally. The loop continues with results in the reply's call order, including when calls share an ID.
|
|
346
|
+
|
|
347
|
+
A `ScopeManager` registry stores reusable named scopes, keyed by a minted `id` field; scopes can share a `name` field. It is observable like the other managers.
|
|
342
348
|
|
|
343
349
|
### Customizing the format (the cascade)
|
|
344
350
|
|
|
@@ -413,6 +419,22 @@ The engine's failures arrive as one error class. A `ProviderError` carries a mac
|
|
|
413
419
|
|
|
414
420
|
A cancel is not in that taxonomy: the call's own deadline and the caller's signal are folded into one bound, and a cancel throws `ProviderAbortError` carrying the partial assembled so far.
|
|
415
421
|
|
|
422
|
+
### The judge engine
|
|
423
|
+
|
|
424
|
+
The `JudgeInterface` contract is the decision boundary beside `ProviderInterface`, and it is never a provider. Its `ask(request, signal)` method answers typed questions about one state with a probability distribution per question. A `JudgeRequest` carries the `state` and the `questions` keyed by your own ids, which the model never sees; each question is a `ChoiceQuestion`, a `ScoreQuestion`, or a `NoulQuestion`, named by its `form` member. A `JudgeResult` carries the answering model in its `model` member, the `answers` map keyed by the same ids, the `refusals` map of the questions a wire refused, and the `usage` member with the tokens the calls spent. An answer stores only the distribution the server returned: `probabilities` for a choice or a score, and `noul`, the probability of yes, for a noul question. The `computeReading` function derives every measure from that distribution, so every server reads on one scale and no stored measure can drift from its distribution (the derived-reading clause).
|
|
425
|
+
|
|
426
|
+
The `AgentJudge` class is the engine behind that boundary, as the `AgentProvider` class is the engine behind `ProviderInterface`. It owns each call's deadline, the transport, the `headers` hook, the validation before inference, the bounded error read, and the merge of every call's result, and it leaves the wire open: a subclass supplies `name`, `body(request)`, which projects one call's `JudgeRequest` onto the protocol's request shape, and `read(value, request)`, which decodes one call's parsed response into a `JudgeResult`. A one-shot JSON response has no framing state and no buffered tail, so the engine has no `frame` or `finish` seam. In place of the `split` and `strict` switches it takes one `batch` switch: `true`, the default, sends every question in one call, and `false` sends one call per question in key order.
|
|
427
|
+
|
|
428
|
+
The `SystemOneJudge` class is the wire this package ships on that engine, and the `createSystemOneJudge` factory returns it behind `JudgeInterface`. It speaks the System One decision protocol at `SYSTEM_ONE_PATH` and sends every question in one call. See [Asking a System One server a choice, a noul, and a score](#asking-a-system-one-server-a-choice-a-noul-and-a-score) for the servers it serves and a whole request; see [Writing a judge wire](#writing-a-judge-wire) for a wire of your own.
|
|
429
|
+
|
|
430
|
+
The engine's failures arrive as one error class. A `JudgeError` carries a machine-readable `code` and, for an HTTP failure alone, the response `status`; the `isJudgeError` function narrows a caught value so a caller can branch on the code:
|
|
431
|
+
|
|
432
|
+
- `'QUESTION'` — a request refused before inference: an empty question map, a malformed question named by its id, a state that is not a `JudgeEntry`, or a request a wire's `body` method refuses. No call is made.
|
|
433
|
+
- `'HTTP'` — a non-OK response. The message is `judge error: <status>`, with ` - <excerpt>` appended only when the response body carried text, read to at most `MAX_ERROR_BODY_LENGTH` bytes.
|
|
434
|
+
- `'PROTOCOL'` — a successful response with no body or with a body that is not JSON, or a response the wire cannot read.
|
|
435
|
+
|
|
436
|
+
A cancel is not in that taxonomy: it throws `JudgeAbortError`, whose `partial` merges the calls that completed before the cancel, and the `isJudgeAbortError` function narrows a caught value to it (the abort-partial clause).
|
|
437
|
+
|
|
416
438
|
### The relay
|
|
417
439
|
|
|
418
440
|
In the page-to-Node placement, the page owns `Agent`, `RelayProvider`, and its tool registry. Node owns the `createRelay` handler and upstream provider. The hop carries inference requests and replies; a tool handler executes where its registry lives. The Chromium receipt is planned in the `@orkestrel/mcp` distribution proof named under [Placement proofs](#placement-proofs) and is not recorded here as passed.
|
|
@@ -463,6 +485,7 @@ The wire is strictly narrower than the domain on purpose. `providerRequestContra
|
|
|
463
485
|
| `createAuthority` | function | Creates a policy gate — an `AuthorityInterface` the agent loop consults before each tool call runs, evaluating the ordered rules first-match-wins and falling back to the configured default when none match. |
|
|
464
486
|
| `createRelay` | function | Creates an authorized relay handler that validates a bounded JSON request before streaming. |
|
|
465
487
|
| `createRelayProvider` | function | Creates a provider that carries calls through a relay endpoint. |
|
|
488
|
+
| `createSystemOneJudge` | function | Creates a judge that sends every question through the configured System One server. |
|
|
466
489
|
| `createThinkSplitter` | function | Creates a fresh stream-stateful `<think>` separator — a `ThinkSplitterInterface` that splits a thinking model's in-content `<think>…</think>` reasoning spans away from the answer, delta by delta, so a provider yields clean content alone and surfaces the accumulated reasoning as `ProviderResult.thinking`. One splitter serves one stream. |
|
|
467
490
|
| `createChannel` | function | Creates an empty unbounded async channel — a `ChannelInterface` a producer writes values into (`push`) and ends (`close` / `fail`) regardless of consumption, while a consumer reads them back live through `drain`. |
|
|
468
491
|
| `createAgentRegistry` | function | Creates an agent registry — an `AgentRegistryInterface` holding the named pools of live, non-serializable pieces (providers, tools, authorities, schedulers) that a serializable `AgentJobInput`'s names resolve against, and `build`ing a seeded, signal-wired `AgentInterface` from a job. |
|
|
@@ -476,6 +499,8 @@ The wire is strictly narrower than the domain on purpose. `providerRequestContra
|
|
|
476
499
|
| `AgentProvider` | class | Implements bounded HTTP streaming and result assembly behind concrete wire seams. |
|
|
477
500
|
| `RelayProvider` | class | Carries provider calls over an authenticated NDJSON relay endpoint. |
|
|
478
501
|
| `RelayStream` | class | Streams a provider call as validated NDJSON frames under response backpressure. |
|
|
502
|
+
| `AgentJudge` | class | Implements the bounded HTTP calls, validation, and result merging of a judge behind concrete wire seams. |
|
|
503
|
+
| `SystemOneJudge` | class | Carries judge questions over the System One protocol and derives answers from server distributions. |
|
|
479
504
|
| `Conversation` | class | Represents a conversation — a live uncompacted tail of messages it owns directly above a flat message store, plus compacted, summarized `Section`s, a regenerated rollup `summary`, and a `summarizable` flag, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic `ConversationSummaryHandler` seam so `core` never imports a provider. Observable through its own `emitter`. |
|
|
480
505
|
| `ConversationManager` | class | Registers `Conversation`s keyed by `id`, in insertion order, with an active pointer — the id-keyed store over the conversation layer, the `active` / `switch` seam the `AgentContext` renders, and the durable `open` / `save` store seam. Event-free (a registry, like `WorkspaceManager`); the observability lives on each `Conversation`. |
|
|
481
506
|
| `MemoryConversationStore` | class | Implements the `ConversationStoreInterface` in memory — a process-lifetime `Map` of `ConversationSnapshot`s keyed by conversation id, the default store `createMemoryConversationStore` builds and the default backing for `open` / `save`. The exact twin of `MemoryWorkspaceStore`. |
|
|
@@ -515,6 +540,7 @@ A `Shape` cell holds the constant's declared type.
|
|
|
515
540
|
| `INVALID_RELAY_STATUS` | const | `number` | Names the status a relay answers for a body that is missing, unreadable, or rejected by `providerRequestContract` — `400`, carried with no body and reaching the browser as a `ProviderError` with the `HTTP` code. |
|
|
516
541
|
| `OVERSIZED_RELAY_STATUS` | const | `number` | Names the status a relay answers for a request body at or above its byte budget — `413`, carried with no body and answered for an aborted inbound read as well. |
|
|
517
542
|
| `UPSTREAM_RELAY_STATUS` | const | `number` | Names the status a relay answers when the upstream provider call cannot be constructed — `502`, carried with no body after `provider.stream` was entered and threw before returning its iterator. |
|
|
543
|
+
| `SYSTEM_ONE_PATH` | const | `string` | Names the System One decision endpoint shared by compatible servers. |
|
|
518
544
|
|
|
519
545
|
### Shapes and contracts
|
|
520
546
|
|
|
@@ -555,35 +581,42 @@ relayFrameContract.parse({ channel: 'error', message: 'oops', code: 'X' }) // {
|
|
|
555
581
|
|
|
556
582
|
### Helpers
|
|
557
583
|
|
|
558
|
-
| API
|
|
559
|
-
|
|
|
560
|
-
| `agentResultToJSON`
|
|
561
|
-
| `filterAllowList`
|
|
562
|
-
| `estimateTokens`
|
|
563
|
-
| `estimateMessages`
|
|
564
|
-
| `sanitizeToken`
|
|
565
|
-
| `sanitizeUsage`
|
|
566
|
-
| `settleAgentJob`
|
|
567
|
-
| `handleAgentQueueJob`
|
|
568
|
-
| `handleAgentRunnerJob`
|
|
569
|
-
| `renderFencedFile`
|
|
570
|
-
| `joinThinking`
|
|
571
|
-
| `sumUsage`
|
|
572
|
-
| `assembleResult`
|
|
573
|
-
| `denyCall`
|
|
574
|
-
| `renderSection`
|
|
575
|
-
| `resolveOpen`
|
|
576
|
-
| `resolveClose`
|
|
577
|
-
| `resolveItem`
|
|
578
|
-
| `attachImages`
|
|
579
|
-
| `attachUserImages`
|
|
580
|
-
| `collectImageData`
|
|
581
|
-
| `buildSummaryMessage`
|
|
582
|
-
| `buildRecapMessage`
|
|
583
|
-
| `buildProviderResult`
|
|
584
|
-
| `readText`
|
|
585
|
-
| `readChunks`
|
|
586
|
-
| `intersectKeys`
|
|
584
|
+
| API | Kind | Summary |
|
|
585
|
+
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
586
|
+
| `agentResultToJSON` | function | Projects an unknown value onto a fresh, exact `JSONValue` representation of an `AgentResult` — capturing each structural field once through a total boundary, accepting conforming accessors and inherited properties, preserving finite negative and fractional usage counts, dropping extras, and resolving `undefined` for a malformed field, a non-finite usage number, a throwing getter, or a hostile or revoked proxy. |
|
|
587
|
+
| `filterAllowList` | function | Filters a list of items by a `ScopeInterface` allow-list of keys — `undefined` passes everything, `[]` passes nothing, and a non-empty list passes the listed keys alone, order preserved. The pure, total set-membership primitive the context's build step and the agent loop's tool-advertise step apply a scope through. |
|
|
588
|
+
| `estimateTokens` | function | Estimates the context-token footprint of a string — the deterministic `ceil(length / 4)` character heuristic `estimateMessages` sums over a conversation's messages (the default context-budget estimator). |
|
|
589
|
+
| `estimateMessages` | function | Estimates the context-token footprint of a batch of messages — each message's content plus `MESSAGE_TOKEN_OVERHEAD`, a tool-call JSON estimate, and `IMAGE_TOKEN_ESTIMATE` for each attached image. The default `consumer` estimator for an agent's context budget (the `AgentOptions` `window`), total and never throwing, and a deliberate provider-agnostic approximation rather than an exact tokenizer count. |
|
|
590
|
+
| `sanitizeToken` | function | Sanitizes one reported token count into a safe non-negative integer — a non-finite or non-positive value becomes `0`, and a positive fractional value floors down. |
|
|
591
|
+
| `sanitizeUsage` | function | Sanitizes a `TokenUsage` into safe, non-negative integers — the guard an agent's abort-usage path applies to a provider's partial usage before it is charged against a budget or folded into the run total. |
|
|
592
|
+
| `settleAgentJob` | function | Runs one rehydrated agent and applies the partial-as-configurable-failure policy — a partial run throws an `AgentJobError` unless the `partial` policy allows it, and a natural finish resolves. The shared job-handler step `createAgentQueue` and `createAgentRunner` both settle each job through, so the policy can never diverge between them. |
|
|
593
|
+
| `handleAgentQueueJob` | function | Handles one queued agent job by rehydrating it through a registry with the queue attempt's signal, then applying the shared partial-result policy. |
|
|
594
|
+
| `handleAgentRunnerJob` | function | Handles one runner agent job by fanning out its declared children, rehydrating the parent through a registry with the controller signal, and applying the shared partial-result policy. |
|
|
595
|
+
| `renderFencedFile` | function | Renders a path-addressed text body as a fenced reference block — a `File: <path>` label line over a language-tagged fence, the framing an `AgentContext`'s active-workspace text-file render emits. |
|
|
596
|
+
| `joinThinking` | function | Joins the reasoning a run's provider calls separated from the answer — the first call seeds the accumulation, a later call appends blank-line separated so each turn's reasoning stays readable. |
|
|
597
|
+
| `sumUsage` | function | Adds two `TokenUsage` values field by field — the running total an agent run keeps across its provider calls. |
|
|
598
|
+
| `assembleResult` | function | Assembles the settled `AgentResult` from a run's `RunOutcome` — `thinking` and `usage` are carried only when the run surfaced them, and the loop-internal `exhausted` flag is left out. |
|
|
599
|
+
| `denyCall` | function | Synthesizes the denial `ToolResult` an authority-blocked call is fed back with — the call's `id` / `name` keyed back, carrying a denial `error` instead of a value. |
|
|
600
|
+
| `renderSection` | function | Renders one context section — the resolved `open`, each item's rendering, and the resolved `close` when one exists, blank-line joined; `undefined` when the section has no items. |
|
|
601
|
+
| `resolveOpen` | function | Resolves one section's open text through the format cascade — manager-options override > provider default > built-in header. |
|
|
602
|
+
| `resolveClose` | function | Resolves one section's close text through the format cascade — manager-options override > provider default; `undefined` when neither sets one, because there is no built-in close. |
|
|
603
|
+
| `resolveItem` | function | Resolves one item's rendering through the format cascade — item override > manager-options override > provider default > built-in rendering. |
|
|
604
|
+
| `attachImages` | function | Copies a message with image data merged onto its `images` — the message's own images first, then the attached data, carrying `calls` only when present and never mutating the original. |
|
|
605
|
+
| `attachUserImages` | function | Attaches image data to a conversation's last user message — the turn a vision provider reads images off — as a new array with that one message replaced by its carrying copy, and unchanged when there is no data or no user turn. |
|
|
606
|
+
| `collectImageData` | function | Collects the `base64` payload of the image files in a workspace file list — the data an agent context attaches to the last user message. |
|
|
607
|
+
| `buildSummaryMessage` | function | Builds the raw synthetic summary message for one compacted section — role `'assistant'`, the section's stable `id`, and its `summary` verbatim as content. |
|
|
608
|
+
| `buildRecapMessage` | function | Builds the framed recap message for one compacted section — the same role and stable `id` as `buildSummaryMessage`, with the content prefixed by `CONVERSATION_RECAP_PREFIX`. |
|
|
609
|
+
| `buildProviderResult` | function | Assembles a provider result with only populated optional fields. |
|
|
610
|
+
| `readText` | function | Reads a UTF-8 prefix of a byte stream and cancels its remainder. |
|
|
611
|
+
| `readChunks` | function | Decodes UTF-8 chunks with a final flush and releases the stream on every exit. |
|
|
612
|
+
| `intersectKeys` | function | Intersects two scope category lists under the "`undefined` is the universal set" rule — a fresh copy that can only tighten, and the primitive a scope narrows through. |
|
|
613
|
+
| `computeReading` | function | Derives the winner, its probability, the published confidence, and a score answer's expected level from a judge answer's distribution. |
|
|
614
|
+
| `buildJudgeResult` | function | Merges the results of a judge request's calls into one result. |
|
|
615
|
+
| `copyJSON` | function | Owns a value by serializing it to JSON and parsing the text, so the copy shares nothing with its source. |
|
|
616
|
+
| `readHeaders` | function | Builds a request's JSON headers, awaiting the caller's header hook inside the call's cancellation bound. |
|
|
617
|
+
| `questionToSystemOne` | function | Projects a judge question onto the System One wire while preserving omitted members. |
|
|
618
|
+
| `extractSystemOneAnswer` | function | Extracts a System One distribution in question criteria order and drops server measures. |
|
|
619
|
+
| `extractSystemOneUsage` | function | Maps complete System One token counts onto validated token usage. |
|
|
587
620
|
|
|
588
621
|
Project an agent result at its originating package before carrying it through a JSON boundary:
|
|
589
622
|
|
|
@@ -619,6 +652,10 @@ In a guard table a `Shape` cell holds the type the guard narrows to. Each guard
|
|
|
619
652
|
| `isMessage` | function | `Message` | Checks whether a value satisfies the domain conversation-message contract. |
|
|
620
653
|
| `isSection` | function | `Section` | Checks whether an `unknown` is structurally a `Section` record — a `string` `id` and `summary` beside a `messages` array of valid `Message`s, the per-section step of the `isConversationSnapshot` read-boundary narrow. Total, never throwing, and never an assertion. |
|
|
621
654
|
| `isConversationSnapshot` | function | `ConversationSnapshot` | Narrows an `unknown` to a `ConversationSnapshot` — a `string` `id`, an optional `string` `summary`, and valid `sections` and `messages` arrays; the total boundary guard for an untrusted snapshot read (a storage row a `DatabaseConversationStore` reads back from its opaque JSON column, a snapshot loaded from disk), never throwing. The exact analogue of `isWorkspaceSnapshot`. |
|
|
655
|
+
| `isJudgeEntry` | function | `JudgeEntry` | Checks whether a value is a judge entry: a string, a JSON record, or a JSON array. |
|
|
656
|
+
| `isJudgeQuestion` | function | `JudgeQuestion` | Checks whether a value is a well-formed judge question of the choice, score, or noul form. |
|
|
657
|
+
| `isSystemOneResponse` | function | `SystemOneResponse` | Checks whether a value is a System One response envelope with optional model and usage. |
|
|
658
|
+
| `isSystemOneAnswer` | function | `SystemOneAnswer` | Checks whether a value is a System One answer whose type and distribution the wire can read. |
|
|
622
659
|
|
|
623
660
|
A `DatabaseConversationStore` reads its snapshot column back as `unknown` and narrows it through the last of them, so a malformed blob resolves `undefined` rather than a broken conversation:
|
|
624
661
|
|
|
@@ -643,6 +680,10 @@ const snapshot = isConversationSnapshot(row) ? row : undefined
|
|
|
643
680
|
| `isConversationError` | function | Narrows an unknown caught value to a `ConversationError` through `instanceof`, so a `catch` can branch on its `code`. |
|
|
644
681
|
| `AgentError` | class | Reports a concurrent run that would corrupt shared per-agent accounting, or a rehydration name absent from its registry pool — thrown synchronously by an `AgentInterface`'s `stream()` (and so by `generate()`, which calls it) and by an `AgentRegistryInterface`'s accessors, carrying the machine-readable `code` `'CONCURRENCY' \| 'REGISTRY'`. Synchronous means a fire-and-forget `agent.generate().catch(…)` never catches it: `await` the call inside `try`/`catch`, or wrap the call expression itself. |
|
|
645
682
|
| `isAgentError` | function | Narrows an unknown caught value to an `AgentError` through `instanceof`, so a `catch` can branch on its `code`. |
|
|
683
|
+
| `JudgeError` | class | Reports a coded judge failure with its HTTP status and underlying cause when available. |
|
|
684
|
+
| `isJudgeError` | function | Narrows a caught value to the judge failure class through `instanceof`. |
|
|
685
|
+
| `JudgeAbortError` | class | Reports a judge call cancelled by the caller's signal or its deadline, carrying the `JudgeResult` merged from the calls that completed before the cancel and the machine-readable `code` `'ABORT'`. |
|
|
686
|
+
| `isJudgeAbortError` | function | Narrows a caught value to a `JudgeAbortError` through `instanceof`, so a `catch` can recover its `partial` result. |
|
|
646
687
|
|
|
647
688
|
### Types
|
|
648
689
|
|
|
@@ -723,12 +764,42 @@ A `Shape` cell holds an interface's data members as bare names in braces, `?` ma
|
|
|
723
764
|
| `ConversationSnapshot` | interface | `{ id, summary?, sections, messages }` | Holds a JSON-serializable snapshot of a conversation's state — its `id`, the rollup `summary`, the compacted `sections`, and the live tail `messages` — the durable payload the `ConversationStoreInterface` persists. The exact analogue of `WorkspaceSnapshot`. |
|
|
724
765
|
| `ConversationStoreInterface` | interface | `{} plus get, set, delete` | Persists a `ConversationSnapshot` durably — the async `get` / `set` / `delete` primitives, keyed by a conversation id and holding no expiry, the exact analogue of `WorkspaceStoreInterface`. |
|
|
725
766
|
| `ConversationSnapshotRow` | interface | `{ id, snapshot }` | Represents one row of the table a `DatabaseConversationStore` persists — a conversation `id` plus its `ConversationSnapshot` held as one opaque JSON column, read back as `unknown` and narrowed on `get`. The exact analogue of `WorkspaceSnapshotRow`. |
|
|
767
|
+
| `JudgeEntry` | type | `string \| JSONRecord \| readonly JSONValue[]` | Carries text or structured JSON the model reads; mirrors the TypeSafe `EntryType` without its null arm. |
|
|
768
|
+
| `ChoiceCriteria` | type | `Readonly<Record<string, JudgeEntry \| null>>` | Maps each option name the model sees to its description; null keeps an undescribed option in the map. |
|
|
769
|
+
| `ScoreCriteria` | type | `readonly [JudgeEntry \| null, JudgeEntry \| null, ...Array<JudgeEntry \| null>]` | Lists at least two score levels from level 0 upward; null leaves a level undescribed. |
|
|
770
|
+
| `NoulCriteria` | interface | `{ true?, false? }` | Carries what makes a noul answer true and what makes it false; an omitted side is undescribed. |
|
|
771
|
+
| `ChoiceQuestion` | interface | `{ form, instructions?, criteria }` | Asks the model to pick one named option from its criteria. |
|
|
772
|
+
| `ScoreQuestion` | interface | `{ form, instructions?, criteria }` | Asks the model to place the state on an ordered scale of levels. |
|
|
773
|
+
| `NoulQuestion` | interface | `{ form, instructions?, criteria? }` | Asks the model whether a statement about the state holds. |
|
|
774
|
+
| `JudgeQuestion` | type | `ChoiceQuestion \| ScoreQuestion \| NoulQuestion` | Names one question by its form, the protocol's type field under the fleet's named discriminant. |
|
|
775
|
+
| `JudgeRequest` | interface | `{ state, questions }` | Carries one state and the questions asked about it, keyed by caller ids the model never sees; each question is evaluated on its own. |
|
|
776
|
+
| `ChoiceAnswer` | interface | `{ form, probabilities }` | Carries a choice distribution keyed by option name, in criteria order. |
|
|
777
|
+
| `ScoreAnswer` | interface | `{ form, probabilities }` | Carries a score distribution indexed by level. |
|
|
778
|
+
| `NoulAnswer` | interface | `{ form, noul }` | Carries the probability that the answer is yes; the protocol's noul field. |
|
|
779
|
+
| `JudgeAnswer` | type | `ChoiceAnswer \| ScoreAnswer \| NoulAnswer` | Names one answer by its form; it stores only the distribution a server returned. |
|
|
780
|
+
| `Refusal` | interface | `{ missing }` | Reports a question a wire could not read a candidate for; it lists the caller's keys and invents no probability. |
|
|
781
|
+
| `JudgeResult` | interface | `{ model, answers, refusals?, usage? }` | Carries the answering model, the answers keyed by question id, the refusals, and the usage the request's calls spent. |
|
|
782
|
+
| `JudgeInterface` | interface | `{ id, name, model } plus ask` | Answers typed questions about one state with probabilities; the sibling of `ProviderInterface`, never a provider. |
|
|
783
|
+
| `Reading` | interface | `{ winner, probability, confidence, score? }` | Carries the measures `computeReading` derives from an answer; nothing stores them. |
|
|
784
|
+
| `AgentJudgeInput` | interface | `Pick<ProviderOptions, 'timeout' \| 'fetch' \| 'headers'> plus { url, path?, model, batch? }` | Configures the judge engine's destination, identity, and call split. |
|
|
785
|
+
| `AgentJudgeInterface` | interface | `JudgeInterface plus body, read` | Defines the wire seams of the shared judge engine. |
|
|
786
|
+
| `JudgeErrorCode` | type | `'HTTP' \| 'PROTOCOL' \| 'QUESTION'` | Names the machine-readable judge failure conditions. |
|
|
787
|
+
| `SystemOneJudgeOptions` | interface | `Pick<ProviderOptions, 'timeout' \| 'fetch' \| 'headers'> plus { url, model }` | Configures the System One server, model, transport, authentication, and deadline. |
|
|
788
|
+
| `SystemOneEntry` | type | `JudgeEntry \| null` | Carries a TypeSafe System One entry, including the wire's explicit null. |
|
|
789
|
+
| `SystemOneQuestion` | interface | `{ type, instructions?, criteria? }` | Transliterates a TypeSafe System One question with its protocol discriminant and criteria. |
|
|
790
|
+
| `SystemOneRequest` | interface | `{ state, model, questions }` | Transliterates the TypeSafe System One request body. |
|
|
791
|
+
| `SystemOneUsage` | interface | `{ input_tokens?, output_tokens? }` | Transliterates TypeSafe System One token counts with missing or null counts permitted. |
|
|
792
|
+
| `SystemOneChoiceAnswer` | interface | `{ type, probabilities, choice?, confidence? }` | Transliterates a TypeSafe System One choice distribution and optional server measures. |
|
|
793
|
+
| `SystemOneScoreAnswer` | interface | `{ type, probabilities, score?, legend?, confidence? }` | Accepts System One score probabilities and legends as maps or llama.cpp arrays. |
|
|
794
|
+
| `SystemOneNoulAnswer` | interface | `{ type, noul, confidence? }` | Transliterates a TypeSafe System One yes probability and optional server confidence. |
|
|
795
|
+
| `SystemOneAnswer` | type | `SystemOneChoiceAnswer \| SystemOneScoreAnswer \| SystemOneNoulAnswer` | Unites the TypeSafe System One answer forms. |
|
|
796
|
+
| `SystemOneResponse` | interface | `{ model?, answers, usage? }` | Transliterates the System One response envelope before question-specific answer validation. |
|
|
726
797
|
|
|
727
798
|
Agent-owned readonly data members stay in the preceding Surface tables; their call-signature methods are documented under [`## Methods`](#methods). Tool contracts resolve to [`tool.md`](tool.md) and workspace contracts to [`workspace.md`](workspace.md) — neither dependency surface is duplicated or re-exported here. Note where the boundary falls inside the context: `instructions`, `conversations`, and `workspaces` are the managers `build()` renders a prompt from, while `tools` is loop machinery for advertising and dispatch and is never read by `build()` at all.
|
|
728
799
|
|
|
729
800
|
## Methods
|
|
730
801
|
|
|
731
|
-
The tables list every public call-signature member of `ProviderInterface`, `AgentProviderInterface`, `ProviderParserInterface`, `RelayProvider`, `ThinkSplitterInterface`, `MessageManagerInterface`, `InstructionManagerInterface`, `ContextSectionSourceInterface`, `ScopeInterface`, `ScopeManagerInterface`, `AgentContextInterface`, `AgentInterface`, `StreamInterface`, `ChannelInterface`, `AuthorityInterface`, `AgentRegistryInterface`, `ConversationInterface`, `ConversationManagerInterface`, `ConversationStoreInterface`, `MemoryConversationStore`, and `DatabaseConversationStore`. Their readonly data members remain Surface rows. `AgentProvider`, `ThinkSplitter`, `InstructionManager`, `Scope`, `ScopeManager`, `AgentContext`, `Agent`, `Authority`, `AgentRegistry`, `Conversation`, and `ConversationManager` implement their interfaces exactly, so the tables also describe those classes' instance methods. `RelayProvider` and the store classes keep explicit tables because their class names have no same-name interface contract. `MessageManagerInterface` has no separate concrete class here: the active `Conversation` satisfies it structurally. `RelayStream` exposes `response` alone, a data member, so it keeps its Surface row and takes no table. Tool and workspace methods live in their dependency guides.
|
|
802
|
+
The tables list every public call-signature member of `ProviderInterface`, `AgentProviderInterface`, `ProviderParserInterface`, `RelayProvider`, `JudgeInterface`, `AgentJudgeInterface`, `SystemOneJudge`, `ThinkSplitterInterface`, `MessageManagerInterface`, `InstructionManagerInterface`, `ContextSectionSourceInterface`, `ScopeInterface`, `ScopeManagerInterface`, `AgentContextInterface`, `AgentInterface`, `StreamInterface`, `ChannelInterface`, `AuthorityInterface`, `AgentRegistryInterface`, `ConversationInterface`, `ConversationManagerInterface`, `ConversationStoreInterface`, `MemoryConversationStore`, and `DatabaseConversationStore`. Their readonly data members remain Surface rows. `AgentProvider`, `AgentJudge`, `ThinkSplitter`, `InstructionManager`, `Scope`, `ScopeManager`, `AgentContext`, `Agent`, `Authority`, `AgentRegistry`, `Conversation`, and `ConversationManager` implement their interfaces exactly, so the tables also describe those classes' instance methods. `RelayProvider`, `SystemOneJudge`, and the store classes keep explicit tables because their class names have no same-name interface contract. `MessageManagerInterface` has no separate concrete class here: the active `Conversation` satisfies it structurally. `RelayStream` exposes `response` alone, a data member, so it keeps its Surface row and takes no table. Tool and workspace methods live in their dependency guides.
|
|
732
803
|
|
|
733
804
|
#### `ProviderInterface`
|
|
734
805
|
|
|
@@ -772,6 +843,33 @@ The relay wire over `AgentProvider`: it fills every seam and inherits `generate`
|
|
|
772
843
|
| `read` | `ProviderIncrement` | Validates a relay frame and translates its channel into the shared stream engine. |
|
|
773
844
|
| `finish` | `ReadonlyArray<Readonly<Record<string, unknown>>>` | Recovers an unterminated final frame by completing its NDJSON line. |
|
|
774
845
|
|
|
846
|
+
#### `JudgeInterface`
|
|
847
|
+
|
|
848
|
+
The decision boundary. `ask` is its only method; the `id`, `name`, and `model` data members stay Surface rows. It takes the request and a bounding `AbortSignal`.
|
|
849
|
+
|
|
850
|
+
| Method | Returns | Summary |
|
|
851
|
+
| ------ | ---------------------- | ---------------------------------------------------------------------------------- |
|
|
852
|
+
| `ask` | `Promise<JudgeResult>` | Asks every question of the request about its state and returns the merged answers. |
|
|
853
|
+
|
|
854
|
+
#### `AgentJudgeInterface`
|
|
855
|
+
|
|
856
|
+
The seams a judge wire fills, beneath the boundary method it inherits. `AgentJudgeInterface` extends `JudgeInterface`, so `ask` is part of it and repeats here with its boundary contract; `AgentJudge` implements `ask` once, for every subclass. A subclass writes `body` and `read` and nothing else. The `id`, `name`, and `model` data members stay Surface rows.
|
|
857
|
+
|
|
858
|
+
| Method | Returns | Summary |
|
|
859
|
+
| ------ | ---------------------- | ----------------------------------------------------------------------------------- |
|
|
860
|
+
| `ask` | `Promise<JudgeResult>` | Asks every question of the request about its state and returns the merged answers. |
|
|
861
|
+
| `body` | `object` | Projects one call's request onto the concrete protocol's serializable body. |
|
|
862
|
+
| `read` | `JudgeResult` | Decodes one call's parsed response body into the answers for that call's questions. |
|
|
863
|
+
|
|
864
|
+
#### `SystemOneJudge`
|
|
865
|
+
|
|
866
|
+
The System One wire over `AgentJudge`: it fills both seams and inherits `ask` from the engine, so a caller drives it through `JudgeInterface` like any judge. Its `name` reports `'systemone'` and stays a data member.
|
|
867
|
+
|
|
868
|
+
| Method | Returns | Summary |
|
|
869
|
+
| ------ | ------------------ | --------------------------------------------------------------------------------------- |
|
|
870
|
+
| `body` | `SystemOneRequest` | Projects the state and questions onto the System One request with the configured model. |
|
|
871
|
+
| `read` | `JudgeResult` | Decodes requested System One answers and reports the server model and available usage. |
|
|
872
|
+
|
|
775
873
|
#### `ThinkSplitterInterface`
|
|
776
874
|
|
|
777
875
|
The stream-stateful `<think>…</think>` separator a provider routes raw content deltas through, so it yields clean content and surfaces the reasoning as `ProviderResult.thinking`. The `content` / `thinking` data members (the authoritative clean-content + reasoning accumulations) stay Surface rows — `content` matters because some chat templates pre-seed `<think>` into the prompt scaffold (the qwen3 shape), so only a bare `</think>` ever appears on the wire: before any tag event, that bare close reclassifies everything surfaced so far into `thinking` (one-shot — afterwards a bare close is plain text), correcting `content` retroactively where the already-returned deltas cannot be recalled. One splitter serves one stream — create a fresh one for each call (`createThinkSplitter`).
|
|
@@ -977,7 +1075,7 @@ The driver-backed implementation keeps an explicit table because its class name
|
|
|
977
1075
|
These invariants hold across `src/core` ↔ `agent.md`:
|
|
978
1076
|
|
|
979
1077
|
1. **Doc ↔ source bijection.** Every `function` / `class` / `const` / `interface` / `type` row in the `## Surface` tables is a real export of `src/core`, and every export appears as a Surface row — exhaustive, both directions.
|
|
980
|
-
2. **`ProviderInterface` is the inference boundary; `AgentProvider` is the engine behind it.** A provider turns a conversation (plus optional `tools`, a non-empty `ToolDefinition[]`) into a turn: `generate` resolves the assembled `ProviderResult`, `stream` yields channel-tagged `ProviderDelta`s and returns the assembled result. Both methods accept optional `ProviderStreamOptions`; `think` is the per-call reasoning override. It carries an `id` (a per-instance trace label) and `name` (the backend identifier). This module defines that contract and the host-independent HTTP engine every provider over it would otherwise repeat — the deadline, the transport, the authorization hook, the bounded error read, the decode loop, reasoning separation, and result assembly (the provider-engine clause). One vendor's wire stays the concrete provider's own: it reaches the engine through `frame` / `body` / `read` / `finish`, and
|
|
1078
|
+
2. **`ProviderInterface` is the inference boundary; `AgentProvider` is the engine behind it.** A provider turns a conversation (plus optional `tools`, a non-empty `ToolDefinition[]`) into a turn: `generate` resolves the assembled `ProviderResult`, `stream` yields channel-tagged `ProviderDelta`s and returns the assembled result. Both methods accept optional `ProviderStreamOptions`; `think` is the per-call reasoning override. It carries an `id` (a per-instance trace label) and `name` (the backend identifier). This module defines that contract and the host-independent HTTP engine every provider over it would otherwise repeat — the deadline, the transport, the authorization hook, the bounded error read, the decode loop, reasoning separation, and result assembly (the provider-engine clause). One vendor's wire stays the concrete provider's own: it reaches the engine through `frame` / `body` / `read` / `finish`, and no provider in `src/core` speaks a vendor's wire. The loop runs with its tools in Node or in a page; a page can delegate inference to a Node relay while keeping tool execution local. See [Placement proofs](#placement-proofs) for each project and receipt.
|
|
981
1079
|
3. **`stream` yields deltas + returns the assembled result.** A `ProviderInterface.stream` yields each non-empty answer delta as `{ channel: 'content', text }` and each native live reasoning delta as `{ channel: 'thinking', text }`; its return value is the assembled `ProviderResult` whose `content` is the provider's authoritative clean answer, plus any tool calls and usage the turn reported. So a caller can render tokens and reasoning live while still recovering the complete outcome from the generator's return value. A thinking model's reasoning is separated once, at the wire that carried it. A provider reading a raw wire routes its content through a `ThinkSplitter` (`createThinkSplitter` — one per stream, armed by the base's `split` switch), yields the clean content, assembles `content` from the splitter's authoritative accumulation (an implicit pre-seeded open — the qwen3-template bare `</think>` — reclassifies the already-yielded prefix into thinking, the one shape where live content deltas can transiently over-report), and surfaces the accumulated reasoning as `ProviderResult.thinking` — which never re-enters the conversation (the `Agent` joins it across a run's calls onto `AgentResult.thinking` as display/audit metadata). A relay's deltas arrive already separated by the upstream provider that did that work, so `RelayProvider` constructs with `split: false` and preserves each delta verbatim, a literal `<think>` tag in the answer included: splitting a second time would re-classify text the upstream already ruled on (the relay-protocol clause).
|
|
982
1080
|
4. **Usage reuses `TokenUsage`.** `ProviderResult.usage` is the [budgets](budget.md) `TokenUsage` shape (`{ prompt, completion, total }`), imported not redefined, present only when the turn reported it — so a caller folds it straight into a token budget. A provider surfaces it when the wire carries it (for example a stream's `done` line / a non-stream body) and omits it otherwise.
|
|
983
1081
|
5. **The caller's signal and the engine's own deadline both bound the call.** Both `generate` and `stream` take an `AbortSignal`, so a caller bounds the request — a cancel, a [timeout](timeout.md), and a token [budget](budget.md) folded into one signal through `AbortSignal.any`. An already-aborted signal rejects the call before any content streams. `AgentProvider` arms a second bound the caller does not have to supply: a per-call `Timeout` of `AgentProviderInput.timeout` milliseconds (`DEFAULT_PROVIDER_TIMEOUT` when omitted), folded with the caller's signal through `AbortSignal.any`. Whichever trips first cancels the call, the deadline covers the `headers` hook and the error-body read as well as the stream, and it is cleared in a `finally` on every exit — a completion, a transport rejection, a non-OK response, a decoder failure, an early generator return, and a remote abort alike.
|
|
@@ -985,7 +1083,7 @@ These invariants hold across `src/core` ↔ `agent.md`:
|
|
|
985
1083
|
7. **The message-store contract (`MessageManagerInterface`).** `MessageManagerInterface` is the immutable message-store contract `AgentContextInterface.messages` is typed to — `count` + `add` / `message` / `messages` / `remove` / `clear`. It has no concrete class in this module: the active `Conversation` (which owns its live tail directly) satisfies it structurally, so `context.messages` is the active conversation (the conversation-layer clause). `add` takes one `MessageInput` or a batch and mints each message's `id` (`crypto.randomUUID()`), carrying the input's `role` / `content` and `calls` only when supplied (an absent `calls` is omitted, never present-but-undefined) — returning the created message(s). A stored message is immutable: assembled once from its input and never mutated, and the object `add` returns is the same one `message(id)` later resolves. `count` is the live tail size, `message(id)` looks one up (`undefined` when absent), `messages()` lists them in insertion order, `remove` (one or a batch) returns `true` only when every supplied id was removed, and `clear` empties it.
|
|
986
1084
|
8. **The richer turn context (`AgentContext`).** `AgentContext` composes the optional `system` prompt; the agent-owned `instructions` / `conversations` managers; a `WorkspaceManagerInterface` consumed from `@orkestrel/workspace`; `messages` (the active conversation's live tail); a `ToolManagerInterface` consumed from `@orkestrel/tool` solely for provider advertising and call dispatch; and a readonly active `scope`. Each omitted manager is created fresh, and the context ensures the conversation registry has an active default, so `messages` is always defined. `build()` folds the system prompt, scope-filtered instructions, and the active workspace's scope-filtered text files (the active-workspace clause) into one leading `system` message, then appends the active conversation's `view()`. The tool registry is the one member `build()` never reads: it is loop machinery for advertising and dispatch, not a prompt-context manager, so no tool renders into the prompt. The result is computed fresh on every call and never mutates a manager or stored message.
|
|
987
1085
|
9. **Scope filtering + image attachment.** A `Scope` is a named allow-list filter with one list per category (`instructions` by `name`, `tools` by `name`, `files` by the active workspace file's `path`), each three-way through `filterAllowList`: `undefined` ⇒ all pass, `[]` ⇒ none, a non-empty list ⇒ only-listed. `build()` applies the active `scope` to the instruction + workspace-file categories before rendering them (`scope.files` filters the active workspace's `files()` before the carrier split — the active-workspace clause). Conversation messages are deliberately not a scope category: the active conversation owns message inclusion through compaction (the active-conversation clause), so its `view()` is authoritative and a second, competing message filter would only let them disagree. `narrow(config)` composes a tighter child by set-intersection over every category's list (an `undefined` side imposes no constraint — `undefined ∩ list = list`, `undefined ∩ undefined = undefined`), so narrowing only tightens. The active workspace's scoped-in image files' `base64` payload is attached to the last user message (rebuilt as a copy carrying the merged `images`, never mutating the stored message; skipped when no user message exists — the active workspace is the sole image source); `build()` still returns `readonly Message[]`. **Tools stay structural, never in the prompt.** The loop hands the model its tools through `tools.definitions()` (the `tools` argument to `provider.generate` / `.stream`), not by serializing them — and it filters those definitions by the active `scope.tools` first (through `filterAllowList`), so a scoped-out tool is neither advertised nor callable (the model never sees it). `AgentContext.build()`'s output therefore never contains a tool's `name`, `description`, `parameters`, or definition, scoped or not. A `ScopeManager` (`createScopeManager`) is the optional reuse registry of named scopes, keyed by a minted `id` (two scopes may share a `name`), observable like the other managers.
|
|
988
|
-
10. **The agent loop (`Agent` / `createAgent`).** `Agent` composes a `ProviderInterface`, an `AgentContext`, and its `@orkestrel/tool` registry into the bounded context → provider → tools → repeat turn. It builds the provider input, then iterates up to `limit`: stream a provider turn, accumulate content/thinking/usage, append any assistant tool calls, and execute them. Each discriminated `ToolResult` becomes a tool message
|
|
1086
|
+
10. **The agent loop (`Agent` / `createAgent`).** `Agent` composes a `ProviderInterface`, an `AgentContext`, and its `@orkestrel/tool` registry into the bounded context → provider → tools → repeat turn. It builds the provider input, then iterates up to `limit`: stream a provider turn, accumulate content/thinking/usage, append any assistant tool calls, and execute them. Each discriminated `ToolResult` becomes a tool message. On success, a string value becomes its content unchanged; any other value becomes `JSON.stringify(result.value)`. On failure, the content is `result.error` unchanged. A final assistant turn stops the loop.
|
|
989
1087
|
11. **One `#run` shared by `generate` + `stream`.** A single private async generator drives the whole turn. `stream()` exposes it as an `AgentStreamInterface` (`events` + `result` + `abort`); `generate()` drains that same stream (iterating `events`, discarding chunks) and returns its `result` — it has zero loop logic of its own, so `generate` and `stream` can never diverge (a `generate()` result deep-equals draining `stream()` on the same input).
|
|
990
1088
|
12. **The `AgentChunk` stream.** `stream().events` yields, per turn, each content delta as `{ category: 'token', content }`, each live reasoning delta as `{ category: 'think', content }`, then optional usage, then one `{ category: 'tool', call, result }` per dispatched call. The call and discriminated result use the contracts imported from `@orkestrel/tool`. `result` resolves the settled `AgentResult`: final or partial content, joined thinking, summed optional usage, and `partial`.
|
|
991
1089
|
13. **Bounded, paced, capped.** Each run arms one cancel through `createAbort({ signal: AbortSignal.any([…]) })` folding whichever of the external `signal`, the `timeout` deadline (a started `Timeout`), and the `budget` signal (a started `Budget`) are present; `agent.abort(reason)` / `stream.abort(reason)` fires it, and the timeout is always cleared in a `finally`. A cancel — external, deadline, budget, or `abort()` — stops the loop and the `result` promise resolves `{ partial: true, content: <accumulated> }`; it is not an error. The accumulated `content` already holds whatever streamed before the cancel — the loop accumulates each delta as it yields the `token` chunk, and a `ProviderAbortError.partial.content` is exactly those same yielded deltas (the `stream` contract), so the loop never re-adds it. A genuine provider / tool error (the signal is not aborted) rejects the `result` (and `status` → `error`). The optional `scheduler.yield({ signal })`s between turns (before turns 2…N, never after the last); tool iteration is capped at `limit` so the loop always terminates. `status` transitions `idle` → `running` → `done` / `error`.
|
|
@@ -1023,6 +1121,16 @@ These invariants hold across `src/core` ↔ `agent.md`:
|
|
|
1023
1121
|
|
|
1024
1122
|
37. **`ProviderError` and its codes.** `ProviderError` carries a machine-readable `code` and a `status` that is present for an `HTTP` failure and `undefined` under every other code; `ProviderErrorOptions` carries that `status` and the underlying `cause`. `isProviderError` narrows a caught value so a caller can branch on the code. `'HTTP'` reports a non-OK response: the message is `provider error: <status>`, and ` - <excerpt>` is appended only when the bounded error read returned text, so an empty body leaves no separator and no trailing space; when that read fails outright the message is `provider error: <status> - (error body unavailable)` and the read's failure rides as the `cause`. `'PROTOCOL'` reports a successful response with no body, a record the concrete provider refuses (a frame outside `relayFrameContract`, a request the JSON wire cannot carry), or a `strict` stream that ended with no settled result. `'PROVIDER'` reports an upstream failure a relay carried in an `error` frame, and its message is `RELAY_PROVIDER_MESSAGE`. A cancel is in none of them: it is `ProviderAbortError` (the local-and-remote-cancel clause).
|
|
1025
1123
|
|
|
1124
|
+
38. **The judge boundary (`JudgeInterface`).** `JudgeInterface` is the sibling of `ProviderInterface` and never a provider. It carries an `id` (a per-instance trace label), a `name` (the wire), and the configured `model`, and `ask(request, signal)` answers every question of a `JudgeRequest` about its one `state`. Questions are keyed by the caller's ids, which the model never sees, and each question is evaluated on its own. A `JudgeResult` keys each answer by the same id; an id appears in `answers` or in `refusals`, never both, and `refusals` is absent when no question was refused. A `Refusal` lists the candidate keys a wire could not read and invents no probability for them. The `form` member names the kind of a question and of an answer where the protocol writes `type`.
|
|
1125
|
+
39. **The stored distribution and the derived reading (`computeReading`).** An answer stores only its distribution: a `ChoiceAnswer` keys `probabilities` by option name in criteria order, a `ScoreAnswer` indexes them by level, and a `NoulAnswer` holds `noul`, the probability of yes. `computeReading(answer)` derives a `Reading`, and nothing stores it. `winner` is the first strictly greatest candidate in enumeration order: an option name, a level index written as a string, or `'false'` then `'true'` for a noul, so a noul of exactly 0.5 names `'false'`. `probability` is the winner's probability. `confidence` follows the published TypeSafe formulas: `(max(p) - 1/n) / (1 - 1/n)` for a choice, `|2p - 1|` for a noul (the choice formula at n = 2), and `max(0, 1 - spread / even_spread)` for a score, where `spread` sums each level's probability times its distance from the winning level and `even_spread` is the mean distance from the middle level. `score` is the expected level `sum(i * p_i)` and is set for a score answer alone. A choice or a score with fewer than 2 candidates throws `JudgeError` with code `PROTOCOL`, because both formulas divide by the candidate count.
|
|
1126
|
+
40. **The protocol's null and the absence law.** An undescribed choice option stays `null` in `ChoiceCriteria`, because a map holding an option with no description and a map without that option ask different questions and JSON carries no `undefined` member. An undescribed score level stays `null` in `ScoreCriteria`, because a level is positional. `instructions`, `NoulCriteria`, and each side of a `NoulCriteria` value are omitted when absent and never `null`, and `isJudgeQuestion` refuses a `null` in those places. `state` is required and never `null`. On the System One wire an omitted member stays omitted and a `null` criterion is sent as `null`.
|
|
1127
|
+
41. **Validation before inference, a wire's own limits, and the server's limits.** `ask` checks the universal shape before any call: a `state` that `isJudgeEntry` accepts, a non-empty question map, and every question accepted by `isJudgeQuestion`, which needs at least 2 choice options and 2 score levels and each entry a `JudgeEntry` or a permitted `null`. A failure throws `JudgeError` with code `QUESTION` and one of the messages `judge error: state is not a judge entry`, `judge error: no questions`, and `judge error: question <id> is malformed`, and no call is made. A wire can refuse its own limit before transport with code `QUESTION`: the Ollama judge in the `@orkestrel/ollama` package refuses a choice of more than 20 options or a score of more than 10 levels. The System One wire leaves every other limit, such as a server's cap on options, levels, questions, or body bytes, to the server. A server refusal throws `JudgeError` with code `HTTP` and carries the response status in its `status` member. Its message is `judge error: <status> - <excerpt>` when the error body is not empty, with the excerpt read from at most `MAX_ERROR_BODY_LENGTH` bytes of the body, and `judge error: <status>` with no separator when the body is empty, such as `judge error: 400`. A successful response with a `null` body or a body that is not JSON throws `JudgeError` with code `PROTOCOL`. Retrying is the caller's decision.
|
|
1128
|
+
42. **Each call's own deadline, and the abort partial (`JudgeAbortError`).** Each call runs under a `Timeout` of `AgentJudgeInput.timeout` milliseconds (`DEFAULT_PROVIDER_TIMEOUT` when omitted) folded with the caller's signal through `AbortSignal.any`. That bound covers the `headers` hook, the transport, the error-body read, the response read, and `read`, and the timer is cleared on every exit. A signal already aborted throws `JudgeAbortError` with an empty `partial` before any call. A cancel by either bound throws `JudgeAbortError` whose `code` is `'ABORT'` and whose `partial` is the `buildJudgeResult` merge of the calls that completed, with their answers, refusals, and usage, so usage reported by completed calls is retained. A throw that raced the cancel rides as the error's `cause`. A cancel that lands while `read` decodes the answer is reported with no `cause`, and the answer that call decoded is left out of the `partial`. A transport failure with neither bound fired reaches the caller unchanged.
|
|
1129
|
+
43. **The batch switch and the merge (`buildJudgeResult`).** `AgentJudgeInput.batch` (default `true`) is the one binary behavior a judge wire varies on: `true` sends every question in one call, and `false` sends one call per question in the request's key order, each carrying the shared `state`. `ask` builds and serializes every call's body before the first call, so a wire's refusal from `body` lands before any inference, and then runs the calls one after another. `buildJudgeResult` merges their results: answers and refusals joined by id, `refusals` omitted when empty, each call's usage passed through `sanitizeUsage` and added with `sumUsage`, `usage` omitted when no call reported one, and `model` taken from the first call, or the judge's configured `model` when no call completed.
|
|
1130
|
+
44. **The System One wire (`SystemOneJudge` / `createSystemOneJudge`).** `SystemOneJudge` passes `SYSTEM_ONE_PATH` and `batch: true` to the engine, so every question travels in one `POST` to `url` plus `'/v1/systemone'`, and its `name` is `'systemone'`. `body` sends the TypeSafe System One request: the `state`, the configured `model`, and each question through `questionToSystemOne`, which writes `form` as `type` and leaves an omitted member omitted. `read` checks the envelope with `isSystemOneResponse`, then decodes each requested id with `isSystemOneAnswer` and `extractSystemOneAnswer`. A score's `probabilities` member is accepted as a map keyed by level (TypeSafe Jev, Ollama, and Mica's server) or as an array (the llama.cpp server). The requested candidates are kept in criteria order, and unrequested answers, extra candidates, and unknown members are ignored. The server's `confidence`, `score`, and `choice` members are ignored, as is its `legend` member, which repeats the question's criteria: the servers compute those measures by different formulas, while the stored distribution reads the same on all of them, so the `computeReading` function derives every measure locally. A System One server answers only from the supplied options, so the wire never reports a refusal: an answer that is missing, whose `type` differs from the question's `form`, that lacks a finite probability in `[0, 1]` for a requested label or level, or whose `noul` lies outside `[0, 1]` throws `JudgeError` with code `PROTOCOL` naming the question id. `url` has no default, so no state leaves the host until the caller names a server.
|
|
1131
|
+
45. **The System One response model and usage.** On the System One wire, the `JudgeResult.model` member reports the model the response named, verbatim, so a configured alias or a gateway's provider prefix reaches the caller as the server reported it; a response that names no model reports the configured `model` option. Another wire reports the identity its guide defines. `extractSystemOneUsage` maps `input_tokens` and `output_tokens` onto `{ prompt, completion, total }`, and `usage` is absent when either count is missing, `null`, negative, or not finite.
|
|
1132
|
+
46. **Header-hook authentication.** A judge authenticates through the `headers` hook, as a provider does, and both engines read it through `readHeaders`: the headers start from `Content-Type: application/json`, the hook is awaited with the call's combined signal, each entry it returns is set over the defaults, and the hook races that signal, so an unresolved hook cannot outlive the call. A server that takes a key, such as TypeSafe Jev, receives it from a hook that returns an `authorization` header; a local server that takes none, such as Ollama, needs no hook.
|
|
1133
|
+
|
|
1026
1134
|
## Patterns
|
|
1027
1135
|
|
|
1028
1136
|
### Bounding any provider call
|
|
@@ -1094,6 +1202,102 @@ class TextProvider extends AgentProvider<string> {
|
|
|
1094
1202
|
|
|
1095
1203
|
That provider drives the whole runtime unchanged — `createAgent`, the tool loop, the authority gate, and durable jobs all read it through `ProviderInterface`. Take the switches deliberately. Leave `split` at its default when the backend inlines its reasoning as `<think>` spans in the answer, and set `split: false` when the backend already separates reasoning onto its own field, because splitting twice re-classifies text the backend has already ruled on. Set `strict: true` when the protocol always ends with a settled record and a stream that stops short is a protocol failure worth reporting; leave it `false` when the answer is assembled from the deltas themselves.
|
|
1096
1204
|
|
|
1205
|
+
### Asking a System One server a choice, a noul, and a score
|
|
1206
|
+
|
|
1207
|
+
Ask every question about one state in one request, and read each answer with the `computeReading` function. The `SystemOneJudge` class speaks the System One decision protocol. Its wire was recorded against Ollama 0.40.0 serving the `tev1:0.8b` model on 2026-10-07, and it decodes the llama.cpp server's array form and the extra members of Mica's `typesafe_server.py` script as their sources document them. Point the `url` option at the server origin; the wire appends the `SYSTEM_ONE_PATH` constant and fills the request's `model` member from the judge's configured model.
|
|
1208
|
+
|
|
1209
|
+
```ts
|
|
1210
|
+
import { computeReading, createSystemOneJudge } from '@orkestrel/agent'
|
|
1211
|
+
|
|
1212
|
+
const judge = createSystemOneJudge({ url: 'http://localhost:11434', model: 'tev1:0.8b' })
|
|
1213
|
+
const result = await judge.ask(
|
|
1214
|
+
{
|
|
1215
|
+
state: 'Our checkout has returned 500 errors since 9am. I want a refund for today.',
|
|
1216
|
+
questions: {
|
|
1217
|
+
label: {
|
|
1218
|
+
form: 'choice',
|
|
1219
|
+
instructions: 'Which label fits this ticket?',
|
|
1220
|
+
criteria: { billing: 'Payments and refunds', bug: 'Software errors', account: null },
|
|
1221
|
+
},
|
|
1222
|
+
refund: {
|
|
1223
|
+
form: 'noul',
|
|
1224
|
+
instructions: 'Does the customer ask for money back?',
|
|
1225
|
+
criteria: {
|
|
1226
|
+
true: 'The customer asks for a refund or for money back.',
|
|
1227
|
+
false: 'The customer does not ask for money back.',
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
severity: {
|
|
1231
|
+
form: 'score',
|
|
1232
|
+
instructions: 'How severe is the reported issue?',
|
|
1233
|
+
criteria: ['Cosmetic; no impact', 'Degraded, workaround exists', 'Blocking; no workaround'],
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
},
|
|
1237
|
+
AbortSignal.timeout(30_000),
|
|
1238
|
+
)
|
|
1239
|
+
const readings = Object.fromEntries(
|
|
1240
|
+
Object.entries(result.answers).map(([id, answer]) => [id, computeReading(answer)]),
|
|
1241
|
+
)
|
|
1242
|
+
result.model // 'tev1:0.8b' — the model the server named
|
|
1243
|
+
result.usage // { prompt: 975, completion: 4, total: 979 }
|
|
1244
|
+
readings.label // { winner: 'bug', probability: 0.9691, confidence: 0.9536 } to four decimals
|
|
1245
|
+
readings.refund // { winner: 'true', probability: 0.9979, confidence: 0.9958 } to four decimals
|
|
1246
|
+
readings.severity // { winner: '1', probability: 0.9494, confidence: 0.9241, score: 0.9919 } to four decimals
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
The values in the comments are the readings of the response Ollama 0.40.0 returned for this request on 2026-10-07. That same response carried the server's own measures, and the wire drops them: a `confidence` of 0.8718 for `label` and 0.7864 for `severity`, where the published formulas read 0.9536 and 0.9241. The servers disagree on those formulas: Ollama reports an entropy measure, Mica's server reports the greatest probability as its `confidence` and the most likely level as its `score`, and Jev reports the published ratio. The stored `probabilities` and `noul` mean the same on every server, so the `computeReading` function derives each measure locally and every server reads on one scale. A `choice` member repeats the most likely option and a `legend` member repeats the question's criteria, so the wire drops both as well (the System One wire clause).
|
|
1250
|
+
|
|
1251
|
+
A server that takes a key receives it through the `headers` hook, which each call awaits inside its own deadline (the header-hook authentication clause). TypeSafe Jev takes a bearer key:
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
import { createSystemOneJudge } from '@orkestrel/agent'
|
|
1255
|
+
|
|
1256
|
+
declare const key: string // the TypeSafe API key, read from the host's secret store
|
|
1257
|
+
|
|
1258
|
+
const jev = createSystemOneJudge({
|
|
1259
|
+
url: 'https://api.typesafe.ai',
|
|
1260
|
+
model: 'jev-latest',
|
|
1261
|
+
headers: () => ({ authorization: `Bearer ${key}` }), // awaited inside each call's deadline
|
|
1262
|
+
})
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
The executed transcription in [`tests/guides.test.ts`](../tests/guides.test.ts) serves the recorded Ollama response from a loopback `@orkestrel/server` listener and points the first fence's `url` option at that listener in place of `http://localhost:11434`, and it supplies a recording `fetch` transport to the second fence in place of the network. It also decodes the same distribution in the llama.cpp array form and with Mica's server measures, both transliterated from that recording, and reads the same answers from all three.
|
|
1266
|
+
|
|
1267
|
+
### Writing a judge wire
|
|
1268
|
+
|
|
1269
|
+
Reach a decision server this package does not speak by extending `AgentJudge` rather than implementing `JudgeInterface` from nothing. The engine already holds each call's deadline, the transport, the `headers` hook, the validation before inference, the bounded error read, and the merge of the calls (the batch-switch clause), so a subclass is the wire and nothing else: `name` identifies it, `body` projects one call's `JudgeRequest` onto the server's request shape, and `read` decodes one call's parsed response into a `JudgeResult`. This server reads one question per call, so the fence sets `batch: false`.
|
|
1270
|
+
|
|
1271
|
+
```ts
|
|
1272
|
+
import type { JudgeRequest, JudgeResult } from '@orkestrel/agent'
|
|
1273
|
+
import { AgentJudge, JudgeError } from '@orkestrel/agent'
|
|
1274
|
+
import { isFiniteNumber, isRecord } from '@orkestrel/contract'
|
|
1275
|
+
|
|
1276
|
+
// A wire whose server answers one yes/no question per call as { "yes": 0.93 }.
|
|
1277
|
+
class YesJudge extends AgentJudge {
|
|
1278
|
+
readonly name = 'yes'
|
|
1279
|
+
body(request: JudgeRequest): object {
|
|
1280
|
+
return { model: this.model, state: request.state, questions: request.questions }
|
|
1281
|
+
}
|
|
1282
|
+
read(value: unknown, request: JudgeRequest): JudgeResult {
|
|
1283
|
+
const [id] = Object.keys(request.questions)
|
|
1284
|
+
if (id === undefined || !isRecord(value) || !isFiniteNumber(value.yes)) {
|
|
1285
|
+
throw new JudgeError('PROTOCOL', 'judge error: unreadable answer')
|
|
1286
|
+
}
|
|
1287
|
+
return { model: this.model, answers: { [id]: { form: 'noul', noul: value.yes } } }
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const judge = new YesJudge({
|
|
1292
|
+
url: 'http://localhost:8010',
|
|
1293
|
+
path: '/v1/yes',
|
|
1294
|
+
model: 'yes-1',
|
|
1295
|
+
batch: false,
|
|
1296
|
+
})
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
That judge answers through `ask` like any other. With `batch: false` the engine makes one call per question in key order, each carrying the shared `state` and one question, and merges their answers into one `JudgeResult`. Throw `JudgeError` with code `PROTOCOL` from `read` for a response the wire cannot read; the engine passes it to the caller unchanged. Throw `JudgeError` with code `QUESTION` from `body` for a question the protocol cannot carry; the engine builds every body before the first call, so that refusal lands before any inference. A wire that reads candidates out of a model's own output and cannot find one returns a `Refusal` under `refusals`, listing the caller's keys it could not read, instead of inventing a probability.
|
|
1300
|
+
|
|
1097
1301
|
### Relaying a browser provider through your own server
|
|
1098
1302
|
|
|
1099
1303
|
When you compose `createAgent(browser, { tools })` in the page, the agent dispatches page tools there after the Node provider returns a tool call. The Node server runs the relay handler and upstream inference. See [Placement proofs](#placement-proofs) for the planned Chromium receipt; the executed transcription described next proves an HTTP hop in Node.
|
|
@@ -1522,7 +1726,7 @@ void agent
|
|
|
1522
1726
|
|
|
1523
1727
|
## Tests
|
|
1524
1728
|
|
|
1525
|
-
- [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` bijection for agent-owned values and types, exhaustive method parity for every agent-owned interface and class pair documented here, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Conversations & compaction` 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 their comments claim, including the relay halves driven over a started `@orkestrel/server` listener on loopback — the declared route's own `405` and `404`, the `401` a wrong bearer answers, the round trip, and the upstream turn a disconnected reader cancels.
|
|
1729
|
+
- [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔ `src/core` bijection for agent-owned values and types, exhaustive method parity for every agent-owned interface and class pair documented here, and the equality gate: every `Summary` cell against its declaration's description paragraph, the titled `Conversations & compaction` 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 their comments claim, including the relay halves driven over a started `@orkestrel/server` listener on loopback — the declared route's own `405` and `404`, the `401` a wrong bearer answers, the round trip, and the upstream turn a disconnected reader cancels. It runs the judge fences the same way — the System One request against the recorded Ollama response served from a loopback listener, the readings `computeReading` derives from it, the `headers` hook fence, and the judge-wire fence — and backs each judge Contract clause with an executed assertion.
|
|
1526
1730
|
- [`tests/src/core/conversations/Conversation.test.ts`](../tests/src/core/conversations/Conversation.test.ts) — `add` single mints a fresh `id` + carries `role` / `content` (and `calls` only when given, the field omitted otherwise); `add` batch returns the created messages in order with unique ids; the returned message reflects its input and is the same object `message(id)` resolves (immutable); `message` lookup / miss; `messages()` insertion order; `remove` single + batch (`true` only when every supplied id was removed) + `clear` + `count`; and hydration through the `ConversationOptions.snapshot` seam — the one way to restore (a `createConversation` restore of the stored id, rollup, sections, and live tail that re-snapshots identically, the snapshot id winning over an options id, a restored conversation's `view` / `search` / `count` and continued compaction, and no event emitted while restoring).
|
|
1527
1731
|
- [`tests/src/core/AgentContext.test.ts`](../tests/src/core/AgentContext.test.ts) — `build()` with a system prompt prepends `{ role: 'system', content }` then the conversation in order; without one (and empty managers) returns only the conversation (no system turn, `system === undefined`, empty → `[]`, and an explicit `''` / whitespace system still prepended); `context.tools` is the passed registry (or a fresh empty `ToolManager`) and `build()` never includes a tool's name / description; `build()` is fresh each call (reflects messages added between builds, mints a new system `id`, snapshot-independent). And the richer assembly: the `instructions` manager is fresh empty (or reused by identity); an empty manager contributes nothing (lean behavior preserved); it folds into the system block under its `description` + per-item `override`; the readonly `scope` getter (default `undefined`, initial through options) + `apply(scope)` / `apply(undefined)` through an `AgentContextInterface` binding; and scope filtering per category — `undefined` ⇒ all, a named allow-list ⇒ only-listed, `[]` ⇒ none over the instructions, with the conversation passing through unfiltered, recomputed fresh when the scope is changed between builds. And the active-workspace render by carrier (the only document/image channel): `context.workspaces` is always present (fresh empty `WorkspaceManager`) or supplied structurally through options; the active workspace's text files fold into a `## Workspace` system section (fenced through `renderFencedFile`, placed after instructions) and its image files' base64 attaches to the last user message (own-images-first merge; multiple in insertion order; skipped when no user message; the stored message is never mutated); `scope.files` filters both carriers; the render is active-only (a non-active workspace's files never render, reflected through a `switch`); no active workspace ⇒ nothing rendered. And the injected-conversation message source (a data-stub summarizer): with one, `context.messages` is its live tail and `build()` folds its `view()` (the compacted view after a `compact()`), no scope filtering over messages (the conversation is authoritative) while scope still filters instructions; without one, the plain message-store path. And the structural conversation registry (the multi-conversation switch): supplied through options, its active conversation changes through `conversations.switch(id)`, re-pointing `messages` to the new live tail by identity (no duplication), and `build()` follows the switch; constructing with an empty supplied registry adds a default active conversation; a `compact()` on the active conversation is reflected through the switch.
|
|
1528
1732
|
- [`tests/src/core/scopes/Scope.test.ts`](../tests/src/core/scopes/Scope.test.ts) — `Scope` construction (a minted `id` + the per-category lists including `files`, copied in so a later mutation of the caller array can't leak in; `[]` distinct from `undefined`); and `narrow`'s set-intersection semantics — `list ∩ list` = the keys in both, `undefined ∩ list` = the list (no parent constraint), `list ∩ undefined` = the list, `undefined ∩ undefined` = `undefined`, `[]` ⇒ none either side, narrowing only tightens (a parent-excluded key never returns), per-category independence (incl. `files`), name preserved, immutable (a new scope, parent untouched), and chained narrows compose.
|
|
@@ -1538,6 +1742,8 @@ void agent
|
|
|
1538
1742
|
- [`tests/src/core/AgentProvider.test.ts`](../tests/src/core/AgentProvider.test.ts) — the shared HTTP engine over a scripted wire subclass and real `Response` bodies (no transport mock): identity and exact `format` exposure, the default transport bound to its global receiver, the posted body and case-insensitive header overrides, and a `headers` hook that adds only authentication leaving the JSON content type intact. Stream assembly — content, native reasoning, tool calls, and replacing usage; the qwen3 implicit-open reclassification in the authoritative result; a held content tail flushed as the final delta; verbatim content when `split` is disabled; a buffered record fed through `finish` with the parser cleared; a multibyte character split across byte chunks; a settled `result` record returned unchanged, accepted from `finish`, and leaving a following poison record undecoded with the body cancelled; and `strict` end of input with no settled result rejected. HTTP failures — the bounded error-body read with its remainder cancelled, the documented single-chunk overshoot, an exact-bound stalled body rejected before the deadline, the omitted separator for an empty excerpt, the retained status and cause when the body cannot be read at all, a successful response with no body as a protocol failure, and code / status / cause / `instanceof` narrowing. Failures that reach the caller unchanged — a hostile record decoder error and a remotely reported abort, the remote abort's identity preserved and its open body cancelled with the local signal unaborted. The `headers` hook inside the bound — raced against the deadline, a rejection preserved with no request issued, cancellation through the caller signal, and every abort listener removed after success, rejection, caller cancellation, and deadline expiry. Cancellation and partials — an already-aborted call rejected before framing or fetching, held content flushed into the partial, a stop between channels retaining the complete increment, a stalled body cancelled on the deadline, buffered `finish` records refused after a cancel, a transport `AbortError` normalized, a caller `ProviderAbortError` reason replaced by the local partial, a decoder failure that raced the cancel carried as the abort's `cause`, and that cause left `undefined` when the cancel was the only failure. Deadline clearing and reader release on every exit, and isolation between concurrent calls on one instance (including `generate` deep-equal to a drained `stream`).
|
|
1539
1743
|
- [`tests/src/core/RelayStream.test.ts`](../tests/src/core/RelayStream.test.ts) — the relay's response half over real `Response` bodies: validated deltas and the authoritative result written with `RELAY_CONTENT_TYPE` and `cache-control: no-store`; an upstream failure reduced to the fixed `RELAY_PROVIDER_MESSAGE` with no secret text; a remote abort's partial written through the same compiled frame contract; a result outside the JSON frame contract refused; serialized `next` calls that stop pulling while the response queue is full (the backpressure proof); upstream aborted before the iterator returns with a late pending pull suppressed; an already-aborted inbound signal linked before the upstream turn starts; and an inbound abort with a frame still queued unread returning and finalizing the generator.
|
|
1540
1744
|
- [`tests/src/core/providers/RelayProvider.test.ts`](../tests/src/core/providers/RelayProvider.test.ts) — the browser end: a synthetic `parameters`, `schema`, or call-`arguments` serializer ignored with the snapshot sent in its place, a projection failure carried as the refusal's `cause`, a valid request owned as a snapshot whose JSON values survive caller mutation, an `error` frame carrying a decorative `code` refused, the declared request fields projected with extra execution context omitted across a JSON round trip, non-JSON arguments / parameters / schemas rejected, validated `content` and `thinking` frames mapped with literal `<think>` tags preserved, an `abort` frame reconstructed with its complete partial while the local signal stays unaborted, malformed frames and a missing terminal result refused, and a fragmented unterminated final result recovered with fresh framing for each call.
|
|
1745
|
+
- [`tests/src/core/AgentJudge.test.ts`](../tests/src/core/AgentJudge.test.ts) — the judge engine over a scripted wire subclass and real `Response` bodies: a minted `id` and the configured `model`; one call carrying every question when `batch` is omitted, and one call per question in key order with summed usage when it is `false`; the configured model and no usage when a response names neither; the global transport bound when `fetch` is omitted; an empty question map, a malformed question, and a state outside `JudgeEntry` refused with no call; every body built before the first call; the HTTP status with a bounded excerpt, an empty or unreadable error body, and a null or non-JSON body; a wire or transport failure passed through unchanged; the call's combined signal handed to the `headers` hook; and the cancellation partials — an already-aborted signal, an abort in the second call keeping the first call's answer and usage, a cancel during decoding with no cause, a stalled call aborted at its deadline, and an unresolved header hook cancelled.
|
|
1746
|
+
- [`tests/src/core/providers/SystemOneJudge.test.ts`](../tests/src/core/providers/SystemOneJudge.test.ts) — the System One wire against the bodies recorded on 2026-10-07: the TypeSafe quickstart projected with no extra members, the exact recorded Ollama request posted once with authentication and its response decoded, a structured state and structured instructions preserved, the llama.cpp array form and Mica's server measures and extra fields decoded to the same answers, a gateway model name reported verbatim, unrequested answers ignored, and a malformed envelope, a missing, mismatched, or incomplete answer, and each recorded HTTP 400 refusal reported with its code.
|
|
1541
1747
|
- [`tests/src/core/integration.test.ts`](../tests/src/core/integration.test.ts) — the in-process hop, browser end to server end with no network: each refusal (`401`, `413`, `400`) carried to the browser as a `ProviderError` with its status while the upstream provider is never entered; a full round trip of identified messages, tools, options, deltas, and the authoritative result, deep-equal to the same provider driven directly; browser cancellation propagated through the request and upstream signals; a server-side abort reconstructed while the browser signal stays unaborted; and a secret upstream failure translated to the fixed public provider error. Beside it, provider-agnosticism over a minimal provider driving the full loop, a drop-in swap between differently named providers, and each provider's `format` reaching `build()`.
|
|
1542
1748
|
- [`tests/src/core/contracts.test.ts`](../tests/src/core/contracts.test.ts) — the compiled wire contracts: a message, a request, a result, and every relay channel round-tripped, each reporting the path of its malformed field; function-valued tool arguments accepted in the domain and refused on the wire; non-JSON parameters and schemas refused before serialization can drop them; and every accepted message fixture still inside the domain guard.
|
|
1543
1749
|
- [`tests/src/core/shapers.test.ts`](../tests/src/core/shapers.test.ts) — the wire shapes at the type level: each projection inferred assignable to the domain type it mirrors, and execution context kept separate from the call envelope and excluded from the tool wire shape.
|