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