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