@orkestrel/agent 0.0.20 → 0.0.22

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.
@@ -1,46 +1,46 @@
1
- import { attempt, isArray, isBoolean, isFiniteNumber, isObject, isRecord, isString, parseJSONValue, rawShape, stringShape } from "@orkestrel/contract";
1
+ import { arrayOf, arrayShape, attempt, booleanShape, cloneJSONValue, createContract, isArray, isBoolean, isFiniteNumber, isObject, isRecord, isString, jsonShape, literalShape, numberShape, objectShape, optionalShape, parseJSONAs, parseJSONValue, rawShape, recordShape, stringShape, unionShape } from "@orkestrel/contract";
2
2
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
3
  import { createQueue } from "@orkestrel/queue";
4
4
  import { createRunner, errorToMessage } from "@orkestrel/workflow";
5
5
  import { createAbort } from "@orkestrel/abort";
6
- import { createTimeout } from "@orkestrel/timeout";
6
+ import { Timeout, createTimeout } from "@orkestrel/timeout";
7
7
  import { Emitter } from "@orkestrel/emitter";
8
8
  import { ToolManager, isToolCall } from "@orkestrel/tool";
9
9
  import { WorkspaceManager, isBinary, isText } from "@orkestrel/workspace";
10
10
  import { createTokenBudget } from "@orkestrel/budget";
11
11
  //#region src/core/constants.ts
12
12
  /**
13
- * Caps an {@link AgentInterface} turn's tool iterations by default — the maximum
14
- * number of context → provider → tools cycles before the loop stops, so a model that
15
- * keeps requesting tools can never loop forever. Overridable per agent through
16
- * `AgentOptions.limit`.
13
+ * Caps an {@link AgentInterface} turn's tool iterations by default — `10` context → provider →
14
+ * tools cycles before the loop stops, so a model that keeps requesting tools can never loop
15
+ * forever. Overridable per agent through `AgentOptions.limit`.
17
16
  */
18
17
  var DEFAULT_AGENT_LIMIT = 10;
19
18
  /**
20
19
  * Names the zone an {@link AuthorityInterface}'s default fallback {@link AuthorityDecision}
21
- * carries — the classification for a tool call that matched no rule. Paired with the
22
- * default `allowed: true` fallback, an unmatched call is allowed under this zone, so a
23
- * rules list of denials acts as a denylist; a caller wanting deny-by-default supplies
24
- * an `allowed: false` `fallback` of their own (see `AuthorityOptions`).
20
+ * carries — `'default'`, the classification for a tool call that matched no rule. Paired with
21
+ * the default `allowed: true` fallback, an unmatched call is allowed under this zone, so a
22
+ * rules list of denials acts as a denylist; a caller wanting deny-by-default supplies an
23
+ * `allowed: false` `fallback` of their own (see `AuthorityOptions`).
25
24
  */
26
25
  var DEFAULT_AUTHORITY_ZONE = "default";
27
26
  /**
28
27
  * Sets the default number of recent live messages a {@link ConversationInterface}'s `compact()`
29
- * RETAINS verbatim — `0`, so a manual `compact()` folds ALL of the current live messages
30
- * into one summarized section (no tail kept). A caller retains a recent tail by passing
31
- * `keep` (on {@link ConversationOptions}, {@link ConversationManagerOptions}, or per-fold
32
- * through {@link CompactOptions}), folding only the older `count - keep` messages and leaving
33
- * the most recent `keep` live for the next turn. Overridable everywhere `keep` is accepted.
28
+ * retains verbatim — `0`, so a manual `compact()` folds every current live message into one
29
+ * summarized section and keeps no tail. A caller retains a recent tail by passing `keep` (on
30
+ * {@link ConversationOptions}, {@link ConversationManagerOptions}, or per-fold through
31
+ * {@link CompactOptions}), folding only the older `count - keep` messages and leaving the most
32
+ * recent `keep` live for the next turn. Overridable everywhere `keep` is accepted.
34
33
  */
35
34
  var DEFAULT_CONVERSATION_KEEP = 0;
36
35
  /**
37
36
  * Names the framing label a {@link ConversationInterface}'s `view()` prefixes onto each compacted
38
- * section's summary so a small model reads it as a CONDENSED RECAP of earlier turns — not a
39
- * literal assistant turn to echo or treat as the live answer.
37
+ * section's summary so a small model reads it as a condensed recap of earlier turns — the lean
38
+ * `'[Summary of earlier messages] '` marker, never a literal assistant turn to echo or treat as
39
+ * the live answer.
40
40
  *
41
41
  * @remarks
42
- * Deliberately a FIXED, lean handful of tokens (a short bracketed marker) so the framing adds a
43
- * bounded `prefix × sections` overhead and NEVER an open-ended blow-up — the
42
+ * Deliberately a fixed, lean handful of tokens (a short bracketed marker) so the framing adds a
43
+ * bounded `prefix × sections` overhead and never an open-ended blow-up — the
44
44
  * {@link ConversationInterface} no-bloat test guard pins exactly that. Kept here (beside
45
45
  * {@link DEFAULT_CONVERSATION_KEEP}) as the conversation layer's one tunable framing constant, so
46
46
  * the wording has a single source of truth as it is optimized against real small-model behavior
@@ -50,73 +50,129 @@ var DEFAULT_CONVERSATION_KEEP = 0;
50
50
  var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
51
51
  /**
52
52
  * Names the opening tag a {@link import('./ThinkSplitter.js').ThinkSplitter} recognizes as the start of
53
- * an in-content reasoning span — the de-facto wire convention thinking models (qwen3, DeepSeek-R1
54
- * family) emit their chain-of-thought under when a daemon renders it inline instead of on a
55
- * separate wire field. Paired with {@link THINK_CLOSE}.
53
+ * an in-content reasoning span — `'<think>'`, the de-facto wire convention thinking models
54
+ * (qwen3, DeepSeek-R1 family) emit their chain-of-thought under when a daemon renders it inline
55
+ * instead of on a separate wire field. Paired with {@link THINK_CLOSE}.
56
56
  */
57
57
  var THINK_OPEN = "<think>";
58
58
  /**
59
- * Names the closing tag that ends a {@link THINK_OPEN} reasoning span. A span the stream never closes
60
- * (the model was cut off mid-reasoning) is treated as thinking to its end —
61
- * {@link import('./types.js').ThinkSplitterInterface.flush} settles it.
59
+ * Names the closing tag that ends a {@link THINK_OPEN} reasoning span — `'</think>'`. A span the
60
+ * stream never closes (the model was cut off mid-reasoning) is treated as thinking to its end,
61
+ * and {@link import('./types.js').ThinkSplitterInterface.flush} settles it.
62
62
  */
63
63
  var THINK_CLOSE = "</think>";
64
64
  /**
65
65
  * Names the section header {@link import('./AgentContext.js').AgentContext}'s `build()` renders the
66
- * ACTIVE workspace's TEXT files under — the leading line of the dedicated workspace block in the
67
- * system message, the carrier-split counterpart to the documents / images section headers.
66
+ * active workspace's text files under — `'## Workspace'`, the leading line of the dedicated
67
+ * workspace block in the system message and the carrier-split counterpart to the documents and
68
+ * images section headers.
68
69
  *
69
70
  * @remarks
70
- * `build()` OWNS the workspace render (a `Workspace` / `WorkspaceManager` stays file-focused — no
71
+ * `build()` owns the workspace render (a `Workspace` / `WorkspaceManager` stays file-focused — no
71
72
  * `open` / `format` getters), so this header lives here as the agents module's one
72
73
  * workspace-section framing constant rather than on a manager. Each workspace text file renders
73
- * beneath it as a fenced `` File: <path>\n```<language>\n<text>\n``` `` block — the SAME framing
74
+ * beneath it as a fenced `` File: <path>\n```<language>\n<text>\n``` `` block — the same framing
74
75
  * the documents section uses — placed immediately after the documents section in the system block.
75
76
  */
76
77
  var WORKSPACE_SECTION_HEADER = "## Workspace";
77
78
  /**
78
- * Estimates the per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
79
- * adds on top of a message's content estimate — accounts for the fixed wire framing every
80
- * conversation turn carries (its role tag, delimiters) that {@link import('./helpers.js').estimateTokens}'s
81
- * content-only heuristic does not otherwise capture.
79
+ * Estimates the per-message role and framing overhead {@link import('./helpers.js').estimateMessages}
80
+ * adds on top of a message's content estimate — `4` tokens for the fixed wire framing every
81
+ * conversation turn carries (its role tag, its delimiters) that
82
+ * {@link import('./helpers.js').estimateTokens}'s content-only heuristic does not otherwise
83
+ * capture.
82
84
  */
83
85
  var MESSAGE_TOKEN_OVERHEAD = 4;
84
86
  /**
85
- * Names the coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
86
- * charges for each attached image.
87
+ * Names the coarse, deliberately approximate per-image token cost
88
+ * {@link import('./helpers.js').estimateMessages} charges for each attached image — `512`, because
89
+ * a base64 payload's length is no reliable token proxy.
87
90
  *
88
91
  * @remarks
89
- * A base64 image payload's LENGTH is NOT a reliable token proxy (a vision model's actual image
92
+ * A base64 image payload's length is not a reliable token proxy (a vision model's actual image
90
93
  * token cost depends on resolution / tiling, not byte size), so this is a fixed, coarse
91
94
  * per-image estimate rather than a derivation from `image.length` — a planning heuristic, not an
92
95
  * exact count.
93
96
  */
94
97
  var IMAGE_TOKEN_ESTIMATE = 512;
98
+ /**
99
+ * Holds the default provider deadline in milliseconds — `120_000`, the wall-clock bound a call
100
+ * runs under when `AgentProviderInput.timeout` is omitted, folded with the caller's signal so
101
+ * whichever trips first cancels the call.
102
+ */
103
+ var DEFAULT_PROVIDER_TIMEOUT = 12e4;
104
+ /**
105
+ * Bounds the decoded error excerpt's input in bytes — `2048`, the leading bytes of a non-OK
106
+ * response body handed to the decoder before the read cancels the remainder, so a `ProviderError`
107
+ * message never carries a longer excerpt.
108
+ */
109
+ var MAX_ERROR_BODY_LENGTH = 2048;
110
+ /**
111
+ * Holds the default relay request limit in bytes — `1_048_576`, the byte budget a relay applies
112
+ * to an inbound body when `RelayOptions.limit` is omitted, refusing a body that reaches it.
113
+ */
114
+ var DEFAULT_RELAY_LIMIT = 1048576;
115
+ /**
116
+ * Names the relay's newline-delimited JSON content type — `'application/x-ndjson; charset=utf-8'`,
117
+ * the header a relay response carries beside `cache-control: no-store`.
118
+ */
119
+ var RELAY_CONTENT_TYPE = "application/x-ndjson; charset=utf-8";
120
+ /**
121
+ * Names the public message for an unexpected upstream relay failure — `'relay provider failed'`,
122
+ * the fixed text every `error` frame carries, so an upstream failure's own message never reaches
123
+ * the browser.
124
+ */
125
+ var RELAY_PROVIDER_MESSAGE = "relay provider failed";
126
+ /**
127
+ * Names the status a relay answers when the `authorize` callback returns anything but `true` or throws —
128
+ * `401`, carried with no body and reaching the browser as a `ProviderError` with the `HTTP` code.
129
+ */
130
+ var UNAUTHORIZED_RELAY_STATUS = 401;
131
+ /**
132
+ * Names the status a relay answers when the upstream provider call cannot be constructed — `502`,
133
+ * carried with no body after `provider.stream` was entered and threw before returning its
134
+ * iterator.
135
+ */
136
+ var UPSTREAM_RELAY_STATUS = 502;
137
+ /**
138
+ * Names the status a relay answers for a body that is missing, unreadable, or rejected by
139
+ * `providerRequestContract` — `400`, carried with no body and reaching the browser as a
140
+ * `ProviderError` with the `HTTP` code.
141
+ */
142
+ var INVALID_RELAY_STATUS = 400;
143
+ /**
144
+ * Names the status a relay answers for a request body at or above its byte budget — `413`,
145
+ * carried with no body and answered for an aborted inbound read as well.
146
+ */
147
+ var OVERSIZED_RELAY_STATUS = 413;
95
148
  //#endregion
96
149
  //#region src/core/errors.ts
97
150
  /**
98
151
  * Reports a provider stream cancelled mid-flight by its bound signal — thrown by a
99
- * {@link ProviderInterface}'s `stream`, carrying the {@link ProviderResult} assembled
100
- * from whatever streamed before the cancel.
152
+ * {@link ProviderInterface}'s `stream`, carrying the {@link ProviderResult} assembled from
153
+ * whatever streamed before the cancel and the machine-readable `code` `'ABORT'`.
101
154
  *
102
155
  * @remarks
103
156
  * Lets a caller recover the partial content (and any tool calls / usage seen so far)
104
157
  * on cancellation: `catch` the throw, narrow with {@link isProviderAbortError}, and
105
158
  * read `partial`. `code` is the machine-readable condition (`'ABORT'` — the only one this
106
- * error reports), so a `catch` branches on it rather than on the message string.
159
+ * error reports), so a `catch` branches on it rather than on the message string. `cause`
160
+ * holds the failure the cancel superseded when a throw raced the abort — the wire decoder's
161
+ * {@link ProviderError}, say — and is undefined when the cancel was the only failure.
107
162
  */
108
163
  var ProviderAbortError = class extends Error {
109
164
  /** Names the machine-readable condition — `'ABORT'`: a stream cancelled mid-flight. */
110
165
  code = "ABORT";
111
166
  partial;
112
- constructor(partial) {
113
- super("provider stream aborted");
167
+ constructor(partial, options) {
168
+ super("provider stream aborted", options);
114
169
  this.name = "ProviderAbortError";
115
170
  this.partial = partial;
116
171
  }
117
172
  };
118
173
  /**
119
- * Narrows an unknown caught value to a {@link ProviderAbortError}.
174
+ * Narrows an unknown caught value to a {@link ProviderAbortError} through `instanceof`, so a
175
+ * `catch` can recover its `partial` result.
120
176
  *
121
177
  * @param value - The value to test (typically a `catch` binding)
122
178
  * @returns True if `value` is a {@link ProviderAbortError}; false otherwise
@@ -136,13 +192,13 @@ function isProviderAbortError(value) {
136
192
  /**
137
193
  * Reports an {@link AgentInterface} run that ended {@link AgentResult.partial} under a
138
194
  * `partial` policy of `false` (the default) — thrown by an agent-job handler (a
139
- * `createAgentQueue` / `createAgentRunner` job), carrying the partial
140
- * {@link AgentResult} so the failure stays inspectable.
195
+ * `createAgentQueue` / `createAgentRunner` job), carrying the partial {@link AgentResult} so
196
+ * the failure stays inspectable, and the machine-readable `code` `'PARTIAL'`.
141
197
  *
142
198
  * @remarks
143
199
  * A partial result means the agent was cancelled (an external `signal` abort, a queue /
144
200
  * runner abort threaded in, a `timeout` deadline, or an exhausted token `budget`) rather
145
- * than finishing naturally. For a durable JOB that is a failure by default: throwing this
201
+ * than finishing naturally. For a durable job that is a failure by default: throwing this
146
202
  * lets the Queue's retries re-run the job and a Runner's fail-fast abort its siblings.
147
203
  * Set `partial: true` (see `AgentQueueOptions` / `AgentRunnerOptions`) to treat a
148
204
  * partial as success instead, in which case this is never thrown. Narrow a caught value
@@ -162,7 +218,8 @@ var AgentJobError = class extends Error {
162
218
  }
163
219
  };
164
220
  /**
165
- * Narrows an unknown caught value to an {@link AgentJobError}.
221
+ * Narrows an unknown caught value to an {@link AgentJobError} through `instanceof`, so a
222
+ * `catch` can recover its `partial` result.
166
223
  *
167
224
  * @param value - The value to test (typically a `catch` binding or a rejected enqueue)
168
225
  * @returns True if `value` is an {@link AgentJobError}; false otherwise
@@ -180,12 +237,13 @@ function isAgentJobError(value) {
180
237
  return value instanceof AgentJobError;
181
238
  }
182
239
  /**
183
- * Reports a conversation with no {@link ConversationSummaryHandler} to fold its messages
184
- * with, or with a structurally invalid `sections` cap — thrown by a
185
- * {@link ConversationInterface}'s `compact()`, carrying a machine-readable `code`.
240
+ * Reports a conversation with no {@link ConversationSummaryHandler} to fold its messages with,
241
+ * or with a `sections` cap below `1` — thrown by a {@link ConversationInterface}'s `compact()`
242
+ * or its construction, carrying the machine-readable `code`
243
+ * `'SUMMARIZER' | 'SECTIONS'`.
186
244
  *
187
245
  * @remarks
188
- * Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
246
+ * Compaction requires a summarizer (it digests the folded slice into a section summary and
189
247
  * regenerates the rollup); a conversation created without one can still store + `view()` its
190
248
  * live tail, but a `compact()` is a programmer error and throws this with `'SUMMARIZER'`.
191
249
  * A `sections` cap (on {@link import('./types.js').ConversationOptions} /
@@ -204,7 +262,8 @@ var ConversationError = class extends Error {
204
262
  }
205
263
  };
206
264
  /**
207
- * Narrows an unknown caught value to a {@link ConversationError}.
265
+ * Narrows an unknown caught value to a {@link ConversationError} through `instanceof`, so a
266
+ * `catch` can branch on its `code`.
208
267
  *
209
268
  * @param value - The value to test (typically a `catch` binding)
210
269
  * @returns True if `value` is a {@link ConversationError}; false otherwise
@@ -222,13 +281,16 @@ function isConversationError(value) {
222
281
  return value instanceof ConversationError;
223
282
  }
224
283
  /**
225
- * Reports a concurrent run that would corrupt SHARED per-agent accounting, or a rehydration
284
+ * Reports a concurrent run that would corrupt shared per-agent accounting, or a rehydration
226
285
  * name absent from its registry pool — thrown synchronously by an {@link AgentInterface}'s
227
- * `stream()` and by an {@link AgentRegistryInterface}'s accessors, carrying a machine-readable
228
- * `code`.
286
+ * `stream()` (and so by `generate()`, which calls it) and by an
287
+ * {@link AgentRegistryInterface}'s accessors, carrying the machine-readable `code`
288
+ * `'CONCURRENCY' | 'REGISTRY'`. Synchronous means a fire-and-forget
289
+ * `agent.generate().catch(…)` never catches it: `await` the call inside `try`/`catch`, or wrap
290
+ * the call expression itself.
229
291
  *
230
292
  * @remarks
231
- * `'CONCURRENCY'` reports a run already in flight on the same agent, PLUS a construction-level
293
+ * `'CONCURRENCY'` reports a run already in flight on the same agent, plus a construction-level
232
294
  * `window` (a shared context budget) or a construction-level `budget` with no per-run override
233
295
  * (a shared cost budget) — a second concurrent `stream()` would race its charges against the
234
296
  * same shared instance, corrupting the accounting. Use separate agents, or per-run `budget`
@@ -246,7 +308,8 @@ var AgentError = class extends Error {
246
308
  }
247
309
  };
248
310
  /**
249
- * Narrows an unknown caught value to an {@link AgentError}.
311
+ * Narrows an unknown caught value to an {@link AgentError} through `instanceof`, so a `catch`
312
+ * can branch on its `code`.
250
313
  *
251
314
  * @param value - The value to test (typically a `catch` binding)
252
315
  * @returns True if `value` is an {@link AgentError}; false otherwise
@@ -263,11 +326,148 @@ var AgentError = class extends Error {
263
326
  function isAgentError(value) {
264
327
  return value instanceof AgentError;
265
328
  }
329
+ /**
330
+ * Reports a coded provider failure with its HTTP status and underlying cause when available.
331
+ *
332
+ * @remarks
333
+ * The provider base throws this error for a non-OK HTTP response, a missing response
334
+ * body, or a missing settled result when strict assembly is enabled. Concrete wire
335
+ * decoders also use it for malformed records and relayed provider failures.
336
+ * `status` is present only for an `HTTP` failure; other codes leave it undefined.
337
+ */
338
+ var ProviderError = class extends Error {
339
+ /** Names the machine-readable condition — `'HTTP'`: a non-OK response, including a relay refusing an oversized request with 413; `'PROTOCOL'`: a missing response body, a malformed wire record, or a strict stream with no settled result; `'PROVIDER'`: an upstream failure carried by a relay error record. */
340
+ code;
341
+ /** Holds the response status for an HTTP failure, or undefined for other codes. */
342
+ status;
343
+ constructor(code, message, options) {
344
+ super(message, options);
345
+ this.name = "ProviderError";
346
+ this.code = code;
347
+ this.status = options?.status;
348
+ }
349
+ };
350
+ /**
351
+ * Narrows a caught value to the provider failure class through instanceof.
352
+ *
353
+ * @param value - The caught value
354
+ * @returns True if the value is a provider failure; false otherwise
355
+ * @example
356
+ * ```ts
357
+ * isProviderError(new ProviderError('HTTP', 'unavailable', { status: 503 })) // true
358
+ * ```
359
+ */
360
+ function isProviderError(value) {
361
+ return value instanceof ProviderError;
362
+ }
363
+ //#endregion
364
+ //#region src/core/shapers.ts
365
+ /**
366
+ * Describes a tool call's JSON wire projection.
367
+ *
368
+ * @remarks
369
+ * The wire is strictly narrower than the domain: non-JSON arguments are refused.
370
+ * ToolCall.caller is not in this shape and never crosses a wire. The guard refuses
371
+ * that extra member; the contract parser drops it.
372
+ */
373
+ var toolCallShape = objectShape({
374
+ id: stringShape(),
375
+ name: stringShape(),
376
+ arguments: recordShape(jsonShape())
377
+ });
378
+ /**
379
+ * Describes a conversation message's JSON wire projection.
380
+ *
381
+ * @remarks
382
+ * The wire is strictly narrower than Message: non-JSON call arguments are refused,
383
+ * and caller context is refused by the guard or dropped by the parser.
384
+ */
385
+ var messageShape = objectShape({
386
+ id: stringShape(),
387
+ role: literalShape([
388
+ "system",
389
+ "user",
390
+ "assistant",
391
+ "tool"
392
+ ]),
393
+ content: stringShape(),
394
+ calls: optionalShape(arrayShape(toolCallShape)),
395
+ images: optionalShape(arrayShape(stringShape()))
396
+ });
397
+ /**
398
+ * Describes a provider request's JSON wire projection.
399
+ *
400
+ * @remarks
401
+ * The wire is strictly narrower than ProviderRequest: non-JSON arguments, parameters,
402
+ * or schema members are refused, and caller context is refused or dropped.
403
+ */
404
+ var providerRequestShape = objectShape({
405
+ messages: arrayShape(messageShape),
406
+ tools: optionalShape(arrayShape(objectShape({
407
+ name: stringShape(),
408
+ description: optionalShape(stringShape()),
409
+ parameters: optionalShape(recordShape(jsonShape()))
410
+ }))),
411
+ options: optionalShape(objectShape({
412
+ think: optionalShape(booleanShape()),
413
+ schema: optionalShape(recordShape(jsonShape()))
414
+ }))
415
+ });
416
+ /**
417
+ * Describes a provider result's JSON wire projection.
418
+ *
419
+ * @remarks
420
+ * The wire is strictly narrower than ProviderResult: non-JSON call arguments are
421
+ * refused, and caller context is refused or dropped.
422
+ */
423
+ var providerResultShape = objectShape({
424
+ content: stringShape(),
425
+ thinking: optionalShape(stringShape()),
426
+ tools: optionalShape(arrayShape(toolCallShape)),
427
+ usage: optionalShape(objectShape({
428
+ prompt: numberShape(),
429
+ completion: numberShape(),
430
+ total: numberShape()
431
+ }))
432
+ });
433
+ /**
434
+ * Describes the channel-discriminated JSON relay wire projection.
435
+ *
436
+ * @remarks
437
+ * The wire is strictly narrower than RelayFrame through its result and partial
438
+ * fields: non-JSON arguments are refused, and caller context is refused or dropped.
439
+ */
440
+ var relayFrameShape = unionShape(objectShape({
441
+ channel: literalShape(["content", "thinking"]),
442
+ text: stringShape()
443
+ }), objectShape({
444
+ channel: literalShape(["result"]),
445
+ result: providerResultShape
446
+ }), objectShape({
447
+ channel: literalShape(["abort"]),
448
+ partial: providerResultShape
449
+ }), objectShape({
450
+ channel: literalShape(["error"]),
451
+ message: stringShape()
452
+ }));
453
+ //#endregion
454
+ //#region src/core/contracts.ts
455
+ /** Validates and projects conversation messages at a JSON wire boundary. */
456
+ var messageContract = createContract(messageShape);
457
+ /** Validates and projects provider requests at a JSON wire boundary. */
458
+ var providerRequestContract = createContract(providerRequestShape);
459
+ /** Validates and projects provider results at a JSON wire boundary. */
460
+ var providerResultContract = createContract(providerResultShape);
461
+ /** Validates and projects channel-discriminated relay frames at a JSON wire boundary. */
462
+ var relayFrameContract = createContract(relayFrameShape);
266
463
  //#endregion
267
464
  //#region src/core/helpers.ts
268
465
  /**
269
- * Projects an unknown value onto the canonical JSON representation of an
270
- * {@link AgentResult}.
466
+ * Projects an unknown value onto a fresh, exact `JSONValue` representation of an
467
+ * {@link AgentResult} — capturing each structural field once through a total boundary,
468
+ * accepting conforming accessors and inherited properties, preserving finite negative and
469
+ * fractional usage counts, dropping extras, and resolving `undefined` for a malformed field, a
470
+ * non-finite usage number, a throwing getter, or a hostile or revoked proxy.
271
471
  *
272
472
  * @remarks
273
473
  * This is a total hostile-boundary projection. Each structural field is captured once
@@ -321,15 +521,16 @@ function agentResultToJSON(value) {
321
521
  return captured.success ? parseJSONValue(captured.value) : void 0;
322
522
  }
323
523
  /**
324
- * Filters a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
325
- * keys — the pure, total set-membership primitive the context's build step and the agent
326
- * loop's tool-advertise step apply a scope through.
524
+ * Filters a list of items by a {@link import('./types.js').ScopeInterface} allow-list of keys —
525
+ * `undefined` passes everything, `[]` passes nothing, and a non-empty list passes the listed keys
526
+ * alone, order preserved. The pure, total set-membership primitive the context's build step and
527
+ * the agent loop's tool-advertise step apply a scope through.
327
528
  *
328
529
  * @remarks
329
530
  * Three-way by the allow-list's shape, so a `Scope` category cleanly expresses "all /
330
531
  * none / only these":
331
- * - `undefined` ⇒ NO constraint — every item passes (returned unchanged).
332
- * - `[]` (empty) ⇒ NONE pass (no key is in an empty set).
532
+ * - `undefined` ⇒ no constraint — every item passes (returned unchanged).
533
+ * - `[]` (empty) ⇒ none pass (no key is in an empty set).
333
534
  * - a non-empty list ⇒ only items whose `key(item)` is in the list pass.
334
535
  *
335
536
  * Order-preserving (it filters `items` in place order, never reorders) and total — never
@@ -356,15 +557,15 @@ function filterAllowList(allow, items, key) {
356
557
  return items.filter((item) => set.has(key(item)));
357
558
  }
358
559
  /**
359
- * Estimates the context-token footprint of a string — the deterministic char-based heuristic
360
- * {@link estimateMessages} sums over a conversation's messages (the default context-budget
361
- * estimator).
560
+ * Estimates the context-token footprint of a string — the deterministic `ceil(length / 4)`
561
+ * character heuristic {@link estimateMessages} sums over a conversation's messages (the default
562
+ * context-budget estimator).
362
563
  *
363
564
  * @remarks
364
565
  * Approximates `ceil(length / 4)` (≈ four characters per token — the rough average for
365
566
  * English text), so the same input always yields the same estimate (no model round-trip).
366
567
  * Empty text is `0`. This is a planning heuristic for reasoning about how much a turn's
367
- * messages cost the next request, NOT an exact tokenizer count — it never calls a provider,
568
+ * messages cost the next request, not an exact tokenizer count — it never calls a provider,
368
569
  * so the agent layer stays provider-agnostic and synchronous where it can be.
369
570
  *
370
571
  * @param text - The text to estimate (a section summary, a message's content)
@@ -381,16 +582,19 @@ function estimateTokens(text) {
381
582
  return Math.ceil(text.length / 4);
382
583
  }
383
584
  /**
384
- * Estimates the context-token footprint of a batch of messages — the default `consumer`
385
- * estimator for an agent's context `BudgetInterface` (a budgets surface's tracking contract)
386
- * (the {@link import('./types.js').AgentOptions} `window`).
585
+ * Estimates the context-token footprint of a batch of messages — each message's content plus
586
+ * {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD}, a tool-call JSON estimate, and
587
+ * {@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} for each attached image. The default
588
+ * `consumer` estimator for an agent's context budget (the
589
+ * {@link import('./types.js').AgentOptions} `window`), total and never throwing, and a
590
+ * deliberate provider-agnostic approximation rather than an exact tokenizer count.
387
591
  *
388
592
  * @remarks
389
593
  * Sums, per message, {@link estimateTokens} over its `content` (the `ceil(length / 4)` char
390
- * heuristic) PLUS {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (a fixed per-message
391
- * role/framing overhead) PLUS, when present, {@link estimateTokens} over its JSON-stringified
392
- * `calls` PLUS `images.length * `{@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} (a coarse,
393
- * deliberately-approximate per-image cost — a base64 length is NOT a token proxy). Deterministic
594
+ * heuristic) plus {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (a fixed per-message
595
+ * role/framing overhead) plus, when present, {@link estimateTokens} over its JSON-stringified
596
+ * `calls` plus `images.length * `{@link import('./constants.js').IMAGE_TOKEN_ESTIMATE} (a coarse,
597
+ * deliberately-approximate per-image cost — a base64 length is not a token proxy). Deterministic
394
598
  * and provider-free — the same messages always yield the same estimate, with an empty batch `0`.
395
599
  * It is the fully-swappable default an agent's auto-compaction context budget charges each
396
600
  * turn's new messages through; a caller wanting a sharper count supplies its own `consumer` to
@@ -422,15 +626,17 @@ function estimateMessages(messages) {
422
626
  }, 0);
423
627
  }
424
628
  /**
425
- * Runs one rehydrated agent and applies the partial-as-configurable-failure policy — the
426
- * shared job-handler step BOTH `createAgentQueue` and `createAgentRunner` settle each job
427
- * through, so the policy can never diverge between them.
629
+ * Runs one rehydrated agent and applies the partial-as-configurable-failure policy — a partial
630
+ * run throws an {@link import('./errors.js').AgentJobError} unless the `partial` policy allows
631
+ * it, and a natural finish resolves. The shared job-handler step `createAgentQueue` and
632
+ * `createAgentRunner` both settle each job through, so the policy can never diverge between
633
+ * them.
428
634
  *
429
635
  * @remarks
430
- * A turn that committed PARTIAL (a cancel — abort / budget / timeout) is by default a
431
- * FAILURE, so it THROWS an {@link import('./errors.js').AgentJobError} carrying the partial
432
- * (the Queue's retries + a Runner's fail-fast then engage); the `partial` policy RESOLVES
433
- * it as success instead. A natural finish ALWAYS resolves with its result.
636
+ * A turn that committed partial (a cancel — abort / budget / timeout) is by default a
637
+ * failure, so it throws an {@link import('./errors.js').AgentJobError} carrying the partial
638
+ * (the Queue's retries + a Runner's fail-fast then engage); the `partial` policy resolves
639
+ * it as success instead. A natural finish always resolves with its result.
434
640
  *
435
641
  * @param agent - The rehydrated {@link AgentInterface} to run to its {@link AgentResult}
436
642
  * @param partial - The partial policy. If `true`, a partial result resolves as success; if
@@ -482,9 +688,9 @@ function handleAgentRunnerJob(registry, partial, controller) {
482
688
  return settleAgentJob(registry.build(controller.input, controller.signal), partial);
483
689
  }
484
690
  /**
485
- * Renders a path-addressed text body as a fenced reference block — the framing an
486
- * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
487
- * active workspace is the SOLE document/image context).
691
+ * Renders a path-addressed text body as a fenced reference block — a `File: <path>` label line
692
+ * over a language-tagged fence, the framing an
693
+ * {@link import('./AgentContext.js').AgentContext}'s active-workspace text-file render emits.
488
694
  *
489
695
  * @remarks
490
696
  * Produces `` File: <path>\n```<language>\n<content>\n``` `` — the `File:` label line, then a
@@ -510,7 +716,8 @@ function renderFencedFile(path, language, content) {
510
716
  return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
511
717
  }
512
718
  /**
513
- * Sanitizes one reported token count into a safe non-negative integer.
719
+ * Sanitizes one reported token count into a safe non-negative integer — a non-finite or
720
+ * non-positive value becomes `0`, and a positive fractional value floors down.
514
721
  *
515
722
  * @param value - The token count to sanitize
516
723
  * @returns The floored count, or `0` when the value is non-finite or non-positive
@@ -560,11 +767,13 @@ function sanitizeUsage(usage) {
560
767
  * @example
561
768
  * ```ts
562
769
  * joinThinking(undefined, 'first') // 'first'
770
+ * joinThinking('', 'x') // 'x'
563
771
  * joinThinking('first', 'second') // 'first\n\nsecond'
564
772
  * ```
565
773
  */
566
774
  function joinThinking(running, next) {
567
- return running === void 0 ? next : `${running}\n\n${next}`;
775
+ if (running === void 0 || running.length === 0) return next;
776
+ return next.length === 0 ? running : `${running}\n\n${next}`;
568
777
  }
569
778
  /**
570
779
  * Adds two {@link TokenUsage} values field by field — the running total an agent run keeps
@@ -596,10 +805,11 @@ function sumUsage(running, next) {
596
805
  }
597
806
  /**
598
807
  * Assembles the settled {@link AgentResult} from a run's {@link RunOutcome} — `thinking` and
599
- * `usage` are carried only when the run surfaced them.
808
+ * `usage` are carried only when the run surfaced them, and the loop-internal `exhausted` flag is
809
+ * left out.
600
810
  *
601
811
  * @remarks
602
- * Pure and total. An absent optional is OMITTED rather than stored as `undefined` (the
812
+ * Pure and total. An absent optional is omitted rather than stored as `undefined` (the
603
813
  * present-when-given convention the message store follows), so a settled result JSON
604
814
  * round-trips without an explicit `undefined` field. `exhausted` is loop bookkeeping and does
605
815
  * not reach the public result — the `exhaust` event carries it instead.
@@ -651,10 +861,10 @@ function denyCall(call, reason) {
651
861
  }
652
862
  /**
653
863
  * Renders one context section — the resolved `open`, each item's rendering, and the resolved
654
- * `close` when one exists, blank-line joined.
864
+ * `close` when one exists, blank-line joined; `undefined` when the section has no items.
655
865
  *
656
866
  * @remarks
657
- * Pure and total. A section with NO items renders nothing (`undefined`), so an empty or fully
867
+ * Pure and total. A section with no items renders nothing (`undefined`), so an empty or fully
658
868
  * scoped-out manager stays silent — its `open` / `close` never appear without items. `close`
659
869
  * is the only optional slot: an unset one (there is no built-in close) drops the
660
870
  * trailing line.
@@ -680,13 +890,13 @@ function renderSection(open, items, render, close) {
680
890
  return lines.join("\n\n");
681
891
  }
682
892
  /**
683
- * Resolves one section's OPEN text through the format cascade — manager-options override >
893
+ * Resolves one section's open text through the format cascade — manager-options override >
684
894
  * provider default > built-in header.
685
895
  *
686
896
  * @remarks
687
- * Pure and total. The leading text has NO per-item level. A manager's `open` already
897
+ * Pure and total. The leading text has no per-item level. A manager's `open` already
688
898
  * encapsulates `[options-override → built-in]`, so it is reached only when neither the
689
- * override's `open` nor the provider's `open` applies — and there it IS the built-in header.
899
+ * override's `open` nor the provider's `open` applies — and there it is the built-in header.
690
900
  *
691
901
  * @typeParam T - The section item the manager renders
692
902
  * @param manager - The section source (its `format` override + its built-in `open`)
@@ -703,13 +913,13 @@ function resolveOpen(manager, provider) {
703
913
  return manager.format?.open ?? provider?.open ?? manager.open;
704
914
  }
705
915
  /**
706
- * Resolves one section's CLOSE text through the format cascade — manager-options override >
707
- * provider default.
916
+ * Resolves one section's close text through the format cascade — manager-options override >
917
+ * provider default; `undefined` when neither sets one, because there is no built-in close.
708
918
  *
709
919
  * @remarks
710
- * Pure and total. There is NO built-in close, so a section with neither level set returns
920
+ * Pure and total. There is no built-in close, so a section with neither level set returns
711
921
  * `undefined` and {@link renderSection} appends no closing line. Paired with
712
- * {@link resolveOpen}, one level can WRAP the whole group.
922
+ * {@link resolveOpen}, one level can wrap the whole group.
713
923
  *
714
924
  * @typeParam T - The section item the manager renders
715
925
  * @param manager - The section source (its `format` override)
@@ -726,14 +936,14 @@ function resolveClose(manager, provider) {
726
936
  return manager.format?.close ?? provider?.close;
727
937
  }
728
938
  /**
729
- * Resolves ONE item's rendering through the format cascade — item override >
730
- * manager-options override > provider default > built-in rendering.
939
+ * Resolves one item's rendering through the format cascade — item override > manager-options
940
+ * override > provider default > built-in rendering.
731
941
  *
732
942
  * @remarks
733
943
  * Pure and total. The item's own `override` is the most-specific level (a fully-rendered
734
944
  * string for that item alone). A manager's `render(item)` already encapsulates
735
945
  * `[options-override → built-in]`, so it is reached only when no higher level applies — and
736
- * there it IS the built-in rendering.
946
+ * there it is the built-in rendering.
737
947
  *
738
948
  * @typeParam T - The section item being rendered (it may carry its own `override`)
739
949
  * @param manager - The section source (its `format` override + its built-in `render`)
@@ -752,11 +962,11 @@ function resolveItem(manager, provider, item) {
752
962
  return item.override ?? manager.format?.render?.(item) ?? provider?.render?.(item) ?? manager.render(item);
753
963
  }
754
964
  /**
755
- * Copies a message with image data merged onto its `images` — the message's own images
756
- * first, then the attached data.
965
+ * Copies a message with image data merged onto its `images` — the message's own images first,
966
+ * then the attached data, carrying `calls` only when present and never mutating the original.
757
967
  *
758
968
  * @remarks
759
- * Pure and total: the original message is NEVER mutated. `calls` is carried only when the
969
+ * Pure and total: the original message is never mutated. `calls` is carried only when the
760
970
  * source message has one (kept omitted otherwise, mirroring the store's present-when-given
761
971
  * convention).
762
972
  *
@@ -786,13 +996,14 @@ function attachImages(message, data) {
786
996
  };
787
997
  }
788
998
  /**
789
- * Attaches image data to a conversation's LAST user message — the turn a vision provider
790
- * reads images off.
999
+ * Attaches image data to a conversation's last user message — the turn a vision provider reads
1000
+ * images off — as a new array with that one message replaced by its carrying copy, and unchanged
1001
+ * when there is no data or no user turn.
791
1002
  *
792
1003
  * @remarks
793
- * Pure and total: the conversation and its messages are NEVER mutated, and the returned array
1004
+ * Pure and total: the conversation and its messages are never mutated, and the returned array
794
1005
  * replaces exactly the one target message with the copy {@link attachImages} builds. Empty
795
- * data returns the conversation unchanged; a conversation with NO user message returns it
1006
+ * data returns the conversation unchanged; a conversation with no user message returns it
796
1007
  * unchanged too (there is nowhere to attach, and the images already rode the system block).
797
1008
  *
798
1009
  * @param conversation - The messages to attach into (left unchanged)
@@ -816,11 +1027,11 @@ function attachUserImages(conversation, data) {
816
1027
  return conversation.map((message, index) => index === target ? attachImages(message, data) : message);
817
1028
  }
818
1029
  /**
819
- * Collects the `base64` payload of the IMAGE files in a workspace file list — the data an
820
- * agent context attaches to the last user message.
1030
+ * Collects the `base64` payload of the image files in a workspace file list — the data an agent
1031
+ * context attaches to the last user message.
821
1032
  *
822
1033
  * @remarks
823
- * Pure and total. `isBinary` NARROWS the tagless content to its binary arm (a total guard,
1034
+ * Pure and total. `isBinary` narrows the tagless content to its binary arm (a total guard,
824
1035
  * never an assertion), then the MIME prefix gates it to an image, so a text file and a non-image
825
1036
  * binary (a PDF) are both skipped. Order follows the file list.
826
1037
  *
@@ -839,12 +1050,12 @@ function collectImageData(files) {
839
1050
  return data;
840
1051
  }
841
1052
  /**
842
- * Builds the RAW synthetic summary message for one compacted section — role `'assistant'`,
843
- * the section's stable `id`, its `summary` VERBATIM as content.
1053
+ * Builds the raw synthetic summary message for one compacted section — role `'assistant'`, the
1054
+ * section's stable `id`, and its `summary` verbatim as content.
844
1055
  *
845
1056
  * @remarks
846
1057
  * Pure and total. This is the unframed form the rollup regeneration digests (a
847
- * summary-of-summaries over the section summaries); the recap LABEL is a `view()`
1058
+ * summary-of-summaries over the section summaries); the recap label is a `view()`
848
1059
  * presentation concern kept out of what the summarizer re-reads — see
849
1060
  * {@link buildRecapMessage}.
850
1061
  *
@@ -865,13 +1076,13 @@ function buildSummaryMessage(section) {
865
1076
  };
866
1077
  }
867
1078
  /**
868
- * Builds the FRAMED recap message for one compacted section — the same role and stable `id`
869
- * as {@link buildSummaryMessage}, with the content prefixed by
870
- * {@link import('./constants.js').CONVERSATION_RECAP_PREFIX}.
1079
+ * Builds the framed recap message for one compacted section — the same role and stable `id` as
1080
+ * {@link buildSummaryMessage}, with the content prefixed by {@link
1081
+ * import('./constants.js').CONVERSATION_RECAP_PREFIX}.
871
1082
  *
872
1083
  * @remarks
873
- * Pure and total. The prefix is what makes a small model read the message as a CONDENSED
874
- * RECAP of earlier turns rather than a literal assistant turn to echo or answer from. It is a
1084
+ * Pure and total. The prefix is what makes a small model read the message as a condensed
1085
+ * recap of earlier turns rather than a literal assistant turn to echo or answer from. It is a
875
1086
  * fixed handful of tokens, so a conversation's `view()` stays lean however many sections it
876
1087
  * carries.
877
1088
  *
@@ -892,12 +1103,12 @@ function buildRecapMessage(section) {
892
1103
  };
893
1104
  }
894
1105
  /**
895
- * Intersects two scope category lists under the "`undefined` is the universal set" rule — the
896
- * primitive a scope narrows through.
1106
+ * Intersects two scope category lists under the "`undefined` is the universal set" rule — a
1107
+ * fresh copy that can only tighten, and the primitive a scope narrows through.
897
1108
  *
898
1109
  * @remarks
899
- * Pure and total, and it can only TIGHTEN: `undefined` ∩ `undefined` is `undefined` (still no
900
- * constraint); `undefined` ∩ a list is a COPY of that list (the `undefined` side imposes
1110
+ * Pure and total, and it can only tighten: `undefined` ∩ `undefined` is `undefined` (still no
1111
+ * constraint); `undefined` ∩ a list is a copy of that list (the `undefined` side imposes
901
1112
  * nothing); a list ∩ a list keeps the child keys the parent also allows, so a parent-excluded
902
1113
  * key can never be re-admitted. Every returned list is a fresh copy, so a later mutation of
903
1114
  * either input cannot leak into the result.
@@ -919,39 +1130,167 @@ function intersectKeys(parent, child) {
919
1130
  const allowed = new Set(parent);
920
1131
  return child.filter((key) => allowed.has(key));
921
1132
  }
1133
+ /**
1134
+ * Assembles a provider result with only populated optional fields.
1135
+ *
1136
+ * @param content - The authoritative answer
1137
+ * @param thinking - The joined reasoning
1138
+ * @param tools - The accumulated calls
1139
+ * @param usage - The reported token usage
1140
+ * @returns The assembled result
1141
+ * @example
1142
+ * ```ts
1143
+ * buildProviderResult('ok', '', [], undefined) // { content: 'ok' }
1144
+ * ```
1145
+ */
1146
+ function buildProviderResult(content, thinking, tools, usage) {
1147
+ return {
1148
+ content,
1149
+ ...thinking.length === 0 ? {} : { thinking },
1150
+ ...tools.length === 0 ? {} : { tools },
1151
+ ...usage === void 0 ? {} : { usage }
1152
+ };
1153
+ }
1154
+ /**
1155
+ * Reads a UTF-8 prefix of a byte stream and cancels its remainder.
1156
+ *
1157
+ * @remarks
1158
+ * The `limit` parameter bounds bytes passed to the decoder, including a partial final
1159
+ * character. An omitted limit reads to completion. A source may deliver a chunk larger
1160
+ * than the remaining limit; its unused bytes are discarded without decoding.
1161
+ * The read can overshoot by one source chunk. An abort leaves completion false.
1162
+ *
1163
+ * @param body - The readable byte stream
1164
+ * @param limit - The maximum byte prefix, or undefined for the complete stream
1165
+ * @param signal - The optional cancellation bound; abort returns the decoded prefix
1166
+ * @returns The decoded prefix and whether EOF occurred within the byte budget
1167
+ * @example
1168
+ * ```ts
1169
+ * import { readText } from '@orkestrel/agent'
1170
+ *
1171
+ * const body = new Response('answer').body
1172
+ * if (body !== null) await readText(body, 3) // { text: 'ans', complete: false }
1173
+ * ```
1174
+ */
1175
+ async function readText(body, limit, signal) {
1176
+ const reader = body.getReader();
1177
+ const decoder = new TextDecoder();
1178
+ const cleanup = new AbortController();
1179
+ let remaining = limit ?? Infinity;
1180
+ let text = "";
1181
+ let complete = false;
1182
+ try {
1183
+ signal?.addEventListener("abort", () => {
1184
+ reader.cancel(signal.reason).catch(() => {});
1185
+ }, {
1186
+ once: true,
1187
+ signal: cleanup.signal
1188
+ });
1189
+ if (signal?.aborted) await reader.cancel(signal.reason).catch(() => {});
1190
+ while (remaining > 0) {
1191
+ if (signal?.aborted) break;
1192
+ const step = await reader.read();
1193
+ if (signal?.aborted) break;
1194
+ if (step.done) {
1195
+ complete = true;
1196
+ break;
1197
+ }
1198
+ const bytes = step.value.subarray(0, remaining);
1199
+ text += decoder.decode(bytes, { stream: true });
1200
+ remaining -= bytes.byteLength;
1201
+ }
1202
+ return {
1203
+ text: text + decoder.decode(),
1204
+ complete
1205
+ };
1206
+ } finally {
1207
+ cleanup.abort();
1208
+ try {
1209
+ await reader.cancel();
1210
+ } catch {} finally {
1211
+ reader.releaseLock();
1212
+ }
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Decodes UTF-8 chunks with a final flush and releases the stream on every exit.
1217
+ *
1218
+ * @param body - The readable byte stream
1219
+ * @param signal - The optional cancellation bound; abort ends iteration without further chunks
1220
+ * @returns Decoded text chunks, including a held decoder tail
1221
+ * @example
1222
+ * ```ts
1223
+ * const body = new Response('answer').body
1224
+ * if (body !== null) for await (const chunk of readChunks(body)) render(chunk)
1225
+ * ```
1226
+ */
1227
+ async function* readChunks(body, signal) {
1228
+ const reader = body.getReader();
1229
+ const decoder = new TextDecoder();
1230
+ const cleanup = new AbortController();
1231
+ try {
1232
+ signal?.addEventListener("abort", () => {
1233
+ reader.cancel(signal.reason).catch(() => {});
1234
+ }, {
1235
+ once: true,
1236
+ signal: cleanup.signal
1237
+ });
1238
+ if (signal?.aborted) {
1239
+ await reader.cancel(signal.reason).catch(() => {});
1240
+ return;
1241
+ }
1242
+ for (;;) {
1243
+ const step = await reader.read();
1244
+ if (signal?.aborted) return;
1245
+ if (step.done) break;
1246
+ const chunk = decoder.decode(step.value, { stream: true });
1247
+ if (chunk.length > 0) yield chunk;
1248
+ }
1249
+ const tail = decoder.decode();
1250
+ if (tail.length > 0) yield tail;
1251
+ } finally {
1252
+ cleanup.abort();
1253
+ try {
1254
+ await reader.cancel();
1255
+ } catch {} finally {
1256
+ reader.releaseLock();
1257
+ }
1258
+ }
1259
+ }
922
1260
  //#endregion
923
1261
  //#region src/core/conversations/Conversation.ts
924
1262
  /**
925
- * Represents a conversation — a live uncompacted tail of messages it OWNS DIRECTLY ABOVE a
926
- * flat message store, plus compacted, summarized {@link Section}s and a regenerated rollup
927
- * `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
928
- * {@link ConversationSummaryHandler} seam (so `core` never imports a provider).
1263
+ * Represents a conversation — a live uncompacted tail of messages it owns directly above a flat
1264
+ * message store, plus compacted, summarized {@link Section}s, a regenerated rollup `summary`, and
1265
+ * a `summarizable` flag, with on-demand `rehydrate` and substring `search`, driven by a
1266
+ * provider-agnostic {@link ConversationSummaryHandler} seam so `core` never imports a provider.
1267
+ * Observable through its own `emitter`.
929
1268
  *
930
1269
  * @remarks
931
- * - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
1270
+ * - **Live tail + sections.** The conversation owns its live tail directly — `#messages` is an
932
1271
  * insertion-ordered `Map` of immutable {@link Message}s keyed by their minted id
933
- * (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
1272
+ * (the same store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
934
1273
  * `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
935
1274
  * per-value manager). `#sections` are the compacted history (oldest → newest), each a
936
- * summarized slice that RETAINS its originals. `#summary` is the rollup (a
1275
+ * summarized slice that retains its originals. `#summary` is the rollup (a
937
1276
  * summary-of-summaries over all sections), regenerated on each compaction (`undefined`
938
1277
  * until the first).
939
- * - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
1278
+ * - **`view()`.** Each section folds to one synthetic summary message (role `'assistant'` — a
940
1279
  * prior-context recap — keyed by the section's stable `id`), then the live messages
941
- * verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
942
- * carries the per-section summaries, which ARE the compaction benefit.
1280
+ * verbatim. The rollup `summary` is not injected (it is separately pull-able); `view()`
1281
+ * carries the per-section summaries, which are the compaction benefit.
943
1282
  * - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
944
1283
  * (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
945
- * rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
1284
+ * rollup (a second `#summarize` over all section summaries), and emits `summary` then
946
1285
  * `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
947
- * THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
948
- * per compaction.
1286
+ * Throws a {@link ConversationError} when no `#summarize` was supplied. A compaction calls
1287
+ * the summarizer for the section digest and again for the rollup.
949
1288
  * - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
950
1289
  * messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (the caller decides
951
1290
  * whether to re-add them; `rehydrate` never reinserts). `search` is a case-insensitive
952
- * substring scan of `content` across ALL messages (every section's originals + the live tail).
1291
+ * substring scan of `content` across all messages (every section's originals + the live tail).
953
1292
  * - **Observable.** The owned {@link emitter} ({@link ConversationEventMap}) carries
954
- * `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
1293
+ * `compact` / `summary` / `rehydrate`, emitted directly, strictly after the state change;
955
1294
  * the emitter isolates a listener throw and routes it to its `error` handler (the `error`
956
1295
  * option), so a buggy observer can never corrupt a compaction.
957
1296
  *
@@ -1117,26 +1456,27 @@ var Conversation = class {
1117
1456
  //#endregion
1118
1457
  //#region src/core/conversations/ConversationManager.ts
1119
1458
  /**
1120
- * Registers {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
1121
- * — the id-keyed store over the conversation layer PLUS the `active` / `switch` seam the
1122
- * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
1123
- * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
1124
- * on each {@link Conversation}.
1459
+ * Registers {@link Conversation}s keyed by `id`, in insertion order, with an active pointer —
1460
+ * the id-keyed store over the conversation layer, the `active` / `switch` seam the
1461
+ * {@link import('../AgentContext.js').AgentContext} renders, and the durable `open` / `save`
1462
+ * store seam. Event-free (a registry, like
1463
+ * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives on each
1464
+ * {@link Conversation}.
1125
1465
  *
1126
1466
  * @remarks
1127
1467
  * - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
1128
1468
  * mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
1129
- * manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
1130
- * it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
1469
+ * manager's default `#summarize` / `#keep` in unless the `input` overrides them, and stores
1470
+ * it (an already-present `id` overwrites — last write wins). `count` is the map size,
1131
1471
  * `conversation(id)` looks one up, `conversations()` lists them in insertion order.
1132
1472
  * - **Active pointer.** `active` is the active conversation (the agent's message source the
1133
- * context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
1473
+ * context renders), `undefined` until the first `add` (which auto-activates it — a registry
1134
1474
  * with conversations always has one active). A subsequent `add` leaves `active` unchanged.
1135
1475
  * `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
1136
1476
  * returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
1137
1477
  * no new error code).
1138
- * - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id was
1139
- * removed; removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
1478
+ * - **Removal.** `remove` drops one by id, or a batch — `true` only when every supplied id was
1479
+ * removed; removing the active conversation sets `active` to `undefined`. `clear` empties the registry
1140
1480
  * and sets `active` to `undefined`.
1141
1481
  * - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
1142
1482
  * its own observable `emitter`).
@@ -1236,15 +1576,15 @@ var ConversationManager = class {
1236
1576
  //#endregion
1237
1577
  //#region src/core/instructions/Instruction.ts
1238
1578
  /**
1239
- * Represents an immutable named directive — a {@link InstructionInterface} assembled once
1240
- * from its input (`name` / `content`, an optional `priority` defaulting to `0`), the `id`
1241
- * minted at construction.
1579
+ * Represents an immutable named directive — an {@link InstructionInterface} assembled once from
1580
+ * its input (`name` / `content`, an optional `priority` defaulting to `0`), the `id` minted at
1581
+ * construction.
1242
1582
  *
1243
1583
  * @remarks
1244
1584
  * A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
1245
1585
  * constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
1246
1586
  * `content`, resolves `priority` to the input's value or `0`, and carries the input's
1247
- * per-item `override` ONLY when supplied (assigned when present, mirroring a
1587
+ * per-item `override` only when supplied (assigned when present, mirroring a
1248
1588
  * message's `images` / `calls` present-when-given convention — kept absent otherwise).
1249
1589
  * Never mutated after construction. An
1250
1590
  * {@link import('./InstructionManager.js').InstructionManager} keys it by `name` and
@@ -1273,28 +1613,29 @@ var Instruction = class {
1273
1613
  //#endregion
1274
1614
  //#region src/core/instructions/InstructionManager.ts
1275
1615
  /**
1276
- * Registers the immutable {@link Instruction}s a richer context assembles a directives
1277
- * block from — keyed by `name`, listed by descending `priority`.
1616
+ * Registers the immutable {@link Instruction}s a richer context assembles a directives block
1617
+ * from — keyed by `name` so a re-`add` overwrites, last write wins, and listed by descending
1618
+ * `priority`, carrying the `open` / `render` build contract and an observable `emitter`.
1278
1619
  *
1279
1620
  * @remarks
1280
1621
  * - **Registry.** Instructions live in an insertion-ordered `Map` keyed by `name`;
1281
- * `add` takes one {@link InstructionInput} or a batch, MINTS each instruction's
1282
- * `id`, and a re-`add` of the same name OVERWRITES it (last write wins). `count` is the
1283
- * map size, `instruction(name)` looks one up, and `instructions()` lists them SORTED by
1622
+ * `add` takes one {@link InstructionInput} or a batch, mints each instruction's
1623
+ * `id`, and a re-`add` of the same name overwrites it (last write wins). `count` is the
1624
+ * map size, `instruction(name)` looks one up, and `instructions()` lists them sorted by
1284
1625
  * descending `priority` (a stable sort, so equal priorities keep insertion order).
1285
1626
  * - **Build contract (with the manager-options override).** `open` is the section
1286
1627
  * header a context renders the instructions under; `render(instruction)` renders one
1287
- * instruction (its `content`). Each ENCAPSULATES the cascade's `[options-override →
1628
+ * instruction (its `content`). Each encapsulates the cascade's `[options-override →
1288
1629
  * built-in]` half: when `InstructionManagerOptions.format` supplies an `open` /
1289
- * `render`, `open` / `render` return IT, else the built-in — so a richer context
1630
+ * `render`, `open` / `render` return it, else the built-in — so a richer context
1290
1631
  * reads one consistent pair and layers the provider default + per-item override on top
1291
1632
  * (see {@link import('../AgentContext.js').AgentContext}). The per-item
1292
1633
  * {@link InstructionInput.override} is round-tripped onto the stored instruction.
1293
- * - **Removal.** `remove` drops one by name, or a batch — `true` only when EVERY supplied
1634
+ * - **Removal.** `remove` drops one by name, or a batch — `true` only when every supplied
1294
1635
  * name was removed; `clear` empties the registry.
1295
1636
  * - **Observable.** The owned {@link emitter} ({@link InstructionManagerEventMap})
1296
1637
  * carries `add` (the created instruction) / `remove` (the name) / `clear` for
1297
- * fire-and-forget observers. Every event is emitted directly, strictly AFTER the map
1638
+ * fire-and-forget observers. Every event is emitted directly, strictly after the map
1298
1639
  * mutation completes; the emitter isolates a listener throw and routes it to its `error`
1299
1640
  * handler (the `error` option), so a buggy observer can never corrupt a mutation.
1300
1641
  *
@@ -1373,58 +1714,59 @@ var InstructionManager = class {
1373
1714
  /**
1374
1715
  * Assembles a provider request from the richer turn context — the optional system prompt, the
1375
1716
  * observable context managers (instructions / workspaces), the
1376
- * {@link ConversationManagerInterface} message source (whose active conversation IS `messages`),
1377
- * the {@link ToolManagerInterface} registry, and an active {@link ScopeInterface} changed through
1378
- * {@link AgentContextInterface.apply}.
1717
+ * {@link ConversationManagerInterface} message source whose active conversation is `messages`, the
1718
+ * {@link ToolManagerInterface} registry, and an active {@link ScopeInterface} changed through
1719
+ * {@link AgentContextInterface.apply}. `build()` folds the scoped managers and the active
1720
+ * workspace into one system block, then the conversation, and never reads `tools`.
1379
1721
  *
1380
1722
  * @remarks
1381
1723
  * - **Composition.** `system` is the optional system prompt; `instructions` / `tools` /
1382
1724
  * `workspaces` / `conversations` are the registries passed in `options` (bring your own), or
1383
- * fresh empty ones when omitted (so `workspaces` is ALWAYS present); `messages` is the ACTIVE
1384
- * conversation's live tail (ALWAYS defined — see below). `scope` is the active filter —
1725
+ * fresh empty ones when omitted (so `workspaces` is always present); `messages` is the active
1726
+ * conversation's live tail (always defined — see the following item). `scope` is the active filter —
1385
1727
  * `undefined` (the default) ⇒ no filtering; change it through `apply(scope)`. The structural
1386
1728
  * `workspaces` / `conversations` registries are fixed at construction; switch their active
1387
1729
  * members through their own `switch(id)` methods.
1388
- * - **The message source — the conversation registry's ACTIVE conversation.** `conversations` is a
1389
- * {@link ConversationManagerInterface}; the context ENSURES it always has an active conversation
1390
- * (at construction it `add`s a default when the manager has none), so the DYNAMIC `messages`
1391
- * getter — `this.#conversations.active` — is ALWAYS defined. `messages` returns the active
1392
- * conversation ITSELF (it owns the live tail + the message verbs directly, satisfying
1730
+ * - **The message source — the conversation registry's active conversation.** `conversations` is a
1731
+ * {@link ConversationManagerInterface}; the context ensures it always has an active conversation
1732
+ * (at construction it `add`s a default when the manager has none), so the dynamic `messages`
1733
+ * getter — `this.#conversations.active` — is always defined. `messages` returns the active
1734
+ * conversation itself (it owns the live tail + the message verbs directly, satisfying
1393
1735
  * {@link MessageManagerInterface} structurally — the same reference, no duplication), and
1394
1736
  * `build()` folds that conversation's `view()` (its per-section summaries + live tail) as the
1395
- * AUTHORITATIVE message inclusion — the scope does NOT filter the conversation (it owns inclusion
1737
+ * authoritative message inclusion — the scope does not filter the conversation (it owns inclusion
1396
1738
  * through compaction; scope filters only instructions / tools / workspace files). Because `messages`
1397
- * is read dynamically, an agent SWITCHES the active
1398
- * conversation BETWEEN runs (`conversations.switch(id)`) to serve MANY threads (the real
1739
+ * is read dynamically, an agent switches the active
1740
+ * conversation between runs (`conversations.switch(id)`) to serve many threads (the real
1399
1741
  * multi-conversation pattern); switch between runs, not during a run, and use separate agents
1400
1742
  * for concurrent threads.
1401
1743
  * - **`build(format?)` — the scoped assembly + the format cascade.** It folds, in order,
1402
- * the system prompt then the scope-filtered instructions → the ACTIVE workspace's text files
1744
+ * the system prompt then the scope-filtered instructions → the active workspace's text files
1403
1745
  * (each as a block: the section's resolved `open` text, each item's resolved rendering, then
1404
- * any resolved `close` text) into ONE leading `system` message (prepended only when at least
1405
- * one part exists), then appends the ACTIVE conversation's `view()` (the conversation owns
1406
- * message inclusion through compaction — the scope does NOT filter the conversation). Each
1746
+ * any resolved `close` text) into one leading `system` message (prepended only when at least
1747
+ * one part exists), then appends the active conversation's `view()` (the conversation owns
1748
+ * message inclusion through compaction — the scope does not filter the conversation). Each
1407
1749
  * `open` / item / `close`
1408
- * resolves INDEPENDENTLY, MOST-SPECIFIC-FIRST — `build()`'s optional `format` (a provider's
1409
- * per-section default) is the PROVIDER level: `open` = manager-options-override > provider >
1750
+ * resolves independently, most-specific-first — `build()`'s optional `format` (a provider's
1751
+ * per-section default) is the provider level: `open` = manager-options-override > provider >
1410
1752
  * built-in; per item = item-override > manager-options-override > provider > built-in; `close` =
1411
- * manager-options-override > provider (NO built-in ⇒ no closing line when unset) (see
1412
- * {@link AgentContextInterface.build}). Passing NO `format` (and with no overrides / no per-item
1753
+ * manager-options-override > provider (no built-in ⇒ no closing line when unset) (see
1754
+ * {@link AgentContextInterface.build}). Passing no `format` (and with no overrides / no per-item
1413
1755
  * override) reproduces the built-in framing byte-for-byte (each section is its built-in header +
1414
1756
  * items, no closing line). The active workspace's scoped-in image files' `base64` payload is attached to
1415
- * the LAST user message (a vision provider reads images off a user turn); when no user message
1757
+ * the last user message (a vision provider reads images off a user turn); when no user message
1416
1758
  * exists the attachment is skipped. Built fresh each call (recomputed, never cached), so it
1417
1759
  * always reflects the current managers / messages / scope / active workspace; it never mutates a
1418
1760
  * manager or the stored messages.
1419
- * - **The ACTIVE workspace, rendered BY CARRIER — the SOLE document/image context.**
1761
+ * - **The active workspace, rendered by carrier — the sole document/image context.**
1420
1762
  * `workspaces.active` (when set) has its {@link import('@orkestrel/workspace').FileInterface}s scope-filtered by `scope.files`,
1421
- * then split: TEXT files fold into a dedicated `## Workspace` system section (fenced reference
1422
- * blocks — placed right after the instructions section), and IMAGE files' `base64` payload attaches
1423
- * to the last user message. ACTIVE-ONLY — never the other registered workspaces; with no active
1424
- * workspace nothing renders for workspaces. `build()` OWNS this render (a `Workspace` /
1763
+ * then split: text files fold into a dedicated `## Workspace` system section (fenced reference
1764
+ * blocks — placed right after the instructions section), and image files' `base64` payload attaches
1765
+ * to the last user message. Active-only — never the other registered workspaces; with no active
1766
+ * workspace nothing renders for workspaces. `build()` owns this render (a `Workspace` /
1425
1767
  * `WorkspaceManager` stays file-focused).
1426
1768
  * - **Tools are structural, not in the prompt.** The registry is advertised to the provider
1427
- * through `tools.definitions()` (scope-filtered by the loop), NEVER serialized into the
1769
+ * through `tools.definitions()` (scope-filtered by the loop), never serialized into the
1428
1770
  * message array — so `build()`'s output carries no tool content, scoped or not.
1429
1771
  * - **Event-free context; observable managers.** The context itself owns no Emitter; the
1430
1772
  * context managers each carry their own (the push observation surface).
@@ -1503,10 +1845,11 @@ var AgentContext = class {
1503
1845
  //#endregion
1504
1846
  //#region src/core/Channel.ts
1505
1847
  /**
1506
- * Buffers chunks in a minimal unbounded async channel — the eager pump WRITES them in
1507
- * (`push`) and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
1508
- * live through the `drain` async-iterator. Decoupling write from read is what lets a
1509
- * producer make progress without a consumer pulling.
1848
+ * Buffers chunks in a minimal unbounded async channel — the eager pump writes them in (`push`)
1849
+ * and ends it (`close` / `fail`) regardless of consumption, while a consumer reads them back live
1850
+ * through the `drain` async-iterator. Decoupling write from read is what lets a producer make
1851
+ * progress without a consumer pulling, and it is why an agent's `result` settles whether or not
1852
+ * its `events` are drained.
1510
1853
  *
1511
1854
  * @remarks
1512
1855
  * The standard resolver-swap: a waiting `drain` parks on `#wake` (a void resolver);
@@ -1567,8 +1910,10 @@ var Channel = class {
1567
1910
  //#region src/core/Agent.ts
1568
1911
  /**
1569
1912
  * Composes a {@link ProviderInterface}, an {@link AgentContext}, and a
1570
- * {@link ToolManagerInterface} into a bounded context → provider → tools → repeat
1571
- * turn, exposed as both a one-shot `generate` and a live `stream`.
1913
+ * {@link ToolManagerInterface} into a bounded context → provider → tools → repeat turn, exposed
1914
+ * as both a one-shot `generate` and a live `stream` that share one private run — bounded by the
1915
+ * run `signal`, the `timeout`, and the `budget` folded through `AbortSignal.any`, paced by
1916
+ * `scheduler`, with tool iteration capped at `limit`.
1572
1917
  *
1573
1918
  * @remarks
1574
1919
  * - **One loop, two faces.** A single private async generator (`#run`) drives the
@@ -1585,15 +1930,15 @@ var Channel = class {
1585
1930
  * assistant message and stop.
1586
1931
  * - **Bounded.** Each run arms one cancel through `createAbort({ signal: AbortSignal.any([
1587
1932
  * …]) })` folding the external `signal`, the `timeout` deadline, and the `budget`
1588
- * signal; `abort()` fires it. Any trip stops the loop and commits a PARTIAL result
1589
- * (the `result` promise RESOLVES, never rejects, on a cancel) — only a genuine
1933
+ * signal; `abort()` fires it. Any trip stops the loop and commits a partial result
1934
+ * (the `result` promise resolves, never rejects, on a cancel) — only a genuine
1590
1935
  * provider / tool error rejects.
1591
1936
  * - **Paced + capped.** The `scheduler` (when given) `yield`s between turns; tool
1592
1937
  * iteration is capped at `limit`.
1593
- * - **Two observation surfaces.** The PULL {@link AgentChunk} stream carries per-token
1594
- * deltas (+ usage/tool chunks); the PUSH {@link emitter} ({@link AgentEventMap}) carries
1938
+ * - **Two observation surfaces.** The pull {@link AgentChunk} stream carries per-token
1939
+ * deltas (+ usage/tool chunks); the push {@link emitter} ({@link AgentEventMap}) carries
1595
1940
  * lifecycle + usage/tool/deny moments for fire-and-forget observers. Every event is
1596
- * emitted directly, AFTER the relevant state transition / settle; the emitter isolates a
1941
+ * emitted directly, after the relevant state transition / settle; the emitter isolates a
1597
1942
  * listener throw and routes it to its `error` handler (the `error` option), so a buggy
1598
1943
  * observer can never escape into / reorder / corrupt the settle-once loop — observation is
1599
1944
  * purely a side-channel.
@@ -1784,7 +2129,7 @@ var Agent = class {
1784
2129
  } catch (error) {
1785
2130
  if (abort.signal.aborted) {
1786
2131
  if (isProviderAbortError(error)) {
1787
- if (error.partial.thinking !== void 0) thinking = joinThinking(thinking, error.partial.thinking);
2132
+ if (error.partial.thinking !== void 0 && error.partial.thinking.length > 0) thinking = joinThinking(thinking, error.partial.thinking);
1788
2133
  if (error.partial.usage !== void 0) {
1789
2134
  const abortUsage = sanitizeUsage(error.partial.usage);
1790
2135
  budget?.consume({
@@ -1945,9 +2290,10 @@ var Agent = class {
1945
2290
  //#endregion
1946
2291
  //#region src/core/AgentRegistry.ts
1947
2292
  /**
1948
- * Makes a durable, JSON-serializable {@link AgentJobInput} runnable — holds the named
1949
- * pools of live, non-serializable pieces (providers, tools, authorities, schedulers) and
1950
- * rehydrates a seeded, signal-wired {@link Agent} from a job's names + data.
2293
+ * Makes a durable, JSON-serializable {@link AgentJobInput} runnable — holds the named pools of
2294
+ * live, non-serializable pieces (providers, tools, authorities, schedulers), throws on a name
2295
+ * absent from its pool, and `build`s a seeded, signal-wired {@link Agent} from a job's names and
2296
+ * data.
1951
2297
  *
1952
2298
  * @remarks
1953
2299
  * - **Why it exists.** An `AgentJobInput` is serializable so it can survive a crash in a
@@ -1956,9 +2302,9 @@ var Agent = class {
1956
2302
  * construct it once with the live pools, then a queue / runner handler calls `build`
1957
2303
  * on each (possibly restored) job to get a ready agent.
1958
2304
  * - **Accessors throw on a miss.** `provider` / `tool` / `authority` / `scheduler` resolve a
1959
- * name against their pool and THROW an {@link AgentError} carrying `code: 'REGISTRY'` and
2305
+ * name against their pool and throw an {@link AgentError} carrying `code: 'REGISTRY'` and
1960
2306
  * the message `unknown <category>: <name>` when it is absent — an unknown name in a
1961
- * rehydrated job is a programmer / config error that must fail LOUDLY at build time, never
2307
+ * rehydrated job is a programmer / config error that must fail loudly at build time, never
1962
2308
  * silently resolve to `undefined` and run an agent missing a dependency.
1963
2309
  * - **`build` rehydrates.** Resolve the job's `provider`; assemble a fresh
1964
2310
  * {@link ToolManager} from the `tools` names; rebuild the token `budget` from its
@@ -2039,18 +2385,20 @@ var AgentRegistry = class {
2039
2385
  //#endregion
2040
2386
  //#region src/core/Authority.ts
2041
2387
  /**
2042
- * Gates the agent loop's tool calls — the synchronous policy consulted before each call
2043
- * runs, turning one {@link AuthorityContext} into an {@link AuthorityDecision}.
2388
+ * Gates the agent loop's tool calls — the synchronous policy consulted before each call runs,
2389
+ * turning one {@link AuthorityContext} into an {@link AuthorityDecision} by walking the ordered
2390
+ * rules first-match-wins and falling back to a configurable default, which allows an unmatched
2391
+ * call unless its `fallback` denies.
2044
2392
  *
2045
2393
  * @remarks
2046
2394
  * - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
2047
- * returns the FIRST whose `match(context)` is true as
2048
- * `{ zone, allowed: rule.allowed ?? true, reason }` — a matched rule ALLOWS by
2049
- * default and DENIES only when its `allowed` is explicitly `false`.
2395
+ * returns the first whose `match(context)` is true as
2396
+ * `{ zone, allowed: rule.allowed ?? true, reason }` — a matched rule allows by
2397
+ * default and denies only when its `allowed` is explicitly `false`.
2050
2398
  * - **Fallback.** When no rule matches, `evaluate` returns the configured `fallback`.
2051
2399
  * It defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }` (allow-unmatched),
2052
- * so a rules list of denials behaves as a DENYLIST. To make the gate deny-by-default
2053
- * (an ALLOWLIST — only matched rules that allow get through), pass an `allowed: false`
2400
+ * so a rules list of denials behaves as a denylist. To make the gate deny-by-default
2401
+ * (an allowlist — only matched rules that allow get through), pass an `allowed: false`
2054
2402
  * `fallback`.
2055
2403
  * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
2056
2404
  * every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
@@ -2092,51 +2440,44 @@ var Authority = class {
2092
2440
  //#endregion
2093
2441
  //#region src/core/validators.ts
2094
2442
  /**
2095
- * Checks whether an `unknown` is structurally a {@link Message} record — the per-message step of
2096
- * the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
2097
- * read, never an assertion). The conversation analogue of
2098
- * {@link import('@orkestrel/workspace').isFile}.
2443
+ * Checks whether a value satisfies the domain conversation-message contract.
2099
2444
  *
2100
2445
  * @remarks
2101
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
2102
- * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
2103
- * — a `calls` array EVERY element of which is a valid
2104
- * {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
2105
- * fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
2106
- * reads back as absent rather than replaying a malformed call) and an `images` that is an array
2107
- * (an absent optional passes). The `role` is left as a broad `string` here (an open
2108
- * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
2109
- * defensively rather than rejected against the current literal set). Enough to safely impose the
2110
- * {@link Message} type at
2111
- * a storage boundary WITHOUT a cast.
2112
- *
2113
- * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
2114
- * @returns True if `value` has the structural shape of a {@link Message}; false otherwise
2446
+ * Roles belong to MessageRole and image elements are strings. Tool arguments may
2447
+ * carry non-JSON values, as the domain type permits; the message wire contract is
2448
+ * narrower. Unreadable fields and hostile inputs return false.
2115
2449
  *
2450
+ * @param value - The unknown message candidate
2451
+ * @returns True if the domain message fields are valid; false otherwise
2116
2452
  * @example
2117
2453
  * ```ts
2118
2454
  * isMessage({ id: '1', role: 'user', content: 'hi' }) // true
2119
- * isMessage({ id: '1', role: 'assistant', content: '', calls: [] }) // true
2120
- * isMessage({ id: '1', role: 'user' }) // false (missing content)
2121
- * isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
2455
+ * isMessage({ id: '1', role: 'other', content: '' }) // false
2456
+ * isMessage({ id: '1', role: 'user', content: '', images: [1] }) // false
2122
2457
  * ```
2123
2458
  */
2124
2459
  function isMessage(value) {
2125
- if (!isRecord(value)) return false;
2126
- if (!isString(value.id) || !isString(value.role) || !isString(value.content)) return false;
2127
- if (value.calls !== void 0 && !(isArray(value.calls) && value.calls.every(isToolCall))) return false;
2128
- return value.images === void 0 || isArray(value.images);
2460
+ const checked = attempt(() => {
2461
+ if (!isRecord(value)) return false;
2462
+ const { id, role, content, calls, images } = value;
2463
+ if (!isString(id) || !isString(content)) return false;
2464
+ if (role !== "system" && role !== "user" && role !== "assistant" && role !== "tool") return false;
2465
+ if (calls !== void 0 && !arrayOf(isToolCall)(calls)) return false;
2466
+ return images === void 0 || arrayOf(isString)(images);
2467
+ });
2468
+ return checked.success && checked.value;
2129
2469
  }
2130
2470
  /**
2131
- * Checks whether an `unknown` is structurally a {@link Section} record — the per-section step of
2132
- * the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
2133
- * read, never an assertion).
2471
+ * Checks whether an `unknown` is structurally a {@link Section} record — a `string` `id` and
2472
+ * `summary` beside a `messages` array of valid {@link Message}s, the per-section step of the
2473
+ * {@link isConversationSnapshot} read-boundary narrow. Total, never throwing, and never an
2474
+ * assertion.
2134
2475
  *
2135
2476
  * @remarks
2136
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the section's
2137
- * SHAPE: a record with a `string` `id`, a `string` `summary`, and a `messages` array EVERY element
2477
+ * A total guard (it never throws — adversarial input returns `false`). It checks the section's
2478
+ * shape: a record with a `string` `id`, a `string` `summary`, and a `messages` array every element
2138
2479
  * of which is a valid {@link Message} record ({@link isMessage}). Enough to safely impose
2139
- * the {@link Section} type at a storage boundary WITHOUT a cast.
2480
+ * the {@link Section} type at a storage boundary without a cast.
2140
2481
  *
2141
2482
  * @param value - The value to test (one element of a snapshot's `sections` array)
2142
2483
  * @returns True if `value` has the structural shape of a {@link Section}; false otherwise
@@ -2154,19 +2495,20 @@ function isSection(value) {
2154
2495
  return isArray(value.messages) && value.messages.every(isMessage);
2155
2496
  }
2156
2497
  /**
2157
- * Narrows an `unknown` to a {@link ConversationSnapshot} — the total boundary guard for an
2158
- * UNTRUSTED snapshot read (a storage row a
2498
+ * Narrows an `unknown` to a {@link ConversationSnapshot} — a `string` `id`, an optional `string`
2499
+ * `summary`, and valid `sections` and `messages` arrays; the total boundary guard for an
2500
+ * untrusted snapshot read (a storage row a
2159
2501
  * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
2160
- * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
2161
- * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
2502
+ * reads back from its opaque JSON column, a snapshot loaded from disk), never throwing. The exact
2503
+ * analogue of {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
2162
2504
  *
2163
2505
  * @remarks
2164
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
2165
- * SHAPE: a `string` `id`, an OPTIONAL `string` `summary` (present-or-absent — the rollup is
2166
- * `undefined` until the first compaction), a `sections` array EVERY element of which is a valid
2167
- * {@link Section} ({@link isSection}), and a `messages` array EVERY element of which is a
2506
+ * A total guard (it never throws — adversarial input returns `false`). It checks the snapshot's
2507
+ * shape: a `string` `id`, an optional `string` `summary` (present-or-absent — the rollup is
2508
+ * `undefined` until the first compaction), a `sections` array every element of which is a valid
2509
+ * {@link Section} ({@link isSection}), and a `messages` array every element of which is a
2168
2510
  * valid {@link Message} ({@link isMessage}) — enough to safely impose the
2169
- * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
2511
+ * {@link ConversationSnapshot} type at a storage boundary without a cast. The structural twin of
2170
2512
  * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
2171
2513
  * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
2172
2514
  * element) resolves `false`, so a
@@ -2195,37 +2537,39 @@ function isConversationSnapshot(value) {
2195
2537
  //#region src/core/conversations/stores/DatabaseConversationStore.ts
2196
2538
  /**
2197
2539
  * Backs a {@link ConversationStoreInterface} with one table of the `databases` layer — a
2198
- * conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
2199
- * / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
2200
- * {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
2540
+ * conversation's durable state is a row holding the snapshot as one opaque JSON column, narrowed
2541
+ * back on `get` by {@link import('../../validators.js').isConversationSnapshot}, so persistence
2542
+ * reduces to keyed point-access (`get` / `set` / `delete`) over a {@link TableInterface}. The
2543
+ * driver-pluggable twin of the plain-`Map`
2544
+ * {@link import('./MemoryConversationStore.js').MemoryConversationStore}, and the exact twin of
2201
2545
  * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}.
2202
2546
  *
2203
2547
  * @remarks
2204
2548
  * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
2205
2549
  * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
2206
- * IndexedDB backend swaps in WITHOUT touching the
2550
+ * IndexedDB backend swaps in without touching the
2207
2551
  * {@link import('../ConversationManager.js').ConversationManager} or the
2208
2552
  * {@link import('../Conversation.js').Conversation} — the same seam as
2209
2553
  * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}. The
2210
2554
  * driver defaults to memory ({@link import('../../factories.js').createDatabaseConversationStore}
2211
- * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
2555
+ * passes `createMemoryDriver()`), so it also works in memory out of the box; you opt into the
2212
2556
  * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
2213
2557
  *
2214
- * The {@link ConversationSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
2558
+ * The {@link ConversationSnapshot} is stored as one opaque JSON column — the table is a row of
2215
2559
  * `{ id; snapshot }` ({@link ConversationSnapshotRow}), the snapshot the whole JSON blob (a
2216
2560
  * `rawShape` column the factory builds) — exactly as `DatabaseWorkspaceStore` stores its snapshot.
2217
- * The snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is
2218
- * lossless AND keeps the row type flat (`snapshot` reads back as `unknown`).
2561
+ * The snapshot is already a complete, self-contained, pure-JSON payload, so storing it whole is
2562
+ * lossless and keeps the row type flat (`snapshot` reads back as `unknown`).
2219
2563
  *
2220
- * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
2564
+ * - **`set(snapshot)` upserts under the snapshot's own `id`** (no separate id param) — it writes
2221
2565
  * the row `{ id: snapshot.id, snapshot }`.
2222
2566
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
2223
2567
  * a {@link ConversationSnapshot} ({@link import('../../validators.js').isConversationSnapshot} — the
2224
2568
  * total guard for an untrusted storage read), or `undefined` if none is stored.
2225
2569
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
2226
2570
  *
2227
- * UNLIKE a session store there is NO idle-TTL / eviction — a persisted conversation lives until an
2228
- * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
2571
+ * Unlike a session store there is no idle-TTL / eviction — a persisted conversation lives until an
2572
+ * explicit `delete`. The public surface is exactly `get` / `set` / `delete` — no extra members (the
2229
2573
  * method bijection with {@link ConversationStoreInterface}). Hydration stays a caller concern: a
2230
2574
  * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
2231
2575
  * the live conversation through the `snapshot` option (its `open` / `save`).
@@ -2260,7 +2604,7 @@ var DatabaseConversationStore = class {
2260
2604
  if (row === void 0) return void 0;
2261
2605
  return isConversationSnapshot(row.snapshot) ? row.snapshot : void 0;
2262
2606
  }
2263
- /** Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
2607
+ /** Inserts or replaces under the snapshot's own `id` (no separate id param) — the row is `{ id, snapshot }`. */
2264
2608
  async set(snapshot) {
2265
2609
  await this.#table.set({
2266
2610
  id: snapshot.id,
@@ -2276,26 +2620,27 @@ var DatabaseConversationStore = class {
2276
2620
  //#region src/core/conversations/stores/MemoryConversationStore.ts
2277
2621
  /**
2278
2622
  * Implements the {@link ConversationStoreInterface} in memory — a process-lifetime `Map` of
2279
- * {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
2280
- * {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
2623
+ * {@link ConversationSnapshot}s keyed by conversation id, the default store
2624
+ * {@link import('../../factories.js').createMemoryConversationStore} builds and the default
2625
+ * backing for `open` / `save`. The exact twin of
2281
2626
  * {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
2282
2627
  *
2283
2628
  * @remarks
2284
2629
  * A plain `Map<string, ConversationSnapshot>` — the snapshot is already pure,
2285
2630
  * self-contained JSON, so no encoding is needed for the memory tier. Like the
2286
2631
  * {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
2287
- * there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
2288
- * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
2632
+ * there is no idle-TTL and no eviction: a persisted conversation lives until an explicit `delete`. A
2633
+ * durable backend (JSON / SQLite / IndexedDB) swaps in through the same interface without touching
2289
2634
  * the {@link import('../ConversationManager.js').ConversationManager} or the
2290
2635
  * {@link import('../Conversation.js').Conversation} — its driver-pluggable twin is
2291
2636
  * {@link import('./DatabaseConversationStore.js').DatabaseConversationStore} (the snapshot as one
2292
2637
  * opaque JSON column).
2293
2638
  *
2294
2639
  * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
2295
- * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
2640
+ * - **`set` inserts / replaces under the snapshot's own `id`** (no separate id param).
2296
2641
  * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
2297
2642
  *
2298
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the method
2643
+ * The public surface is exactly `get` / `set` / `delete` — no extra members (the method
2299
2644
  * bijection with {@link ConversationStoreInterface}). Hydration is a caller concern: a
2300
2645
  * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
2301
2646
  * the live conversation through the `snapshot` option (its `open` / `save`).
@@ -2314,38 +2659,546 @@ var DatabaseConversationStore = class {
2314
2659
  */
2315
2660
  var MemoryConversationStore = class {
2316
2661
  #snapshots = /* @__PURE__ */ new Map();
2662
+ /**
2663
+ * Resolves the persisted snapshot for `id`, or `undefined` if none is stored.
2664
+ *
2665
+ * @param id - The conversation id to resolve (a {@link ConversationSnapshot.id})
2666
+ * @returns The persisted snapshot, or `undefined` if absent
2667
+ */
2317
2668
  get(id) {
2318
2669
  return Promise.resolve(this.#snapshots.get(id));
2319
2670
  }
2671
+ /**
2672
+ * Inserts or replaces a snapshot under its own `snapshot.id` (no separate id param —
2673
+ * mirroring {@link import('@orkestrel/workspace').WorkspaceStoreInterface}'s `set`).
2674
+ *
2675
+ * @param snapshot - The snapshot to store (keyed by its `id`)
2676
+ * @returns A promise that resolves after the snapshot is stored
2677
+ */
2320
2678
  set(snapshot) {
2321
2679
  this.#snapshots.set(snapshot.id, snapshot);
2322
2680
  return Promise.resolve();
2323
2681
  }
2682
+ /**
2683
+ * Drops a snapshot by id; an absent id is a no-op (no throw).
2684
+ *
2685
+ * @param id - The conversation id to drop
2686
+ * @returns A promise that resolves after the snapshot is dropped
2687
+ */
2324
2688
  delete(id) {
2325
2689
  this.#snapshots.delete(id);
2326
2690
  return Promise.resolve();
2327
2691
  }
2328
2692
  };
2329
2693
  //#endregion
2694
+ //#region src/core/AgentProvider.ts
2695
+ /**
2696
+ * Implements bounded HTTP streaming and result assembly behind concrete wire seams.
2697
+ *
2698
+ * @remarks
2699
+ * Every call owns its parser, splitter, deadline, and accumulation. Success bodies
2700
+ * have no size limit. A qwen3 implicit-open reclassification corrects the final
2701
+ * content while content deltas already yielded cannot be recalled. A subclass fills
2702
+ * `name`, `frame`, `body`, `read`, and `finish`; the constructor takes the `split`
2703
+ * and `strict` switches to control reasoning separation and settled-result requirements.
2704
+ *
2705
+ * @example Writing a provider for a new wire
2706
+ * ```ts
2707
+ * import type {
2708
+ * ProviderIncrement,
2709
+ * ProviderOptions,
2710
+ * ProviderParserInterface,
2711
+ * ProviderRequest,
2712
+ * } from '@orkestrel/agent'
2713
+ * import { AgentProvider } from '@orkestrel/agent'
2714
+ *
2715
+ * class TextFrame implements ProviderParserInterface<string> {
2716
+ * parse(chunk: string): readonly string[] {
2717
+ * return [chunk]
2718
+ * }
2719
+ * clear(): void {} // Raw text retains no framing state.
2720
+ * }
2721
+ *
2722
+ * interface TextOptions extends ProviderOptions {
2723
+ * readonly url: string
2724
+ * }
2725
+ *
2726
+ * class TextProvider extends AgentProvider<string> {
2727
+ * readonly name = 'text'
2728
+ * constructor(options: TextOptions) {
2729
+ * super({ ...options, path: '/generate' })
2730
+ * }
2731
+ * frame(): ProviderParserInterface<string> {
2732
+ * return new TextFrame()
2733
+ * }
2734
+ * body(request: ProviderRequest): object {
2735
+ * return { messages: request.messages }
2736
+ * }
2737
+ * read(record: string): ProviderIncrement {
2738
+ * return { content: record, thinking: '', tools: [] }
2739
+ * }
2740
+ * finish(_parser: ProviderParserInterface<string>): readonly string[] {
2741
+ * return [] // Raw text retains no records at end of input.
2742
+ * }
2743
+ * }
2744
+ * ```
2745
+ */
2746
+ var AgentProvider = class {
2747
+ #id;
2748
+ #url;
2749
+ #path;
2750
+ #timeout;
2751
+ #transport;
2752
+ #headers;
2753
+ #format;
2754
+ #split;
2755
+ #strict;
2756
+ constructor(input) {
2757
+ this.#id = crypto.randomUUID();
2758
+ this.#url = input.url;
2759
+ this.#path = input.path ?? "";
2760
+ this.#timeout = input.timeout ?? 12e4;
2761
+ this.#transport = input.fetch ?? globalThis.fetch.bind(globalThis);
2762
+ this.#headers = input.headers;
2763
+ this.#format = input.format;
2764
+ this.#split = input.split ?? true;
2765
+ this.#strict = input.strict ?? false;
2766
+ }
2767
+ /** Exposes the instance's minted UUID. */
2768
+ get id() {
2769
+ return this.#id;
2770
+ }
2771
+ /** Exposes the context framing exactly as supplied. */
2772
+ get format() {
2773
+ return this.#format;
2774
+ }
2775
+ /**
2776
+ * Generates a complete turn by draining the shared stream engine.
2777
+ *
2778
+ * @param messages - The conversation turns
2779
+ * @param signal - The caller's cancellation bound
2780
+ * @param tools - The advertised tool definitions
2781
+ * @param options - The per-call generation configuration
2782
+ * @returns The terminal stream result
2783
+ */
2784
+ async generate(messages, signal, tools, options) {
2785
+ const stream = this.stream(messages, signal, tools, options);
2786
+ let step = await stream.next();
2787
+ while (!step.done) step = await stream.next();
2788
+ return step.value;
2789
+ }
2790
+ /**
2791
+ * Streams decoded deltas and returns the authoritative or assembled turn result.
2792
+ *
2793
+ * @param messages - The conversation turns
2794
+ * @param signal - The caller's cancellation bound
2795
+ * @param tools - The advertised tool definitions
2796
+ * @param options - The per-call generation configuration
2797
+ * @returns Content and native thinking deltas followed by the settled result
2798
+ * @throws ProviderAbortError Thrown when the combined cancellation bound fires
2799
+ * @throws ProviderError Thrown for an HTTP or protocol failure
2800
+ */
2801
+ async *stream(messages, signal, tools, options) {
2802
+ if (signal.aborted) throw new ProviderAbortError({ content: "" });
2803
+ const timeout = new Timeout({ ms: this.#timeout });
2804
+ timeout.start();
2805
+ const combined = AbortSignal.any([timeout.signal, signal]);
2806
+ let parser;
2807
+ let splitter;
2808
+ let state = {
2809
+ content: "",
2810
+ thinking: "",
2811
+ tools: []
2812
+ };
2813
+ try {
2814
+ parser = this.frame();
2815
+ splitter = this.#split ? createThinkSplitter() : void 0;
2816
+ const response = await this.#request({
2817
+ messages,
2818
+ ...tools === void 0 ? {} : { tools },
2819
+ ...options === void 0 ? {} : { options }
2820
+ }, combined);
2821
+ if (response.body === null) throw new ProviderError("PROTOCOL", "provider error: no response body");
2822
+ for await (const chunk of readChunks(response.body, combined)) {
2823
+ combined.throwIfAborted();
2824
+ for (const record of parser.parse(chunk)) {
2825
+ yield* this.#fold(record, state, splitter, combined, (increment) => {
2826
+ state = increment;
2827
+ });
2828
+ combined.throwIfAborted();
2829
+ if (state.result !== void 0) return state.result;
2830
+ }
2831
+ }
2832
+ combined.throwIfAborted();
2833
+ for (const record of this.finish(parser)) {
2834
+ yield* this.#fold(record, state, splitter, combined, (increment) => {
2835
+ state = increment;
2836
+ });
2837
+ combined.throwIfAborted();
2838
+ if (state.result !== void 0) return state.result;
2839
+ }
2840
+ const tail = splitter?.flush() ?? "";
2841
+ if (tail.length > 0) yield {
2842
+ channel: "content",
2843
+ text: tail
2844
+ };
2845
+ combined.throwIfAborted();
2846
+ if (this.#strict) throw new ProviderError("PROTOCOL", "provider error: missing settled result");
2847
+ return buildProviderResult(splitter?.content ?? state.content, joinThinking(splitter?.thinking, state.thinking), state.tools, state.usage);
2848
+ } catch (error) {
2849
+ if (combined.aborted) {
2850
+ splitter?.flush();
2851
+ throw new ProviderAbortError(buildProviderResult(splitter?.content ?? state.content, joinThinking(splitter?.thinking, state.thinking), state.tools, state.usage), error === combined.reason ? void 0 : { cause: error });
2852
+ }
2853
+ throw error;
2854
+ } finally {
2855
+ try {
2856
+ parser?.clear();
2857
+ } finally {
2858
+ timeout.clear();
2859
+ }
2860
+ }
2861
+ }
2862
+ *#fold(record, previous, splitter, signal, commit) {
2863
+ const increment = this.read(record);
2864
+ if (increment.result !== void 0) {
2865
+ commit(increment);
2866
+ return;
2867
+ }
2868
+ const content = splitter?.split(increment.content) ?? increment.content;
2869
+ const usage = increment.usage ?? previous.usage;
2870
+ commit({
2871
+ content: previous.content + increment.content,
2872
+ thinking: previous.thinking + increment.thinking,
2873
+ tools: [...previous.tools, ...increment.tools],
2874
+ ...usage === void 0 ? {} : { usage }
2875
+ });
2876
+ if (content.length > 0) yield {
2877
+ channel: "content",
2878
+ text: content
2879
+ };
2880
+ signal.throwIfAborted();
2881
+ if (increment.thinking.length > 0) yield {
2882
+ channel: "thinking",
2883
+ text: increment.thinking
2884
+ };
2885
+ }
2886
+ async #request(request, signal) {
2887
+ const headers = await this.#requestHeaders(signal);
2888
+ signal.throwIfAborted();
2889
+ const response = await this.#transport(this.#url + this.#path, {
2890
+ method: "POST",
2891
+ headers,
2892
+ body: JSON.stringify(this.body(request)),
2893
+ signal
2894
+ });
2895
+ if (!response.ok) {
2896
+ let detail;
2897
+ try {
2898
+ detail = response.body === null ? "" : (await readText(response.body, MAX_ERROR_BODY_LENGTH, signal)).text;
2899
+ } catch (cause) {
2900
+ throw new ProviderError("HTTP", `provider error: ${response.status} - (error body unavailable)`, {
2901
+ status: response.status,
2902
+ cause
2903
+ });
2904
+ }
2905
+ signal.throwIfAborted();
2906
+ throw new ProviderError("HTTP", `provider error: ${response.status}${detail === "" ? "" : ` - ${detail}`}`, { status: response.status });
2907
+ }
2908
+ return response;
2909
+ }
2910
+ async #requestHeaders(signal) {
2911
+ const headers = new Headers({ "Content-Type": "application/json" });
2912
+ if (this.#headers === void 0) return headers;
2913
+ const cleanup = new AbortController();
2914
+ const aborted = Promise.withResolvers();
2915
+ signal.addEventListener("abort", () => aborted.reject(signal.reason), {
2916
+ once: true,
2917
+ signal: cleanup.signal
2918
+ });
2919
+ try {
2920
+ signal.throwIfAborted();
2921
+ const hook = this.#headers;
2922
+ const entries = await Promise.race([Promise.resolve().then(() => hook(signal)), aborted.promise]);
2923
+ for (const [key, value] of Object.entries(entries)) headers.set(key, value);
2924
+ return headers;
2925
+ } finally {
2926
+ cleanup.abort();
2927
+ }
2928
+ }
2929
+ };
2930
+ //#endregion
2931
+ //#region src/core/providers/RelayProvider.ts
2932
+ /**
2933
+ * Carries provider calls over an authenticated NDJSON relay endpoint.
2934
+ *
2935
+ * @remarks
2936
+ * The `ToolCall.caller` member never crosses the hop. The wire body is an owned snapshot of
2937
+ * the projection read through property descriptors, so a serializer reachable only through a
2938
+ * `get` trap or a prototype is never consulted; an own function-valued property such as a
2939
+ * `toJSON` method is a value outside JSON and is refused before fetching, with the clone's
2940
+ * failure as the refusal's `cause`. A remote abort reconstructs a `ProviderAbortError` instance
2941
+ * without aborting the
2942
+ * local signal; the `Agent` runtime treats that instance as an error unless its own bound
2943
+ * signal is aborted.
2944
+ * Content is preserved verbatim, including literal thinking tags.
2945
+ * A refusal reaches the browser as a `ProviderError` instance with the `HTTP` code and status.
2946
+ * This is the browser end alone; {@link createRelay} mounts the server end, and
2947
+ * {@link createRelayProvider}'s example is the browser half of that pair.
2948
+ *
2949
+ * @example
2950
+ * ```ts
2951
+ * import type { ProviderResult } from '@orkestrel/agent'
2952
+ * import { RelayProvider } from '@orkestrel/agent'
2953
+ * // The browser application supplies this parser dependency.
2954
+ * import { createNDJSONParser } from '@orkestrel/ndjson'
2955
+ *
2956
+ * export function ask(bearer: string, signal: AbortSignal): Promise<ProviderResult> {
2957
+ * const browser = new RelayProvider({
2958
+ * url: 'https://relay.example/relay',
2959
+ * parser: createNDJSONParser,
2960
+ * headers: () => ({ authorization: `Bearer ${bearer}` }),
2961
+ * })
2962
+ * return browser.generate([{ id: 'ask', role: 'user', content: 'ping' }], signal)
2963
+ * }
2964
+ * ```
2965
+ */
2966
+ var RelayProvider = class extends AgentProvider {
2967
+ #parser;
2968
+ constructor(options) {
2969
+ const { url, timeout, fetch, headers, format } = options;
2970
+ super({
2971
+ url,
2972
+ split: false,
2973
+ strict: true,
2974
+ ...timeout === void 0 ? {} : { timeout },
2975
+ ...fetch === void 0 ? {} : { fetch },
2976
+ ...headers === void 0 ? {} : { headers },
2977
+ ...format === void 0 ? {} : { format }
2978
+ });
2979
+ this.#parser = options.parser;
2980
+ }
2981
+ /** Identifies the relay backend. */
2982
+ name = "relay";
2983
+ /** Creates fresh framing state for each response. */
2984
+ frame() {
2985
+ return this.#parser();
2986
+ }
2987
+ /**
2988
+ * Projects declared request fields and refuses values the JSON wire cannot carry.
2989
+ *
2990
+ * @remarks
2991
+ * The snapshot is taken from property descriptors, so a custom serializer a projected
2992
+ * value carries is ignored rather than consulted and cannot reach the wire.
2993
+ *
2994
+ * @param request - The domain conversation and call configuration
2995
+ * @returns The validated wire request with caller context omitted
2996
+ * @throws {ProviderError} Thrown when the snapshot cannot be taken — a hostile read or a
2997
+ * value outside JSON — carrying that failure as its `cause`, and when the snapshot the
2998
+ * projection produced is not a valid wire request
2999
+ */
3000
+ body(request) {
3001
+ let snapshot;
3002
+ try {
3003
+ const projected = {
3004
+ messages: request.messages.map((message) => ({
3005
+ id: message.id,
3006
+ role: message.role,
3007
+ content: message.content,
3008
+ ...message.calls === void 0 ? {} : { calls: message.calls.map((call) => ({
3009
+ id: call.id,
3010
+ name: call.name,
3011
+ arguments: call.arguments
3012
+ })) },
3013
+ ...message.images === void 0 ? {} : { images: message.images }
3014
+ })),
3015
+ ...request.tools === void 0 ? {} : { tools: request.tools.map((tool) => ({
3016
+ name: tool.name,
3017
+ ...tool.description === void 0 ? {} : { description: tool.description },
3018
+ ...tool.parameters === void 0 ? {} : { parameters: tool.parameters }
3019
+ })) },
3020
+ ...request.options === void 0 ? {} : { options: {
3021
+ ...request.options.think === void 0 ? {} : { think: request.options.think },
3022
+ ...request.options.schema === void 0 ? {} : { schema: request.options.schema }
3023
+ } }
3024
+ };
3025
+ snapshot = cloneJSONValue(projected);
3026
+ } catch (cause) {
3027
+ throw new ProviderError("PROTOCOL", "relay request is not JSON", { cause });
3028
+ }
3029
+ if (!providerRequestContract.is(snapshot)) throw new ProviderError("PROTOCOL", "relay request is not JSON");
3030
+ return snapshot;
3031
+ }
3032
+ /**
3033
+ * Validates a relay frame and translates its channel into the shared stream engine.
3034
+ *
3035
+ * @param record - The framed wire record
3036
+ * @returns A delta contribution or authoritative result
3037
+ * @throws {ProviderAbortError} Thrown for a remote abort carrying its partial
3038
+ * @throws {ProviderError} Thrown for a malformed frame or remote provider failure
3039
+ */
3040
+ read(record) {
3041
+ if (!relayFrameContract.is(record)) throw new ProviderError("PROTOCOL", "invalid relay frame");
3042
+ switch (record.channel) {
3043
+ case "content": return {
3044
+ content: record.text,
3045
+ thinking: "",
3046
+ tools: []
3047
+ };
3048
+ case "thinking": return {
3049
+ content: "",
3050
+ thinking: record.text,
3051
+ tools: []
3052
+ };
3053
+ case "result": return {
3054
+ content: "",
3055
+ thinking: "",
3056
+ tools: [],
3057
+ result: record.result
3058
+ };
3059
+ case "abort": throw new ProviderAbortError(record.partial);
3060
+ case "error": throw new ProviderError("PROVIDER", record.message);
3061
+ }
3062
+ }
3063
+ /**
3064
+ * Recovers an unterminated final frame by completing its NDJSON line.
3065
+ *
3066
+ * @param parser - The call's retained framing state
3067
+ * @returns Records completed by the final newline
3068
+ */
3069
+ finish(parser) {
3070
+ return parser.parse("\n");
3071
+ }
3072
+ };
3073
+ //#endregion
3074
+ //#region src/core/RelayStream.ts
3075
+ /**
3076
+ * Streams a provider call as validated NDJSON frames under response backpressure.
3077
+ *
3078
+ * @remarks
3079
+ * Cancellation aborts upstream before returning its iterator. Request listeners are
3080
+ * released on settlement. Unexpected provider failures carry a fixed public message.
3081
+ * An inbound abort leaves the response body neither closed nor errored — a server runtime
3082
+ * cancels that body when the client disconnects — so a consumer that aborts the inbound
3083
+ * signal itself must cancel the body rather than keep reading it.
3084
+ *
3085
+ * @example
3086
+ * ```ts
3087
+ * import type { ProviderInterface } from '@orkestrel/agent'
3088
+ * import { RelayStream } from '@orkestrel/agent'
3089
+ *
3090
+ * export function respond(provider: ProviderInterface, signal: AbortSignal): Response {
3091
+ * return new RelayStream({ provider, request: { messages: [] }, signal }).response
3092
+ * }
3093
+ * ```
3094
+ */
3095
+ var RelayStream = class {
3096
+ #upstream = new AbortController();
3097
+ #encoder = new TextEncoder();
3098
+ #iterator;
3099
+ #signal;
3100
+ #listener;
3101
+ #response;
3102
+ #settled = false;
3103
+ constructor(options) {
3104
+ this.#signal = options.signal;
3105
+ this.#listener = this.#cancel.bind(this);
3106
+ if (this.#signal.aborted) this.#abortProvider();
3107
+ this.#iterator = options.provider.stream(options.request.messages, this.#upstream.signal, options.request.tools, options.request.options);
3108
+ this.#signal.addEventListener("abort", this.#listener, { once: true });
3109
+ this.#response = new Response(new ReadableStream({
3110
+ pull: this.#pull.bind(this),
3111
+ cancel: this.#cancel.bind(this)
3112
+ }), { headers: {
3113
+ "content-type": RELAY_CONTENT_TYPE,
3114
+ "cache-control": "no-store"
3115
+ } });
3116
+ }
3117
+ /** Exposes the pull-driven response for the provider call. */
3118
+ get response() {
3119
+ return this.#response;
3120
+ }
3121
+ async #pull(controller) {
3122
+ if (this.#settled) return;
3123
+ try {
3124
+ const step = await this.#iterator.next();
3125
+ if (this.#settled) return;
3126
+ this.#write(controller, step.done ? {
3127
+ channel: "result",
3128
+ result: step.value
3129
+ } : step.value);
3130
+ if (step.done) this.#finish(controller);
3131
+ } catch (error) {
3132
+ if (this.#settled) return;
3133
+ const frame = error instanceof ProviderAbortError ? {
3134
+ channel: "abort",
3135
+ partial: error.partial
3136
+ } : {
3137
+ channel: "error",
3138
+ message: RELAY_PROVIDER_MESSAGE
3139
+ };
3140
+ this.#write(controller, relayFrameContract.is(frame) ? frame : {
3141
+ channel: "error",
3142
+ message: RELAY_PROVIDER_MESSAGE
3143
+ });
3144
+ this.#finish(controller);
3145
+ this.#abortProvider();
3146
+ await this.#return();
3147
+ }
3148
+ }
3149
+ async #cancel() {
3150
+ if (this.#settled) return;
3151
+ this.#settled = true;
3152
+ this.#abortProvider();
3153
+ try {
3154
+ await this.#return();
3155
+ } finally {
3156
+ this.#release();
3157
+ }
3158
+ }
3159
+ async #return() {
3160
+ try {
3161
+ await this.#iterator.return({ content: "" });
3162
+ } catch {}
3163
+ }
3164
+ #write(controller, frame) {
3165
+ if (!relayFrameContract.is(frame)) throw new ProviderError("PROTOCOL", "invalid relay frame");
3166
+ controller.enqueue(this.#encoder.encode(`${JSON.stringify(frame)}\n`));
3167
+ }
3168
+ #finish(controller) {
3169
+ this.#settled = true;
3170
+ this.#release();
3171
+ controller.close();
3172
+ }
3173
+ #abortProvider() {
3174
+ this.#upstream.abort(this.#signal.reason);
3175
+ }
3176
+ #release() {
3177
+ this.#signal.removeEventListener("abort", this.#listener);
3178
+ }
3179
+ };
3180
+ //#endregion
2330
3181
  //#region src/core/scopes/Scope.ts
2331
3182
  /**
2332
- * Represents a named, immutable filter over a richer context's items — an optional
2333
- * allow-list per category (`instructions` / `tools` / `files`), each keyed by that
2334
- * category's identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
3183
+ * Represents a named, immutable filter over a richer context's items — an optional allow-list per
3184
+ * category (`instructions` / `tools` / `files`), each keyed by that category's identity (an
3185
+ * instruction's `name`, a tool's `name`, a workspace file's `path`) and read as an allow-list:
3186
+ * `undefined` lets everything pass, `[]` lets nothing pass, and a non-empty list passes the listed
3187
+ * keys alone. `narrow` composes a tighter child by set intersection.
2335
3188
  *
2336
3189
  * @remarks
2337
- * - **A category list is three-way.** `undefined` ⇒ NO constraint on that category (all
2338
- * pass); `[]` ⇒ NONE pass; a non-empty list ⇒ only the listed keys pass. The build
3190
+ * - **A category list is three-way.** `undefined` ⇒ no constraint on that category (all
3191
+ * pass); `[]` ⇒ none pass; a non-empty list ⇒ only the listed keys pass. The build
2339
3192
  * step / loop apply this through `filterAllowList`.
2340
- * - **Immutable.** The `id` is minted at construction; every supplied list is COPIED in
3193
+ * - **Immutable.** The `id` is minted at construction; every supplied list is copied in
2341
3194
  * (so a later mutation of the caller's array can't leak in), and the lists are
2342
- * `readonly`. A `Scope` is never mutated after construction — `narrow` returns a NEW
3195
+ * `readonly`. A `Scope` is never mutated after construction — `narrow` returns a new
2343
3196
  * one rather than altering this one.
2344
- * - **`narrow` is set-INTERSECTION (immutable composition).** A child scope's visible set
2345
- * per category is the intersection of THIS scope's list and the config's list — but
3197
+ * - **`narrow` is set-intersection (immutable composition).** A child scope's visible set
3198
+ * per category is the intersection of this scope's list and the config's list — but
2346
3199
  * `undefined` means "no constraint", so it acts as the universal set: intersecting
2347
3200
  * `undefined` with a list yields the list, and `undefined` with `undefined` stays
2348
- * `undefined`. Narrowing can only TIGHTEN, never widen — a key excluded by a parent
3201
+ * `undefined`. Narrowing can only tighten, never widen — a key excluded by a parent
2349
3202
  * can never be re-admitted by a child.
2350
3203
  *
2351
3204
  * @example
@@ -2385,8 +3238,9 @@ var Scope = class Scope {
2385
3238
  //#endregion
2386
3239
  //#region src/core/scopes/ScopeManager.ts
2387
3240
  /**
2388
- * Registers the named filters a richer context reuses — immutable {@link Scope}s keyed by
2389
- * their minted `id`, in insertion order.
3241
+ * Registers the named filters a richer context reuses — immutable {@link Scope}s keyed by their
3242
+ * minted `id`, in insertion order, where `create` always mints and stores rather than overwriting,
3243
+ * and an observable `emitter` reports each change.
2390
3244
  *
2391
3245
  * @remarks
2392
3246
  * - **Registry.** Scopes live in an insertion-ordered `Map` keyed by their minted `id`;
@@ -2395,11 +3249,11 @@ var Scope = class Scope {
2395
3249
  * one up, and `scopes()` lists them in insertion order. (Unlike the name-keyed
2396
3250
  * instruction registry, a scope's key is its minted `id`, so two scopes may share a
2397
3251
  * `name`; `create` therefore always adds — it never overwrites.)
2398
- * - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id
3252
+ * - **Removal.** `remove` drops one by id, or a batch — `true` only when every supplied id
2399
3253
  * was removed; `clear` empties the registry.
2400
3254
  * - **Observable.** The owned {@link emitter} ({@link ScopeManagerEventMap}) carries
2401
3255
  * `create` (the created scope) / `remove` (the id) / `clear`. Every event is emitted
2402
- * directly, strictly AFTER the map mutation completes; the emitter isolates a listener
3256
+ * directly, strictly after the map mutation completes; the emitter isolates a listener
2403
3257
  * throw and routes it to its `error` handler (the `error` option), so a buggy observer can
2404
3258
  * never corrupt a mutation.
2405
3259
  *
@@ -2459,24 +3313,25 @@ var ScopeManager = class {
2459
3313
  //#endregion
2460
3314
  //#region src/core/ThinkSplitter.ts
2461
3315
  /**
2462
- * Feeds raw content deltas through a tiny stream-stateful state machine that routes
2463
- * everything inside a `<think>…</think>` span to `thinking` and returns everything outside
2464
- * it as clean content, so a provider yields ONLY the answer and surfaces the reasoning as
2465
- * {@link import('./types.js').ProviderResult.thinking}.
3316
+ * Feeds raw content deltas through a tiny stream-stateful state machine that routes everything
3317
+ * inside a `<think>…</think>` span to `thinking` and returns everything outside it as clean
3318
+ * content, so a provider yields the answer alone and surfaces the reasoning as
3319
+ * {@link import('./types.js').ProviderResult.thinking}. A tag split across deltas is held until
3320
+ * disambiguated, `flush()` settles the stream end, and one splitter serves one stream.
2466
3321
  *
2467
3322
  * @remarks
2468
3323
  * - **Cross-chunk tags.** A tag may arrive split across wire deltas (`'<thi'` then
2469
- * `'nk>'`): any suffix of the pending text that is a strict PREFIX of a tag being
2470
- * scanned for is HELD BACK (neither surfaced nor routed) until the next delta — or
3324
+ * `'nk>'`): any suffix of the pending text that is a strict prefix of a tag being
3325
+ * scanned for is held back (neither surfaced nor routed) until the next delta — or
2471
3326
  * `flush()` — disambiguates it. A held tag prefix that never completes is real
2472
3327
  * content; a held close-tag prefix inside a span is thinking.
2473
- * - **The IMPLICIT leading open (the qwen3-template shape).** Some chat templates
2474
- * PRE-SEED `<think>` into the prompt scaffold, so the wire stream begins
2475
- * MID-REASONING and only a bare `</think>` appears. Before any tag event, a bare
2476
- * close therefore RECLASSIFIES everything surfaced so far (plus the pre-close
3328
+ * - **The implicit leading open (the qwen3-template shape).** Some chat templates
3329
+ * pre-seed `<think>` into the prompt scaffold, so the wire stream begins
3330
+ * mid-reasoning and only a bare `</think>` appears. Before any tag event, a bare
3331
+ * close therefore reclassifies everything surfaced so far (plus the pre-close
2477
3332
  * pending) as thinking — `content` is corrected retroactively (the already-returned
2478
3333
  * prefix cannot be recalled, so `content` is the authoritative accumulation). The
2479
- * rule is ONE-SHOT: after any tag event a bare `</think>` is plain text.
3334
+ * rule is one-shot: after any tag event a bare `</think>` is plain text.
2480
3335
  * - **Multiple spans** accumulate onto `thinking` in stream order. A nested-looking
2481
3336
  * `<think>` inside an open span is thinking text (no nesting is tracked — the
2482
3337
  * first `</think>` closes the span), matching how the models emit it.
@@ -2577,17 +3432,116 @@ var ThinkSplitter = class {
2577
3432
  //#endregion
2578
3433
  //#region src/core/factories.ts
2579
3434
  /**
3435
+ * Creates an authorized relay handler that validates a bounded JSON request before streaming.
3436
+ *
3437
+ * @remarks
3438
+ * Answers with the `401` status when authorization refuses or throws, the `400`
3439
+ * status when the body is missing, unreadable, or invalid, the `413` status when
3440
+ * the body fills its byte budget or the inbound read is aborted, and the `502`
3441
+ * status when the upstream provider call cannot be constructed. Refusals carry no body.
3442
+ *
3443
+ * @param options - The upstream provider, authorization decision, and optional byte budget
3444
+ * @returns A fetch-standard handler suitable for a router
3445
+ * @example Mounting the relay on your server
3446
+ * ```ts
3447
+ * import type { ProviderInterface } from '@orkestrel/agent'
3448
+ * import { createRelay } from '@orkestrel/agent'
3449
+ * import { createDispatcher } from '@orkestrel/router'
3450
+ * import { createServer } from '@orkestrel/server'
3451
+ *
3452
+ * declare const upstream: ProviderInterface // the server-side provider holding the credential
3453
+ * declare const bearer: string
3454
+ *
3455
+ * const handler = createRelay({
3456
+ * provider: upstream,
3457
+ * authorize: (request) => request.headers.get('authorization') === `Bearer ${bearer}`,
3458
+ * })
3459
+ * const dispatcher = createDispatcher({
3460
+ * routes: [{ method: 'POST', path: '/relay', handler }],
3461
+ * })
3462
+ *
3463
+ * export function serve(request: Request): Promise<Response> {
3464
+ * return dispatcher.handle(request, undefined)
3465
+ * }
3466
+ *
3467
+ * const server = createServer({ dispatcher, state: () => undefined })
3468
+ * await server.start()
3469
+ * process.on('SIGTERM', () => server.stop()) // signal cancellation, drain, then close the listener
3470
+ * ```
3471
+ */
3472
+ function createRelay(options) {
3473
+ const { provider, authorize, limit } = options;
3474
+ return async (request) => {
3475
+ try {
3476
+ if (await authorize(request) !== true) return new Response(void 0, { status: 401 });
3477
+ } catch {
3478
+ return new Response(void 0, { status: 401 });
3479
+ }
3480
+ if (request.body === null) return new Response(void 0, { status: 400 });
3481
+ let read;
3482
+ try {
3483
+ read = await readText(request.body, limit ?? 1048576, request.signal);
3484
+ } catch {
3485
+ return new Response(void 0, { status: request.signal.aborted ? 413 : 400 });
3486
+ }
3487
+ if (!read.complete) return new Response(void 0, { status: 413 });
3488
+ const parsed = parseJSONAs(read.text, providerRequestContract.is);
3489
+ if (parsed === void 0) return new Response(void 0, { status: 400 });
3490
+ try {
3491
+ return new RelayStream({
3492
+ provider,
3493
+ request: parsed,
3494
+ signal: request.signal
3495
+ }).response;
3496
+ } catch {
3497
+ return new Response(void 0, { status: 502 });
3498
+ }
3499
+ };
3500
+ }
3501
+ /**
3502
+ * Creates a provider that carries calls through a relay endpoint.
3503
+ *
3504
+ * @remarks
3505
+ * This is the browser end alone. {@link createRelay} mounts the server end, and its example
3506
+ * is the server half this one pairs with.
3507
+ *
3508
+ * @param options - The endpoint, parser factory, and HTTP call configuration
3509
+ * @returns The concrete relay provider
3510
+ * @example Reaching the relay from the browser
3511
+ * ```ts
3512
+ * import type { ProviderInterface } from '@orkestrel/agent'
3513
+ * import { createRelayProvider } from '@orkestrel/agent'
3514
+ * import { createAbort } from '@orkestrel/abort'
3515
+ * // The browser application supplies this parser dependency.
3516
+ * import { createNDJSONParser } from '@orkestrel/ndjson'
3517
+ *
3518
+ * declare const bearer: string
3519
+ * const abort = createAbort()
3520
+ * const messages = [{ id: '1', role: 'user', content: 'Say hello.' }] as const
3521
+ *
3522
+ * const browser: ProviderInterface = createRelayProvider({
3523
+ * url: 'https://app.example/relay',
3524
+ * parser: createNDJSONParser,
3525
+ * headers: () => ({ authorization: `Bearer ${bearer}` }),
3526
+ * })
3527
+ * const result = await browser.generate(messages, abort.signal) // a ProviderResult like a local provider's
3528
+ * ```
3529
+ */
3530
+ function createRelayProvider(options) {
3531
+ return new RelayProvider(options);
3532
+ }
3533
+ /**
2580
3534
  * Creates a conversation — a {@link ConversationInterface} grouping messages above a flat
2581
- * message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
3535
+ * message store it owns directly, with compaction into summarized sections, a regenerated
2582
3536
  * rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
2583
3537
  * provider-agnostic {@link ConversationSummaryHandler} seam.
2584
3538
  *
2585
3539
  * @remarks
2586
3540
  * Append turns through the conversation's own `add` (the live tail it owns); `view()` is the model input
2587
3541
  * (each section as a summary message, then the live tail). `compact()` folds the older live
2588
- * messages into a summarized {@link Section} and regenerates the rollup — it REQUIRES
3542
+ * messages into a summarized {@link Section} and regenerates the rollup — it requires
2589
3543
  * a `summarize` (omitted ⇒ `compact()` throws a `ConversationError`); `keep` retains a recent
2590
- * tail (default `DEFAULT_CONVERSATION_KEEP` — fold ALL). `rehydrate(id)` / `search(query)` read
3544
+ * tail (default `DEFAULT_CONVERSATION_KEEP` — fold all). `rehydrate(id)` / `search(query)` read
2591
3545
  * the retained originals. Observable (`emitter` — `compact` / `summary` / `rehydrate`), wired
2592
3546
  * through the reserved `on` option; the emitter isolates a listener throw and routes it to
2593
3547
  * its `error` handler (the `error` option), so it can never corrupt a compaction.
@@ -2595,20 +3549,39 @@ var ThinkSplitter = class {
2595
3549
  * @param options - Optional `id` / `on` hooks + the `summarize` seam + `keep` (see {@link ConversationOptions})
2596
3550
  * @returns A working {@link ConversationInterface}
2597
3551
  *
2598
- * @example
3552
+ * @example Conversations & compaction
2599
3553
  * ```ts
2600
3554
  * import type { ProviderInterface } from '@orkestrel/agent'
2601
3555
  * import { createConversation } from '@orkestrel/agent'
2602
3556
  *
2603
3557
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3558
+ * // The summarizer seam — built from the provider by the runtime; core stays provider-agnostic.
3559
+ * // Append the instruction as the FINAL user turn: a chat model emits nothing when the prompt
3560
+ * // ends on an assistant turn, so a leading-system instruction is unreliable.
2604
3561
  * const conversation = createConversation({
2605
- * // Append the instruction as the FINAL user turn — a chat model emits nothing when the
2606
- * // prompt ends on an assistant turn, so a leading-system instruction is unreliable.
2607
3562
  * summarize: async (messages) =>
2608
- * (await provider.generate([...messages, { id: 's', role: 'user', content: 'Summarize the conversation so far concisely.' }], AbortSignal.timeout(30_000))).content,
3563
+ * (
3564
+ * await provider.generate(
3565
+ * [
3566
+ * ...messages,
3567
+ * { id: 's', role: 'user', content: 'Summarize the conversation so far concisely.' },
3568
+ * ],
3569
+ * AbortSignal.timeout(30_000),
3570
+ * )
3571
+ * ).content,
3572
+ * keep: 2, // retain the two most recent turns verbatim on each compaction
2609
3573
  * })
2610
- * conversation.add({ role: 'user', content: 'Hello' })
2611
- * await conversation.compact() // folds the live tail into a summarized section
3574
+ * conversation.add([
3575
+ * { role: 'user', content: 'My name is Ada.' },
3576
+ * { role: 'assistant', content: 'Nice to meet you, Ada.' },
3577
+ * { role: 'user', content: 'What did I say my name was?' },
3578
+ * ])
3579
+ *
3580
+ * const section = await conversation.compact() // folds the older turns → a summarized section
3581
+ * conversation.view() // [<section summary message>, ...the retained recent tail] — the model input
3582
+ * conversation.summary // the regenerated rollup (a summary-of-summaries over all sections)
3583
+ * conversation.search('ada') // case-insensitive across sections' originals + the live tail
3584
+ * section && conversation.rehydrate(section.id) // the section's full original messages (a pure read)
2612
3585
  * ```
2613
3586
  */
2614
3587
  function createConversation(options) {
@@ -2616,21 +3589,22 @@ function createConversation(options) {
2616
3589
  }
2617
3590
  /**
2618
3591
  * Creates a conversation registry — a {@link ConversationManagerInterface} holding
2619
- * {@link ConversationInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
3592
+ * {@link ConversationInterface}s keyed by their `id`, in insertion order, with an active pointer:
2620
3593
  * the id-keyed store over the conversation layer plus the `active` / `switch` seam the context
2621
- * renders.
3594
+ * renders. `add` auto-activates the first conversation and flows the registry's default
3595
+ * `summarize` / `keep` into every conversation it creates.
2622
3596
  *
2623
3597
  * @remarks
2624
3598
  * Starts empty; `add(input?)` mints a {@link ConversationInterface} (its `id` from the input
2625
3599
  * or a random UUID), flowing the manager's default `summarize` / `keep` in unless the input
2626
3600
  * overrides them, and stores it (an already-present `id` overwrites — last write wins) — and
2627
- * AUTO-ACTIVATES the FIRST one (a registry with conversations always has one `active`); a later
3601
+ * auto-activates the first one (a registry with conversations always has one `active`); a later
2628
3602
  * `add` leaves `active` unchanged. `switch(id)` re-points `active` (an unknown `id` returns
2629
3603
  * `undefined`, leaving `active` unchanged — lenient, never throws); `conversation(id)` /
2630
3604
  * `conversations()` look up; `remove` (one or a batch) reports `true` only when every supplied id
2631
- * was removed AND clears `active` if it was a removed one; `clear` empties it and clears `active`.
3605
+ * was removed and clears `active` if it was a removed one; `clear` empties it and clears `active`.
2632
3606
  * Event-free
2633
- * (each conversation owns its own observable `emitter`). A conversation created with NEITHER a
3607
+ * (each conversation owns its own observable `emitter`). A conversation created with neither a
2634
3608
  * manager default nor a per-`add` `summarize` cannot `compact` (it throws a `ConversationError`).
2635
3609
  *
2636
3610
  * @param options - Optional default `summarize` / `keep` (see {@link ConversationManagerOptions})
@@ -2651,7 +3625,7 @@ function createConversationManager(options) {
2651
3625
  /**
2652
3626
  * Creates the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
2653
3627
  * process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
2654
- * id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
3628
+ * id, the default backing for the durable {@link ConversationManagerInterface.open} /
2655
3629
  * {@link ConversationManagerInterface.save} seam. The exact twin of
2656
3630
  * {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
2657
3631
  *
@@ -2659,10 +3633,10 @@ function createConversationManager(options) {
2659
3633
  * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
2660
3634
  * the structural twin of {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
2661
3635
  * `get` / `set` / `delete` are async (the
2662
- * same shape a durable backend fits); UNLIKE a session store there is NO idle-TTL / eviction — a
3636
+ * same shape a durable backend fits); unlike a session store there is no idle-TTL / eviction — a
2663
3637
  * persisted conversation lives until an explicit `delete`. Its driver-pluggable twin is
2664
3638
  * {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
2665
- * table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB driver, and it swaps in WITHOUT
3639
+ * table) — for a durable store pass it a JSON / SQLite / IndexedDB driver, and it swaps in without
2666
3640
  * touching the manager or the conversation. Hydration stays a manager concern: read a snapshot back
2667
3641
  * and rebuild the live conversation through the `snapshot` option (re-supplying the live
2668
3642
  * `summarize` / `keep`).
@@ -2684,23 +3658,24 @@ function createMemoryConversationStore() {
2684
3658
  return new MemoryConversationStore();
2685
3659
  }
2686
3660
  /**
2687
- * Creates a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
2688
- * driver-pluggable backing for the conversation persistence seam, the opt-in twin of
3661
+ * Creates a {@link DatabaseConversationStore} over any {@link DriverInterface}, defaulting to
3662
+ * `createMemoryDriver()` — the durable, driver-pluggable backing for the conversation persistence
3663
+ * seam, holding each snapshot as one opaque JSON column and standing as the opt-in twin of
2689
3664
  * {@link createMemoryConversationStore}. The exact twin of
2690
3665
  * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
2691
3666
  *
2692
3667
  * @remarks
2693
3668
  * Builds a one-table database (`conversations`, keyed by `id`) over the supplied driver, the snapshot
2694
- * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
3669
+ * held as one opaque JSON column — the column map is `{ id; snapshot }` where `snapshot` is a
2695
3670
  * `rawShape` (a JSON blob), exactly as
2696
3671
  * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
2697
- * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
2698
- * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
2699
- * {@link import('./validators.js').isConversationSnapshot}). The `driver` DEFAULTS to
2700
- * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
3672
+ * snapshot is already a complete, self-contained, pure-JSON payload, so storing it whole is lossless
3673
+ * and keeps the row type flat (the column reads back as `unknown`, narrowed on `get` by
3674
+ * {@link import('./validators.js').isConversationSnapshot}). The `driver` defaults to
3675
+ * {@link createMemoryDriver}, so the store also works in memory out of the box; pass a server
2701
3676
  * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
2702
3677
  * the durability is the driver's job, the store engine is shared. It swaps in behind
2703
- * {@link ConversationStoreInterface} WITHOUT touching the manager or the conversation.
3678
+ * {@link ConversationStoreInterface} without touching the manager or the conversation.
2704
3679
  *
2705
3680
  * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
2706
3681
  * @returns A {@link ConversationStoreInterface} over the driver
@@ -2755,7 +3730,7 @@ function createInstruction(input) {
2755
3730
  * immutable instructions keyed by `name`, listed by descending `priority`.
2756
3731
  *
2757
3732
  * @remarks
2758
- * Starts empty; `add` (one or a batch) MINTS each `id` and OVERWRITES a same-name
3733
+ * Starts empty; `add` (one or a batch) mints each `id` and overwrites a same-name
2759
3734
  * instruction (last write wins); `instructions()` lists them sorted by descending
2760
3735
  * `priority` (stable for ties); `open` / `render` are the build contract a richer
2761
3736
  * context renders an instructions block with; `remove` (one or a batch) reports `true` only
@@ -2786,9 +3761,9 @@ function createInstructionManager(options) {
2786
3761
  * per-category allow-lists, the `id` minted at construction.
2787
3762
  *
2788
3763
  * @remarks
2789
- * Each list is THREE-WAY: `undefined` ⇒ NO constraint on that category (all pass), `[]` ⇒
2790
- * NONE pass, a non-empty list ⇒ only the listed keys pass. `narrow(config)` composes a
2791
- * tighter child by set-INTERSECTION (an `undefined` side imposing no constraint). Stored
3764
+ * Each list is three-way: `undefined` ⇒ no constraint on that category (all pass), `[]` ⇒
3765
+ * none pass, a non-empty list ⇒ only the listed keys pass. `narrow(config)` composes a
3766
+ * tighter child by set-intersection (an `undefined` side imposing no constraint). Stored
2792
3767
  * immutable — never mutated after creation (`narrow` returns a new scope).
2793
3768
  *
2794
3769
  * @param input - `name` (required) and the optional `instructions` / `tools` / `files`
@@ -2834,19 +3809,20 @@ function createScopeManager(options) {
2834
3809
  return new ScopeManager(options);
2835
3810
  }
2836
3811
  /**
2837
- * Creates a richer turn context — an {@link AgentContextInterface} assembling a provider
2838
- * request from the optional system prompt, the instruction registry, the workspace registry,
2839
- * the conversation store, the tool registry, and the active scope.
3812
+ * Creates a richer turn context — an {@link AgentContextInterface} assembling a provider request
3813
+ * from the optional system prompt, the instruction registry, the workspace registry (the only
3814
+ * document channel), the conversation registry that is its `messages` source, the tool registry,
3815
+ * and the active scope, which `build()` folds into the next turn's input.
2840
3816
  *
2841
3817
  * @remarks
2842
3818
  * `system` is the optional system prompt; `tools` / `instructions` / `workspaces` are pre-built
2843
- * managers to reuse (empty ones are created when omitted, so `context.workspaces` is ALWAYS
3819
+ * managers to reuse (empty ones are created when omitted, so `context.workspaces` is always
2844
3820
  * present); `scope` is the initial active filter (`undefined` ⇒ no filtering, changeable afterwards
2845
3821
  * through `context.apply(...)`). The `messages` store is always fresh. `build()` folds the scoped
2846
- * instructions — PLUS the ACTIVE workspace's scope-filtered text files (fenced) — into ONE leading
3822
+ * instructions — plus the active workspace's scope-filtered text files (fenced) — into one leading
2847
3823
  * `system` message and appends the scoped conversation (attaching the active workspace's
2848
3824
  * scope-filtered image files' `base64` payload to the last user message), built fresh each call; the active
2849
- * workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (through
3825
+ * workspace is the sole document/image context. Tools are advertised structurally (through
2850
3826
  * `tools.definitions()`, scope-filtered by the loop), never serialized into the prompt.
2851
3827
  *
2852
3828
  * @param options - Optional `system` / `tools` / `instructions` / `workspaces` / `scope`
@@ -2873,10 +3849,10 @@ function createAgentContext(options) {
2873
3849
  * `generate` and a live `stream`.
2874
3850
  *
2875
3851
  * @remarks
2876
- * One private loop drives the turn; `generate` DRAINS the same stream `stream`
3852
+ * One private loop drives the turn; `generate` drains the same stream `stream`
2877
3853
  * exposes, so they can never diverge. Each turn is bounded by one cancel folded from
2878
3854
  * `signal` + `timeout` + `budget` (through `AbortSignal.any`) — any trip (or `abort()`)
2879
- * commits a PARTIAL result (the stream's `result` RESOLVES on a cancel, rejects only
3855
+ * commits a partial result (the stream's `result` resolves on a cancel, rejects only
2880
3856
  * on a genuine provider / tool error). The `scheduler` paces between turns; tool
2881
3857
  * iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Tools are advertised
2882
3858
  * structurally through `context.tools.definitions()`. Two observation surfaces: the
@@ -2912,18 +3888,19 @@ function createAgent(provider, options) {
2912
3888
  return new Agent(provider, options);
2913
3889
  }
2914
3890
  /**
2915
- * Creates a stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
2916
- * splits a thinking model's in-content `<think>…</think>` reasoning spans away from
2917
- * the answer, delta by delta, so a provider yields ONLY clean content and surfaces the
2918
- * accumulated reasoning as {@link import('./types.js').ProviderResult.thinking}.
3891
+ * Creates a fresh stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
3892
+ * splits a thinking model's in-content `<think>…</think>` reasoning spans away from the answer,
3893
+ * delta by delta, so a provider yields clean content alone and surfaces the accumulated
3894
+ * reasoning as {@link import('./types.js').ProviderResult.thinking}. One splitter serves one
3895
+ * stream.
2919
3896
  *
2920
3897
  * @remarks
2921
3898
  * Feed each raw wire delta through `split(delta)` (it returns the clean content to
2922
3899
  * surface — possibly `''` mid-think) and settle the stream end with `flush()` (a held
2923
- * partial open tag that never completed returns as final content; an UNCLOSED think
2924
- * span lands on `thinking`). Tags split ACROSS deltas are held back until
3900
+ * partial open tag that never completed returns as final content; an unclosed think
3901
+ * span lands on `thinking`). Tags split across deltas are held back until
2925
3902
  * disambiguated, multiple spans accumulate in order, and a nested-looking `<think>`
2926
- * inside an open span is thinking text. One splitter serves ONE stream — create
3903
+ * inside an open span is thinking text. One splitter serves one stream — create
2927
3904
  * a fresh one per provider call.
2928
3905
  *
2929
3906
  * @returns A fresh {@link ThinkSplitterInterface} (state empty, outside any span)
@@ -2950,7 +3927,7 @@ function createThinkSplitter() {
2950
3927
  * Write and read are decoupled, so the producer never waits for a consumer: an agent's eager
2951
3928
  * pump writes each chunk into one, which is why a run's `result` settles whether or not the
2952
3929
  * live events are drained. A value pushed at an already-parked consumer is delivered, buffered
2953
- * values are yielded before the end is reported, and the FIRST failure wins.
3930
+ * values are yielded before the end is reported, and the first failure wins.
2954
3931
  *
2955
3932
  * @typeParam T - The value type the channel carries
2956
3933
  * @returns A fresh, empty {@link ChannelInterface}
@@ -2976,11 +3953,11 @@ function createChannel() {
2976
3953
  * to the configured default when none match.
2977
3954
  *
2978
3955
  * @remarks
2979
- * `rules` are evaluated in order — the FIRST whose `match` is true decides (a matched
2980
- * rule ALLOWS unless its `allowed` is explicitly `false`). When no rule matches, the
3956
+ * `rules` are evaluated in order — the first whose `match` is true decides (a matched
3957
+ * rule allows unless its `allowed` is explicitly `false`). When no rule matches, the
2981
3958
  * `fallback` decides; it defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }`
2982
- * (allow-unmatched — a rules list of denials acts as a DENYLIST). Pass an
2983
- * `allowed: false` `fallback` to flip the gate to deny-by-default (an ALLOWLIST). Wire
3959
+ * (allow-unmatched — a rules list of denials acts as a denylist). Pass an
3960
+ * `allowed: false` `fallback` to flip the gate to deny-by-default (an allowlist). Wire
2984
3961
  * the result into `createAgent` through `AgentOptions.authority`: a denied call is fed back
2985
3962
  * to the model as a denial `ToolResult` (not executed, no budget cost), so the model
2986
3963
  * can react. Synchronous — `evaluate` returns the verdict directly.
@@ -3010,7 +3987,7 @@ function createAuthority(options) {
3010
3987
  *
3011
3988
  * @remarks
3012
3989
  * `providers` is required; `tools` / `authorities` / `schedulers` are optional pools.
3013
- * The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW an
3990
+ * The accessors (`provider` / `tool` / `authority` / `scheduler`) throw an
3014
3991
  * {@link import('./errors.js').AgentError} carrying `code: 'REGISTRY'` and the message
3015
3992
  * `unknown <category>: <name>` on an unregistered name — a misconfigured or crash-restored
3016
3993
  * job fails loudly rather than running with a missing dependency. `build(input, signal)`
@@ -3040,7 +4017,7 @@ function createAgentRegistry(options) {
3040
4017
  }
3041
4018
  /**
3042
4019
  * Creates a durable, bounded-concurrency agent-job queue — a {@link QueueInterface} over
3043
- * serializable {@link AgentJobInput}s that COMPOSES `createQueue`: each job is rehydrated
4020
+ * serializable {@link AgentJobInput}s that composes `createQueue`: each job is rehydrated
3044
4021
  * through the `registry` into a live {@link AgentInterface}, run to its {@link AgentResult},
3045
4022
  * and subjected to the partial-as-configurable-failure policy.
3046
4023
  *
@@ -3053,9 +4030,9 @@ function createAgentRegistry(options) {
3053
4030
  * (for example `createMemoryQueueStore` / `createDatabaseQueueStore`) persists outstanding
3054
4031
  * jobs; `restore()` re-enqueues them after a restart and the `registry` rehydrates the
3055
4032
  * live pieces from the names — so a job survives a crash.
3056
- * - **Partial policy.** A partial result THROWS an
4033
+ * - **Partial policy.** A partial result throws an
3057
4034
  * {@link import('./errors.js').AgentJobError} by default, so a job cancelled by its
3058
- * attempt deadline / a queue abort RETRIES while attempts remain; `partial: true`
4035
+ * attempt deadline / a queue abort retries while attempts remain; `partial: true`
3059
4036
  * resolves the partial as success instead.
3060
4037
  * - **Cancellation threads through.** The handler passes `context.signal` into
3061
4038
  * `registry.build`, so a queue `abort()` or a per-attempt timeout cancels the in-flight
@@ -3088,21 +4065,21 @@ function createAgentQueue(options) {
3088
4065
  }
3089
4066
  /**
3090
4067
  * Creates an agent-job runner — a {@link RunnerInterface} over serializable
3091
- * {@link AgentJobInput}s that COMPOSES `createRunner` (one-shot, ordered, fail-fast), each
3092
- * unit rehydrated through the `registry` and subjected to the partial policy. The runner
3093
- * enables **sub-agent fan-out**: a parent job's handler can `controller.spawn(childJob)`.
4068
+ * {@link AgentJobInput}s that composes `createRunner` (one-shot, ordered, fail-fast), each unit
4069
+ * rehydrated through the `registry` and subjected to the partial policy. The runner also carries
4070
+ * sub-agent fan-out: a parent job's handler can `controller.spawn(childJob)`.
3094
4071
  *
3095
4072
  * @remarks
3096
4073
  * - **Composes the substrate (no new engine).** Bounded `concurrency`, `retries`, the
3097
4074
  * per-attempt `timeout`, ordered results, and fail-fast are all the backing Runner's;
3098
4075
  * the handler adds only rehydration + the partial policy.
3099
4076
  * - **Sub-agent fan-out.** Each unit's handler receives a `ControllerInterface` whose
3100
- * `spawn(childJob)` launches a CHILD agent job through the same bounded queue (the
4077
+ * `spawn(childJob)` launches a child agent job through the same bounded queue (the
3101
4078
  * child's result joins the run after the declared units, in spawn order). On a bounded
3102
- * runner, FAN OUT and return — do NOT inline-`await` a spawn from within the handler (a
4079
+ * runner, fan out and return — do not inline-`await` a spawn from within the handler (a
3103
4080
  * slot-holding handler awaiting its own spawn can deadlock; see `ControllerInterface`).
3104
4081
  * - **Partial policy + cancellation.** Same as `createAgentQueue`: a partial result
3105
- * THROWS by default (the run's fail-fast engages), `partial: true` resolves it; the
4082
+ * throws by default (the run's fail-fast engages), `partial: true` resolves it; the
3106
4083
  * handler threads `controller.signal` into `registry.build`, so a runner abort / a
3107
4084
  * per-attempt timeout cancels the agent.
3108
4085
  *
@@ -3132,6 +4109,6 @@ function createAgentRunner(options) {
3132
4109
  });
3133
4110
  }
3134
4111
  //#endregion
3135
- export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildRecapMessage, buildSummaryMessage, collectImageData, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, denyCall, estimateMessages, estimateTokens, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isSection, joinThinking, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage };
4112
+ export { Agent, AgentContext, AgentError, AgentJobError, AgentProvider, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DEFAULT_PROVIDER_TIMEOUT, DEFAULT_RELAY_LIMIT, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, INVALID_RELAY_STATUS, Instruction, InstructionManager, MAX_ERROR_BODY_LENGTH, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, OVERSIZED_RELAY_STATUS, ProviderAbortError, ProviderError, RELAY_CONTENT_TYPE, RELAY_PROVIDER_MESSAGE, RelayProvider, RelayStream, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, UNAUTHORIZED_RELAY_STATUS, UPSTREAM_RELAY_STATUS, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildProviderResult, buildRecapMessage, buildSummaryMessage, collectImageData, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createRelay, createRelayProvider, createScope, createScopeManager, createThinkSplitter, denyCall, estimateMessages, estimateTokens, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isProviderError, isSection, joinThinking, messageContract, messageShape, providerRequestContract, providerRequestShape, providerResultContract, providerResultShape, readChunks, readText, relayFrameContract, relayFrameShape, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage, toolCallShape };
3136
4113
 
3137
4114
  //# sourceMappingURL=index.js.map