@orkestrel/agent 0.0.19 → 0.0.21

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