@orkestrel/agent 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { attempt, isArray, isBoolean, isFiniteNumber, isObject, isRecord, isString, parseJSONValue, rawShape, stringShape } from "@orkestrel/contract";
2
2
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
3
  import { createQueue } from "@orkestrel/queue";
4
- import { createDeferred, createRunner } from "@orkestrel/workflow";
4
+ import { createRunner, errorToMessage } from "@orkestrel/workflow";
5
5
  import { createAbort } from "@orkestrel/abort";
6
6
  import { createTimeout } from "@orkestrel/timeout";
7
7
  import { Emitter } from "@orkestrel/emitter";
@@ -10,14 +10,14 @@ import { WorkspaceManager, isBinary, isText } from "@orkestrel/workspace";
10
10
  import { createTokenBudget } from "@orkestrel/budget";
11
11
  //#region src/core/constants.ts
12
12
  /**
13
- * The default cap on an {@link AgentInterface} turn's tool iterations — the maximum
13
+ * Caps an {@link AgentInterface} turn's tool iterations by default — the maximum
14
14
  * number of context → provider → tools cycles before the loop stops, so a model that
15
- * keeps requesting tools can never loop forever. Overridable per agent via
15
+ * keeps requesting tools can never loop forever. Overridable per agent through
16
16
  * `AgentOptions.limit`.
17
17
  */
18
18
  var DEFAULT_AGENT_LIMIT = 10;
19
19
  /**
20
- * The zone an {@link AuthorityInterface}'s default fallback {@link AuthorityDecision}
20
+ * Names the zone an {@link AuthorityInterface}'s default fallback {@link AuthorityDecision}
21
21
  * carries — the classification for a tool call that matched no rule. Paired with the
22
22
  * default `allowed: true` fallback, an unmatched call is allowed under this zone, so a
23
23
  * rules list of denials acts as a denylist; a caller wanting deny-by-default supplies
@@ -25,16 +25,16 @@ var DEFAULT_AGENT_LIMIT = 10;
25
25
  */
26
26
  var DEFAULT_AUTHORITY_ZONE = "default";
27
27
  /**
28
- * The default number of recent live messages a {@link ConversationInterface}'s `compact()`
28
+ * Sets the default number of recent live messages a {@link ConversationInterface}'s `compact()`
29
29
  * RETAINS verbatim — `0`, so a manual `compact()` folds ALL of the current live messages
30
30
  * into one summarized section (no tail kept). A caller retains a recent tail by passing
31
31
  * `keep` (on {@link ConversationOptions}, {@link ConversationManagerOptions}, or per-fold
32
- * via {@link CompactOptions}), folding only the older `count - keep` messages and leaving
32
+ * through {@link CompactOptions}), folding only the older `count - keep` messages and leaving
33
33
  * the most recent `keep` live for the next turn. Overridable everywhere `keep` is accepted.
34
34
  */
35
35
  var DEFAULT_CONVERSATION_KEEP = 0;
36
36
  /**
37
- * The framing label a {@link ConversationInterface}'s `view()` prefixes onto each compacted
37
+ * Names the framing label a {@link ConversationInterface}'s `view()` prefixes onto each compacted
38
38
  * section's summary so a small model reads it as a CONDENSED RECAP of earlier turns — not a
39
39
  * literal assistant turn to echo or treat as the live answer.
40
40
  *
@@ -49,40 +49,40 @@ var DEFAULT_CONVERSATION_KEEP = 0;
49
49
  */
50
50
  var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
51
51
  /**
52
- * The opening tag a {@link import('./ThinkSplitter.js').ThinkSplitter} recognizes as the start of
52
+ * Names the opening tag a {@link import('./ThinkSplitter.js').ThinkSplitter} recognizes as the start of
53
53
  * an in-content reasoning span — the de-facto wire convention thinking models (qwen3, DeepSeek-R1
54
54
  * family) emit their chain-of-thought under when a daemon renders it inline instead of on a
55
55
  * separate wire field. Paired with {@link THINK_CLOSE}.
56
56
  */
57
57
  var THINK_OPEN = "<think>";
58
58
  /**
59
- * The closing tag that ends a {@link THINK_OPEN} reasoning span. A span the stream never closes
59
+ * Names the closing tag that ends a {@link THINK_OPEN} reasoning span. A span the stream never closes
60
60
  * (the model was cut off mid-reasoning) is treated as thinking to its end —
61
61
  * {@link import('./types.js').ThinkSplitterInterface.flush} settles it.
62
62
  */
63
63
  var THINK_CLOSE = "</think>";
64
64
  /**
65
- * The section header {@link import('./AgentContext.js').AgentContext}'s `build()` renders the
65
+ * Names the section header {@link import('./AgentContext.js').AgentContext}'s `build()` renders the
66
66
  * ACTIVE workspace's TEXT files under — the leading line of the dedicated workspace block in the
67
67
  * system message, the carrier-split counterpart to the documents / images section headers.
68
68
  *
69
69
  * @remarks
70
70
  * `build()` OWNS the workspace render (a `Workspace` / `WorkspaceManager` stays file-focused — no
71
- * `description` / `framing` getters), so this header lives here as the agents module's one
71
+ * `open` / `format` getters), so this header lives here as the agents module's one
72
72
  * workspace-section framing constant rather than on a manager. Each workspace text file renders
73
73
  * beneath it as a fenced `` File: <path>\n```<language>\n<text>\n``` `` block — the SAME framing
74
- * the documents section uses — placed just after the documents section in the system block.
74
+ * the documents section uses — placed immediately after the documents section in the system block.
75
75
  */
76
76
  var WORKSPACE_SECTION_HEADER = "## Workspace";
77
77
  /**
78
- * The estimated per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
78
+ * Estimates the per-message role/framing overhead {@link import('./helpers.js').estimateMessages}
79
79
  * adds on top of a message's content estimate — accounts for the fixed wire framing every
80
80
  * conversation turn carries (its role tag, delimiters) that {@link import('./helpers.js').estimateTokens}'s
81
81
  * content-only heuristic does not otherwise capture.
82
82
  */
83
83
  var MESSAGE_TOKEN_OVERHEAD = 4;
84
84
  /**
85
- * The coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
85
+ * Names the coarse, deliberately-approximate per-image token cost {@link import('./helpers.js').estimateMessages}
86
86
  * charges for each attached image.
87
87
  *
88
88
  * @remarks
@@ -95,16 +95,19 @@ var IMAGE_TOKEN_ESTIMATE = 512;
95
95
  //#endregion
96
96
  //#region src/core/errors.ts
97
97
  /**
98
- * Thrown by a {@link ProviderInterface}'s `stream` when its bound signal aborts
99
- * mid-flight — carries the {@link ProviderResult} assembled from whatever streamed
100
- * before the cancel.
98
+ * Reports a provider stream cancelled mid-flight by its bound signal — thrown by a
99
+ * {@link ProviderInterface}'s `stream`, carrying the {@link ProviderResult} assembled
100
+ * from whatever streamed before the cancel.
101
101
  *
102
102
  * @remarks
103
103
  * Lets a caller recover the partial content (and any tool calls / usage seen so far)
104
104
  * on cancellation: `catch` the throw, narrow with {@link isProviderAbortError}, and
105
- * read `partial`.
105
+ * read `partial`. `code` is the machine-readable condition (`'ABORT'` — the only one this
106
+ * error reports), so a `catch` branches on it rather than on the message string.
106
107
  */
107
108
  var ProviderAbortError = class extends Error {
109
+ /** Names the machine-readable condition — `'ABORT'`: a stream cancelled mid-flight. */
110
+ code = "ABORT";
108
111
  partial;
109
112
  constructor(partial) {
110
113
  super("provider stream aborted");
@@ -113,10 +116,10 @@ var ProviderAbortError = class extends Error {
113
116
  }
114
117
  };
115
118
  /**
116
- * Narrow an unknown caught value to a {@link ProviderAbortError}.
119
+ * Narrows an unknown caught value to a {@link ProviderAbortError}.
117
120
  *
118
121
  * @param value - The value to test (typically a `catch` binding)
119
- * @returns `true` when `value` is a {@link ProviderAbortError}
122
+ * @returns True if `value` is a {@link ProviderAbortError}; false otherwise
120
123
  *
121
124
  * @example
122
125
  * ```ts
@@ -131,9 +134,9 @@ function isProviderAbortError(value) {
131
134
  return value instanceof ProviderAbortError;
132
135
  }
133
136
  /**
134
- * Thrown by an agent-job handler (a `createAgentQueue` / `createAgentRunner` job) when
135
- * its {@link AgentInterface} run ended {@link AgentResult.partial} and the job's
136
- * `allowPartial` policy is `false` (the default) — carries the partial
137
+ * Reports an {@link AgentInterface} run that ended {@link AgentResult.partial} under a
138
+ * `partial` policy of `false` (the default) — thrown by an agent-job handler (a
139
+ * `createAgentQueue` / `createAgentRunner` job), carrying the partial
137
140
  * {@link AgentResult} so the failure stays inspectable.
138
141
  *
139
142
  * @remarks
@@ -141,12 +144,16 @@ function isProviderAbortError(value) {
141
144
  * runner abort threaded in, a `timeout` deadline, or an exhausted token `budget`) rather
142
145
  * than finishing naturally. For a durable JOB that is a failure by default: throwing this
143
146
  * lets the Queue's retries re-run the job and a Runner's fail-fast abort its siblings.
144
- * Set `allowPartial: true` (see `AgentQueueOptions` / `AgentRunnerOptions`) to treat a
147
+ * Set `partial: true` (see `AgentQueueOptions` / `AgentRunnerOptions`) to treat a
145
148
  * partial as success instead, in which case this is never thrown. Narrow a caught value
146
- * with {@link isAgentJobError} to read `partial`.
149
+ * with {@link isAgentJobError} to read `partial`. `code` is the machine-readable condition
150
+ * (`'PARTIAL'` — the only one this error reports), so a `catch` branches on it rather than on
151
+ * the message string.
147
152
  */
148
153
  var AgentJobError = class extends Error {
149
- /** The partial {@link AgentResult} the cancelled job produced. */
154
+ /** Names the machine-readable condition — `'PARTIAL'`: a job that ended partial under a disallowing policy. */
155
+ code = "PARTIAL";
156
+ /** Holds the partial {@link AgentResult} the cancelled job produced. */
150
157
  partial;
151
158
  constructor(message, partial) {
152
159
  super(message);
@@ -155,10 +162,10 @@ var AgentJobError = class extends Error {
155
162
  }
156
163
  };
157
164
  /**
158
- * Narrow an unknown caught value to an {@link AgentJobError}.
165
+ * Narrows an unknown caught value to an {@link AgentJobError}.
159
166
  *
160
167
  * @param value - The value to test (typically a `catch` binding or a rejected enqueue)
161
- * @returns `true` when `value` is an {@link AgentJobError}
168
+ * @returns True if `value` is an {@link AgentJobError}; false otherwise
162
169
  *
163
170
  * @example
164
171
  * ```ts
@@ -173,22 +180,22 @@ function isAgentJobError(value) {
173
180
  return value instanceof AgentJobError;
174
181
  }
175
182
  /**
176
- * Thrown by a {@link ConversationInterface}'s `compact()` when the conversation has no
177
- * {@link ConversationSummarizer} to fold its messages with, or when its `sections` cap is
178
- * structurally invalid — carries a machine-readable `code`.
183
+ * Reports a conversation with no {@link ConversationSummaryHandler} to fold its messages
184
+ * with, or with a structurally invalid `sections` cap — thrown by a
185
+ * {@link ConversationInterface}'s `compact()`, carrying a machine-readable `code`.
179
186
  *
180
187
  * @remarks
181
188
  * Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
182
189
  * regenerates the rollup); a conversation created without one can still store + `view()` its
183
- * live tail, but a `compact()` is a programmer error (§12) and throws this with `'SUMMARIZER'`.
184
- * A `sections` cap (§F2, on {@link import('./types.js').ConversationOptions} /
190
+ * live tail, but a `compact()` is a programmer error and throws this with `'SUMMARIZER'`.
191
+ * A `sections` cap (on {@link import('./types.js').ConversationOptions} /
185
192
  * {@link import('./types.js').ConversationManagerOptions} /
186
193
  * {@link import('./types.js').CompactOptions}) must be `>= 1` — a sub-1 cap is a programmer
187
194
  * error and throws this with `'SECTIONS'`. Narrow a caught value with
188
195
  * {@link isConversationError} and branch on `error.code`.
189
196
  */
190
197
  var ConversationError = class extends Error {
191
- /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap. */
198
+ /** Names the machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer; `'SECTIONS'`: a sub-1 `sections` cap. */
192
199
  code;
193
200
  constructor(code, message) {
194
201
  super(message);
@@ -197,10 +204,10 @@ var ConversationError = class extends Error {
197
204
  }
198
205
  };
199
206
  /**
200
- * Narrow an unknown caught value to a {@link ConversationError}.
207
+ * Narrows an unknown caught value to a {@link ConversationError}.
201
208
  *
202
209
  * @param value - The value to test (typically a `catch` binding)
203
- * @returns `true` when `value` is a {@link ConversationError}
210
+ * @returns True if `value` is a {@link ConversationError}; false otherwise
204
211
  *
205
212
  * @example
206
213
  * ```ts
@@ -215,19 +222,22 @@ function isConversationError(value) {
215
222
  return value instanceof ConversationError;
216
223
  }
217
224
  /**
218
- * Thrown synchronously by an {@link AgentInterface}'s `stream()` when a concurrent run would
219
- * corrupt SHARED per-agent accounting — carries a machine-readable `code`.
225
+ * Reports a concurrent run that would corrupt SHARED per-agent accounting, or a rehydration
226
+ * name absent from its registry pool — thrown synchronously by an {@link AgentInterface}'s
227
+ * `stream()` and by an {@link AgentRegistryInterface}'s accessors, carrying a machine-readable
228
+ * `code`.
220
229
  *
221
230
  * @remarks
222
- * A run already in flight on the same agent, PLUS a construction-level `window` (a shared
223
- * context budget) or a construction-level `budget` with no per-run override (a shared cost
224
- * budget), means a second concurrent `stream()` would race its charges against the same
225
- * shared instance — corrupting the accounting. `code` is `'CONCURRENCY'` (the only condition
226
- * so far). Use separate agents, or per-run `budget` overrides with no `window`, for genuinely
227
- * concurrent runs. Narrow a caught value with {@link isAgentError} and branch on `error.code`.
231
+ * `'CONCURRENCY'` reports a run already in flight on the same agent, PLUS a construction-level
232
+ * `window` (a shared context budget) or a construction-level `budget` with no per-run override
233
+ * (a shared cost budget) — a second concurrent `stream()` would race its charges against the
234
+ * same shared instance, corrupting the accounting. Use separate agents, or per-run `budget`
235
+ * overrides with no `window`, for genuinely concurrent runs. `'REGISTRY'` reports a rehydration
236
+ * name absent from its registry pool, on `provider` / `tool` / `authority` / `scheduler` /
237
+ * `build`. Narrow a caught value with {@link isAgentError} and branch on `error.code`.
228
238
  */
229
239
  var AgentError = class extends Error {
230
- /** The machine-readable condition — `'CONCURRENCY'`: a concurrent run on a shared accounting agent. */
240
+ /** Names the machine-readable condition — `'CONCURRENCY'`: a concurrent run on a shared accounting agent; `'REGISTRY'`: a rehydration name absent from its registry pool. */
231
241
  code;
232
242
  constructor(code, message) {
233
243
  super(message);
@@ -236,10 +246,10 @@ var AgentError = class extends Error {
236
246
  }
237
247
  };
238
248
  /**
239
- * Narrow an unknown caught value to an {@link AgentError}.
249
+ * Narrows an unknown caught value to an {@link AgentError}.
240
250
  *
241
251
  * @param value - The value to test (typically a `catch` binding)
242
- * @returns `true` when `value` is an {@link AgentError}
252
+ * @returns True if `value` is an {@link AgentError}; false otherwise
243
253
  *
244
254
  * @example
245
255
  * ```ts
@@ -254,335 +264,9 @@ function isAgentError(value) {
254
264
  return value instanceof AgentError;
255
265
  }
256
266
  //#endregion
257
- //#region src/core/conversations/Conversation.ts
258
- /**
259
- * A conversation grouping messages ABOVE a flat message store — a live uncompacted tail it
260
- * OWNS DIRECTLY plus compacted, summarized {@link SectionInterface}s and a regenerated rollup
261
- * `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
262
- * {@link ConversationSummarizer} seam (so `core` never imports a provider).
263
- *
264
- * @remarks
265
- * - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
266
- * insertion-ordered `Map` of immutable {@link MessageInterface}s keyed by their minted id
267
- * (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
268
- * `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
269
- * per-value manager). `#sections` are the compacted history (oldest → newest), each a
270
- * summarized slice that RETAINS its originals. `#summary` is the rollup (a
271
- * summary-of-summaries over all sections), regenerated on each compaction (`undefined`
272
- * until the first).
273
- * - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
274
- * prior-context recap — keyed by the section's stable `id`), then the live messages
275
- * verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
276
- * carries the per-section summaries, which ARE the compaction benefit.
277
- * - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
278
- * (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
279
- * rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
280
- * `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
281
- * THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
282
- * per compaction.
283
- * - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
284
- * messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (v1 never
285
- * auto-reinserts). `search` is a case-insensitive substring scan of `content` across ALL
286
- * messages (every section's originals + the live tail).
287
- * - **Observable (§13).** The owned {@link emitter} ({@link ConversationEventMap}) carries
288
- * `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
289
- * the emitter isolates a listener throw and routes it to its `error` handler (the `error`
290
- * option), so a buggy observer can never corrupt a compaction.
291
- *
292
- * @example
293
- * ```ts
294
- * const conversation = new Conversation({ summarize: async (m) => `recap of ${m.length}` })
295
- * conversation.add([
296
- * { role: 'user', content: 'Hello' },
297
- * { role: 'assistant', content: 'Hi there' },
298
- * ])
299
- * const section = await conversation.compact() // folds both into one summarized section
300
- * conversation.view() // [{ role: 'assistant', content: 'recap of 2' }] — the live tail is empty
301
- * conversation.summary // 'recap of 1' — the rollup over the one section
302
- * ```
303
- */
304
- var Conversation = class {
305
- #id;
306
- #emitter;
307
- #summarize;
308
- #keep;
309
- #cap;
310
- #sections = [];
311
- #summary;
312
- #messages = /* @__PURE__ */ new Map();
313
- constructor(options, seed) {
314
- this.#id = seed?.id ?? options?.id ?? crypto.randomUUID();
315
- this.#emitter = new Emitter({
316
- ...options?.on === void 0 ? {} : { on: options.on },
317
- ...options?.error === void 0 ? {} : { error: options.error }
318
- });
319
- this.#summarize = options?.summarize;
320
- this.#keep = options?.keep ?? 0;
321
- if (options?.sections !== void 0 && options.sections < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
322
- this.#cap = options?.sections;
323
- if (seed) {
324
- this.#summary = seed.summary;
325
- for (const section of seed.sections) this.#sections.push(section);
326
- for (const message of seed.messages) this.#messages.set(message.id, message);
327
- }
328
- }
329
- get id() {
330
- return this.#id;
331
- }
332
- get emitter() {
333
- return this.#emitter;
334
- }
335
- get summary() {
336
- return this.#summary;
337
- }
338
- get sections() {
339
- return [...this.#sections];
340
- }
341
- get summarizable() {
342
- return this.#summarize !== void 0;
343
- }
344
- get count() {
345
- return this.#messages.size;
346
- }
347
- add(input) {
348
- if (isArray(input)) return input.map((one) => this.#create(one));
349
- return this.#create(input);
350
- }
351
- message(id) {
352
- return this.#messages.get(id);
353
- }
354
- messages() {
355
- return [...this.#messages.values()];
356
- }
357
- remove(ids) {
358
- if (isArray(ids)) {
359
- let removed = false;
360
- for (const id of ids) if (this.#messages.delete(id)) removed = true;
361
- return removed;
362
- }
363
- return this.#messages.delete(ids);
364
- }
365
- clear() {
366
- this.#messages.clear();
367
- }
368
- view() {
369
- return [...this.#sections.map((section) => this.#recapMessage(section)), ...this.#messages.values()];
370
- }
371
- async compact(options) {
372
- const summarize = this.#summarize;
373
- if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
374
- const cap = options?.sections ?? this.#cap;
375
- if (cap !== void 0 && cap < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
376
- const keep = options?.keep ?? this.#keep;
377
- const live = [...this.#messages.values()];
378
- const fold = keep <= 0 ? live.length : live.length - keep;
379
- if (fold <= 0) return void 0;
380
- const slice = live.slice(0, fold);
381
- const summary = await summarize(slice);
382
- const section = {
383
- id: crypto.randomUUID(),
384
- summary,
385
- messages: slice
386
- };
387
- for (const message of slice) this.#messages.delete(message.id);
388
- this.#sections.push(section);
389
- if (cap !== void 0 && this.#sections.length > cap) {
390
- const overflow = this.#sections.length - cap + 1;
391
- const folded = this.#sections.slice(0, overflow);
392
- try {
393
- const merged = {
394
- id: crypto.randomUUID(),
395
- summary: await summarize(folded.map((one) => this.#summaryMessage(one))),
396
- messages: folded.flatMap((one) => one.messages)
397
- };
398
- this.#sections.splice(0, overflow, merged);
399
- this.#emitter.emit("collapse", merged);
400
- } catch (error) {
401
- this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
402
- this.#emitter.emit("summary", this.#summary);
403
- throw error;
404
- }
405
- }
406
- this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
407
- this.#emitter.emit("summary", this.#summary);
408
- this.#emitter.emit("compact", section);
409
- return section;
410
- }
411
- rehydrate(id) {
412
- const section = this.#sections.find((one) => one.id === id);
413
- this.#emitter.emit("rehydrate", id);
414
- return section === void 0 ? [] : section.messages;
415
- }
416
- search(query) {
417
- const needle = query.toLowerCase();
418
- return [...this.#sections.flatMap((section) => section.messages), ...this.#messages.values()].filter((message) => message.content.toLowerCase().includes(needle));
419
- }
420
- reference(options) {
421
- const lines = [`[Reference — conversation "${options?.label ?? this.#id}" — NOT part of this conversation]`];
422
- if (options?.summary !== false && this.#summary !== void 0) lines.push(`Summary: ${this.#summary}`);
423
- const messages = options?.messages ?? [];
424
- if (messages.length > 0) {
425
- lines.push("Relevant messages:");
426
- for (const message of messages) lines.push(`- ${message.role}: ${message.content}`);
427
- }
428
- return lines.join("\n");
429
- }
430
- snapshot() {
431
- return {
432
- id: this.#id,
433
- ...this.#summary === void 0 ? {} : { summary: this.#summary },
434
- sections: this.sections,
435
- messages: this.messages()
436
- };
437
- }
438
- #summaryMessage(section) {
439
- return {
440
- id: section.id,
441
- role: "assistant",
442
- content: section.summary
443
- };
444
- }
445
- #recapMessage(section) {
446
- return {
447
- id: section.id,
448
- role: "assistant",
449
- content: `${CONVERSATION_RECAP_PREFIX}${section.summary}`
450
- };
451
- }
452
- #create(input) {
453
- const message = {
454
- id: crypto.randomUUID(),
455
- role: input.role,
456
- content: input.content,
457
- ...input.calls === void 0 ? {} : { calls: input.calls },
458
- ...input.images === void 0 ? {} : { images: input.images }
459
- };
460
- this.#messages.set(message.id, message);
461
- return message;
462
- }
463
- };
464
- //#endregion
465
- //#region src/core/conversations/ConversationManager.ts
466
- /**
467
- * The registry of {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
468
- * — the §9 store over the conversation layer PLUS the `active` / `switch` seam the
469
- * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
470
- * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
471
- * on each {@link Conversation}.
472
- *
473
- * @remarks
474
- * - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
475
- * mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
476
- * manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
477
- * it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
478
- * `conversation(id)` looks one up, `conversations()` lists them in insertion order.
479
- * - **Active pointer.** `active` is the active conversation (the agent's message source the
480
- * context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
481
- * with conversations always has one active). A subsequent `add` leaves `active` unchanged.
482
- * `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
483
- * returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
484
- * no new error code).
485
- * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
486
- * removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
487
- * and sets `active` to `undefined`.
488
- * - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
489
- * its own observable `emitter`).
490
- *
491
- * @example
492
- * ```ts
493
- * const manager = new ConversationManager({ summarize: async (m) => `recap of ${m.length}` })
494
- * const conversation = manager.add() // auto-activates — active === conversation
495
- * manager.add({ id: 'scratch' }) // leaves active unchanged
496
- * manager.switch('scratch') // re-points active to the 'scratch' conversation
497
- * manager.count // 2
498
- * ```
499
- */
500
- var ConversationManager = class {
501
- #conversations = /* @__PURE__ */ new Map();
502
- #active;
503
- #summarize;
504
- #keep;
505
- #sections;
506
- #store;
507
- constructor(options) {
508
- this.#summarize = options?.summarize;
509
- this.#keep = options?.keep ?? 0;
510
- this.#sections = options?.sections;
511
- this.#store = options?.store;
512
- }
513
- get count() {
514
- return this.#conversations.size;
515
- }
516
- get active() {
517
- return this.#active === void 0 ? void 0 : this.#conversations.get(this.#active);
518
- }
519
- conversation(id) {
520
- return this.#conversations.get(id);
521
- }
522
- conversations() {
523
- return [...this.#conversations.values()];
524
- }
525
- add(input) {
526
- const sections = input?.sections ?? this.#sections;
527
- const summarize = input?.summarize ?? this.#summarize;
528
- const conversation = new Conversation({
529
- ...input?.id === void 0 ? {} : { id: input.id },
530
- ...input?.on === void 0 ? {} : { on: input.on },
531
- ...summarize === void 0 ? {} : { summarize },
532
- keep: input?.keep ?? this.#keep,
533
- ...sections === void 0 ? {} : { sections }
534
- }, input?.snapshot);
535
- this.#conversations.set(conversation.id, conversation);
536
- if (this.#active === void 0) this.#active = conversation.id;
537
- return conversation;
538
- }
539
- switch(id) {
540
- const conversation = this.#conversations.get(id);
541
- if (conversation === void 0) return void 0;
542
- this.#active = id;
543
- return conversation;
544
- }
545
- async open(id) {
546
- const existing = this.#conversations.get(id);
547
- if (existing !== void 0) {
548
- this.#active = id;
549
- return existing;
550
- }
551
- if (this.#store === void 0) return void 0;
552
- const snapshot = await this.#store.get(id);
553
- if (snapshot === void 0) return void 0;
554
- const conversation = this.add({ snapshot });
555
- this.#active = conversation.id;
556
- return conversation;
557
- }
558
- async save(id) {
559
- const conversation = this.#conversations.get(id);
560
- if (this.#store === void 0 || conversation === void 0) return false;
561
- await this.#store.set(conversation.snapshot());
562
- return true;
563
- }
564
- remove(ids) {
565
- if (isArray(ids)) {
566
- let removed = false;
567
- for (const id of ids) if (this.#drop(id)) removed = true;
568
- return removed;
569
- }
570
- return this.#drop(ids);
571
- }
572
- clear() {
573
- this.#conversations.clear();
574
- this.#active = void 0;
575
- }
576
- #drop(id) {
577
- const removed = this.#conversations.delete(id);
578
- if (removed && this.#active === id) this.#active = void 0;
579
- return removed;
580
- }
581
- };
582
- //#endregion
583
267
  //#region src/core/helpers.ts
584
268
  /**
585
- * Project an unknown value onto the canonical JSON representation of an
269
+ * Projects an unknown value onto the canonical JSON representation of an
586
270
  * {@link AgentResult}.
587
271
  *
588
272
  * @remarks
@@ -637,7 +321,7 @@ function agentResultToJSON(value) {
637
321
  return captured.success ? parseJSONValue(captured.value) : void 0;
638
322
  }
639
323
  /**
640
- * Filter a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
324
+ * Filters a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
641
325
  * keys — the pure, total set-membership primitive the context's build step and the agent
642
326
  * loop's tool-advertise step apply a scope through.
643
327
  *
@@ -654,7 +338,7 @@ function agentResultToJSON(value) {
654
338
  * @typeParam T - The item type being filtered
655
339
  * @param allow - The allow-list of keys (`undefined` ⇒ all, `[]` ⇒ none, else only-listed)
656
340
  * @param items - The items to filter (returned unchanged when `allow` is `undefined`)
657
- * @param key - Extracts the key an item is matched on (e.g. an instruction's `name`)
341
+ * @param key - Extracts the key an item is matched on (for example an instruction's `name`)
658
342
  * @returns The items that pass the allow-list, in their original order
659
343
  *
660
344
  * @example
@@ -672,7 +356,7 @@ function filterAllowList(allow, items, key) {
672
356
  return items.filter((item) => set.has(key(item)));
673
357
  }
674
358
  /**
675
- * Estimate the context-token footprint of a string — the deterministic char-based heuristic
359
+ * Estimates the context-token footprint of a string — the deterministic char-based heuristic
676
360
  * {@link estimateMessages} sums over a conversation's messages (the default context-budget
677
361
  * estimator).
678
362
  *
@@ -697,7 +381,7 @@ function estimateTokens(text) {
697
381
  return Math.ceil(text.length / 4);
698
382
  }
699
383
  /**
700
- * Estimate the context-token footprint of a batch of messages — the default `consume`
384
+ * Estimates the context-token footprint of a batch of messages — the default `consumer`
701
385
  * estimator for an agent's context `BudgetInterface` (a budgets surface's tracking contract)
702
386
  * (the {@link import('./types.js').AgentOptions} `window`).
703
387
  *
@@ -709,7 +393,7 @@ function estimateTokens(text) {
709
393
  * deliberately-approximate per-image cost — a base64 length is NOT a token proxy). Deterministic
710
394
  * and provider-free — the same messages always yield the same estimate, with an empty batch `0`.
711
395
  * It is the fully-swappable default an agent's auto-compaction context budget charges each
712
- * turn's new messages through; a caller wanting a sharper count supplies its own `consume` to
396
+ * turn's new messages through; a caller wanting a sharper count supplies its own `consumer` to
713
397
  * `createBudget` instead. Total — never throws: a `calls` `JSON.stringify` that throws (a
714
398
  * circular `ToolCall.arguments`) is caught and replaced with a conservative fixed contribution of
715
399
  * {@link import('./constants.js').MESSAGE_TOKEN_OVERHEAD} (the same per-message overhead scale)
@@ -738,47 +422,48 @@ function estimateMessages(messages) {
738
422
  }, 0);
739
423
  }
740
424
  /**
741
- * Run one rehydrated agent and apply the partial-as-configurable-failure policy — the
425
+ * Runs one rehydrated agent and applies the partial-as-configurable-failure policy — the
742
426
  * shared job-handler step BOTH `createAgentQueue` and `createAgentRunner` settle each job
743
427
  * through, so the policy can never diverge between them.
744
428
  *
745
429
  * @remarks
746
430
  * A turn that committed PARTIAL (a cancel — abort / budget / timeout) is by default a
747
431
  * FAILURE, so it THROWS an {@link import('./errors.js').AgentJobError} carrying the partial
748
- * (the Queue's retries + a Runner's fail-fast then engage); with `allowPartial` it RESOLVES
749
- * the partial as success instead. A natural finish ALWAYS resolves with its result.
432
+ * (the Queue's retries + a Runner's fail-fast then engage); the `partial` policy RESOLVES
433
+ * it as success instead. A natural finish ALWAYS resolves with its result.
750
434
  *
751
435
  * @param agent - The rehydrated {@link AgentInterface} to run to its {@link AgentResult}
752
- * @param allowPartial - When `true`, a partial result resolves as success; when `false`
753
- * (the default policy), a partial result throws an {@link AgentJobError}
754
- * @returns The agent's {@link AgentResult} (a natural finish, or a partial when `allowPartial`)
755
- * @throws {AgentJobError} When the run ended `partial` and `allowPartial` is `false`
436
+ * @param partial - The partial policy. If `true`, a partial result resolves as success; if
437
+ * `false` (the default policy), a partial result throws an {@link AgentJobError}
438
+ * @returns The agent's {@link AgentResult} (a natural finish, or a partial one under the
439
+ * `partial` policy)
440
+ * @throws {AgentJobError} Thrown when the run ended partial and the `partial` policy is `false`
756
441
  *
757
442
  * @example
758
443
  * ```ts
759
444
  * const result = await settleAgentJob(registry.build(input, signal), false)
760
445
  * ```
761
446
  */
762
- async function settleAgentJob(agent, allowPartial) {
447
+ async function settleAgentJob(agent, partial) {
763
448
  const result = await agent.generate();
764
- if (result.partial && !allowPartial) throw new AgentJobError("agent job ended partial", result);
449
+ if (result.partial && !partial) throw new AgentJobError("agent job ended partial", result);
765
450
  return result;
766
451
  }
767
452
  /**
768
- * Handle one queued agent job by rehydrating it through a registry with the queue
453
+ * Handles one queued agent job by rehydrating it through a registry with the queue
769
454
  * attempt's signal, then applying the shared partial-result policy.
770
455
  *
771
456
  * @param registry - The registry that rehydrates the serializable job
772
- * @param allowPartial - Whether a partial result resolves instead of throwing
457
+ * @param partial - The partial policy. If `true`, a partial result resolves; if `false`, it throws
773
458
  * @param input - The serializable agent job
774
- * @param execution - The queue attempt whose signal bounds the agent
459
+ * @param context - The queue attempt whose signal bounds the agent
775
460
  * @returns The settled agent result
776
461
  */
777
- function handleAgentQueueJob(registry, allowPartial, input, execution) {
778
- return settleAgentJob(registry.build(input, execution.signal), allowPartial);
462
+ function handleAgentQueueJob(registry, partial, input, context) {
463
+ return settleAgentJob(registry.build(input, context.signal), partial);
779
464
  }
780
465
  /**
781
- * Handle one runner agent job by fanning out its declared children, rehydrating the
466
+ * Handles one runner agent job by fanning out its declared children, rehydrating the
782
467
  * parent through a registry with the controller signal, and applying the shared
783
468
  * partial-result policy.
784
469
  *
@@ -787,189 +472,779 @@ function handleAgentQueueJob(registry, allowPartial, input, execution) {
787
472
  * inline, preserving bounded-runner progress.
788
473
  *
789
474
  * @param registry - The registry that rehydrates serializable jobs
790
- * @param allowPartial - Whether a partial result resolves instead of throwing
475
+ * @param partial - The partial policy. If `true`, a partial result resolves; if `false`, it throws
791
476
  * @param controller - The runner controller for this parent job
792
477
  * @returns The settled parent agent result
793
478
  */
794
- function handleAgentRunnerJob(registry, allowPartial, controller) {
479
+ function handleAgentRunnerJob(registry, partial, controller) {
795
480
  const children = controller.input.children;
796
481
  if (children !== void 0) for (const child of children) controller.spawn(child);
797
- return settleAgentJob(registry.build(controller.input, controller.signal), allowPartial);
482
+ return settleAgentJob(registry.build(controller.input, controller.signal), partial);
798
483
  }
799
484
  /**
800
- * Whether an `unknown` is structurally a {@link MessageInterface} record — the per-message step of
801
- * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
802
- * read via a guard, never an `as`). The conversation analogue of
803
- * {@link import('@orkestrel/workspace').isFile}.
485
+ * Renders a path-addressed text body as a fenced reference block — the framing an
486
+ * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
487
+ * active workspace is the SOLE document/image context).
804
488
  *
805
489
  * @remarks
806
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
807
- * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
808
- * — a `calls` array EVERY element of which is a valid
809
- * {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
810
- * ASI06 fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
811
- * reads back as absent rather than replaying a malformed call) and an `images` that is an array
812
- * (an absent optional passes). The `role` is left as a broad `string` here (an open
813
- * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
814
- * defensively rather than rejected against the current literal set). Enough to safely impose the
815
- * {@link MessageInterface} type at
816
- * a storage boundary WITHOUT a cast.
490
+ * Produces `` File: <path>\n```<language>\n<content>\n``` `` — the `File:` label line, then a
491
+ * fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
492
+ * total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
493
+ * frames an active workspace's text files with it (each carries its own `language` on its
494
+ * {@link import('@orkestrel/workspace').FileContent} text arm).
495
+ *
496
+ * @param path - The file path shown on the `File:` label line
497
+ * @param language - The fenced-code language tag (for example `'typescript'`)
498
+ * @param content - The file body rendered verbatim inside the fence
499
+ * @returns The fenced reference block
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * import { renderFencedFile } from '@orkestrel/agent'
504
+ *
505
+ * renderFencedFile('src/main.ts', 'typescript', 'const x = 1')
506
+ * // 'File: src/main.ts\n```typescript\nconst x = 1\n```'
507
+ * ```
508
+ */
509
+ function renderFencedFile(path, language, content) {
510
+ return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
511
+ }
512
+ /**
513
+ * Sanitizes one reported token count into a safe non-negative integer.
514
+ *
515
+ * @param value - The token count to sanitize
516
+ * @returns The floored count, or `0` when the value is non-finite or non-positive
517
+ */
518
+ function sanitizeToken(value) {
519
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
520
+ }
521
+ /**
522
+ * Sanitizes a {@link TokenUsage} into safe, non-negative integers — the guard an agent's
523
+ * abort-usage path applies to a provider's partial usage before it is charged against a
524
+ * budget or folded into the run total.
525
+ *
526
+ * @remarks
527
+ * Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
528
+ * `-Infinity`) or a negative value floors to `0`; a fractional value floors to its
529
+ * non-negative integer part. No upper cap is applied. Total — never throws.
530
+ *
531
+ * @param usage - The token usage to sanitize (for example a provider's abort-partial usage)
532
+ * @returns A new {@link TokenUsage} with every field a safe non-negative integer
533
+ *
534
+ * @example
535
+ * ```ts
536
+ * sanitizeUsage({ prompt: -5, completion: NaN, total: 12.7 }) // { prompt: 0, completion: 0, total: 12 }
537
+ * ```
538
+ */
539
+ function sanitizeUsage(usage) {
540
+ return {
541
+ prompt: sanitizeToken(usage.prompt),
542
+ completion: sanitizeToken(usage.completion),
543
+ total: sanitizeToken(usage.total)
544
+ };
545
+ }
546
+ /**
547
+ * Joins the reasoning a run's provider calls separated from the answer — the first call
548
+ * seeds the accumulation, a later call appends blank-line separated so each turn's reasoning
549
+ * stays readable.
550
+ *
551
+ * @remarks
552
+ * Pure and total. `running` is `undefined` until a call surfaces reasoning, so the first join
553
+ * returns `next` verbatim (no leading separator). The result is display/audit metadata that
554
+ * never re-enters the conversation.
555
+ *
556
+ * @param running - The reasoning accumulated so far (`undefined` before the first)
557
+ * @param next - This call's separated reasoning
558
+ * @returns The joined reasoning
559
+ *
560
+ * @example
561
+ * ```ts
562
+ * joinThinking(undefined, 'first') // 'first'
563
+ * joinThinking('first', 'second') // 'first\n\nsecond'
564
+ * ```
565
+ */
566
+ function joinThinking(running, next) {
567
+ return running === void 0 ? next : `${running}\n\n${next}`;
568
+ }
569
+ /**
570
+ * Adds two {@link TokenUsage} values field by field — the running total an agent run keeps
571
+ * across its provider calls.
572
+ *
573
+ * @remarks
574
+ * Pure and total: the first call seeds the total (`running` `undefined` returns `next`
575
+ * unchanged), later calls accumulate. No sanitization happens here — charge a provider's
576
+ * reported usage through {@link sanitizeUsage} first.
577
+ *
578
+ * @param running - The total so far (`undefined` before the first usage-bearing call)
579
+ * @param next - This call's reported usage
580
+ * @returns The summed usage
581
+ *
582
+ * @example
583
+ * ```ts
584
+ * sumUsage(undefined, { prompt: 2, completion: 1, total: 3 }) // { prompt: 2, completion: 1, total: 3 }
585
+ * sumUsage({ prompt: 2, completion: 1, total: 3 }, { prompt: 1, completion: 1, total: 2 })
586
+ * // { prompt: 3, completion: 2, total: 5 }
587
+ * ```
588
+ */
589
+ function sumUsage(running, next) {
590
+ if (running === void 0) return next;
591
+ return {
592
+ prompt: running.prompt + next.prompt,
593
+ completion: running.completion + next.completion,
594
+ total: running.total + next.total
595
+ };
596
+ }
597
+ /**
598
+ * Assembles the settled {@link AgentResult} from a run's {@link RunOutcome} — `thinking` and
599
+ * `usage` are carried only when the run surfaced them.
600
+ *
601
+ * @remarks
602
+ * Pure and total. An absent optional is OMITTED rather than stored as `undefined` (the
603
+ * present-when-given convention the message store follows), so a settled result JSON
604
+ * round-trips without an explicit `undefined` field. `exhausted` is loop bookkeeping and does
605
+ * not reach the public result — the `exhaust` event carries it instead.
606
+ *
607
+ * @param outcome - The run's settled outcome
608
+ * @returns The public {@link AgentResult}
609
+ *
610
+ * @example
611
+ * ```ts
612
+ * assembleResult({ content: 'hi', thinking: undefined, usage: undefined, partial: false, exhausted: false })
613
+ * // { content: 'hi', partial: false }
614
+ * ```
615
+ */
616
+ function assembleResult(outcome) {
617
+ const result = {
618
+ content: outcome.content,
619
+ partial: outcome.partial
620
+ };
621
+ if (outcome.thinking !== void 0) result.thinking = outcome.thinking;
622
+ if (outcome.usage !== void 0) result.usage = outcome.usage;
623
+ return result;
624
+ }
625
+ /**
626
+ * Synthesizes the denial {@link ToolResult} an authority-blocked call is fed back with — the
627
+ * call's `id` / `name` keyed back, carrying a denial `error` instead of a value.
628
+ *
629
+ * @remarks
630
+ * Pure and total. The rule's `reason` is rendered as `denied: <reason>` when one was given,
631
+ * else the generic `denied by authority`. There is no `value`, so the agent loop feeds it back
632
+ * exactly like a tool error and the model can react to it.
633
+ *
634
+ * @param call - The denied {@link ToolCall}
635
+ * @param reason - The rule's explanation, or `undefined` for the generic denial
636
+ * @returns The failure-arm {@link ToolResult}
637
+ *
638
+ * @example
639
+ * ```ts
640
+ * denyCall({ id: '1', name: 'drop', arguments: {} }, 'read-only mode')
641
+ * // { success: false, id: '1', name: 'drop', error: 'denied: read-only mode' }
642
+ * ```
643
+ */
644
+ function denyCall(call, reason) {
645
+ return {
646
+ success: false,
647
+ id: call.id,
648
+ name: call.name,
649
+ error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
650
+ };
651
+ }
652
+ /**
653
+ * Renders one context section — the resolved `open`, each item's rendering, and the resolved
654
+ * `close` when one exists, blank-line joined.
655
+ *
656
+ * @remarks
657
+ * Pure and total. A section with NO items renders nothing (`undefined`), so an empty or fully
658
+ * scoped-out manager stays silent — its `open` / `close` never appear without items. `close`
659
+ * is the only optional slot: an unset one (there is no built-in close) drops the
660
+ * trailing line.
661
+ *
662
+ * @typeParam T - The section item being rendered
663
+ * @param open - The section's resolved leading text
664
+ * @param items - The already scope-filtered items
665
+ * @param render - Renders one item to its prompt text
666
+ * @param close - The section's resolved trailing text, or `undefined` for none
667
+ * @returns The rendered section, or `undefined` when there are no items
668
+ *
669
+ * @example
670
+ * ```ts
671
+ * renderSection('## Instructions', [{ content: 'Be terse.' }], (one) => one.content, undefined)
672
+ * // '## Instructions\n\nBe terse.'
673
+ * renderSection('<rules>', [], (one) => one.content, '</rules>') // undefined (no items)
674
+ * ```
675
+ */
676
+ function renderSection(open, items, render, close) {
677
+ if (items.length === 0) return void 0;
678
+ const lines = [open, ...items.map(render)];
679
+ if (close !== void 0) lines.push(close);
680
+ return lines.join("\n\n");
681
+ }
682
+ /**
683
+ * Resolves one section's OPEN text through the format cascade — manager-options override >
684
+ * provider default > built-in header.
685
+ *
686
+ * @remarks
687
+ * Pure and total. The leading text has NO per-item level. A manager's `open` already
688
+ * encapsulates `[options-override → built-in]`, so it is reached only when neither the
689
+ * override's `open` nor the provider's `open` applies — and there it IS the built-in header.
690
+ *
691
+ * @typeParam T - The section item the manager renders
692
+ * @param manager - The section source (its `format` override + its built-in `open`)
693
+ * @param provider - The provider-default framing for this section, or `undefined`
694
+ * @returns The section's leading text
695
+ *
696
+ * @example
697
+ * ```ts
698
+ * resolveOpen(instructions, undefined) // '## Instructions' (the built-in header)
699
+ * resolveOpen(instructions, { open: '<rules>' }) // '<rules>' (the provider default)
700
+ * ```
701
+ */
702
+ function resolveOpen(manager, provider) {
703
+ return manager.format?.open ?? provider?.open ?? manager.open;
704
+ }
705
+ /**
706
+ * Resolves one section's CLOSE text through the format cascade — manager-options override >
707
+ * provider default.
708
+ *
709
+ * @remarks
710
+ * Pure and total. There is NO built-in close, so a section with neither level set returns
711
+ * `undefined` and {@link renderSection} appends no closing line. Paired with
712
+ * {@link resolveOpen}, one level can WRAP the whole group.
713
+ *
714
+ * @typeParam T - The section item the manager renders
715
+ * @param manager - The section source (its `format` override)
716
+ * @param provider - The provider-default framing for this section, or `undefined`
717
+ * @returns The section's trailing text, or `undefined` when no level sets one
718
+ *
719
+ * @example
720
+ * ```ts
721
+ * resolveClose(instructions, undefined) // undefined (no built-in close)
722
+ * resolveClose(instructions, { close: '</rules>' }) // '</rules>'
723
+ * ```
724
+ */
725
+ function resolveClose(manager, provider) {
726
+ return manager.format?.close ?? provider?.close;
727
+ }
728
+ /**
729
+ * Resolves ONE item's rendering through the format cascade — item override >
730
+ * manager-options override > provider default > built-in rendering.
731
+ *
732
+ * @remarks
733
+ * Pure and total. The item's own `override` is the most-specific level (a fully-rendered
734
+ * string for that item alone). A manager's `render(item)` already encapsulates
735
+ * `[options-override → built-in]`, so it is reached only when no higher level applies — and
736
+ * there it IS the built-in rendering.
737
+ *
738
+ * @typeParam T - The section item being rendered (it may carry its own `override`)
739
+ * @param manager - The section source (its `format` override + its built-in `render`)
740
+ * @param provider - The provider-default framing for this section, or `undefined`
741
+ * @param item - The item to render
742
+ * @returns The item's prompt text
743
+ *
744
+ * @example
745
+ * ```ts
746
+ * resolveItem(instructions, undefined, terse) // 'Be terse.' (the built-in rendering)
747
+ * resolveItem(instructions, { render: (one) => `<rule>${one.content}</rule>` }, terse)
748
+ * // '<rule>Be terse.</rule>'
749
+ * ```
750
+ */
751
+ function resolveItem(manager, provider, item) {
752
+ return item.override ?? manager.format?.render?.(item) ?? provider?.render?.(item) ?? manager.render(item);
753
+ }
754
+ /**
755
+ * Copies a message with image data merged onto its `images` — the message's own images
756
+ * first, then the attached data.
757
+ *
758
+ * @remarks
759
+ * Pure and total: the original message is NEVER mutated. `calls` is carried only when the
760
+ * source message has one (kept omitted otherwise, mirroring the store's present-when-given
761
+ * convention).
762
+ *
763
+ * @param message - The message to copy (left unchanged)
764
+ * @param data - The base64 image data to attach
765
+ * @returns A new message carrying the merged `images`
766
+ *
767
+ * @example
768
+ * ```ts
769
+ * attachImages({ id: '1', role: 'user', content: 'Describe' }, ['<payload>'])
770
+ * // { id: '1', role: 'user', content: 'Describe', images: ['<payload>'] }
771
+ * ```
772
+ */
773
+ function attachImages(message, data) {
774
+ const images = [...message.images ?? [], ...data];
775
+ return message.calls === void 0 ? {
776
+ id: message.id,
777
+ role: message.role,
778
+ content: message.content,
779
+ images
780
+ } : {
781
+ id: message.id,
782
+ role: message.role,
783
+ content: message.content,
784
+ calls: message.calls,
785
+ images
786
+ };
787
+ }
788
+ /**
789
+ * Attaches image data to a conversation's LAST user message — the turn a vision provider
790
+ * reads images off.
791
+ *
792
+ * @remarks
793
+ * Pure and total: the conversation and its messages are NEVER mutated, and the returned array
794
+ * replaces exactly the one target message with the copy {@link attachImages} builds. Empty
795
+ * data returns the conversation unchanged; a conversation with NO user message returns it
796
+ * unchanged too (there is nowhere to attach, and the images already rode the system block).
817
797
  *
818
- * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
819
- * @returns `true` when `value` has the structural shape of a {@link MessageInterface}
798
+ * @param conversation - The messages to attach into (left unchanged)
799
+ * @param data - The base64 image data to attach
800
+ * @returns The conversation with its last user message replaced by the carrying copy
820
801
  *
821
802
  * @example
822
803
  * ```ts
823
- * isMessage({ id: '1', role: 'user', content: 'hi' }) // true
824
- * isMessage({ id: '1', role: 'assistant', content: '', calls: [] }) // true
825
- * isMessage({ id: '1', role: 'user' }) // false (missing content)
826
- * isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
804
+ * attachUserImages([{ id: '1', role: 'user', content: 'Describe' }], ['<payload>'])
805
+ * // [{ id: '1', role: 'user', content: 'Describe', images: ['<payload>'] }]
827
806
  * ```
828
807
  */
829
- function isMessage(value) {
830
- if (!isRecord(value)) return false;
831
- if (!isString(value.id) || !isString(value.role) || !isString(value.content)) return false;
832
- if (value.calls !== void 0 && !(isArray(value.calls) && value.calls.every(isToolCall))) return false;
833
- return value.images === void 0 || isArray(value.images);
808
+ function attachUserImages(conversation, data) {
809
+ if (data.length === 0) return conversation;
810
+ let target = -1;
811
+ for (let index = conversation.length - 1; index >= 0; index -= 1) if (conversation[index]?.role === "user") {
812
+ target = index;
813
+ break;
814
+ }
815
+ if (target === -1) return conversation;
816
+ return conversation.map((message, index) => index === target ? attachImages(message, data) : message);
834
817
  }
835
818
  /**
836
- * Whether an `unknown` is structurally a {@link SectionInterface} record — the per-section step of
837
- * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
838
- * read via a guard, never an `as`).
819
+ * Collects the `base64` payload of the IMAGE files in a workspace file list — the data an
820
+ * agent context attaches to the last user message.
839
821
  *
840
822
  * @remarks
841
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the section's
842
- * SHAPE: a record with a `string` `id`, a `string` `summary`, and a `messages` array EVERY element
843
- * of which is a valid {@link MessageInterface} record ({@link isMessage}). Enough to safely impose
844
- * the {@link SectionInterface} type at a storage boundary WITHOUT a cast.
823
+ * Pure and total. `isBinary` NARROWS the tagless content to its binary arm (a total guard,
824
+ * never an assertion), then the MIME prefix gates it to an image, so a text file and a non-image
825
+ * binary (a PDF) are both skipped. Order follows the file list.
845
826
  *
846
- * @param value - The value to test (one element of a snapshot's `sections` array)
847
- * @returns `true` when `value` has the structural shape of a {@link SectionInterface}
827
+ * @param files - The (already scope-filtered) workspace files
828
+ * @returns The `base64` payload of each image file, in file order
848
829
  *
849
830
  * @example
850
831
  * ```ts
851
- * isSection({ id: 's', summary: 'recap', messages: [{ id: '1', role: 'user', content: 'hi' }] }) // true
852
- * isSection({ id: 's', summary: 'recap', messages: 'nope' }) // false
853
- * isSection({ id: 's', messages: [] }) // false (missing summary)
832
+ * collectImageData([createFile({ path: 'a.png', content: { base64: '<payload>', mime: 'image/png' } })])
833
+ * // ['<payload>']
854
834
  * ```
855
835
  */
856
- function isSection(value) {
857
- if (!isRecord(value)) return false;
858
- if (!isString(value.id) || !isString(value.summary)) return false;
859
- return isArray(value.messages) && value.messages.every(isMessage);
836
+ function collectImageData(files) {
837
+ const data = [];
838
+ for (const file of files) if (isBinary(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.base64);
839
+ return data;
860
840
  }
861
841
  /**
862
- * Narrow an `unknown` to a {@link ConversationSnapshot} — the AGENTS §14 boundary guard for an
863
- * UNTRUSTED snapshot read (a storage row a
864
- * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
865
- * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
866
- * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
842
+ * Builds the RAW synthetic summary message for one compacted section — role `'assistant'`,
843
+ * the section's stable `id`, its `summary` VERBATIM as content.
867
844
  *
868
845
  * @remarks
869
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
870
- * SHAPE: a `string` `id`, an OPTIONAL `string` `summary` (present-or-absent — the rollup is
871
- * `undefined` until the first compaction), a `sections` array EVERY element of which is a valid
872
- * {@link SectionInterface} ({@link isSection}), and a `messages` array EVERY element of which is a
873
- * valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
874
- * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
875
- * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
876
- * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
877
- * element) resolves `false`, so a
878
- * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
879
- * read yields `undefined` rather than a broken conversation.
846
+ * Pure and total. This is the unframed form the rollup regeneration digests (a
847
+ * summary-of-summaries over the section summaries); the recap LABEL is a `view()`
848
+ * presentation concern kept out of what the summarizer re-reads — see
849
+ * {@link buildRecapMessage}.
880
850
  *
881
- * @param value - The value to test (an opaque storage read)
882
- * @returns `true` when `value` has the structural shape of a {@link ConversationSnapshot}
851
+ * @param section - The compacted section to render
852
+ * @returns The synthetic summary message
883
853
  *
884
854
  * @example
885
855
  * ```ts
886
- * isConversationSnapshot({ id: 'c1', sections: [], messages: [] }) // true
887
- * isConversationSnapshot({ id: 'c1', summary: 'recap', sections: [], messages: [] }) // true
888
- * isConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // false
889
- * isConversationSnapshot({ sections: [], messages: [] }) // false (missing id)
856
+ * buildSummaryMessage({ id: 's1', summary: 'recap', messages: [] })
857
+ * // { id: 's1', role: 'assistant', content: 'recap' }
890
858
  * ```
891
859
  */
892
- function isConversationSnapshot(value) {
893
- if (!isRecord(value)) return false;
894
- if (!isString(value.id)) return false;
895
- if (value.summary !== void 0 && !isString(value.summary)) return false;
896
- if (!isArray(value.sections) || !value.sections.every(isSection)) return false;
897
- return isArray(value.messages) && value.messages.every(isMessage);
860
+ function buildSummaryMessage(section) {
861
+ return {
862
+ id: section.id,
863
+ role: "assistant",
864
+ content: section.summary
865
+ };
898
866
  }
899
867
  /**
900
- * Render a path-addressed text body as a fenced reference block — the framing an
901
- * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
902
- * active workspace is the SOLE document/image context).
868
+ * Builds the FRAMED recap message for one compacted section — the same role and stable `id`
869
+ * as {@link buildSummaryMessage}, with the content prefixed by
870
+ * {@link import('./constants.js').CONVERSATION_RECAP_PREFIX}.
903
871
  *
904
872
  * @remarks
905
- * Produces `` File: <path>\n```<language>\n<content>\n``` `` — the `File:` label line, then a
906
- * fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
907
- * total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
908
- * frames an active workspace's text files with it (each carries its own `language` on its
909
- * {@link import('@orkestrel/workspace').FileContent} text arm).
873
+ * Pure and total. The prefix is what makes a small model read the message as a CONDENSED
874
+ * RECAP of earlier turns rather than a literal assistant turn to echo or answer from. It is a
875
+ * fixed handful of tokens, so a conversation's `view()` stays lean however many sections it
876
+ * carries.
910
877
  *
911
- * @param path - The file path shown on the `File:` label line
912
- * @param language - The fenced-code language tag (e.g. `'typescript'`)
913
- * @param content - The file body rendered verbatim inside the fence
914
- * @returns The fenced reference block
878
+ * @param section - The compacted section to render
879
+ * @returns The framed recap message
915
880
  *
916
881
  * @example
917
882
  * ```ts
918
- * import { fencedFile } from '@src/core'
919
- *
920
- * fencedFile('src/main.ts', 'typescript', 'const x = 1')
921
- * // 'File: src/main.ts\n```typescript\nconst x = 1\n```'
883
+ * buildRecapMessage({ id: 's1', summary: 'recap', messages: [] })
884
+ * // { id: 's1', role: 'assistant', content: `${CONVERSATION_RECAP_PREFIX}recap` }
922
885
  * ```
923
886
  */
924
- function fencedFile(path, language, content) {
925
- return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
887
+ function buildRecapMessage(section) {
888
+ return {
889
+ id: section.id,
890
+ role: "assistant",
891
+ content: `${CONVERSATION_RECAP_PREFIX}${section.summary}`
892
+ };
926
893
  }
927
894
  /**
928
- * Sanitize one reported token count into a safe non-negative integer.
895
+ * Intersects two scope category lists under the "`undefined` is the universal set" rule — the
896
+ * primitive a scope narrows through.
929
897
  *
930
- * @param value - The token count to sanitize
931
- * @returns The floored count, or `0` when the value is non-finite or non-positive
898
+ * @remarks
899
+ * Pure and total, and it can only TIGHTEN: `undefined` ∩ `undefined` is `undefined` (still no
900
+ * constraint); `undefined` ∩ a list is a COPY of that list (the `undefined` side imposes
901
+ * nothing); a list ∩ a list keeps the child keys the parent also allows, so a parent-excluded
902
+ * key can never be re-admitted. Every returned list is a fresh copy, so a later mutation of
903
+ * either input cannot leak into the result.
904
+ *
905
+ * @param parent - The parent's allow-list (`undefined` ⇒ no constraint)
906
+ * @param child - The narrowing allow-list (`undefined` ⇒ no constraint)
907
+ * @returns The intersected allow-list, or `undefined` when neither side constrains
908
+ *
909
+ * @example
910
+ * ```ts
911
+ * intersectKeys(['read', 'write'], ['write', 'admin']) // ['write']
912
+ * intersectKeys(undefined, ['read']) // ['read']
913
+ * intersectKeys(undefined, undefined) // undefined
914
+ * ```
932
915
  */
933
- function sanitizeToken(value) {
934
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
916
+ function intersectKeys(parent, child) {
917
+ if (parent === void 0) return child === void 0 ? void 0 : [...child];
918
+ if (child === void 0) return [...parent];
919
+ const allowed = new Set(parent);
920
+ return child.filter((key) => allowed.has(key));
935
921
  }
922
+ //#endregion
923
+ //#region src/core/conversations/Conversation.ts
936
924
  /**
937
- * Sanitize a {@link TokenUsage} into safe, non-negative integers — the guard an agent's
938
- * abort-usage path applies to a provider's partial usage before it is charged against a
939
- * budget or folded into the run total.
925
+ * Represents a conversation — a live uncompacted tail of messages it OWNS DIRECTLY ABOVE a
926
+ * flat message store, plus compacted, summarized {@link Section}s and a regenerated rollup
927
+ * `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
928
+ * {@link ConversationSummaryHandler} seam (so `core` never imports a provider).
940
929
  *
941
930
  * @remarks
942
- * Per field (`prompt` / `completion` / `total`): a non-finite value (`NaN`, `+Infinity`,
943
- * `-Infinity`) or a negative value floors to `0`; a fractional value floors to its
944
- * non-negative integer part. No upper cap is applied. Total — never throws.
931
+ * - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
932
+ * insertion-ordered `Map` of immutable {@link Message}s keyed by their minted id
933
+ * (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
934
+ * `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
935
+ * per-value manager). `#sections` are the compacted history (oldest → newest), each a
936
+ * summarized slice that RETAINS its originals. `#summary` is the rollup (a
937
+ * summary-of-summaries over all sections), regenerated on each compaction (`undefined`
938
+ * until the first).
939
+ * - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
940
+ * prior-context recap — keyed by the section's stable `id`), then the live messages
941
+ * verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
942
+ * carries the per-section summaries, which ARE the compaction benefit.
943
+ * - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
944
+ * (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
945
+ * rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
946
+ * `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
947
+ * THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
948
+ * per compaction.
949
+ * - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
950
+ * messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (the caller decides
951
+ * whether to re-add them; `rehydrate` never reinserts). `search` is a case-insensitive
952
+ * substring scan of `content` across ALL messages (every section's originals + the live tail).
953
+ * - **Observable.** The owned {@link emitter} ({@link ConversationEventMap}) carries
954
+ * `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
955
+ * the emitter isolates a listener throw and routes it to its `error` handler (the `error`
956
+ * option), so a buggy observer can never corrupt a compaction.
945
957
  *
946
- * @param usage - The token usage to sanitize (e.g. a provider's abort-partial usage)
947
- * @returns A new {@link TokenUsage} with every field a safe non-negative integer
958
+ * @example
959
+ * ```ts
960
+ * const conversation = new Conversation({ summarize: async (m) => `recap of ${m.length}` })
961
+ * conversation.add([
962
+ * { role: 'user', content: 'Hello' },
963
+ * { role: 'assistant', content: 'Hi there' },
964
+ * ])
965
+ * const section = await conversation.compact() // folds both into one summarized section
966
+ * conversation.view() // [{ role: 'assistant', content: 'recap of 2' }] — the live tail is empty
967
+ * conversation.summary // 'recap of 1' — the rollup over the one section
968
+ * ```
969
+ */
970
+ var Conversation = class {
971
+ #id;
972
+ #emitter;
973
+ #summarize;
974
+ #keep;
975
+ #cap;
976
+ #sections = [];
977
+ #summary;
978
+ #messages = /* @__PURE__ */ new Map();
979
+ constructor(options) {
980
+ const snapshot = options?.snapshot;
981
+ this.#id = snapshot?.id ?? options?.id ?? crypto.randomUUID();
982
+ this.#emitter = new Emitter({
983
+ ...options?.on === void 0 ? {} : { on: options.on },
984
+ ...options?.error === void 0 ? {} : { error: options.error }
985
+ });
986
+ this.#summarize = options?.summarize;
987
+ this.#keep = options?.keep ?? 0;
988
+ if (options?.sections !== void 0 && options.sections < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
989
+ this.#cap = options?.sections;
990
+ if (snapshot !== void 0) {
991
+ this.#summary = snapshot.summary;
992
+ for (const section of snapshot.sections) this.#sections.push(section);
993
+ for (const message of snapshot.messages) this.#messages.set(message.id, message);
994
+ }
995
+ }
996
+ get id() {
997
+ return this.#id;
998
+ }
999
+ get emitter() {
1000
+ return this.#emitter;
1001
+ }
1002
+ get summary() {
1003
+ return this.#summary;
1004
+ }
1005
+ get sections() {
1006
+ return [...this.#sections];
1007
+ }
1008
+ get summarizable() {
1009
+ return this.#summarize !== void 0;
1010
+ }
1011
+ get count() {
1012
+ return this.#messages.size;
1013
+ }
1014
+ add(input) {
1015
+ if (isArray(input)) return input.map((one) => this.#create(one));
1016
+ return this.#create(input);
1017
+ }
1018
+ message(id) {
1019
+ return this.#messages.get(id);
1020
+ }
1021
+ messages() {
1022
+ return [...this.#messages.values()];
1023
+ }
1024
+ remove(ids) {
1025
+ if (isArray(ids)) {
1026
+ let removed = true;
1027
+ for (const id of ids) if (!this.#messages.delete(id)) removed = false;
1028
+ return removed;
1029
+ }
1030
+ return this.#messages.delete(ids);
1031
+ }
1032
+ clear() {
1033
+ this.#messages.clear();
1034
+ }
1035
+ view() {
1036
+ return [...this.#sections.map((section) => buildRecapMessage(section)), ...this.#messages.values()];
1037
+ }
1038
+ async compact(options) {
1039
+ const summarize = this.#summarize;
1040
+ if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
1041
+ const cap = options?.sections ?? this.#cap;
1042
+ if (cap !== void 0 && cap < 1) throw new ConversationError("SECTIONS", "a sections cap must be >= 1");
1043
+ const keep = options?.keep ?? this.#keep;
1044
+ const live = [...this.#messages.values()];
1045
+ const fold = keep <= 0 ? live.length : live.length - keep;
1046
+ if (fold <= 0) return void 0;
1047
+ const slice = live.slice(0, fold);
1048
+ const summary = await summarize(slice);
1049
+ const section = {
1050
+ id: crypto.randomUUID(),
1051
+ summary,
1052
+ messages: slice
1053
+ };
1054
+ for (const message of slice) this.#messages.delete(message.id);
1055
+ this.#sections.push(section);
1056
+ if (cap !== void 0 && this.#sections.length > cap) {
1057
+ const overflow = this.#sections.length - cap + 1;
1058
+ const folded = this.#sections.slice(0, overflow);
1059
+ try {
1060
+ const merged = {
1061
+ id: crypto.randomUUID(),
1062
+ summary: await summarize(folded.map((one) => buildSummaryMessage(one))),
1063
+ messages: folded.flatMap((one) => one.messages)
1064
+ };
1065
+ this.#sections.splice(0, overflow, merged);
1066
+ this.#emitter.emit("collapse", merged);
1067
+ } catch (error) {
1068
+ this.#summary = await summarize(this.#sections.map((one) => buildSummaryMessage(one)));
1069
+ this.#emitter.emit("summary", this.#summary);
1070
+ throw error;
1071
+ }
1072
+ }
1073
+ this.#summary = await summarize(this.#sections.map((one) => buildSummaryMessage(one)));
1074
+ this.#emitter.emit("summary", this.#summary);
1075
+ this.#emitter.emit("compact", section);
1076
+ return section;
1077
+ }
1078
+ rehydrate(id) {
1079
+ const section = this.#sections.find((one) => one.id === id);
1080
+ this.#emitter.emit("rehydrate", id);
1081
+ return section === void 0 ? [] : section.messages;
1082
+ }
1083
+ search(query) {
1084
+ const needle = query.toLowerCase();
1085
+ return [...this.#sections.flatMap((section) => section.messages), ...this.#messages.values()].filter((message) => message.content.toLowerCase().includes(needle));
1086
+ }
1087
+ reference(options) {
1088
+ const lines = [`[Reference — conversation "${options?.label ?? this.#id}" — NOT part of this conversation]`];
1089
+ if (options?.summary !== false && this.#summary !== void 0) lines.push(`Summary: ${this.#summary}`);
1090
+ const messages = options?.messages ?? [];
1091
+ if (messages.length > 0) {
1092
+ lines.push("Relevant messages:");
1093
+ for (const message of messages) lines.push(`- ${message.role}: ${message.content}`);
1094
+ }
1095
+ return lines.join("\n");
1096
+ }
1097
+ snapshot() {
1098
+ return {
1099
+ id: this.#id,
1100
+ ...this.#summary === void 0 ? {} : { summary: this.#summary },
1101
+ sections: this.sections,
1102
+ messages: this.messages()
1103
+ };
1104
+ }
1105
+ #create(input) {
1106
+ const message = {
1107
+ id: crypto.randomUUID(),
1108
+ role: input.role,
1109
+ content: input.content,
1110
+ ...input.calls === void 0 ? {} : { calls: input.calls },
1111
+ ...input.images === void 0 ? {} : { images: input.images }
1112
+ };
1113
+ this.#messages.set(message.id, message);
1114
+ return message;
1115
+ }
1116
+ };
1117
+ //#endregion
1118
+ //#region src/core/conversations/ConversationManager.ts
1119
+ /**
1120
+ * Registers {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
1121
+ * — the id-keyed store over the conversation layer PLUS the `active` / `switch` seam the
1122
+ * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
1123
+ * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
1124
+ * on each {@link Conversation}.
1125
+ *
1126
+ * @remarks
1127
+ * - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
1128
+ * mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
1129
+ * manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
1130
+ * it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
1131
+ * `conversation(id)` looks one up, `conversations()` lists them in insertion order.
1132
+ * - **Active pointer.** `active` is the active conversation (the agent's message source the
1133
+ * context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
1134
+ * with conversations always has one active). A subsequent `add` leaves `active` unchanged.
1135
+ * `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
1136
+ * returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
1137
+ * no new error code).
1138
+ * - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id was
1139
+ * removed; removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
1140
+ * and sets `active` to `undefined`.
1141
+ * - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
1142
+ * its own observable `emitter`).
948
1143
  *
949
1144
  * @example
950
1145
  * ```ts
951
- * sanitizeUsage({ prompt: -5, completion: NaN, total: 12.7 }) // { prompt: 0, completion: 0, total: 12 }
1146
+ * const manager = new ConversationManager({ summarize: async (m) => `recap of ${m.length}` })
1147
+ * const conversation = manager.add() // auto-activates — active === conversation
1148
+ * manager.add({ id: 'scratch' }) // leaves active unchanged
1149
+ * manager.switch('scratch') // re-points active to the 'scratch' conversation
1150
+ * manager.count // 2
952
1151
  * ```
953
1152
  */
954
- function sanitizeUsage(usage) {
955
- return {
956
- prompt: sanitizeToken(usage.prompt),
957
- completion: sanitizeToken(usage.completion),
958
- total: sanitizeToken(usage.total)
959
- };
960
- }
1153
+ var ConversationManager = class {
1154
+ #conversations = /* @__PURE__ */ new Map();
1155
+ #active;
1156
+ #summarize;
1157
+ #keep;
1158
+ #sections;
1159
+ #store;
1160
+ constructor(options) {
1161
+ this.#summarize = options?.summarize;
1162
+ this.#keep = options?.keep ?? 0;
1163
+ this.#sections = options?.sections;
1164
+ this.#store = options?.store;
1165
+ }
1166
+ get count() {
1167
+ return this.#conversations.size;
1168
+ }
1169
+ get active() {
1170
+ return this.#active === void 0 ? void 0 : this.#conversations.get(this.#active);
1171
+ }
1172
+ conversation(id) {
1173
+ return this.#conversations.get(id);
1174
+ }
1175
+ conversations() {
1176
+ return [...this.#conversations.values()];
1177
+ }
1178
+ add(input) {
1179
+ const sections = input?.sections ?? this.#sections;
1180
+ const summarize = input?.summarize ?? this.#summarize;
1181
+ const conversation = new Conversation({
1182
+ ...input?.id === void 0 ? {} : { id: input.id },
1183
+ ...input?.on === void 0 ? {} : { on: input.on },
1184
+ ...summarize === void 0 ? {} : { summarize },
1185
+ keep: input?.keep ?? this.#keep,
1186
+ ...sections === void 0 ? {} : { sections },
1187
+ ...input?.snapshot === void 0 ? {} : { snapshot: input.snapshot }
1188
+ });
1189
+ this.#conversations.set(conversation.id, conversation);
1190
+ if (this.#active === void 0) this.#active = conversation.id;
1191
+ return conversation;
1192
+ }
1193
+ switch(id) {
1194
+ const conversation = this.#conversations.get(id);
1195
+ if (conversation === void 0) return void 0;
1196
+ this.#active = id;
1197
+ return conversation;
1198
+ }
1199
+ async open(id) {
1200
+ const existing = this.#conversations.get(id);
1201
+ if (existing !== void 0) {
1202
+ this.#active = id;
1203
+ return existing;
1204
+ }
1205
+ if (this.#store === void 0) return void 0;
1206
+ const snapshot = await this.#store.get(id);
1207
+ if (snapshot === void 0) return void 0;
1208
+ const conversation = this.add({ snapshot });
1209
+ this.#active = conversation.id;
1210
+ return conversation;
1211
+ }
1212
+ async save(id) {
1213
+ const conversation = this.#conversations.get(id);
1214
+ if (this.#store === void 0 || conversation === void 0) return false;
1215
+ await this.#store.set(conversation.snapshot());
1216
+ return true;
1217
+ }
1218
+ remove(ids) {
1219
+ if (isArray(ids)) {
1220
+ let removed = true;
1221
+ for (const id of ids) if (!this.#drop(id)) removed = false;
1222
+ return removed;
1223
+ }
1224
+ return this.#drop(ids);
1225
+ }
1226
+ clear() {
1227
+ this.#conversations.clear();
1228
+ this.#active = void 0;
1229
+ }
1230
+ #drop(id) {
1231
+ const removed = this.#conversations.delete(id);
1232
+ if (removed && this.#active === id) this.#active = void 0;
1233
+ return removed;
1234
+ }
1235
+ };
961
1236
  //#endregion
962
1237
  //#region src/core/instructions/Instruction.ts
963
1238
  /**
964
- * An immutable named directive — a {@link InstructionInterface} assembled once from its
965
- * input (`name` / `content`, an optional `priority` defaulting to `0`), the `id` minted
966
- * at construction.
1239
+ * Represents an immutable named directive — a {@link InstructionInterface} assembled once
1240
+ * from its input (`name` / `content`, an optional `priority` defaulting to `0`), the `id`
1241
+ * minted at construction.
967
1242
  *
968
1243
  * @remarks
969
1244
  * A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
970
1245
  * constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
971
1246
  * `content`, resolves `priority` to the input's value or `0`, and carries the input's
972
- * per-item `format` override ONLY when supplied (assigned just when present, mirroring a
1247
+ * per-item `override` ONLY when supplied (assigned when present, mirroring a
973
1248
  * message's `images` / `calls` present-when-given convention — kept absent otherwise).
974
1249
  * Never mutated after construction. An
975
1250
  * {@link import('./InstructionManager.js').InstructionManager} keys it by `name` and
@@ -986,38 +1261,38 @@ var Instruction = class {
986
1261
  name;
987
1262
  content;
988
1263
  priority;
989
- format;
1264
+ override;
990
1265
  constructor(input) {
991
1266
  this.id = crypto.randomUUID();
992
1267
  this.name = input.name;
993
1268
  this.content = input.content;
994
1269
  this.priority = input.priority ?? 0;
995
- if (input.format !== void 0) this.format = input.format;
1270
+ if (input.override !== void 0) this.override = input.override;
996
1271
  }
997
1272
  };
998
1273
  //#endregion
999
1274
  //#region src/core/instructions/InstructionManager.ts
1000
1275
  /**
1001
- * The instruction registry a richer context assembles a directives block from —
1002
- * immutable {@link Instruction}s keyed by `name`, listed by descending `priority`.
1276
+ * Registers the immutable {@link Instruction}s a richer context assembles a directives
1277
+ * block from — keyed by `name`, listed by descending `priority`.
1003
1278
  *
1004
1279
  * @remarks
1005
1280
  * - **Registry.** Instructions live in an insertion-ordered `Map` keyed by `name`;
1006
- * `add` takes one {@link InstructionInput} or a batch (§9.2), MINTS each instruction's
1281
+ * `add` takes one {@link InstructionInput} or a batch, MINTS each instruction's
1007
1282
  * `id`, and a re-`add` of the same name OVERWRITES it (last write wins). `count` is the
1008
1283
  * map size, `instruction(name)` looks one up, and `instructions()` lists them SORTED by
1009
1284
  * descending `priority` (a stable sort, so equal priorities keep insertion order).
1010
- * - **Build contract (with the manager-options override).** `description` is the section
1011
- * header a context renders the instructions under; `format(instruction)` renders one
1285
+ * - **Build contract (with the manager-options override).** `open` is the section
1286
+ * header a context renders the instructions under; `render(instruction)` renders one
1012
1287
  * instruction (its `content`). Each ENCAPSULATES the cascade's `[options-override →
1013
1288
  * built-in]` half: when `InstructionManagerOptions.format` supplies an `open` /
1014
- * `render`, `description` / `format` return IT, else the built-in — so a richer context
1289
+ * `render`, `open` / `render` return IT, else the built-in — so a richer context
1015
1290
  * reads one consistent pair and layers the provider default + per-item override on top
1016
1291
  * (see {@link import('../AgentContext.js').AgentContext}). The per-item
1017
- * {@link InstructionInput.format} is round-tripped onto the stored instruction.
1018
- * - **Removal.** `remove` drops one by name, or a batch (§9.2) — `true` when any was
1019
- * removed; `clear` empties the registry.
1020
- * - **Observable (§13).** The owned {@link emitter} ({@link InstructionManagerEventMap})
1292
+ * {@link InstructionInput.override} is round-tripped onto the stored instruction.
1293
+ * - **Removal.** `remove` drops one by name, or a batch — `true` only when EVERY supplied
1294
+ * name was removed; `clear` empties the registry.
1295
+ * - **Observable.** The owned {@link emitter} ({@link InstructionManagerEventMap})
1021
1296
  * carries `add` (the created instruction) / `remove` (the name) / `clear` for
1022
1297
  * fire-and-forget observers. Every event is emitted directly, strictly AFTER the map
1023
1298
  * mutation completes; the emitter isolates a listener throw and routes it to its `error`
@@ -1050,10 +1325,10 @@ var InstructionManager = class {
1050
1325
  get count() {
1051
1326
  return this.#instructions.size;
1052
1327
  }
1053
- get description() {
1328
+ get open() {
1054
1329
  return this.#format?.open ?? "## Instructions";
1055
1330
  }
1056
- get framing() {
1331
+ get format() {
1057
1332
  return this.#format;
1058
1333
  }
1059
1334
  add(input) {
@@ -1066,13 +1341,13 @@ var InstructionManager = class {
1066
1341
  instructions() {
1067
1342
  return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
1068
1343
  }
1069
- format(instruction) {
1344
+ render(instruction) {
1070
1345
  return this.#format?.render?.(instruction) ?? instruction.content;
1071
1346
  }
1072
1347
  remove(names) {
1073
1348
  if (isArray(names)) {
1074
- let removed = false;
1075
- for (const name of names) if (this.#delete(name)) removed = true;
1349
+ let removed = true;
1350
+ for (const name of names) if (!this.#delete(name)) removed = false;
1076
1351
  return removed;
1077
1352
  }
1078
1353
  return this.#delete(names);
@@ -1096,8 +1371,8 @@ var InstructionManager = class {
1096
1371
  //#endregion
1097
1372
  //#region src/core/AgentContext.ts
1098
1373
  /**
1099
- * The richer turn context the agent loop assembles a provider request from — the optional
1100
- * system prompt, the observable context managers (instructions / workspaces), the
1374
+ * Assembles a provider request from the richer turn context — the optional system prompt, the
1375
+ * observable context managers (instructions / workspaces), the
1101
1376
  * {@link ConversationManagerInterface} message source (whose active conversation IS `messages`),
1102
1377
  * the {@link ToolManagerInterface} registry, and an active {@link ScopeInterface} changed through
1103
1378
  * {@link AgentContextInterface.apply}.
@@ -1118,7 +1393,7 @@ var InstructionManager = class {
1118
1393
  * {@link MessageManagerInterface} structurally — the same reference, no duplication), and
1119
1394
  * `build()` folds that conversation's `view()` (its per-section summaries + live tail) as the
1120
1395
  * AUTHORITATIVE message inclusion — the scope does NOT filter the conversation (it owns inclusion
1121
- * via compaction; scope filters only instructions / tools / workspace files). Because `messages`
1396
+ * through compaction; scope filters only instructions / tools / workspace files). Because `messages`
1122
1397
  * is read dynamically, an agent SWITCHES the active
1123
1398
  * conversation BETWEEN runs (`conversations.switch(id)`) to serve MANY threads (the real
1124
1399
  * multi-conversation pattern); switch between runs, not during a run, and use separate agents
@@ -1128,31 +1403,31 @@ var InstructionManager = class {
1128
1403
  * (each as a block: the section's resolved `open` text, each item's resolved rendering, then
1129
1404
  * any resolved `close` text) into ONE leading `system` message (prepended only when at least
1130
1405
  * one part exists), then appends the ACTIVE conversation's `view()` (the conversation owns
1131
- * message inclusion via compaction — the scope does NOT filter the conversation). Each
1406
+ * message inclusion through compaction — the scope does NOT filter the conversation). Each
1132
1407
  * `open` / item / `close`
1133
1408
  * resolves INDEPENDENTLY, MOST-SPECIFIC-FIRST — `build()`'s optional `format` (a provider's
1134
1409
  * per-section default) is the PROVIDER level: `open` = manager-options-override > provider >
1135
1410
  * built-in; per item = item-override > manager-options-override > provider > built-in; `close` =
1136
1411
  * manager-options-override > provider (NO built-in ⇒ no closing line when unset) (see
1137
1412
  * {@link AgentContextInterface.build}). Passing NO `format` (and with no overrides / no per-item
1138
- * format) reproduces the built-in framing byte-for-byte (each section is its built-in header +
1139
- * items, no closing line). The active workspace's scoped-in image files' `data` is attached to
1413
+ * override) reproduces the built-in framing byte-for-byte (each section is its built-in header +
1414
+ * items, no closing line). The active workspace's scoped-in image files' `base64` payload is attached to
1140
1415
  * the LAST user message (a vision provider reads images off a user turn); when no user message
1141
1416
  * exists the attachment is skipped. Built fresh each call (recomputed, never cached), so it
1142
1417
  * always reflects the current managers / messages / scope / active workspace; it never mutates a
1143
1418
  * manager or the stored messages.
1144
1419
  * - **The ACTIVE workspace, rendered BY CARRIER — the SOLE document/image context.**
1145
- * `workspaces.active` (when set) has its {@link FileInterface}s scope-filtered by `scope.files`,
1420
+ * `workspaces.active` (when set) has its {@link import('@orkestrel/workspace').FileInterface}s scope-filtered by `scope.files`,
1146
1421
  * then split: TEXT files fold into a dedicated `## Workspace` system section (fenced reference
1147
- * blocks — placed right after the instructions section), and IMAGE files' base64 `data` attaches
1422
+ * blocks — placed right after the instructions section), and IMAGE files' `base64` payload attaches
1148
1423
  * to the last user message. ACTIVE-ONLY — never the other registered workspaces; with no active
1149
1424
  * workspace nothing renders for workspaces. `build()` OWNS this render (a `Workspace` /
1150
1425
  * `WorkspaceManager` stays file-focused).
1151
1426
  * - **Tools are structural, not in the prompt.** The registry is advertised to the provider
1152
- * via `tools.definitions()` (scope-filtered by the loop), NEVER serialized into the
1427
+ * through `tools.definitions()` (scope-filtered by the loop), NEVER serialized into the
1153
1428
  * message array — so `build()`'s output carries no tool content, scoped or not.
1154
1429
  * - **Event-free context; observable managers.** The context itself owns no Emitter; the
1155
- * context managers each carry their own (the §13 observation surface).
1430
+ * context managers each carry their own (the push observation surface).
1156
1431
  *
1157
1432
  * @example
1158
1433
  * ```ts
@@ -1207,12 +1482,12 @@ var AgentContext = class {
1207
1482
  const parts = [];
1208
1483
  if (this.#system !== void 0) parts.push(this.#system);
1209
1484
  const instructions = filterAllowList(scope?.instructions, this.#instructions.instructions(), (one) => one.name);
1210
- this.#section(parts, this.#header(this.#instructions, format?.instructions), instructions, (one) => this.#render(this.#instructions, format?.instructions, one), this.#footer(this.#instructions, format?.instructions));
1485
+ const instructed = renderSection(resolveOpen(this.#instructions, format?.instructions), instructions, (one) => resolveItem(this.#instructions, format?.instructions, one), resolveClose(this.#instructions, format?.instructions));
1486
+ if (instructed !== void 0) parts.push(instructed);
1211
1487
  const files = filterAllowList(scope?.files, this.#workspaces.active?.files() ?? [], (one) => one.path);
1212
- const workspaceTexts = files.filter((file) => isText(file.content));
1213
- this.#section(parts, WORKSPACE_SECTION_HEADER, workspaceTexts, (file) => this.#fenced(file), void 0);
1214
- const conversation = (this.#conversations.active ?? this.#ensure()).view();
1215
- const tail = this.#attach(conversation, this.#workspaceImages(files));
1488
+ const documented = renderSection(WORKSPACE_SECTION_HEADER, files.filter((file) => isText(file.content)), (file) => isText(file.content) ? renderFencedFile(file.path, file.content.language, file.content.text) : renderFencedFile(file.path, "text", ""), void 0);
1489
+ if (documented !== void 0) parts.push(documented);
1490
+ const tail = attachUserImages((this.#conversations.active ?? this.#ensure()).view(), collectImageData(files));
1216
1491
  if (parts.length === 0) return tail;
1217
1492
  return [{
1218
1493
  id: crypto.randomUUID(),
@@ -1220,55 +1495,6 @@ var AgentContext = class {
1220
1495
  content: parts.join("\n\n")
1221
1496
  }, ...tail];
1222
1497
  }
1223
- #section(parts, open, items, format, close) {
1224
- if (items.length === 0) return;
1225
- const lines = [open, ...items.map(format)];
1226
- if (close !== void 0) lines.push(close);
1227
- parts.push(lines.join("\n\n"));
1228
- }
1229
- #header(manager, provider) {
1230
- return manager.framing?.open ?? provider?.open ?? manager.description;
1231
- }
1232
- #footer(manager, provider) {
1233
- return manager.framing?.close ?? provider?.close;
1234
- }
1235
- #render(manager, provider, item) {
1236
- return item.format ?? manager.framing?.render?.(item) ?? provider?.render?.(item) ?? manager.format(item);
1237
- }
1238
- #attach(conversation, data) {
1239
- if (data.length === 0) return conversation;
1240
- let target = -1;
1241
- for (let index = conversation.length - 1; index >= 0; index -= 1) if (conversation[index]?.role === "user") {
1242
- target = index;
1243
- break;
1244
- }
1245
- if (target === -1) return conversation;
1246
- return conversation.map((message, index) => index === target ? this.#withImages(message, data) : message);
1247
- }
1248
- #withImages(message, data) {
1249
- const images = [...message.images ?? [], ...data];
1250
- return message.calls === void 0 ? {
1251
- id: message.id,
1252
- role: message.role,
1253
- content: message.content,
1254
- images
1255
- } : {
1256
- id: message.id,
1257
- role: message.role,
1258
- content: message.content,
1259
- calls: message.calls,
1260
- images
1261
- };
1262
- }
1263
- #fenced(file) {
1264
- if (isText(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
1265
- return fencedFile(file.path, "text", "");
1266
- }
1267
- #workspaceImages(files) {
1268
- const data = [];
1269
- for (const file of files) if (isBinary(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.data);
1270
- return data;
1271
- }
1272
1498
  #ensure() {
1273
1499
  const conversation = this.#conversations.add();
1274
1500
  return this.#conversations.active ?? conversation;
@@ -1277,9 +1503,9 @@ var AgentContext = class {
1277
1503
  //#endregion
1278
1504
  //#region src/core/Channel.ts
1279
1505
  /**
1280
- * A minimal unbounded async channel — the eager pump WRITES chunks into it (`push`)
1281
- * and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
1282
- * live via the `drain` async-iterator. Decoupling write from read is what lets a
1506
+ * Buffers chunks in a minimal unbounded async channel — the eager pump WRITES them in
1507
+ * (`push`) and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
1508
+ * live through the `drain` async-iterator. Decoupling write from read is what lets a
1283
1509
  * producer make progress without a consumer pulling.
1284
1510
  *
1285
1511
  * @remarks
@@ -1340,15 +1566,15 @@ var Channel = class {
1340
1566
  //#endregion
1341
1567
  //#region src/core/Agent.ts
1342
1568
  /**
1343
- * The agent loop — composes a {@link ProviderInterface}, an {@link AgentContext}, and
1344
- * a {@link ToolManagerInterface} into a bounded context → provider → tools → repeat
1569
+ * Composes a {@link ProviderInterface}, an {@link AgentContext}, and a
1570
+ * {@link ToolManagerInterface} into a bounded context → provider → tools → repeat
1345
1571
  * turn, exposed as both a one-shot `generate` and a live `stream`.
1346
1572
  *
1347
1573
  * @remarks
1348
1574
  * - **One loop, two faces.** A single private async generator (`#run`) drives the
1349
1575
  * whole turn. `stream` kicks off an eager pump that iterates `#run` into a private
1350
1576
  * {@link Channel}, settling `result` from the run's outcome — so `result` settles
1351
- * whether or not the live `events` are drained; `generate` simply awaits that same
1577
+ * whether or not the live `events` are drained; `generate` awaits that same
1352
1578
  * settled `result` — so the two can never diverge.
1353
1579
  * - **The turn.** `#run` builds the provider input once (`context.build()` into a
1354
1580
  * working array) then loops up to `limit`: drive `provider.stream(...)` accumulating
@@ -1357,7 +1583,7 @@ var Channel = class {
1357
1583
  * tools, append the assistant turn, `execute` them, yield a `tool` chunk per call,
1358
1584
  * append each tool result message, and continue; otherwise append the final
1359
1585
  * assistant message and stop.
1360
- * - **Bounded.** Each run arms one cancel via `createAbort({ signal: AbortSignal.any([
1586
+ * - **Bounded.** Each run arms one cancel through `createAbort({ signal: AbortSignal.any([
1361
1587
  * …]) })` folding the external `signal`, the `timeout` deadline, and the `budget`
1362
1588
  * signal; `abort()` fires it. Any trip stops the loop and commits a PARTIAL result
1363
1589
  * (the `result` promise RESOLVES, never rejects, on a cancel) — only a genuine
@@ -1380,6 +1606,7 @@ var Channel = class {
1380
1606
  * ```
1381
1607
  */
1382
1608
  var Agent = class {
1609
+ #id;
1383
1610
  #provider;
1384
1611
  #context;
1385
1612
  #limit;
@@ -1391,10 +1618,10 @@ var Agent = class {
1391
1618
  #window;
1392
1619
  #strict;
1393
1620
  #emitter;
1394
- id = crypto.randomUUID();
1395
- #status = "idle";
1621
+ #settled = "idle";
1396
1622
  #runs = /* @__PURE__ */ new Set();
1397
1623
  constructor(provider, options) {
1624
+ this.#id = crypto.randomUUID();
1398
1625
  this.#provider = provider;
1399
1626
  this.#context = new AgentContext({
1400
1627
  ...options?.system === void 0 ? {} : { system: options.system },
@@ -1417,11 +1644,14 @@ var Agent = class {
1417
1644
  ...options?.error === void 0 ? {} : { error: options.error }
1418
1645
  });
1419
1646
  }
1647
+ get id() {
1648
+ return this.#id;
1649
+ }
1420
1650
  get emitter() {
1421
1651
  return this.#emitter;
1422
1652
  }
1423
1653
  get status() {
1424
- return this.#status;
1654
+ return this.#runs.size > 0 ? "running" : this.#settled;
1425
1655
  }
1426
1656
  get context() {
1427
1657
  return this.#context;
@@ -1440,18 +1670,10 @@ var Agent = class {
1440
1670
  const signal = this.#parents(timeout, budget, options?.signal);
1441
1671
  const abort = createAbort(signal === void 0 ? {} : { signal });
1442
1672
  this.#runs.add(abort);
1443
- this.#status = "running";
1444
1673
  this.#emitter.emit("start", this.id);
1445
- const state = { outcome: {
1446
- content: "",
1447
- thinking: void 0,
1448
- usage: void 0,
1449
- partial: false,
1450
- exhausted: false
1451
- } };
1452
1674
  const channel = new Channel();
1453
- const settled = createDeferred();
1454
- this.#pump(abort, state, timeout, channel, settled, options?.think, options?.schema, limit, budget);
1675
+ const settled = Promise.withResolvers();
1676
+ this.#pump(abort, timeout, channel, settled, options?.think, options?.schema, limit, budget);
1455
1677
  settled.promise.catch(() => {});
1456
1678
  return {
1457
1679
  events: this.#events(channel, abort),
@@ -1462,26 +1684,38 @@ var Agent = class {
1462
1684
  abort(reason) {
1463
1685
  for (const abort of [...this.#runs]) abort.abort(reason);
1464
1686
  }
1465
- async #pump(abort, state, timeout, channel, settled, think, schema, limit, budget) {
1687
+ async #pump(abort, timeout, channel, settled, think, schema, limit, budget) {
1466
1688
  let failure;
1689
+ let outcome = {
1690
+ content: "",
1691
+ thinking: void 0,
1692
+ usage: void 0,
1693
+ partial: false,
1694
+ exhausted: false
1695
+ };
1467
1696
  try {
1468
- for await (const chunk of this.#run(abort, state, think, schema, limit, budget)) channel.push(chunk);
1697
+ const run = this.#run(abort, think, schema, limit, budget);
1698
+ let next = await run.next();
1699
+ while (next.done !== true) {
1700
+ channel.push(next.value);
1701
+ next = await run.next();
1702
+ }
1703
+ outcome = next.value;
1469
1704
  } catch (error) {
1470
1705
  failure = { error };
1471
1706
  } finally {
1472
1707
  timeout?.clear();
1473
1708
  this.#runs.delete(abort);
1474
1709
  if (failure === void 0) {
1475
- const outcome = state.outcome;
1476
- this.#status = "done";
1710
+ this.#settled = "done";
1477
1711
  channel.close();
1478
- const result = this.#result(outcome);
1712
+ const result = assembleResult(outcome);
1479
1713
  settled.resolve(result);
1480
1714
  if (outcome.exhausted) this.#emitter.emit("exhaust", limit);
1481
1715
  else if (outcome.partial) this.#emitter.emit("abort", abort.signal.reason);
1482
1716
  this.#emitter.emit("finish", result);
1483
1717
  } else {
1484
- this.#status = "error";
1718
+ this.#settled = "error";
1485
1719
  channel.fail(failure.error);
1486
1720
  settled.reject(failure.error);
1487
1721
  this.#emitter.emit("error", failure.error);
@@ -1495,7 +1729,7 @@ var Agent = class {
1495
1729
  abort.abort();
1496
1730
  }
1497
1731
  }
1498
- async *#run(abort, state, think, schema, limit, budget) {
1732
+ async *#run(abort, think, schema, limit, budget) {
1499
1733
  const messages = [...this.#context.build(this.#provider.format)];
1500
1734
  const tools = this.#context.tools;
1501
1735
  let content = "";
@@ -1503,11 +1737,13 @@ var Agent = class {
1503
1737
  let usage;
1504
1738
  let pending = false;
1505
1739
  let broke = false;
1506
- const compaction = { state: { futile: false } };
1740
+ let partial = false;
1741
+ let exhausted = false;
1742
+ let futile = false;
1507
1743
  const compacting = this.#window !== void 0 && this.#context.conversations.active?.summarizable === true;
1508
1744
  if (compacting) {
1509
1745
  this.#window?.clear();
1510
- if (!abort.signal.aborted) await this.#trim(messages, compaction, false);
1746
+ if (!abort.signal.aborted) futile = await this.#trim(messages, false);
1511
1747
  }
1512
1748
  for (let turn = 0; turn < limit; turn += 1) {
1513
1749
  this.#emitter.emit("turn", turn);
@@ -1515,20 +1751,14 @@ var Agent = class {
1515
1751
  await this.#scheduler?.yield({ signal: abort.signal });
1516
1752
  } catch (error) {
1517
1753
  if (abort.signal.aborted) {
1518
- state.outcome = {
1519
- ...state.outcome,
1520
- partial: true
1521
- };
1754
+ partial = true;
1522
1755
  broke = true;
1523
1756
  break;
1524
1757
  }
1525
1758
  throw error;
1526
1759
  }
1527
1760
  if (abort.signal.aborted) {
1528
- state.outcome = {
1529
- ...state.outcome,
1530
- partial: true
1531
- };
1761
+ partial = true;
1532
1762
  broke = true;
1533
1763
  break;
1534
1764
  }
@@ -1554,7 +1784,7 @@ var Agent = class {
1554
1784
  } catch (error) {
1555
1785
  if (abort.signal.aborted) {
1556
1786
  if (isProviderAbortError(error)) {
1557
- if (error.partial.thinking !== void 0) thinking = this.#thought(thinking, error.partial.thinking);
1787
+ if (error.partial.thinking !== void 0) thinking = joinThinking(thinking, error.partial.thinking);
1558
1788
  if (error.partial.usage !== void 0) {
1559
1789
  const abortUsage = sanitizeUsage(error.partial.usage);
1560
1790
  budget?.consume({
@@ -1562,19 +1792,16 @@ var Agent = class {
1562
1792
  completion: Math.max(0, abortUsage.completion - charged),
1563
1793
  total: Math.max(0, abortUsage.total - charged)
1564
1794
  });
1565
- usage = this.#sum(usage, abortUsage);
1795
+ usage = sumUsage(usage, abortUsage);
1566
1796
  }
1567
1797
  }
1568
- state.outcome = {
1569
- ...state.outcome,
1570
- partial: true
1571
- };
1798
+ partial = true;
1572
1799
  broke = true;
1573
1800
  break;
1574
1801
  }
1575
1802
  throw error;
1576
1803
  }
1577
- if (result.thinking !== void 0 && result.thinking.length > 0) thinking = this.#thought(thinking, result.thinking);
1804
+ if (result.thinking !== void 0 && result.thinking.length > 0) thinking = joinThinking(thinking, result.thinking);
1578
1805
  if (result.usage !== void 0) {
1579
1806
  const resultUsage = sanitizeUsage(result.usage);
1580
1807
  budget?.consume({
@@ -1582,10 +1809,10 @@ var Agent = class {
1582
1809
  completion: Math.max(0, resultUsage.completion - charged),
1583
1810
  total: Math.max(0, resultUsage.total - charged)
1584
1811
  });
1585
- usage = this.#sum(usage, resultUsage);
1812
+ usage = sumUsage(usage, resultUsage);
1586
1813
  this.#emitter.emit("usage", resultUsage);
1587
1814
  yield {
1588
- type: "usage",
1815
+ category: "usage",
1589
1816
  usage: resultUsage
1590
1817
  };
1591
1818
  }
@@ -1603,7 +1830,7 @@ var Agent = class {
1603
1830
  if (call === void 0 || outcomeResult === void 0) continue;
1604
1831
  this.#emitter.emit("tool", call, outcomeResult);
1605
1832
  yield {
1606
- type: "tool",
1833
+ category: "tool",
1607
1834
  call,
1608
1835
  result: outcomeResult
1609
1836
  };
@@ -1613,7 +1840,7 @@ var Agent = class {
1613
1840
  });
1614
1841
  messages.push(toolMessage);
1615
1842
  }
1616
- if (compacting) await this.#trim(messages, compaction, true);
1843
+ if (compacting && !futile) futile = await this.#trim(messages, true);
1617
1844
  pending = true;
1618
1845
  continue;
1619
1846
  }
@@ -1626,37 +1853,35 @@ var Agent = class {
1626
1853
  broke = true;
1627
1854
  break;
1628
1855
  }
1629
- if (!broke && pending) state.outcome = {
1630
- ...state.outcome,
1631
- partial: true,
1632
- exhausted: !abort.signal.aborted
1633
- };
1634
- state.outcome = {
1635
- ...state.outcome,
1856
+ if (!broke && pending) {
1857
+ partial = true;
1858
+ exhausted = !abort.signal.aborted;
1859
+ }
1860
+ return {
1636
1861
  content,
1637
1862
  thinking,
1638
- usage
1863
+ usage,
1864
+ partial,
1865
+ exhausted
1639
1866
  };
1640
1867
  }
1641
- async #trim(messages, compaction, latchFutile) {
1868
+ async #trim(messages, latch) {
1642
1869
  const conversation = this.#context.conversations.active;
1643
- if (this.#window === void 0 || conversation?.summarizable !== true || compaction.state.futile) return;
1870
+ if (this.#window === void 0 || conversation?.summarizable !== true) return false;
1644
1871
  this.#window.clear();
1645
1872
  this.#window.consume(messages);
1646
- if (!this.#window.exhausted) return;
1873
+ if (!this.#window.exhausted) return false;
1647
1874
  let section;
1648
1875
  try {
1649
1876
  section = await conversation.compact();
1650
1877
  } catch (error) {
1651
- this.#emitter.emit("compactError", error);
1878
+ this.#emitter.emit("fault", error);
1652
1879
  if (this.#strict) throw error;
1653
- return;
1654
- }
1655
- if (section === void 0) {
1656
- if (latchFutile) compaction.state = { futile: true };
1657
- return;
1880
+ return false;
1658
1881
  }
1882
+ if (section === void 0) return latch;
1659
1883
  messages.splice(0, messages.length, ...this.#context.build(this.#provider.format));
1884
+ return false;
1660
1885
  }
1661
1886
  async #authorize(tools, calls) {
1662
1887
  if (this.#authority === void 0) return tools.execute(calls);
@@ -1668,32 +1893,21 @@ var Agent = class {
1668
1893
  try {
1669
1894
  decision = authority.evaluate({ call });
1670
1895
  } catch (error) {
1671
- const reason = this.#reason(error);
1672
- denials.set(call.id, this.#denial(call, reason));
1896
+ const reason = errorToMessage(error);
1897
+ denials.set(call.id, denyCall(call, reason));
1673
1898
  this.#emitter.emit("deny", call, reason);
1674
1899
  continue;
1675
1900
  }
1676
1901
  if (decision.allowed) allowed.push(call);
1677
1902
  else {
1678
- denials.set(call.id, this.#denial(call, decision.reason));
1903
+ denials.set(call.id, denyCall(call, decision.reason));
1679
1904
  this.#emitter.emit("deny", call, decision.reason);
1680
1905
  }
1681
1906
  }
1682
1907
  const executed = allowed.length > 0 ? await tools.execute(allowed) : [];
1683
1908
  const byId = new Map(denials);
1684
1909
  for (const result of executed) byId.set(result.id, result);
1685
- return calls.map((call) => byId.get(call.id) ?? this.#denial(call, void 0));
1686
- }
1687
- #denial(call, reason) {
1688
- return {
1689
- success: false,
1690
- id: call.id,
1691
- name: call.name,
1692
- error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
1693
- };
1694
- }
1695
- #reason(error) {
1696
- return error instanceof Error ? error.message : String(error);
1910
+ return calls.map((call) => byId.get(call.id) ?? denyCall(call, void 0));
1697
1911
  }
1698
1912
  async *#provide(messages, signal, definitions, think, schema, onDelta) {
1699
1913
  const options = {};
@@ -1703,14 +1917,14 @@ var Agent = class {
1703
1917
  let next = await generator.next();
1704
1918
  while (!next.done) {
1705
1919
  const delta = next.value;
1706
- if (delta.type === "content") {
1920
+ if (delta.channel === "content") {
1707
1921
  onDelta(delta.text);
1708
1922
  yield {
1709
- type: "token",
1923
+ category: "token",
1710
1924
  content: delta.text
1711
1925
  };
1712
1926
  } else yield {
1713
- type: "think",
1927
+ category: "think",
1714
1928
  content: delta.text
1715
1929
  };
1716
1930
  next = await generator.next();
@@ -1727,34 +1941,13 @@ var Agent = class {
1727
1941
  if (signals.length === 1) return signals[0];
1728
1942
  return AbortSignal.any(signals);
1729
1943
  }
1730
- #thought(running, next) {
1731
- return running === void 0 ? next : `${running}\n\n${next}`;
1732
- }
1733
- #sum(running, next) {
1734
- if (running === void 0) return next;
1735
- return {
1736
- prompt: running.prompt + next.prompt,
1737
- completion: running.completion + next.completion,
1738
- total: running.total + next.total
1739
- };
1740
- }
1741
- #result(outcome) {
1742
- const result = {
1743
- content: outcome.content,
1744
- partial: outcome.partial
1745
- };
1746
- if (outcome.thinking !== void 0) result.thinking = outcome.thinking;
1747
- if (outcome.usage !== void 0) result.usage = outcome.usage;
1748
- return result;
1749
- }
1750
1944
  };
1751
1945
  //#endregion
1752
1946
  //#region src/core/AgentRegistry.ts
1753
1947
  /**
1754
- * The bridge that makes a durable, JSON-serializable {@link AgentJobInput} runnable —
1755
- * it holds the named pools of live, non-serializable pieces (providers, tools,
1756
- * authorities, schedulers) and rehydrates a seeded, signal-wired {@link Agent} from a
1757
- * job's names + data.
1948
+ * Makes a durable, JSON-serializable {@link AgentJobInput} runnable — holds the named
1949
+ * pools of live, non-serializable pieces (providers, tools, authorities, schedulers) and
1950
+ * rehydrates a seeded, signal-wired {@link Agent} from a job's names + data.
1758
1951
  *
1759
1952
  * @remarks
1760
1953
  * - **Why it exists.** An `AgentJobInput` is serializable so it can survive a crash in a
@@ -1762,18 +1955,18 @@ var Agent = class {
1762
1955
  * schedulers carrying functions) cannot serialize. The registry closes that gap:
1763
1956
  * construct it once with the live pools, then a queue / runner handler calls `build`
1764
1957
  * on each (possibly restored) job to get a ready agent.
1765
- * - **Accessors throw on a miss (§9.1 + §12).** `provider` / `tool` / `authority` /
1766
- * `scheduler` resolve a name against their pool and THROW `unknown <category>: <name>`
1767
- * when it is absent — an unknown name in a rehydrated job is a programmer / config
1768
- * error that must fail LOUDLY at build time, never silently resolve to `undefined` and
1769
- * run an agent missing a dependency.
1958
+ * - **Accessors throw on a miss.** `provider` / `tool` / `authority` / `scheduler` resolve a
1959
+ * name against their pool and THROW an {@link AgentError} carrying `code: 'REGISTRY'` and
1960
+ * the message `unknown <category>: <name>` when it is absent — an unknown name in a
1961
+ * rehydrated job is a programmer / config error that must fail LOUDLY at build time, never
1962
+ * silently resolve to `undefined` and run an agent missing a dependency.
1770
1963
  * - **`build` rehydrates.** Resolve the job's `provider`; assemble a fresh
1771
1964
  * {@link ToolManager} from the `tools` names; rebuild the token `budget` from its
1772
1965
  * ceiling (`createTokenBudget({ max })`); resolve the optional `authority` /
1773
1966
  * `scheduler` names; construct the {@link Agent} with `system` / `limit` / `timeout` /
1774
1967
  * the threaded `signal`; seed its context with the job's `messages`; return it. The
1775
1968
  * `signal` is the queue attempt's / runner unit's cancel, so a bounded abort propagates
1776
- * into the agent (which commits a partial — the job's `allowPartial` policy then
1969
+ * into the agent (which commits a partial — the job's `partial` policy then
1777
1970
  * decides success vs. retry).
1778
1971
  * - **Event-free.** A pure resolver — no Emitter, no events.
1779
1972
  *
@@ -1839,15 +2032,15 @@ var AgentRegistry = class {
1839
2032
  }
1840
2033
  #resolve(pool, category, name) {
1841
2034
  const value = pool.get(name);
1842
- if (value === void 0) throw new Error(`unknown ${category}: ${name}`);
2035
+ if (value === void 0) throw new AgentError("REGISTRY", `unknown ${category}: ${name}`);
1843
2036
  return value;
1844
2037
  }
1845
2038
  };
1846
2039
  //#endregion
1847
2040
  //#region src/core/Authority.ts
1848
2041
  /**
1849
- * The synchronous policy gate the agent loop consults before each tool call runs —
1850
- * it turns one {@link AuthorityContextInterface} into an {@link AuthorityDecision}.
2042
+ * Gates the agent loop's tool calls — the synchronous policy consulted before each call
2043
+ * runs, turning one {@link AuthorityContext} into an {@link AuthorityDecision}.
1851
2044
  *
1852
2045
  * @remarks
1853
2046
  * - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
@@ -1864,8 +2057,7 @@ var AgentRegistry = class {
1864
2057
  * back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
1865
2058
  * chunk + a tool message) instead of being executed — no tool run, no budget cost —
1866
2059
  * so the model sees the denial and can react.
1867
- * - **Synchronous now.** The async human-approval handshake (request / grant / deny) is
1868
- * deferred to a later chunk; `evaluate` returns a verdict directly.
2060
+ * - **Synchronous.** `evaluate` returns the verdict directly.
1869
2061
  * - **Event-free.** A purely functional gate — no Emitter, no events.
1870
2062
  *
1871
2063
  * @example
@@ -1898,9 +2090,111 @@ var Authority = class {
1898
2090
  }
1899
2091
  };
1900
2092
  //#endregion
2093
+ //#region src/core/validators.ts
2094
+ /**
2095
+ * Checks whether an `unknown` is structurally a {@link Message} record — the per-message step of
2096
+ * the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
2097
+ * read, never an assertion). The conversation analogue of
2098
+ * {@link import('@orkestrel/workspace').isFile}.
2099
+ *
2100
+ * @remarks
2101
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
2102
+ * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
2103
+ * — a `calls` array EVERY element of which is a valid
2104
+ * {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
2105
+ * fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
2106
+ * reads back as absent rather than replaying a malformed call) and an `images` that is an array
2107
+ * (an absent optional passes). The `role` is left as a broad `string` here (an open
2108
+ * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
2109
+ * defensively rather than rejected against the current literal set). Enough to safely impose the
2110
+ * {@link Message} type at
2111
+ * a storage boundary WITHOUT a cast.
2112
+ *
2113
+ * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
2114
+ * @returns True if `value` has the structural shape of a {@link Message}; false otherwise
2115
+ *
2116
+ * @example
2117
+ * ```ts
2118
+ * isMessage({ id: '1', role: 'user', content: 'hi' }) // true
2119
+ * isMessage({ id: '1', role: 'assistant', content: '', calls: [] }) // true
2120
+ * isMessage({ id: '1', role: 'user' }) // false (missing content)
2121
+ * isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
2122
+ * ```
2123
+ */
2124
+ function isMessage(value) {
2125
+ if (!isRecord(value)) return false;
2126
+ if (!isString(value.id) || !isString(value.role) || !isString(value.content)) return false;
2127
+ if (value.calls !== void 0 && !(isArray(value.calls) && value.calls.every(isToolCall))) return false;
2128
+ return value.images === void 0 || isArray(value.images);
2129
+ }
2130
+ /**
2131
+ * Checks whether an `unknown` is structurally a {@link Section} record — the per-section step of
2132
+ * the {@link isConversationSnapshot} read-boundary narrow (a total guard over an untrusted storage
2133
+ * read, never an assertion).
2134
+ *
2135
+ * @remarks
2136
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the section's
2137
+ * SHAPE: a record with a `string` `id`, a `string` `summary`, and a `messages` array EVERY element
2138
+ * of which is a valid {@link Message} record ({@link isMessage}). Enough to safely impose
2139
+ * the {@link Section} type at a storage boundary WITHOUT a cast.
2140
+ *
2141
+ * @param value - The value to test (one element of a snapshot's `sections` array)
2142
+ * @returns True if `value` has the structural shape of a {@link Section}; false otherwise
2143
+ *
2144
+ * @example
2145
+ * ```ts
2146
+ * isSection({ id: 's', summary: 'recap', messages: [{ id: '1', role: 'user', content: 'hi' }] }) // true
2147
+ * isSection({ id: 's', summary: 'recap', messages: 'nope' }) // false
2148
+ * isSection({ id: 's', messages: [] }) // false (missing summary)
2149
+ * ```
2150
+ */
2151
+ function isSection(value) {
2152
+ if (!isRecord(value)) return false;
2153
+ if (!isString(value.id) || !isString(value.summary)) return false;
2154
+ return isArray(value.messages) && value.messages.every(isMessage);
2155
+ }
2156
+ /**
2157
+ * Narrows an `unknown` to a {@link ConversationSnapshot} — the total boundary guard for an
2158
+ * UNTRUSTED snapshot read (a storage row a
2159
+ * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
2160
+ * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
2161
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
2162
+ *
2163
+ * @remarks
2164
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
2165
+ * SHAPE: a `string` `id`, an OPTIONAL `string` `summary` (present-or-absent — the rollup is
2166
+ * `undefined` until the first compaction), a `sections` array EVERY element of which is a valid
2167
+ * {@link Section} ({@link isSection}), and a `messages` array EVERY element of which is a
2168
+ * valid {@link Message} ({@link isMessage}) — enough to safely impose the
2169
+ * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
2170
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
2171
+ * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
2172
+ * element) resolves `false`, so a
2173
+ * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
2174
+ * read yields `undefined` rather than a broken conversation.
2175
+ *
2176
+ * @param value - The value to test (an opaque storage read)
2177
+ * @returns True if `value` has the structural shape of a {@link ConversationSnapshot}; false otherwise
2178
+ *
2179
+ * @example
2180
+ * ```ts
2181
+ * isConversationSnapshot({ id: 'c1', sections: [], messages: [] }) // true
2182
+ * isConversationSnapshot({ id: 'c1', summary: 'recap', sections: [], messages: [] }) // true
2183
+ * isConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // false
2184
+ * isConversationSnapshot({ sections: [], messages: [] }) // false (missing id)
2185
+ * ```
2186
+ */
2187
+ function isConversationSnapshot(value) {
2188
+ if (!isRecord(value)) return false;
2189
+ if (!isString(value.id)) return false;
2190
+ if (value.summary !== void 0 && !isString(value.summary)) return false;
2191
+ if (!isArray(value.sections) || !value.sections.every(isSection)) return false;
2192
+ return isArray(value.messages) && value.messages.every(isMessage);
2193
+ }
2194
+ //#endregion
1901
2195
  //#region src/core/conversations/stores/DatabaseConversationStore.ts
1902
2196
  /**
1903
- * A {@link ConversationStoreInterface} backed by one table of the `databases` layer — a
2197
+ * Backs a {@link ConversationStoreInterface} with one table of the `databases` layer — a
1904
2198
  * conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
1905
2199
  * / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
1906
2200
  * {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
@@ -1926,19 +2220,20 @@ var Authority = class {
1926
2220
  * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
1927
2221
  * the row `{ id: snapshot.id, snapshot }`.
1928
2222
  * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
1929
- * a {@link ConversationSnapshot} ({@link import('../../helpers.js').isConversationSnapshot} — the
1930
- * AGENTS §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
2223
+ * a {@link ConversationSnapshot} ({@link import('../../validators.js').isConversationSnapshot} — the
2224
+ * total guard for an untrusted storage read), or `undefined` if none is stored.
1931
2225
  * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
1932
2226
  *
1933
2227
  * UNLIKE a session store there is NO idle-TTL / eviction — a persisted conversation lives until an
1934
2228
  * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
1935
- * §22 method bijection with {@link ConversationStoreInterface}). Hydration stays a caller concern: a
2229
+ * method bijection with {@link ConversationStoreInterface}). Hydration stays a caller concern: a
1936
2230
  * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
1937
- * the live conversation through the constructor `seed` (its `open` / `save`).
2231
+ * the live conversation through the `snapshot` option (its `open` / `save`).
1938
2232
  *
1939
2233
  * @example
1940
2234
  * ```ts
1941
- * import { createConversation, createDatabaseConversationStore, createMemoryDriver } from '@src/core'
2235
+ * import { createConversation, createDatabaseConversationStore } from '@orkestrel/agent'
2236
+ * import { createMemoryDriver } from '@orkestrel/database'
1942
2237
  *
1943
2238
  * const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
1944
2239
  * const conversation = createConversation()
@@ -1951,7 +2246,7 @@ var Authority = class {
1951
2246
  var DatabaseConversationStore = class {
1952
2247
  #table;
1953
2248
  /**
1954
- * Wrap a table as a conversation store.
2249
+ * Wraps a table as a conversation store.
1955
2250
  *
1956
2251
  * @param table - The {@link TableInterface} holding the snapshots — its row is the
1957
2252
  * {@link ConversationSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
@@ -1959,20 +2254,20 @@ var DatabaseConversationStore = class {
1959
2254
  constructor(table) {
1960
2255
  this.#table = table;
1961
2256
  }
1962
- /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `ConversationSnapshot`. */
2257
+ /** Resolves the persisted snapshot for `id`, narrowing the opaque JSON column back to a `ConversationSnapshot`. */
1963
2258
  async get(id) {
1964
2259
  const row = await this.#table.get(id);
1965
2260
  if (row === void 0) return void 0;
1966
2261
  return isConversationSnapshot(row.snapshot) ? row.snapshot : void 0;
1967
2262
  }
1968
- /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
2263
+ /** Inserts or replaces under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
1969
2264
  async set(snapshot) {
1970
2265
  await this.#table.set({
1971
2266
  id: snapshot.id,
1972
2267
  snapshot
1973
2268
  });
1974
2269
  }
1975
- /** Drop a snapshot by id; an absent id is a no-op (no throw). */
2270
+ /** Drops a snapshot by id; an absent id is a no-op (no throw). */
1976
2271
  async delete(id) {
1977
2272
  await this.#table.remove(id);
1978
2273
  }
@@ -1980,14 +2275,14 @@ var DatabaseConversationStore = class {
1980
2275
  //#endregion
1981
2276
  //#region src/core/conversations/stores/MemoryConversationStore.ts
1982
2277
  /**
1983
- * The in-memory {@link ConversationStoreInterface} — a process-lifetime `Map` of
2278
+ * Implements the {@link ConversationStoreInterface} in memory — a process-lifetime `Map` of
1984
2279
  * {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
1985
2280
  * {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
1986
2281
  * {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
1987
2282
  *
1988
2283
  * @remarks
1989
- * A plain `Map<string, ConversationSnapshot>` (AGENTS §21 — the snapshot is already pure,
1990
- * self-contained JSON, so no encoding is needed for the memory tier). Like the
2284
+ * A plain `Map<string, ConversationSnapshot>` — the snapshot is already pure,
2285
+ * self-contained JSON, so no encoding is needed for the memory tier. Like the
1991
2286
  * {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
1992
2287
  * there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
1993
2288
  * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
@@ -2000,14 +2295,14 @@ var DatabaseConversationStore = class {
2000
2295
  * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
2001
2296
  * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
2002
2297
  *
2003
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
2298
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the method
2004
2299
  * bijection with {@link ConversationStoreInterface}). Hydration is a caller concern: a
2005
2300
  * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
2006
- * the live conversation through the constructor `seed` (its `open` / `save`).
2301
+ * the live conversation through the `snapshot` option (its `open` / `save`).
2007
2302
  *
2008
2303
  * @example
2009
2304
  * ```ts
2010
- * import { createConversation, createMemoryConversationStore } from '@src/core'
2305
+ * import { createConversation, createMemoryConversationStore } from '@orkestrel/agent'
2011
2306
  *
2012
2307
  * const store = createMemoryConversationStore()
2013
2308
  * const conversation = createConversation()
@@ -2034,14 +2329,14 @@ var MemoryConversationStore = class {
2034
2329
  //#endregion
2035
2330
  //#region src/core/scopes/Scope.ts
2036
2331
  /**
2037
- * A named, immutable filter over a richer context's items — three optional allow-lists,
2038
- * one per category (`instructions` / `tools` / `files`), each keyed by that category's
2039
- * identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
2332
+ * Represents a named, immutable filter over a richer context's items — an optional
2333
+ * allow-list per category (`instructions` / `tools` / `files`), each keyed by that
2334
+ * category's identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
2040
2335
  *
2041
2336
  * @remarks
2042
2337
  * - **A category list is three-way.** `undefined` ⇒ NO constraint on that category (all
2043
2338
  * pass); `[]` ⇒ NONE pass; a non-empty list ⇒ only the listed keys pass. The build
2044
- * step / loop apply this via `filterAllowList`.
2339
+ * step / loop apply this through `filterAllowList`.
2045
2340
  * - **Immutable.** The `id` is minted at construction; every supplied list is COPIED in
2046
2341
  * (so a later mutation of the caller's array can't leak in), and the lists are
2047
2342
  * `readonly`. A `Scope` is never mutated after construction — `narrow` returns a NEW
@@ -2076,9 +2371,9 @@ var Scope = class Scope {
2076
2371
  if (input.files !== void 0) this.files = [...input.files];
2077
2372
  }
2078
2373
  narrow(config) {
2079
- const instructions = Scope.#intersect(this.instructions, config.instructions);
2080
- const tools = Scope.#intersect(this.tools, config.tools);
2081
- const files = Scope.#intersect(this.files, config.files);
2374
+ const instructions = intersectKeys(this.instructions, config.instructions);
2375
+ const tools = intersectKeys(this.tools, config.tools);
2376
+ const files = intersectKeys(this.files, config.files);
2082
2377
  return new Scope({
2083
2378
  name: this.name,
2084
2379
  ...instructions === void 0 ? {} : { instructions },
@@ -2086,18 +2381,12 @@ var Scope = class Scope {
2086
2381
  ...files === void 0 ? {} : { files }
2087
2382
  });
2088
2383
  }
2089
- static #intersect(parent, child) {
2090
- if (parent === void 0) return child === void 0 ? void 0 : [...child];
2091
- if (child === void 0) return [...parent];
2092
- const allowed = new Set(parent);
2093
- return child.filter((key) => allowed.has(key));
2094
- }
2095
2384
  };
2096
2385
  //#endregion
2097
2386
  //#region src/core/scopes/ScopeManager.ts
2098
2387
  /**
2099
- * The scope registry a richer context reuses named filters from — immutable {@link Scope}s
2100
- * keyed by their minted `id`, in insertion order.
2388
+ * Registers the named filters a richer context reuses — immutable {@link Scope}s keyed by
2389
+ * their minted `id`, in insertion order.
2101
2390
  *
2102
2391
  * @remarks
2103
2392
  * - **Registry.** Scopes live in an insertion-ordered `Map` keyed by their minted `id`;
@@ -2106,9 +2395,9 @@ var Scope = class Scope {
2106
2395
  * one up, and `scopes()` lists them in insertion order. (Unlike the name-keyed
2107
2396
  * instruction registry, a scope's key is its minted `id`, so two scopes may share a
2108
2397
  * `name`; `create` therefore always adds — it never overwrites.)
2109
- * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was
2110
- * removed; `clear` empties the registry.
2111
- * - **Observable (§13).** The owned {@link emitter} ({@link ScopeManagerEventMap}) carries
2398
+ * - **Removal.** `remove` drops one by id, or a batch — `true` only when EVERY supplied id
2399
+ * was removed; `clear` empties the registry.
2400
+ * - **Observable.** The owned {@link emitter} ({@link ScopeManagerEventMap}) carries
2112
2401
  * `create` (the created scope) / `remove` (the id) / `clear`. Every event is emitted
2113
2402
  * directly, strictly AFTER the map mutation completes; the emitter isolates a listener
2114
2403
  * throw and routes it to its `error` handler (the `error` option), so a buggy observer can
@@ -2125,10 +2414,10 @@ var Scope = class Scope {
2125
2414
  var ScopeManager = class {
2126
2415
  #scopes = /* @__PURE__ */ new Map();
2127
2416
  #emitter;
2128
- constructor(on, error) {
2417
+ constructor(options) {
2129
2418
  this.#emitter = new Emitter({
2130
- ...on === void 0 ? {} : { on },
2131
- ...error === void 0 ? {} : { error }
2419
+ ...options?.on === void 0 ? {} : { on: options.on },
2420
+ ...options?.error === void 0 ? {} : { error: options.error }
2132
2421
  });
2133
2422
  }
2134
2423
  get emitter() {
@@ -2151,8 +2440,8 @@ var ScopeManager = class {
2151
2440
  }
2152
2441
  remove(ids) {
2153
2442
  if (isArray(ids)) {
2154
- let removed = false;
2155
- for (const id of ids) if (this.#delete(id)) removed = true;
2443
+ let removed = true;
2444
+ for (const id of ids) if (!this.#delete(id)) removed = false;
2156
2445
  return removed;
2157
2446
  }
2158
2447
  return this.#delete(ids);
@@ -2170,10 +2459,10 @@ var ScopeManager = class {
2170
2459
  //#endregion
2171
2460
  //#region src/core/ThinkSplitter.ts
2172
2461
  /**
2173
- * The stream-stateful `<think>` separator — feeds raw content deltas through a tiny
2174
- * state machine that routes everything inside a `<think>…</think>` span to `thinking`
2175
- * and returns everything outside it as clean content, so a provider yields ONLY the
2176
- * answer and surfaces the reasoning as {@link import('./types.js').ProviderResult.thinking}.
2462
+ * Feeds raw content deltas through a tiny stream-stateful state machine that routes
2463
+ * everything inside a `<think>…</think>` span to `thinking` and returns everything outside
2464
+ * it as clean content, so a provider yields ONLY the answer and surfaces the reasoning as
2465
+ * {@link import('./types.js').ProviderResult.thinking}.
2177
2466
  *
2178
2467
  * @remarks
2179
2468
  * - **Cross-chunk tags.** A tag may arrive split across wire deltas (`'<thi'` then
@@ -2189,7 +2478,7 @@ var ScopeManager = class {
2189
2478
  * prefix cannot be recalled, so `content` is the authoritative accumulation). The
2190
2479
  * rule is ONE-SHOT: after any tag event a bare `</think>` is plain text.
2191
2480
  * - **Multiple spans** accumulate onto `thinking` in stream order. A nested-looking
2192
- * `<think>` inside an open span is just thinking text (no nesting is tracked — the
2481
+ * `<think>` inside an open span is thinking text (no nesting is tracked — the
2193
2482
  * first `</think>` closes the span), matching how the models emit it.
2194
2483
  * - **Unclosed span at stream end.** `flush()` routes the open span's tail (including
2195
2484
  * any held partial close tag) to `thinking` — a cut-off model was still reasoning.
@@ -2288,28 +2577,28 @@ var ThinkSplitter = class {
2288
2577
  //#endregion
2289
2578
  //#region src/core/factories.ts
2290
2579
  /**
2291
- * Create a conversation — a {@link ConversationInterface} grouping messages above a flat
2580
+ * Creates a conversation — a {@link ConversationInterface} grouping messages above a flat
2292
2581
  * message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
2293
2582
  * rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
2294
- * provider-agnostic {@link ConversationSummarizer} seam.
2583
+ * provider-agnostic {@link ConversationSummaryHandler} seam.
2295
2584
  *
2296
2585
  * @remarks
2297
2586
  * Append turns through the conversation's own `add` (the live tail it owns); `view()` is the model input
2298
2587
  * (each section as a summary message, then the live tail). `compact()` folds the older live
2299
- * messages into a summarized {@link SectionInterface} and regenerates the rollup — it REQUIRES
2588
+ * messages into a summarized {@link Section} and regenerates the rollup — it REQUIRES
2300
2589
  * a `summarize` (omitted ⇒ `compact()` throws a `ConversationError`); `keep` retains a recent
2301
2590
  * tail (default `DEFAULT_CONVERSATION_KEEP` — fold ALL). `rehydrate(id)` / `search(query)` read
2302
2591
  * the retained originals. Observable (`emitter` — `compact` / `summary` / `rehydrate`), wired
2303
- * via the reserved `on` option (§8); the emitter isolates a listener throw and routes it to
2304
- * its `error` handler (the `error` option, §13), so it can never corrupt a compaction.
2592
+ * through the reserved `on` option; the emitter isolates a listener throw and routes it to
2593
+ * its `error` handler (the `error` option), so it can never corrupt a compaction.
2305
2594
  *
2306
2595
  * @param options - Optional `id` / `on` hooks + the `summarize` seam + `keep` (see {@link ConversationOptions})
2307
2596
  * @returns A working {@link ConversationInterface}
2308
2597
  *
2309
2598
  * @example
2310
2599
  * ```ts
2311
- * import type { ProviderInterface } from '@src/core'
2312
- * import { createConversation } from '@src/core'
2600
+ * import type { ProviderInterface } from '@orkestrel/agent'
2601
+ * import { createConversation } from '@orkestrel/agent'
2313
2602
  *
2314
2603
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
2315
2604
  * const conversation = createConversation({
@@ -2326,9 +2615,10 @@ function createConversation(options) {
2326
2615
  return new Conversation(options);
2327
2616
  }
2328
2617
  /**
2329
- * Create a conversation registry — a {@link ConversationManagerInterface} holding
2618
+ * Creates a conversation registry — a {@link ConversationManagerInterface} holding
2330
2619
  * {@link ConversationInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
2331
- * the §9 store over the conversation layer plus the `active` / `switch` seam the context renders.
2620
+ * the id-keyed store over the conversation layer plus the `active` / `switch` seam the context
2621
+ * renders.
2332
2622
  *
2333
2623
  * @remarks
2334
2624
  * Starts empty; `add(input?)` mints a {@link ConversationInterface} (its `id` from the input
@@ -2337,8 +2627,9 @@ function createConversation(options) {
2337
2627
  * AUTO-ACTIVATES the FIRST one (a registry with conversations always has one `active`); a later
2338
2628
  * `add` leaves `active` unchanged. `switch(id)` re-points `active` (an unknown `id` returns
2339
2629
  * `undefined`, leaving `active` unchanged — lenient, never throws); `conversation(id)` /
2340
- * `conversations()` look up; `remove` (one or a batch, §9.2) reports whether any was removed AND
2341
- * clears `active` if it was the removed one; `clear` empties it and clears `active`. Event-free
2630
+ * `conversations()` look up; `remove` (one or a batch) reports `true` only when every supplied id
2631
+ * was removed AND clears `active` if it was a removed one; `clear` empties it and clears `active`.
2632
+ * Event-free
2342
2633
  * (each conversation owns its own observable `emitter`). A conversation created with NEITHER a
2343
2634
  * manager default nor a per-`add` `summarize` cannot `compact` (it throws a `ConversationError`).
2344
2635
  *
@@ -2347,7 +2638,7 @@ function createConversation(options) {
2347
2638
  *
2348
2639
  * @example
2349
2640
  * ```ts
2350
- * import { createConversationManager } from '@src/core'
2641
+ * import { createConversationManager } from '@orkestrel/agent'
2351
2642
  *
2352
2643
  * const conversations = createConversationManager({ summarize: async (m) => `recap of ${m.length}` })
2353
2644
  * const chat = conversations.add() // auto-activates — conversations.active === chat
@@ -2358,7 +2649,7 @@ function createConversationManager(options) {
2358
2649
  return new ConversationManager(options);
2359
2650
  }
2360
2651
  /**
2361
- * Create the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
2652
+ * Creates the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
2362
2653
  * process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
2363
2654
  * id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
2364
2655
  * {@link ConversationManagerInterface.save} seam. The exact twin of
@@ -2373,14 +2664,14 @@ function createConversationManager(options) {
2373
2664
  * {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
2374
2665
  * table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB driver, and it swaps in WITHOUT
2375
2666
  * touching the manager or the conversation. Hydration stays a manager concern: read a snapshot back
2376
- * and rebuild the live conversation through the constructor `seed` (re-supplying the live
2667
+ * and rebuild the live conversation through the `snapshot` option (re-supplying the live
2377
2668
  * `summarize` / `keep`).
2378
2669
  *
2379
2670
  * @returns A memory-backed {@link ConversationStoreInterface}
2380
2671
  *
2381
2672
  * @example
2382
2673
  * ```ts
2383
- * import { createConversationManager, createMemoryConversationStore } from '@src/core'
2674
+ * import { createConversationManager, createMemoryConversationStore } from '@orkestrel/agent'
2384
2675
  *
2385
2676
  * const store = createMemoryConversationStore()
2386
2677
  * const manager = createConversationManager({ store })
@@ -2393,7 +2684,7 @@ function createMemoryConversationStore() {
2393
2684
  return new MemoryConversationStore();
2394
2685
  }
2395
2686
  /**
2396
- * Create a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
2687
+ * Creates a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
2397
2688
  * driver-pluggable backing for the conversation persistence seam, the opt-in twin of
2398
2689
  * {@link createMemoryConversationStore}. The exact twin of
2399
2690
  * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
@@ -2405,7 +2696,7 @@ function createMemoryConversationStore() {
2405
2696
  * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
2406
2697
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
2407
2698
  * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
2408
- * {@link import('./helpers.js').isConversationSnapshot}). The `driver` DEFAULTS to
2699
+ * {@link import('./validators.js').isConversationSnapshot}). The `driver` DEFAULTS to
2409
2700
  * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
2410
2701
  * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
2411
2702
  * the durability is the driver's job, the store engine is shared. It swaps in behind
@@ -2416,7 +2707,8 @@ function createMemoryConversationStore() {
2416
2707
  *
2417
2708
  * @example
2418
2709
  * ```ts
2419
- * import { createConversationManager, createDatabaseConversationStore, createMemoryDriver } from '@src/core'
2710
+ * import { createConversationManager, createDatabaseConversationStore } from '@orkestrel/agent'
2711
+ * import { createMemoryDriver } from '@orkestrel/database'
2420
2712
  *
2421
2713
  * const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
2422
2714
  * const manager = createConversationManager({ store })
@@ -2436,7 +2728,7 @@ function createDatabaseConversationStore(driver = createMemoryDriver()) {
2436
2728
  }).table("conversations"));
2437
2729
  }
2438
2730
  /**
2439
- * Create an instruction — an immutable {@link InstructionInterface} (a named directive)
2731
+ * Creates an instruction — an immutable {@link InstructionInterface} (a named directive)
2440
2732
  * from its `name` / `content` and optional `priority`, the `id` minted at construction.
2441
2733
  *
2442
2734
  * @remarks
@@ -2450,7 +2742,7 @@ function createDatabaseConversationStore(driver = createMemoryDriver()) {
2450
2742
  *
2451
2743
  * @example
2452
2744
  * ```ts
2453
- * import { createInstruction } from '@src/core'
2745
+ * import { createInstruction } from '@orkestrel/agent'
2454
2746
  *
2455
2747
  * const instruction = createInstruction({ name: 'tone', content: 'Be concise.', priority: 5 })
2456
2748
  * ```
@@ -2459,28 +2751,28 @@ function createInstruction(input) {
2459
2751
  return new Instruction(input);
2460
2752
  }
2461
2753
  /**
2462
- * Create an instruction registry — an {@link InstructionManagerInterface} holding
2754
+ * Creates an instruction registry — an {@link InstructionManagerInterface} holding
2463
2755
  * immutable instructions keyed by `name`, listed by descending `priority`.
2464
2756
  *
2465
2757
  * @remarks
2466
- * Starts empty; `add` (one or a batch, §9.2) MINTS each `id` and OVERWRITES a same-name
2758
+ * Starts empty; `add` (one or a batch) MINTS each `id` and OVERWRITES a same-name
2467
2759
  * instruction (last write wins); `instructions()` lists them sorted by descending
2468
- * `priority` (stable for ties); `format` / `description` are the build contract a richer
2469
- * context renders an instructions block with; `remove` (one or a batch) reports whether
2470
- * any was removed; `clear` empties it. Carries an observable `emitter`
2471
- * ({@link import('./types.js').InstructionManagerEventMap}) wired via the reserved `on`
2472
- * option (§8); the emitter isolates a listener throw and routes it to its `error` handler
2473
- * (the `error` option, §13), so it can never corrupt a mutation. An optional `format`
2760
+ * `priority` (stable for ties); `open` / `render` are the build contract a richer
2761
+ * context renders an instructions block with; `remove` (one or a batch) reports `true` only
2762
+ * when every supplied name was removed; `clear` empties it. Carries an observable `emitter`
2763
+ * ({@link import('./types.js').InstructionManagerEventMap}) wired through the reserved `on`
2764
+ * option; the emitter isolates a listener throw and routes it to its `error` handler
2765
+ * (the `error` option), so it can never corrupt a mutation. An optional `format`
2474
2766
  * override is the manager-options level of the `AgentContext` build cascade (consulted by
2475
- * `description` / `format`, beating the provider default + built-in; a per-item
2476
- * `InstructionInput.format` still beats it).
2767
+ * `open` / `render`, beating the provider default + built-in; a per-item
2768
+ * `InstructionInput.override` still beats it).
2477
2769
  *
2478
2770
  * @param options - Optional `on` hooks + a `format` override (see {@link InstructionManagerOptions})
2479
2771
  * @returns An empty {@link InstructionManagerInterface}
2480
2772
  *
2481
2773
  * @example
2482
2774
  * ```ts
2483
- * import { createInstructionManager } from '@src/core'
2775
+ * import { createInstructionManager } from '@orkestrel/agent'
2484
2776
  *
2485
2777
  * const instructions = createInstructionManager()
2486
2778
  * instructions.add({ name: 'tone', content: 'Be concise.', priority: 5 })
@@ -2490,8 +2782,8 @@ function createInstructionManager(options) {
2490
2782
  return new InstructionManager(options);
2491
2783
  }
2492
2784
  /**
2493
- * Create a named scope — an immutable {@link ScopeInterface} from its `name` and the four
2494
- * optional per-category allow-lists, the `id` minted at construction.
2785
+ * Creates a named scope — an immutable {@link ScopeInterface} from its `name` and its
2786
+ * per-category allow-lists, the `id` minted at construction.
2495
2787
  *
2496
2788
  * @remarks
2497
2789
  * Each list is THREE-WAY: `undefined` ⇒ NO constraint on that category (all pass), `[]` ⇒
@@ -2499,13 +2791,13 @@ function createInstructionManager(options) {
2499
2791
  * tighter child by set-INTERSECTION (an `undefined` side imposing no constraint). Stored
2500
2792
  * immutable — never mutated after creation (`narrow` returns a new scope).
2501
2793
  *
2502
- * @param input - `name` (required) and the optional `instructions` / `tools` / `messages` /
2503
- * `files` allow-lists (see {@link ScopeInput})
2794
+ * @param input - `name` (required) and the optional `instructions` / `tools` / `files`
2795
+ * allow-lists (see {@link ScopeInput})
2504
2796
  * @returns A working {@link ScopeInterface}
2505
2797
  *
2506
2798
  * @example
2507
2799
  * ```ts
2508
- * import { createScope } from '@src/core'
2800
+ * import { createScope } from '@orkestrel/agent'
2509
2801
  *
2510
2802
  * const reader = createScope({ name: 'reader', tools: ['search', 'read'] })
2511
2803
  * reader.narrow({ tools: ['read', 'write'] }).tools // ['read'] — intersection tightens
@@ -2515,33 +2807,34 @@ function createScope(input) {
2515
2807
  return new Scope(input);
2516
2808
  }
2517
2809
  /**
2518
- * Create a scope registry — a {@link ScopeManagerInterface} holding immutable scopes keyed
2810
+ * Creates a scope registry — a {@link ScopeManagerInterface} holding immutable scopes keyed
2519
2811
  * by their minted `id`, in insertion order.
2520
2812
  *
2521
2813
  * @remarks
2522
2814
  * Starts empty; `create` mints each scope's `id` and stores it (keyed by `id`, so it
2523
2815
  * always adds — two scopes may share a `name`); `scopes()` lists them in insertion order;
2524
- * `remove` (one or a batch, §9.2) reports whether any was removed; `clear` empties it.
2525
- * Carries an observable `emitter` ({@link import('./types.js').ScopeManagerEventMap}) wired
2526
- * via the reserved `on` option (§8); the emitter isolates a listener throw and routes it to
2527
- * its `error` handler (the `error` option, §13), so it can never corrupt a mutation.
2816
+ * `remove` (one or a batch) reports `true` only when every supplied id was removed; `clear`
2817
+ * empties it. Carries an observable `emitter`
2818
+ * ({@link import('./types.js').ScopeManagerEventMap}) wired through the reserved `on`
2819
+ * option; the emitter isolates a listener throw and routes it to its `error` handler
2820
+ * (the `error` option), so it can never corrupt a mutation.
2528
2821
  *
2529
2822
  * @param options - Optional `on` hooks (see {@link ScopeManagerOptions})
2530
2823
  * @returns An empty {@link ScopeManagerInterface}
2531
2824
  *
2532
2825
  * @example
2533
2826
  * ```ts
2534
- * import { createScopeManager } from '@src/core'
2827
+ * import { createScopeManager } from '@orkestrel/agent'
2535
2828
  *
2536
2829
  * const scopes = createScopeManager()
2537
2830
  * const reader = scopes.create({ name: 'reader', tools: ['search'] })
2538
2831
  * ```
2539
2832
  */
2540
2833
  function createScopeManager(options) {
2541
- return new ScopeManager(options?.on, options?.error);
2834
+ return new ScopeManager(options);
2542
2835
  }
2543
2836
  /**
2544
- * Create a richer turn context — an {@link AgentContextInterface} assembling a provider
2837
+ * Creates a richer turn context — an {@link AgentContextInterface} assembling a provider
2545
2838
  * request from the optional system prompt, the instruction registry, the workspace registry,
2546
2839
  * the conversation store, the tool registry, and the active scope.
2547
2840
  *
@@ -2549,11 +2842,11 @@ function createScopeManager(options) {
2549
2842
  * `system` is the optional system prompt; `tools` / `instructions` / `workspaces` are pre-built
2550
2843
  * managers to reuse (empty ones are created when omitted, so `context.workspaces` is ALWAYS
2551
2844
  * present); `scope` is the initial active filter (`undefined` ⇒ no filtering, changeable afterwards
2552
- * via `context.apply(...)`). The `messages` store is always fresh. `build()` folds the scoped
2845
+ * through `context.apply(...)`). The `messages` store is always fresh. `build()` folds the scoped
2553
2846
  * instructions — PLUS the ACTIVE workspace's scope-filtered text files (fenced) — into ONE leading
2554
2847
  * `system` message and appends the scoped conversation (attaching the active workspace's
2555
- * scope-filtered image files' `data` to the last user message), built fresh each call; the active
2556
- * workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (via
2848
+ * scope-filtered image files' `base64` payload to the last user message), built fresh each call; the active
2849
+ * workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (through
2557
2850
  * `tools.definitions()`, scope-filtered by the loop), never serialized into the prompt.
2558
2851
  *
2559
2852
  * @param options - Optional `system` / `tools` / `instructions` / `workspaces` / `scope`
@@ -2562,7 +2855,7 @@ function createScopeManager(options) {
2562
2855
  *
2563
2856
  * @example
2564
2857
  * ```ts
2565
- * import { createAgentContext } from '@src/core'
2858
+ * import { createAgentContext } from '@orkestrel/agent'
2566
2859
  *
2567
2860
  * const context = createAgentContext({ system: 'You are concise.' })
2568
2861
  * context.instructions.add({ name: 'tone', content: 'Be terse.' })
@@ -2574,7 +2867,7 @@ function createAgentContext(options) {
2574
2867
  return new AgentContext(options);
2575
2868
  }
2576
2869
  /**
2577
- * Create an agent loop — an {@link AgentInterface} composing a
2870
+ * Creates an agent loop — an {@link AgentInterface} composing a
2578
2871
  * {@link ProviderInterface}, its {@link AgentContextInterface}, and a tool registry
2579
2872
  * into a bounded context → provider → tools → repeat turn, exposed as a one-shot
2580
2873
  * `generate` and a live `stream`.
@@ -2582,11 +2875,11 @@ function createAgentContext(options) {
2582
2875
  * @remarks
2583
2876
  * One private loop drives the turn; `generate` DRAINS the same stream `stream`
2584
2877
  * exposes, so they can never diverge. Each turn is bounded by one cancel folded from
2585
- * `signal` + `timeout` + `budget` (via `AbortSignal.any`) — any trip (or `abort()`)
2878
+ * `signal` + `timeout` + `budget` (through `AbortSignal.any`) — any trip (or `abort()`)
2586
2879
  * commits a PARTIAL result (the stream's `result` RESOLVES on a cancel, rejects only
2587
2880
  * on a genuine provider / tool error). The `scheduler` paces between turns; tool
2588
2881
  * iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Tools are advertised
2589
- * structurally via `context.tools.definitions()`. Two observation surfaces: the
2882
+ * structurally through `context.tools.definitions()`. Two observation surfaces: the
2590
2883
  * {@link AgentChunk} stream (pull — per-token content) and a typed `emitter` (push —
2591
2884
  * lifecycle + `usage` / `tool` / `deny` for fire-and-forget observers).
2592
2885
  *
@@ -2597,8 +2890,9 @@ function createAgentContext(options) {
2597
2890
  *
2598
2891
  * @example
2599
2892
  * ```ts
2600
- * import type { ProviderInterface } from '@src/core'
2601
- * import { createAgent, createTokenBudget } from '@src/core'
2893
+ * import type { ProviderInterface } from '@orkestrel/agent'
2894
+ * import { createAgent } from '@orkestrel/agent'
2895
+ * import { createTokenBudget } from '@orkestrel/budget'
2602
2896
  *
2603
2897
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
2604
2898
  * const agent = createAgent(provider, {
@@ -2609,7 +2903,7 @@ function createAgentContext(options) {
2609
2903
  *
2610
2904
  * const stream = agent.stream()
2611
2905
  * for await (const chunk of stream.events) {
2612
- * if (chunk.type === 'token') process.stdout.write(chunk.content)
2906
+ * if (chunk.category === 'token') process.stdout.write(chunk.content)
2613
2907
  * }
2614
2908
  * const result = await stream.result // { content, usage?, partial }
2615
2909
  * ```
@@ -2618,7 +2912,7 @@ function createAgent(provider, options) {
2618
2912
  return new Agent(provider, options);
2619
2913
  }
2620
2914
  /**
2621
- * Create a stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
2915
+ * Creates a stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
2622
2916
  * splits a thinking model's in-content `<think>…</think>` reasoning spans away from
2623
2917
  * the answer, delta by delta, so a provider yields ONLY clean content and surfaces the
2624
2918
  * accumulated reasoning as {@link import('./types.js').ProviderResult.thinking}.
@@ -2629,14 +2923,14 @@ function createAgent(provider, options) {
2629
2923
  * partial open tag that never completed returns as final content; an UNCLOSED think
2630
2924
  * span lands on `thinking`). Tags split ACROSS deltas are held back until
2631
2925
  * disambiguated, multiple spans accumulate in order, and a nested-looking `<think>`
2632
- * inside an open span is just thinking text. One splitter serves ONE stream — create
2926
+ * inside an open span is thinking text. One splitter serves ONE stream — create
2633
2927
  * a fresh one per provider call.
2634
2928
  *
2635
2929
  * @returns A fresh {@link ThinkSplitterInterface} (state empty, outside any span)
2636
2930
  *
2637
2931
  * @example
2638
2932
  * ```ts
2639
- * import { createThinkSplitter } from '@src/core'
2933
+ * import { createThinkSplitter } from '@orkestrel/agent'
2640
2934
  *
2641
2935
  * const splitter = createThinkSplitter()
2642
2936
  * const clean = splitter.split('<think>plan the answer</think>Here it is.')
@@ -2648,7 +2942,36 @@ function createThinkSplitter() {
2648
2942
  return new ThinkSplitter();
2649
2943
  }
2650
2944
  /**
2651
- * Create a policy gate — an {@link AuthorityInterface} the agent loop consults before
2945
+ * Creates an empty unbounded async channel — a {@link ChannelInterface} a producer writes
2946
+ * values into (`push`) and ends (`close` / `fail`) regardless of consumption, while a
2947
+ * consumer reads them back live through `drain`.
2948
+ *
2949
+ * @remarks
2950
+ * Write and read are decoupled, so the producer never waits for a consumer: an agent's eager
2951
+ * pump writes each chunk into one, which is why a run's `result` settles whether or not the
2952
+ * live events are drained. A value pushed at an already-parked consumer is delivered, buffered
2953
+ * values are yielded before the end is reported, and the FIRST failure wins.
2954
+ *
2955
+ * @typeParam T - The value type the channel carries
2956
+ * @returns A fresh, empty {@link ChannelInterface}
2957
+ *
2958
+ * @example
2959
+ * ```ts
2960
+ * import { createChannel } from '@orkestrel/agent'
2961
+ *
2962
+ * const channel = createChannel<number>()
2963
+ * channel.push(1)
2964
+ * channel.close()
2965
+ * for await (const value of channel.drain()) {
2966
+ * value // 1
2967
+ * }
2968
+ * ```
2969
+ */
2970
+ function createChannel() {
2971
+ return new Channel();
2972
+ }
2973
+ /**
2974
+ * Creates a policy gate — an {@link AuthorityInterface} the agent loop consults before
2652
2975
  * each tool call runs, evaluating the ordered rules first-match-wins and falling back
2653
2976
  * to the configured default when none match.
2654
2977
  *
@@ -2658,16 +2981,16 @@ function createThinkSplitter() {
2658
2981
  * `fallback` decides; it defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }`
2659
2982
  * (allow-unmatched — a rules list of denials acts as a DENYLIST). Pass an
2660
2983
  * `allowed: false` `fallback` to flip the gate to deny-by-default (an ALLOWLIST). Wire
2661
- * the result into `createAgent` via `AgentOptions.authority`: a denied call is fed back
2984
+ * the result into `createAgent` through `AgentOptions.authority`: a denied call is fed back
2662
2985
  * to the model as a denial `ToolResult` (not executed, no budget cost), so the model
2663
- * can react. Synchronous now — the async human-approval handshake is deferred.
2986
+ * can react. Synchronous — `evaluate` returns the verdict directly.
2664
2987
  *
2665
2988
  * @param options - Optional `rules` (ordered) and `fallback` (see {@link AuthorityOptions})
2666
2989
  * @returns A working {@link AuthorityInterface}
2667
2990
  *
2668
2991
  * @example
2669
2992
  * ```ts
2670
- * import { createAgent, createAuthority } from '@src/core'
2993
+ * import { createAgent, createAuthority } from '@orkestrel/agent'
2671
2994
  *
2672
2995
  * // Deny the `delete` tool, allow everything else.
2673
2996
  * const authority = createAuthority({
@@ -2680,16 +3003,17 @@ function createAuthority(options) {
2680
3003
  return new Authority(options);
2681
3004
  }
2682
3005
  /**
2683
- * Create an agent registry — an {@link AgentRegistryInterface} holding the named pools of
3006
+ * Creates an agent registry — an {@link AgentRegistryInterface} holding the named pools of
2684
3007
  * live, non-serializable pieces (providers, tools, authorities, schedulers) that a
2685
3008
  * serializable {@link AgentJobInput}'s names resolve against, and `build`ing a seeded,
2686
3009
  * signal-wired {@link AgentInterface} from a job.
2687
3010
  *
2688
3011
  * @remarks
2689
3012
  * `providers` is required; `tools` / `authorities` / `schedulers` are optional pools.
2690
- * The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW `unknown <category>:
2691
- * <name>` on an unregistered name (§9.1 + §12) — a misconfigured or crash-restored job
2692
- * fails loudly rather than running with a missing dependency. `build(input, signal)`
3013
+ * The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW an
3014
+ * {@link import('./errors.js').AgentError} carrying `code: 'REGISTRY'` and the message
3015
+ * `unknown <category>: <name>` on an unregistered name — a misconfigured or crash-restored
3016
+ * job fails loudly rather than running with a missing dependency. `build(input, signal)`
2693
3017
  * resolves the names, rebuilds the token budget from its ceiling, seeds the agent's
2694
3018
  * context with the job's messages, and threads `signal` so a queue / runner abort
2695
3019
  * propagates. This is the bridge that makes durable, serializable agent jobs runnable.
@@ -2699,8 +3023,8 @@ function createAuthority(options) {
2699
3023
  *
2700
3024
  * @example
2701
3025
  * ```ts
2702
- * import { createAgentRegistry } from '@src/core'
2703
- * import type { ProviderInterface } from '@src/core'
3026
+ * import { createAgentRegistry } from '@orkestrel/agent'
3027
+ * import type { ProviderInterface } from '@orkestrel/agent'
2704
3028
  * import { createTool } from '@orkestrel/tool'
2705
3029
  *
2706
3030
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
@@ -2715,7 +3039,7 @@ function createAgentRegistry(options) {
2715
3039
  return new AgentRegistry(options);
2716
3040
  }
2717
3041
  /**
2718
- * Create a durable, bounded-concurrency agent-job queue — a {@link QueueInterface} over
3042
+ * Creates a durable, bounded-concurrency agent-job queue — a {@link QueueInterface} over
2719
3043
  * serializable {@link AgentJobInput}s that COMPOSES `createQueue`: each job is rehydrated
2720
3044
  * through the `registry` into a live {@link AgentInterface}, run to its {@link AgentResult},
2721
3045
  * and subjected to the partial-as-configurable-failure policy.
@@ -2723,46 +3047,47 @@ function createAgentRegistry(options) {
2723
3047
  * @remarks
2724
3048
  * - **Composes the substrate (no new engine).** The handler is the only new logic;
2725
3049
  * bounded `concurrency`, `retries`, the per-attempt `timeout`, and durable persistence
2726
- * via `store` (+ `restore()` after a crash) are all the backing Queue's. `enqueue`
3050
+ * through `store` (+ `restore()` after a crash) are all the backing Queue's. `enqueue`
2727
3051
  * returns a per-job promise.
2728
3052
  * - **Durable + serializable.** Because `AgentJobInput` is JSON-serializable, a `store`
2729
- * (e.g. `createMemoryQueueStore` / `createDatabaseQueueStore`) persists outstanding
3053
+ * (for example `createMemoryQueueStore` / `createDatabaseQueueStore`) persists outstanding
2730
3054
  * jobs; `restore()` re-enqueues them after a restart and the `registry` rehydrates the
2731
3055
  * live pieces from the names — so a job survives a crash.
2732
3056
  * - **Partial policy.** A partial result THROWS an
2733
3057
  * {@link import('./errors.js').AgentJobError} by default, so a job cancelled by its
2734
- * attempt deadline / a queue abort RETRIES while attempts remain; `allowPartial: true`
3058
+ * attempt deadline / a queue abort RETRIES while attempts remain; `partial: true`
2735
3059
  * resolves the partial as success instead.
2736
- * - **Cancellation threads through.** The handler passes `execution.signal` into
3060
+ * - **Cancellation threads through.** The handler passes `context.signal` into
2737
3061
  * `registry.build`, so a queue `abort()` or a per-attempt timeout cancels the in-flight
2738
3062
  * agent (which commits a partial → throws → retries / fails per policy).
2739
3063
  *
2740
- * @param options - The `registry`, the `allowPartial` policy, and the substrate knobs
3064
+ * @param options - The `registry`, the `partial` policy, and the substrate knobs
2741
3065
  * (`concurrency` / `retries` / `timeout` / `store`) (see {@link AgentQueueOptions})
2742
3066
  * @returns A {@link QueueInterface} of {@link AgentJobInput} → {@link AgentResult}
2743
3067
  *
2744
3068
  * @example
2745
3069
  * ```ts
2746
- * import { createAgentQueue, createAgentRegistry, createMemoryQueueStore } from '@src/core'
3070
+ * import { createAgentQueue, createAgentRegistry } from '@orkestrel/agent'
3071
+ * import { createMemoryQueueStore } from '@orkestrel/queue'
2747
3072
  *
2748
3073
  * const registry = createAgentRegistry({ providers: { main: provider } })
2749
- * const store = createMemoryQueueStore(agentJobShape) // survives a restart via restore()
3074
+ * const store = createMemoryQueueStore(agentJobShape) // survives a restart through restore()
2750
3075
  * const queue = createAgentQueue({ registry, concurrency: 2, retries: 1, store })
2751
3076
  * const result = await queue.enqueue({ provider: 'main', messages: [{ role: 'user', content: 'ok?' }] })
2752
3077
  * ```
2753
3078
  */
2754
3079
  function createAgentQueue(options) {
2755
- const { registry, allowPartial = false, concurrency, retries, timeout, store } = options;
3080
+ const { registry, partial = false, concurrency, retries, timeout, store } = options;
2756
3081
  return createQueue({
2757
3082
  ...concurrency === void 0 ? {} : { concurrency },
2758
3083
  ...retries === void 0 ? {} : { retries },
2759
3084
  ...timeout === void 0 ? {} : { timeout },
2760
3085
  ...store === void 0 ? {} : { store },
2761
- handler: handleAgentQueueJob.bind(void 0, registry, allowPartial)
3086
+ handler: handleAgentQueueJob.bind(void 0, registry, partial)
2762
3087
  });
2763
3088
  }
2764
3089
  /**
2765
- * Create an agent-job runner — a {@link RunnerInterface} over serializable
3090
+ * Creates an agent-job runner — a {@link RunnerInterface} over serializable
2766
3091
  * {@link AgentJobInput}s that COMPOSES `createRunner` (one-shot, ordered, fail-fast), each
2767
3092
  * unit rehydrated through the `registry` and subjected to the partial policy. The runner
2768
3093
  * enables **sub-agent fan-out**: a parent job's handler can `controller.spawn(childJob)`.
@@ -2777,17 +3102,17 @@ function createAgentQueue(options) {
2777
3102
  * runner, FAN OUT and return — do NOT inline-`await` a spawn from within the handler (a
2778
3103
  * slot-holding handler awaiting its own spawn can deadlock; see `ControllerInterface`).
2779
3104
  * - **Partial policy + cancellation.** Same as `createAgentQueue`: a partial result
2780
- * THROWS by default (the run's fail-fast engages), `allowPartial: true` resolves it; the
3105
+ * THROWS by default (the run's fail-fast engages), `partial: true` resolves it; the
2781
3106
  * handler threads `controller.signal` into `registry.build`, so a runner abort / a
2782
3107
  * per-attempt timeout cancels the agent.
2783
3108
  *
2784
- * @param options - The `registry`, the `allowPartial` policy, and the substrate knobs
3109
+ * @param options - The `registry`, the `partial` policy, and the substrate knobs
2785
3110
  * (`concurrency` / `retries` / `timeout`) (see {@link AgentRunnerOptions})
2786
3111
  * @returns A {@link RunnerInterface} of {@link AgentJobInput} → {@link AgentResult}
2787
3112
  *
2788
3113
  * @example
2789
3114
  * ```ts
2790
- * import { createAgentRunner, createAgentRegistry } from '@src/core'
3115
+ * import { createAgentRunner, createAgentRegistry } from '@orkestrel/agent'
2791
3116
  *
2792
3117
  * const registry = createAgentRegistry({ providers: { main: provider } })
2793
3118
  * const runner = createAgentRunner({ registry, concurrency: 2 })
@@ -2798,15 +3123,15 @@ function createAgentQueue(options) {
2798
3123
  * ```
2799
3124
  */
2800
3125
  function createAgentRunner(options) {
2801
- const { registry, allowPartial = false, concurrency, retries, timeout } = options;
3126
+ const { registry, partial = false, concurrency, retries, timeout } = options;
2802
3127
  return createRunner({
2803
3128
  ...concurrency === void 0 ? {} : { concurrency },
2804
3129
  ...retries === void 0 ? {} : { retries },
2805
3130
  ...timeout === void 0 ? {} : { timeout },
2806
- handler: handleAgentRunnerJob.bind(void 0, registry, allowPartial)
3131
+ handler: handleAgentRunnerJob.bind(void 0, registry, partial)
2807
3132
  });
2808
3133
  }
2809
3134
  //#endregion
2810
- export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, estimateMessages, estimateTokens, fencedFile, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isSection, sanitizeToken, sanitizeUsage, settleAgentJob };
3135
+ export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, assembleResult, attachImages, attachUserImages, buildRecapMessage, buildSummaryMessage, collectImageData, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createChannel, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, denyCall, estimateMessages, estimateTokens, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, intersectKeys, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isSection, joinThinking, renderFencedFile, renderSection, resolveClose, resolveItem, resolveOpen, sanitizeToken, sanitizeUsage, settleAgentJob, sumUsage };
2811
3136
 
2812
3137
  //# sourceMappingURL=index.js.map