@orkestrel/agent 0.0.1

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.
@@ -0,0 +1,4314 @@
1
+ import { booleanShape, createContract, integerShape, isArray, isNumber, isRecord, isString, literalShape, objectShape, optionalShape, rawShape, schemaToParameters, stringShape, unionShape } from "@orkestrel/contract";
2
+ import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
+ import { createQueue } from "@orkestrel/queue";
4
+ import { createDeferred, createRunner } from "@orkestrel/workflow";
5
+ import { createAbort } from "@orkestrel/abort";
6
+ import { createTimeout } from "@orkestrel/timeout";
7
+ import { Emitter } from "@orkestrel/emitter";
8
+ import { createTokenBudget } from "@orkestrel/budget";
9
+ //#region src/core/constants.ts
10
+ /**
11
+ * The default cap on an {@link AgentInterface} turn's tool iterations — the maximum
12
+ * number of context → provider → tools cycles before the loop stops, so a model that
13
+ * keeps requesting tools can never loop forever. Overridable per agent via
14
+ * `AgentOptions.limit`.
15
+ */
16
+ var DEFAULT_AGENT_LIMIT = 10;
17
+ /**
18
+ * The zone an {@link AuthorityInterface}'s default fallback {@link AuthorityDecision}
19
+ * carries — the classification for a tool call that matched no rule. Paired with the
20
+ * default `allowed: true` fallback, an unmatched call is allowed under this zone, so a
21
+ * rules list of denials acts as a denylist; a caller wanting deny-by-default supplies
22
+ * an `allowed: false` `fallback` of their own (see `AuthorityOptions`).
23
+ */
24
+ var DEFAULT_AUTHORITY_ZONE = "default";
25
+ /**
26
+ * The default number of recent live messages a {@link ConversationInterface}'s `compact()`
27
+ * RETAINS verbatim — `0`, so a manual `compact()` folds ALL of the current live messages
28
+ * into one summarized section (no tail kept). A caller retains a recent tail by passing
29
+ * `keep` (on {@link ConversationOptions}, {@link ConversationManagerOptions}, or per-fold
30
+ * via {@link CompactOptions}), folding only the older `count - keep` messages and leaving
31
+ * the most recent `keep` live for the next turn. Overridable everywhere `keep` is accepted.
32
+ */
33
+ var DEFAULT_CONVERSATION_KEEP = 0;
34
+ /**
35
+ * The framing label a {@link ConversationInterface}'s `view()` prefixes onto each compacted
36
+ * section's summary so a small model reads it as a CONDENSED RECAP of earlier turns — not a
37
+ * literal assistant turn to echo or treat as the live answer.
38
+ *
39
+ * @remarks
40
+ * Deliberately a FIXED, lean handful of tokens (a short bracketed marker) so the framing adds a
41
+ * bounded `prefix × sections` overhead and NEVER an open-ended blow-up — the
42
+ * {@link ConversationInterface} no-bloat test guard pins exactly that. Kept here (beside
43
+ * {@link DEFAULT_CONVERSATION_KEEP}) as the conversation layer's one tunable framing constant, so
44
+ * the wording has a single source of truth as it is optimized against real small-model behavior
45
+ * (the `view()` recap framing is distinct from `reference()`'s cross-conversation provenance
46
+ * marker, which is rendered inline since it interpolates the per-call provenance `label`).
47
+ */
48
+ var CONVERSATION_RECAP_PREFIX = "[Summary of earlier messages] ";
49
+ /**
50
+ * The opening tag a {@link import('./ThinkSplitter.js').ThinkSplitter} recognizes as the start of
51
+ * an in-content reasoning span — the de-facto wire convention thinking models (qwen3, DeepSeek-R1
52
+ * family) emit their chain-of-thought under when a daemon renders it inline instead of on a
53
+ * separate wire field. Paired with {@link THINK_CLOSE}.
54
+ */
55
+ var THINK_OPEN = "<think>";
56
+ /**
57
+ * The closing tag that ends a {@link THINK_OPEN} reasoning span. A span the stream never closes
58
+ * (the model was cut off mid-reasoning) is treated as thinking to its end —
59
+ * {@link import('./types.js').ThinkSplitterInterface.flush} settles it.
60
+ */
61
+ var THINK_CLOSE = "</think>";
62
+ /**
63
+ * The section header {@link import('./AgentContext.js').AgentContext}'s `build()` renders the
64
+ * ACTIVE workspace's TEXT files under — the leading line of the dedicated workspace block in the
65
+ * system message, the carrier-split counterpart to the documents / images section headers.
66
+ *
67
+ * @remarks
68
+ * `build()` OWNS the workspace render (a `Workspace` / `WorkspaceManager` stays file-focused — no
69
+ * `description` / `framing` getters), so this header lives here as the agents module's one
70
+ * workspace-section framing constant rather than on a manager. Each workspace text file renders
71
+ * beneath it as a fenced `` File: <path>\n```<language>\n<text>\n``` `` block — the SAME framing
72
+ * the documents section uses — placed just after the documents section in the system block.
73
+ */
74
+ var WORKSPACE_SECTION_HEADER = "## Workspace";
75
+ /**
76
+ * The extension→language table {@link import('./helpers.js').inferLanguage} reads to
77
+ * map a file path's extension to a fenced-code language tag. Covers the common source /
78
+ * markup / data extensions; an unlisted extension falls back to `'text'`.
79
+ */
80
+ var EXTENSION_TO_LANGUAGE = Object.freeze({
81
+ ts: "typescript",
82
+ tsx: "typescript",
83
+ js: "javascript",
84
+ jsx: "javascript",
85
+ mjs: "javascript",
86
+ cjs: "javascript",
87
+ json: "json",
88
+ md: "markdown",
89
+ html: "html",
90
+ htm: "html",
91
+ css: "css",
92
+ scss: "scss",
93
+ sass: "sass",
94
+ less: "less",
95
+ vue: "vue",
96
+ svelte: "svelte",
97
+ py: "python",
98
+ rb: "ruby",
99
+ rs: "rust",
100
+ go: "go",
101
+ java: "java",
102
+ kt: "kotlin",
103
+ swift: "swift",
104
+ c: "c",
105
+ cpp: "cpp",
106
+ h: "c",
107
+ hpp: "cpp",
108
+ cs: "csharp",
109
+ php: "php",
110
+ sql: "sql",
111
+ sh: "bash",
112
+ bash: "bash",
113
+ zsh: "bash",
114
+ ps1: "powershell",
115
+ yaml: "yaml",
116
+ yml: "yaml",
117
+ toml: "toml",
118
+ xml: "xml",
119
+ svg: "xml",
120
+ txt: "text"
121
+ });
122
+ /**
123
+ * The name {@link import('./factories.js').createWorkspaceTool} advertises by default — the key a
124
+ * model calls and the {@link import('./types.js').ToolManagerInterface} registers under.
125
+ */
126
+ var WORKSPACE_TOOL_NAME = "workspace";
127
+ /**
128
+ * A valid {@link WorkspaceOperation} object — the canonical example embedded VERBATIM in
129
+ * {@link WORKSPACE_TOOL_DESCRIPTION} and pinned by a parity test (it must satisfy the compiled
130
+ * contract's `is`), so the doc example can never drift from a real, contract-valid operation.
131
+ *
132
+ * @remarks
133
+ * A `'write'` op (the most common authoring action): create or overwrite `notes.txt` with
134
+ * `hello`. Frozen so it cannot be mutated in place.
135
+ */
136
+ var WORKSPACE_TOOL_EXAMPLE = Object.freeze({
137
+ operation: "write",
138
+ path: "notes.txt",
139
+ content: "hello"
140
+ });
141
+ /**
142
+ * The DESCRIPTION {@link import('./factories.js').createWorkspaceTool} advertises — a multi-line
143
+ * guide that teaches a small model how to drive a workspace through the single `operation`-keyed
144
+ * tool.
145
+ *
146
+ * @remarks
147
+ * Mirrors the analogous `WORKFLOW_TOOL_DESCRIPTION` in `@orkestrel/workflow`'s teaching style:
148
+ * names the `operation` discriminant field, enumerates all 13 operations with their FLAT fields,
149
+ * gives a worked example for the common ones (read / write / search / replace / splice), and embeds
150
+ * {@link WORKSPACE_TOOL_EXAMPLE} verbatim (pinned by a parity test). The range edit is the FLAT
151
+ * `'splice'` op — four positive-integer caret components, NOT a nested range — the ergonomic lever
152
+ * a 2B model can fill. The edit / read ops target the ACTIVE workspace (one is auto-created on the
153
+ * first edit when none is active); the `workspaces` / `switch` ops let the model move between
154
+ * workspaces.
155
+ */
156
+ var WORKSPACE_TOOL_DESCRIPTION = [
157
+ "Read and edit files in a workspace. Every call is ONE operation, chosen by the \"operation\" field.",
158
+ "All file operations act on the ACTIVE workspace; use \"workspaces\" then \"switch\" to move between workspaces.",
159
+ "",
160
+ "Operations (each takes the fields listed):",
161
+ "- read { \"operation\": \"read\", \"path\": \"<file>\" } — return the file's text.",
162
+ "- list { \"operation\": \"list\" } — list every file in the active workspace (path, state, size, lines, kind).",
163
+ "- has { \"operation\": \"has\", \"path\": \"<file>\" } — whether the file exists.",
164
+ "- search { \"operation\": \"search\", \"query\": \"<text>\", \"regex\"?: bool, \"exact\"?: bool, \"limit\"?: int } — find lines matching the query across all files.",
165
+ "- replace { \"operation\": \"replace\", \"query\": \"<text>\", \"replacement\": \"<text>\", \"regex\"?: bool, \"exact\"?: bool, \"limit\"?: int } — replace matches across all files.",
166
+ "- write { \"operation\": \"write\", \"path\": \"<file>\", \"content\": \"<text>\" } — create or overwrite the whole file.",
167
+ "- splice { \"operation\": \"splice\", \"path\": \"<file>\", \"content\": \"<text>\", \"fromLine\": int, \"fromColumn\": int, \"toLine\": int, \"toColumn\": int } — replace a 1-based range (from inclusive, to exclusive) with content.",
168
+ "- prepend { \"operation\": \"prepend\", \"path\": \"<file>\", \"content\": \"<text>\" } — add content to the start of the file.",
169
+ "- append { \"operation\": \"append\", \"path\": \"<file>\", \"content\": \"<text>\" } — add content to the end of the file.",
170
+ "- move { \"operation\": \"move\", \"from\": \"<file>\", \"to\": \"<file>\" } — rename / move a file.",
171
+ "- remove { \"operation\": \"remove\", \"path\": \"<file>\" } — delete a file.",
172
+ "- workspaces { \"operation\": \"workspaces\" } — list the workspaces you can switch between (each id, file count, active).",
173
+ "- switch { \"operation\": \"switch\", \"id\": \"<id>\" } — make the workspace with that id active (ids come from \"workspaces\").",
174
+ "",
175
+ "Notes: lines and columns are 1-based (column 1 is the first character). \"regex\" defaults to false (a literal substring), \"exact\" defaults to true (case-sensitive). \"search\"/\"replace\"/\"splice\" act only on text files. Editing with no active workspace auto-creates one.",
176
+ "",
177
+ "Example — write a file:",
178
+ JSON.stringify(WORKSPACE_TOOL_EXAMPLE)
179
+ ].join("\n");
180
+ //#endregion
181
+ //#region src/core/errors.ts
182
+ /**
183
+ * Thrown by a {@link ProviderInterface}'s `stream` when its bound signal aborts
184
+ * mid-flight — carries the {@link ProviderResult} assembled from whatever streamed
185
+ * before the cancel.
186
+ *
187
+ * @remarks
188
+ * Lets a caller recover the partial content (and any tool calls / usage seen so far)
189
+ * on cancellation: `catch` the throw, narrow with {@link isProviderAbortError}, and
190
+ * read `partial`.
191
+ */
192
+ var ProviderAbortError = class extends Error {
193
+ partial;
194
+ constructor(partial) {
195
+ super("provider stream aborted");
196
+ this.name = "ProviderAbortError";
197
+ this.partial = partial;
198
+ }
199
+ };
200
+ /**
201
+ * Narrow an unknown caught value to a {@link ProviderAbortError}.
202
+ *
203
+ * @param value - The value to test (typically a `catch` binding)
204
+ * @returns `true` when `value` is a {@link ProviderAbortError}
205
+ *
206
+ * @example
207
+ * ```ts
208
+ * try {
209
+ * for await (const delta of provider.stream(messages, signal)) render(delta)
210
+ * } catch (error) {
211
+ * if (isProviderAbortError(error)) keep(error.partial.content) // recover partial
212
+ * }
213
+ * ```
214
+ */
215
+ function isProviderAbortError(value) {
216
+ return value instanceof ProviderAbortError;
217
+ }
218
+ /**
219
+ * Thrown by an agent-job handler (a `createAgentQueue` / `createAgentRunner` job) when
220
+ * its {@link AgentInterface} run ended {@link AgentResult.partial} and the job's
221
+ * `allowPartial` policy is `false` (the default) — carries the partial
222
+ * {@link AgentResult} so the failure stays inspectable.
223
+ *
224
+ * @remarks
225
+ * A partial result means the agent was cancelled (an external `signal` abort, a queue /
226
+ * runner abort threaded in, a `timeout` deadline, or an exhausted token `budget`) rather
227
+ * than finishing naturally. For a durable JOB that is a failure by default: throwing this
228
+ * lets the Queue's retries re-run the job and a Runner's fail-fast abort its siblings.
229
+ * Set `allowPartial: true` (see `AgentQueueOptions` / `AgentRunnerOptions`) to treat a
230
+ * partial as success instead, in which case this is never thrown. Narrow a caught value
231
+ * with {@link isAgentJobError} to read `partial`.
232
+ */
233
+ var AgentJobError = class extends Error {
234
+ /** The partial {@link AgentResult} the cancelled job produced. */
235
+ partial;
236
+ constructor(message, partial) {
237
+ super(message);
238
+ this.name = "AgentJobError";
239
+ this.partial = partial;
240
+ }
241
+ };
242
+ /**
243
+ * Narrow an unknown caught value to an {@link AgentJobError}.
244
+ *
245
+ * @param value - The value to test (typically a `catch` binding or a rejected enqueue)
246
+ * @returns `true` when `value` is an {@link AgentJobError}
247
+ *
248
+ * @example
249
+ * ```ts
250
+ * try {
251
+ * await queue.enqueue(job) // retries: 0 → a partial rejects with the error
252
+ * } catch (error) {
253
+ * if (isAgentJobError(error)) keep(error.partial.content) // recover the partial content
254
+ * }
255
+ * ```
256
+ */
257
+ function isAgentJobError(value) {
258
+ return value instanceof AgentJobError;
259
+ }
260
+ /**
261
+ * Thrown by a {@link ConversationInterface}'s `compact()` when the conversation has no
262
+ * {@link ConversationSummarizer} to fold its messages with — carries a machine-readable
263
+ * `code`.
264
+ *
265
+ * @remarks
266
+ * Compaction REQUIRES a summarizer (it digests the folded slice into a section summary and
267
+ * regenerates the rollup); a conversation created without one can still store + `view()` its
268
+ * live tail, but a `compact()` is a programmer error (§12) and throws this. `code` is
269
+ * `'SUMMARIZER'` (the only condition so far). Narrow a caught value with
270
+ * {@link isConversationError} and branch on `error.code`.
271
+ */
272
+ var ConversationError = class extends Error {
273
+ /** The machine-readable condition — `'SUMMARIZER'`: a `compact()` with no summarizer. */
274
+ code;
275
+ constructor(code, message) {
276
+ super(message);
277
+ this.name = "ConversationError";
278
+ this.code = code;
279
+ }
280
+ };
281
+ /**
282
+ * Narrow an unknown caught value to a {@link ConversationError}.
283
+ *
284
+ * @param value - The value to test (typically a `catch` binding)
285
+ * @returns `true` when `value` is a {@link ConversationError}
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * try {
290
+ * await conversation.compact()
291
+ * } catch (error) {
292
+ * if (isConversationError(error) && error.code === 'SUMMARIZER') addSummarizer()
293
+ * }
294
+ * ```
295
+ */
296
+ function isConversationError(value) {
297
+ return value instanceof ConversationError;
298
+ }
299
+ /**
300
+ * An error thrown by the in-memory {@link import('./workspaces/Workspace.js').Workspace} edit
301
+ * surface.
302
+ *
303
+ * @remarks
304
+ * Carries a {@link WorkspaceErrorCode} and an optional `context` bag naming the offending
305
+ * path / range. Thrown for a text-only operation aimed at an image file (`MODALITY` — a
306
+ * ranged read / write, `prepend`, or `append`), an invalid `search` / `replace` regular
307
+ * expression (`PATTERN`), and a structurally invalid ranged-write {@link import('./types.js').Range}
308
+ * (`RANGE` — inverted, or a sub-1 line / column).
309
+ */
310
+ var WorkspaceError = class extends Error {
311
+ code;
312
+ context;
313
+ constructor(code, message, context) {
314
+ super(message);
315
+ this.name = "WorkspaceError";
316
+ this.code = code;
317
+ this.context = context;
318
+ }
319
+ };
320
+ /**
321
+ * Narrow an unknown caught value to a {@link WorkspaceError}.
322
+ *
323
+ * @param value - The value to test (typically a `catch` binding)
324
+ * @returns `true` when `value` is a {@link WorkspaceError}
325
+ *
326
+ * @example
327
+ * ```ts
328
+ * try {
329
+ * workspace.prepend('icon.png', '// header')
330
+ * } catch (error) {
331
+ * if (isWorkspaceError(error) && error.code === 'MODALITY') skipBinary()
332
+ * }
333
+ * ```
334
+ */
335
+ function isWorkspaceError(value) {
336
+ return value instanceof WorkspaceError;
337
+ }
338
+ //#endregion
339
+ //#region src/core/shapers.ts
340
+ /**
341
+ * The shape of a {@link import('./types.js').WorkspaceOperation} — a descriptive tagged union over
342
+ * the 13 workspace edit / read / navigation operations, discriminated by the `operation` literal
343
+ * (never a bare `kind`; AGENTS §4.8). Each variant leads with its `operation` discriminant then its
344
+ * FLAT fields, every field via `stringShape` / `optionalShape` / `integerShape({ min: 1 })` /
345
+ * `booleanShape`, each carrying a strong field-level `description`.
346
+ *
347
+ * @remarks
348
+ * The union compiles to an `anyOf` JSON Schema + a `unionOf` guard + a first-match parser
349
+ * automatically ({@link import('./factories.js').createWorkspaceTool} types the result to the
350
+ * hand-written `WorkspaceOperation`). `limit` and the four `'splice'` caret components are POSITIVE
351
+ * integers (`integerShape({ min: 1 })`); `regex` / `exact` are `optionalShape(booleanShape(...))`.
352
+ * The two REGISTRY arms — `workspaces` (list the workspaces the model can move between) and `switch`
353
+ * (re-point the active one by `id`) — let a model DISCOVER then CHOOSE which workspace the edit /
354
+ * read arms target. The descriptions are the small-model ergonomic lever — they ride into the
355
+ * advertised schema so a model authoring an operation gets per-field guidance.
356
+ */
357
+ var workspaceToolShape = unionShape(objectShape({
358
+ operation: literalShape(["read"], { description: "Read a whole text file's text by path." }),
359
+ path: stringShape({ description: "The path of the file to read." })
360
+ }), objectShape({ operation: literalShape(["list"], { description: "List every file in the workspace." }) }), objectShape({
361
+ operation: literalShape(["has"], { description: "Check whether a file exists at the path." }),
362
+ path: stringShape({ description: "The path to check for." })
363
+ }), objectShape({
364
+ operation: literalShape(["search"], { description: "Search every text file for a query, returning each hit." }),
365
+ query: stringShape({ description: "The text (or regular-expression source) to search for." }),
366
+ regex: optionalShape(booleanShape({ description: "Treat the query as a regular expression. Defaults to false (a literal substring)." })),
367
+ exact: optionalShape(booleanShape({ description: "Match case-sensitively. Defaults to true (set false for case-insensitive)." })),
368
+ limit: optionalShape(integerShape({
369
+ min: 1,
370
+ description: "Stop after this many matches across all files. Omitted means unlimited."
371
+ }))
372
+ }), objectShape({
373
+ operation: literalShape(["replace"], { description: "Replace a query with a replacement across every text file." }),
374
+ query: stringShape({ description: "The text (or regular-expression source) to replace." }),
375
+ replacement: stringShape({ description: "The text to substitute for each match." }),
376
+ regex: optionalShape(booleanShape({ description: "Treat the query as a regular expression. Defaults to false (a literal substring)." })),
377
+ exact: optionalShape(booleanShape({ description: "Match case-sensitively. Defaults to true (set false for case-insensitive)." })),
378
+ limit: optionalShape(integerShape({
379
+ min: 1,
380
+ description: "Stop after this many replacements across all files. Omitted means unlimited."
381
+ }))
382
+ }), objectShape({
383
+ operation: literalShape(["write"], { description: "Create or overwrite a whole file with content." }),
384
+ path: stringShape({ description: "The path of the file to write." }),
385
+ content: stringShape({ description: "The full new contents of the file." })
386
+ }), objectShape({
387
+ operation: literalShape(["splice"], { description: "Replace a 1-based range of an existing text file (from inclusive, to exclusive) with content." }),
388
+ path: stringShape({ description: "The path of the text file to edit." }),
389
+ content: stringShape({ description: "The text to splice in place of the range." }),
390
+ fromLine: integerShape({
391
+ min: 1,
392
+ description: "The 1-based start line of the range (inclusive)."
393
+ }),
394
+ fromColumn: integerShape({
395
+ min: 1,
396
+ description: "The 1-based start column of the range (inclusive; column 1 is the first character)."
397
+ }),
398
+ toLine: integerShape({
399
+ min: 1,
400
+ description: "The 1-based end line of the range (exclusive)."
401
+ }),
402
+ toColumn: integerShape({
403
+ min: 1,
404
+ description: "The 1-based end column of the range (exclusive)."
405
+ })
406
+ }), objectShape({
407
+ operation: literalShape(["prepend"], { description: "Add content to the start of a file (creating it when absent)." }),
408
+ path: stringShape({ description: "The path of the file to prepend to." }),
409
+ content: stringShape({ description: "The text to add at the start of the file." })
410
+ }), objectShape({
411
+ operation: literalShape(["append"], { description: "Add content to the end of a file (creating it when absent)." }),
412
+ path: stringShape({ description: "The path of the file to append to." }),
413
+ content: stringShape({ description: "The text to add at the end of the file." })
414
+ }), objectShape({
415
+ operation: literalShape(["move"], { description: "Rename or move a file (overwriting an occupied target)." }),
416
+ from: stringShape({ description: "The current path of the file." }),
417
+ to: stringShape({ description: "The new path for the file." })
418
+ }), objectShape({
419
+ operation: literalShape(["remove"], { description: "Delete a file from the workspace." }),
420
+ path: stringShape({ description: "The path of the file to remove." })
421
+ }), objectShape({ operation: literalShape(["workspaces"], { description: "List the workspaces you can move between (each id, file count, and whether it is active), so you can pick an id to switch to." }) }), objectShape({
422
+ operation: literalShape(["switch"], { description: "Switch the active workspace to the one with this id (get ids from the \"workspaces\" operation). Edit and read operations then target it." }),
423
+ id: stringShape({ description: "The id of the workspace to make active (from the \"workspaces\" listing)." })
424
+ }));
425
+ //#endregion
426
+ //#region src/core/conversations/Conversation.ts
427
+ /**
428
+ * A conversation grouping messages ABOVE a flat message store — a live uncompacted tail it
429
+ * OWNS DIRECTLY plus compacted, summarized {@link SectionInterface}s and a regenerated rollup
430
+ * `summary`, with on-demand `rehydrate` and substring `search`, driven by a provider-agnostic
431
+ * {@link ConversationSummarizer} seam (so `core` never imports a provider).
432
+ *
433
+ * @remarks
434
+ * - **Live tail + sections.** The conversation OWNS its live tail DIRECTLY — `#messages` is an
435
+ * insertion-ordered `Map` of immutable {@link MessageInterface}s keyed by their minted id
436
+ * (the SAME store mechanics a flat manager had, folded in: `add` / `message` / `messages` /
437
+ * `remove` / `clear` / `count`), exactly as a `Workspace` owns its files (no separate
438
+ * per-value manager). `#sections` are the compacted history (oldest → newest), each a
439
+ * summarized slice that RETAINS its originals. `#summary` is the rollup (a
440
+ * summary-of-summaries over all sections), regenerated on each compaction (`undefined`
441
+ * until the first).
442
+ * - **`view()`.** Each section folds to ONE synthetic summary message (role `'assistant'` — a
443
+ * prior-context recap — keyed by the section's stable `id`), then the live messages
444
+ * verbatim. The rollup `summary` is NOT injected (it is separately pull-able); `view()`
445
+ * carries the per-section summaries, which ARE the compaction benefit.
446
+ * - **`compact()`.** Folds the oldest `count - keep` live messages into a new section
447
+ * (its `summary` from `#summarize`), removes them from the live tail by id, regenerates the
448
+ * rollup (a SECOND `#summarize` over all section summaries), and emits `summary` then
449
+ * `compact`. Returns the section, or `undefined` when nothing folds (`count <= keep`).
450
+ * THROWS a {@link ConversationError} when no `#summarize` was supplied. Two summarizer calls
451
+ * per compaction.
452
+ * - **`rehydrate(id)` / `search(query)`.** `rehydrate` returns a section's full original
453
+ * messages (`[]` for an unknown id) and emits `rehydrate` — a pure read (v1 never
454
+ * auto-reinserts). `search` is a case-insensitive substring scan of `content` across ALL
455
+ * messages (every section's originals + the live tail).
456
+ * - **Observable (§13).** The owned {@link emitter} ({@link ConversationEventMap}) carries
457
+ * `compact` / `summary` / `rehydrate`, emitted directly, strictly AFTER the state change;
458
+ * the emitter isolates a listener throw and routes it to its `error` handler (the `error`
459
+ * option), so a buggy observer can never corrupt a compaction.
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * const conversation = new Conversation({ summarize: async (m) => `recap of ${m.length}` })
464
+ * conversation.add([
465
+ * { role: 'user', content: 'Hello' },
466
+ * { role: 'assistant', content: 'Hi there' },
467
+ * ])
468
+ * const section = await conversation.compact() // folds both into one summarized section
469
+ * conversation.view() // [{ role: 'assistant', content: 'recap of 2' }] — the live tail is empty
470
+ * conversation.summary // 'recap of 1' — the rollup over the one section
471
+ * ```
472
+ */
473
+ var Conversation = class {
474
+ #id;
475
+ #emitter;
476
+ #summarize;
477
+ #keep;
478
+ #sections = [];
479
+ #summary;
480
+ #messages = /* @__PURE__ */ new Map();
481
+ constructor(options, seed) {
482
+ this.#id = seed?.id ?? options?.id ?? crypto.randomUUID();
483
+ this.#emitter = new Emitter({
484
+ on: options?.on,
485
+ error: options?.error
486
+ });
487
+ this.#summarize = options?.summarize;
488
+ this.#keep = options?.keep ?? 0;
489
+ if (seed) {
490
+ this.#summary = seed.summary;
491
+ for (const section of seed.sections) this.#sections.push(section);
492
+ for (const message of seed.messages) this.#messages.set(message.id, message);
493
+ }
494
+ }
495
+ get id() {
496
+ return this.#id;
497
+ }
498
+ get emitter() {
499
+ return this.#emitter;
500
+ }
501
+ get summary() {
502
+ return this.#summary;
503
+ }
504
+ get sections() {
505
+ return [...this.#sections];
506
+ }
507
+ get summarizable() {
508
+ return this.#summarize !== void 0;
509
+ }
510
+ get count() {
511
+ return this.#messages.size;
512
+ }
513
+ add(input) {
514
+ if (isArray(input)) return input.map((one) => this.#create(one));
515
+ return this.#create(input);
516
+ }
517
+ message(id) {
518
+ return this.#messages.get(id);
519
+ }
520
+ messages() {
521
+ return [...this.#messages.values()];
522
+ }
523
+ remove(ids) {
524
+ if (isArray(ids)) {
525
+ let removed = false;
526
+ for (const id of ids) if (this.#messages.delete(id)) removed = true;
527
+ return removed;
528
+ }
529
+ return this.#messages.delete(ids);
530
+ }
531
+ clear() {
532
+ this.#messages.clear();
533
+ }
534
+ view() {
535
+ return [...this.#sections.map((section) => this.#recapMessage(section)), ...this.#messages.values()];
536
+ }
537
+ async compact(options) {
538
+ const summarize = this.#summarize;
539
+ if (summarize === void 0) throw new ConversationError("SUMMARIZER", "cannot compact a conversation without a summarizer");
540
+ const keep = options?.keep ?? this.#keep;
541
+ const live = [...this.#messages.values()];
542
+ const fold = keep <= 0 ? live.length : live.length - keep;
543
+ if (fold <= 0) return void 0;
544
+ const slice = live.slice(0, fold);
545
+ const summary = await summarize(slice);
546
+ const section = {
547
+ id: crypto.randomUUID(),
548
+ summary,
549
+ messages: slice
550
+ };
551
+ for (const message of slice) this.#messages.delete(message.id);
552
+ this.#sections.push(section);
553
+ this.#summary = await summarize(this.#sections.map((one) => this.#summaryMessage(one)));
554
+ this.#emitter.emit("summary", this.#summary);
555
+ this.#emitter.emit("compact", section);
556
+ return section;
557
+ }
558
+ rehydrate(id) {
559
+ const section = this.#sections.find((one) => one.id === id);
560
+ this.#emitter.emit("rehydrate", id);
561
+ return section === void 0 ? [] : section.messages;
562
+ }
563
+ search(query) {
564
+ const needle = query.toLowerCase();
565
+ return [...this.#sections.flatMap((section) => section.messages), ...this.#messages.values()].filter((message) => message.content.toLowerCase().includes(needle));
566
+ }
567
+ reference(options) {
568
+ const lines = [`[Reference — conversation "${options?.label ?? this.#id}" — NOT part of this conversation]`];
569
+ if (options?.summary !== false && this.#summary !== void 0) lines.push(`Summary: ${this.#summary}`);
570
+ const messages = options?.messages ?? [];
571
+ if (messages.length > 0) {
572
+ lines.push("Relevant messages:");
573
+ for (const message of messages) lines.push(`- ${message.role}: ${message.content}`);
574
+ }
575
+ return lines.join("\n");
576
+ }
577
+ snapshot() {
578
+ return {
579
+ id: this.#id,
580
+ ...this.#summary === void 0 ? {} : { summary: this.#summary },
581
+ sections: this.sections,
582
+ messages: this.messages()
583
+ };
584
+ }
585
+ #summaryMessage(section) {
586
+ return {
587
+ id: section.id,
588
+ role: "assistant",
589
+ content: section.summary
590
+ };
591
+ }
592
+ #recapMessage(section) {
593
+ return {
594
+ id: section.id,
595
+ role: "assistant",
596
+ content: `${CONVERSATION_RECAP_PREFIX}${section.summary}`
597
+ };
598
+ }
599
+ #create(input) {
600
+ const message = {
601
+ id: crypto.randomUUID(),
602
+ role: input.role,
603
+ content: input.content,
604
+ ...input.calls === void 0 ? {} : { calls: input.calls },
605
+ ...input.images === void 0 ? {} : { images: input.images }
606
+ };
607
+ this.#messages.set(message.id, message);
608
+ return message;
609
+ }
610
+ };
611
+ //#endregion
612
+ //#region src/core/conversations/ConversationManager.ts
613
+ /**
614
+ * The registry of {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
615
+ * — the §9 store over the conversation layer PLUS the `active` / `switch` seam the
616
+ * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
617
+ * {@link import('../workspaces/WorkspaceManager.js').WorkspaceManager}); the observability lives
618
+ * on each {@link Conversation}.
619
+ *
620
+ * @remarks
621
+ * - **Registry.** Conversations live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
622
+ * mints a {@link Conversation} (its `id` from `input` or `crypto.randomUUID()`), flowing the
623
+ * manager's default `#summarize` / `#keep` in unless the `input` OVERRIDES them, and stores
624
+ * it (an already-present `id` OVERWRITES — last write wins). `count` is the map size,
625
+ * `conversation(id)` looks one up, `conversations()` lists them in insertion order.
626
+ * - **Active pointer.** `active` is the active conversation (the agent's message source the
627
+ * context renders), `undefined` until the FIRST `add` (which auto-activates it — a registry
628
+ * with conversations always has one active). A subsequent `add` leaves `active` unchanged.
629
+ * `switch(id)` re-points `active` to the conversation with `id` and returns it; an unknown `id`
630
+ * returns `undefined` and leaves `active` unchanged (the lenient lookup style — never throws,
631
+ * no new error code).
632
+ * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
633
+ * removing the ACTIVE conversation sets `active` to `undefined`. `clear` empties the registry
634
+ * and sets `active` to `undefined`.
635
+ * - **Event-free.** A purely registry store — no Emitter, no events (each conversation owns
636
+ * its own observable `emitter`).
637
+ *
638
+ * @example
639
+ * ```ts
640
+ * const manager = new ConversationManager({ summarize: async (m) => `recap of ${m.length}` })
641
+ * const conversation = manager.add() // auto-activates — active === conversation
642
+ * manager.add({ id: 'scratch' }) // leaves active unchanged
643
+ * manager.switch('scratch') // re-points active to the 'scratch' conversation
644
+ * manager.count // 2
645
+ * ```
646
+ */
647
+ var ConversationManager = class {
648
+ #conversations = /* @__PURE__ */ new Map();
649
+ #active;
650
+ #summarize;
651
+ #keep;
652
+ #store;
653
+ constructor(options) {
654
+ this.#summarize = options?.summarize;
655
+ this.#keep = options?.keep ?? 0;
656
+ this.#store = options?.store;
657
+ }
658
+ get count() {
659
+ return this.#conversations.size;
660
+ }
661
+ get active() {
662
+ return this.#active === void 0 ? void 0 : this.#conversations.get(this.#active);
663
+ }
664
+ conversation(id) {
665
+ return this.#conversations.get(id);
666
+ }
667
+ conversations() {
668
+ return [...this.#conversations.values()];
669
+ }
670
+ add(input) {
671
+ const conversation = new Conversation({
672
+ ...input?.id === void 0 ? {} : { id: input.id },
673
+ ...input?.on === void 0 ? {} : { on: input.on },
674
+ summarize: input?.summarize ?? this.#summarize,
675
+ keep: input?.keep ?? this.#keep
676
+ }, input?.snapshot);
677
+ this.#conversations.set(conversation.id, conversation);
678
+ if (this.#active === void 0) this.#active = conversation.id;
679
+ return conversation;
680
+ }
681
+ switch(id) {
682
+ const conversation = this.#conversations.get(id);
683
+ if (conversation === void 0) return void 0;
684
+ this.#active = id;
685
+ return conversation;
686
+ }
687
+ async open(id) {
688
+ const existing = this.#conversations.get(id);
689
+ if (existing !== void 0) {
690
+ this.#active = id;
691
+ return existing;
692
+ }
693
+ if (this.#store === void 0) return void 0;
694
+ const snapshot = await this.#store.get(id);
695
+ if (snapshot === void 0) return void 0;
696
+ const conversation = this.add({ snapshot });
697
+ this.#active = conversation.id;
698
+ return conversation;
699
+ }
700
+ async save(id) {
701
+ const conversation = this.#conversations.get(id);
702
+ if (this.#store === void 0 || conversation === void 0) return false;
703
+ await this.#store.set(conversation.snapshot());
704
+ return true;
705
+ }
706
+ remove(ids) {
707
+ if (isArray(ids)) {
708
+ let removed = false;
709
+ for (const id of ids) if (this.#drop(id)) removed = true;
710
+ return removed;
711
+ }
712
+ return this.#drop(ids);
713
+ }
714
+ clear() {
715
+ this.#conversations.clear();
716
+ this.#active = void 0;
717
+ }
718
+ #drop(id) {
719
+ const removed = this.#conversations.delete(id);
720
+ if (removed && this.#active === id) this.#active = void 0;
721
+ return removed;
722
+ }
723
+ };
724
+ //#endregion
725
+ //#region src/core/helpers.ts
726
+ /**
727
+ * Infer a fenced-code language tag from a file path's extension — what a workspace text file
728
+ * (or the {@link fencedFile} renderer) renders its content block as.
729
+ *
730
+ * @remarks
731
+ * Reads the extension after the last `.` (case-insensitive) and maps it through a
732
+ * fixed extension→language table. An unknown extension, or a path with no extension,
733
+ * falls back to `'text'` (a safe, language-agnostic fence). Total — never throws.
734
+ *
735
+ * @param path - The document's file path (e.g. `'src/main.ts'`)
736
+ * @returns The inferred language tag (e.g. `'typescript'`), or `'text'` when unknown
737
+ *
738
+ * @example
739
+ * ```ts
740
+ * inferLanguage('src/main.ts') // 'typescript'
741
+ * inferLanguage('README.md') // 'markdown'
742
+ * inferLanguage('LICENSE') // 'text'
743
+ * ```
744
+ */
745
+ function inferLanguage(path) {
746
+ const dot = path.lastIndexOf(".");
747
+ if (dot === -1) return "text";
748
+ return EXTENSION_TO_LANGUAGE[path.slice(dot + 1).toLowerCase()] ?? "text";
749
+ }
750
+ /**
751
+ * Filter a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
752
+ * keys — the pure, total set-membership primitive the context's build step and the agent
753
+ * loop's tool-advertise step apply a scope through.
754
+ *
755
+ * @remarks
756
+ * Three-way by the allow-list's shape, so a `Scope` category cleanly expresses "all /
757
+ * none / only these":
758
+ * - `undefined` ⇒ NO constraint — every item passes (returned unchanged).
759
+ * - `[]` (empty) ⇒ NONE pass (no key is in an empty set).
760
+ * - a non-empty list ⇒ only items whose `key(item)` is in the list pass.
761
+ *
762
+ * Order-preserving (it filters `items` in place order, never reorders) and total — never
763
+ * throws. Keys are matched by a `Set` for O(1) membership, so a large list is cheap.
764
+ *
765
+ * @typeParam T - The item type being filtered
766
+ * @param allow - The allow-list of keys (`undefined` ⇒ all, `[]` ⇒ none, else only-listed)
767
+ * @param items - The items to filter (returned unchanged when `allow` is `undefined`)
768
+ * @param key - Extracts the key an item is matched on (e.g. an instruction's `name`)
769
+ * @returns The items that pass the allow-list, in their original order
770
+ *
771
+ * @example
772
+ * ```ts
773
+ * const items = [{ name: 'a' }, { name: 'b' }]
774
+ * filterAllowList(undefined, items, (i) => i.name) // [{ name: 'a' }, { name: 'b' }] (all)
775
+ * filterAllowList([], items, (i) => i.name) // [] (none)
776
+ * filterAllowList(['b'], items, (i) => i.name) // [{ name: 'b' }] (only listed)
777
+ * ```
778
+ */
779
+ function filterAllowList(allow, items, key) {
780
+ if (allow === void 0) return items;
781
+ if (allow.length === 0) return [];
782
+ const set = new Set(allow);
783
+ return items.filter((item) => set.has(key(item)));
784
+ }
785
+ /**
786
+ * Estimate the context-token footprint of a string — the deterministic char-based heuristic
787
+ * {@link estimateMessages} sums over a conversation's messages (the default context-budget
788
+ * estimator).
789
+ *
790
+ * @remarks
791
+ * Approximates `ceil(length / 4)` (≈ four characters per token — the rough average for
792
+ * English text), so the same input always yields the same estimate (no model round-trip).
793
+ * Empty text is `0`. This is a planning heuristic for reasoning about how much a turn's
794
+ * messages cost the next request, NOT an exact tokenizer count — it never calls a provider,
795
+ * so the agent layer stays provider-agnostic and synchronous where it can be.
796
+ *
797
+ * @param text - The text to estimate (a section summary, a message's content)
798
+ * @returns The estimated token count (`ceil(text.length / 4)`; `0` for empty text)
799
+ *
800
+ * @example
801
+ * ```ts
802
+ * estimateTokens('') // 0
803
+ * estimateTokens('hello') // 2 (ceil(5 / 4))
804
+ * estimateTokens('a'.repeat(40)) // 10
805
+ * ```
806
+ */
807
+ function estimateTokens(text) {
808
+ return Math.ceil(text.length / 4);
809
+ }
810
+ /**
811
+ * Estimate the context-token footprint of a batch of messages — the default `consume`
812
+ * estimator for an agent's context `BudgetInterface` (a budgets surface's tracking contract)
813
+ * (the {@link import('./types.js').AgentOptions} `window`).
814
+ *
815
+ * @remarks
816
+ * Sums {@link estimateTokens} over each message's `content` (the `ceil(length / 4)` char
817
+ * heuristic), so it is deterministic and provider-free — the same messages always yield the
818
+ * same estimate, with an empty batch `0`. It is the fully-swappable default an agent's
819
+ * auto-compaction context budget charges each turn's new messages through; a caller wanting a
820
+ * sharper count supplies its own `consume` to `createBudget` instead. Total — never throws.
821
+ *
822
+ * @param messages - The messages to estimate (a turn's appended assistant + tool messages)
823
+ * @returns The summed estimated token count (`Σ estimateTokens(m.content)`; `0` when empty)
824
+ *
825
+ * @example
826
+ * ```ts
827
+ * estimateMessages([]) // 0
828
+ * estimateMessages([{ id: '1', role: 'user', content: 'hello' }]) // 2
829
+ * ```
830
+ */
831
+ function estimateMessages(messages) {
832
+ return messages.reduce((sum, message) => sum + estimateTokens(message.content), 0);
833
+ }
834
+ /**
835
+ * Run one rehydrated agent and apply the partial-as-configurable-failure policy — the
836
+ * shared job-handler step BOTH `createAgentQueue` and `createAgentRunner` settle each job
837
+ * through, so the policy can never diverge between them.
838
+ *
839
+ * @remarks
840
+ * A turn that committed PARTIAL (a cancel — abort / budget / timeout) is by default a
841
+ * FAILURE, so it THROWS an {@link import('./errors.js').AgentJobError} carrying the partial
842
+ * (the Queue's retries + a Runner's fail-fast then engage); with `allowPartial` it RESOLVES
843
+ * the partial as success instead. A natural finish ALWAYS resolves with its result.
844
+ *
845
+ * @param agent - The rehydrated {@link AgentInterface} to run to its {@link AgentResult}
846
+ * @param allowPartial - When `true`, a partial result resolves as success; when `false`
847
+ * (the default policy), a partial result throws an {@link AgentJobError}
848
+ * @returns The agent's {@link AgentResult} (a natural finish, or a partial when `allowPartial`)
849
+ * @throws {AgentJobError} When the run ended `partial` and `allowPartial` is `false`
850
+ *
851
+ * @example
852
+ * ```ts
853
+ * const result = await settleAgentJob(registry.build(input, signal), false)
854
+ * ```
855
+ */
856
+ async function settleAgentJob(agent, allowPartial) {
857
+ const result = await agent.generate();
858
+ if (result.partial && !allowPartial) throw new AgentJobError("agent job ended partial", result);
859
+ return result;
860
+ }
861
+ /**
862
+ * Whether a {@link FileContent} is the TEXT arm — the narrowing guard for the text-vs-binary
863
+ * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
864
+ *
865
+ * @remarks
866
+ * Tests `'text' in content` structurally — there is no `modality` discriminant. A `true`
867
+ * narrows `content` to `{ text: string; language: string }`, unlocking `.text` / `.language`.
868
+ * Total — never throws.
869
+ *
870
+ * @param content - The file content to test
871
+ * @returns `true` when `content` is the text arm (carries `text` + `language`)
872
+ *
873
+ * @example
874
+ * ```ts
875
+ * if (isText(file.content)) file.content.text // the literal text + .language
876
+ * ```
877
+ */
878
+ function isText(content) {
879
+ return "text" in content;
880
+ }
881
+ /**
882
+ * Whether a {@link FileContent} is the BINARY arm — the narrowing guard for the text-vs-binary
883
+ * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
884
+ *
885
+ * @remarks
886
+ * Tests `'data' in content` structurally — there is no `modality` discriminant. A `true`
887
+ * narrows `content` to `{ data: string; mime: BinaryMIME }`, unlocking `.data` / `.mime`. An
888
+ * image is a binary whose `mime` starts with `image/` ({@link isImage}). Total — never throws.
889
+ *
890
+ * @param content - The file content to test
891
+ * @returns `true` when `content` is the binary arm (carries base64 `data` + `mime`)
892
+ *
893
+ * @example
894
+ * ```ts
895
+ * if (isBinary(file.content)) file.content.data // the base64 payload + .mime
896
+ * ```
897
+ */
898
+ function isBinary(content) {
899
+ return "data" in content;
900
+ }
901
+ /**
902
+ * Whether a {@link FileContent} is an IMAGE — a {@link isBinary} arm whose `mime` is an
903
+ * `image/*` type (an image is just a binary with an image MIME).
904
+ *
905
+ * @remarks
906
+ * Narrows to the binary arm first, then checks the `image/` MIME prefix — so a future
907
+ * non-image binary (a PDF, `'application/pdf'`) is binary but NOT an image. Total — never
908
+ * throws.
909
+ *
910
+ * @param content - The file content to test
911
+ * @returns `true` when `content` is a binary arm with an `image/*` MIME
912
+ *
913
+ * @example
914
+ * ```ts
915
+ * isImage({ data: '<base64>', mime: 'image/png' }) // true
916
+ * isImage({ text: 'hi', language: 'text' }) // false
917
+ * ```
918
+ */
919
+ function isImage(content) {
920
+ return isBinary(content) && content.mime.startsWith("image/");
921
+ }
922
+ /**
923
+ * Whether an `unknown` is structurally a {@link FileInterface} record — the per-file step of the
924
+ * {@link isWorkspaceSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage read
925
+ * via a guard, never an `as`).
926
+ *
927
+ * @remarks
928
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the file's SHAPE:
929
+ * a record with a `string` `path`, a `string` `state`, `number` `size` / `lines`, and a `content`
930
+ * that is EITHER the TEXT arm (`{ text: string; language: string }`) OR the BINARY arm
931
+ * (`{ data: string; mime: string }`) — the tagless {@link FileContent} union, told apart
932
+ * structurally exactly as {@link isText} / {@link isBinary} do (no `modality` discriminant). Enough
933
+ * to safely impose the {@link FileInterface} type at a storage boundary WITHOUT a cast; the `mime`
934
+ * is left as a broad `string` here (an open {@link BinaryMIME}, so any storage-read MIME string is
935
+ * accepted defensively rather than rejected against the current literal set).
936
+ *
937
+ * @param value - The value to test (one element of a snapshot's opaque `files` array)
938
+ * @returns `true` when `value` has the structural shape of a {@link FileInterface}
939
+ *
940
+ * @example
941
+ * ```ts
942
+ * isFile({ path: 'a.ts', content: { text: 'x', language: 'typescript' }, state: 'created', size: 1, lines: 1 }) // true
943
+ * isFile({ path: 'a.png', content: { data: 'AAAA', mime: 'image/png' }, state: 'created', size: 3, lines: 0 }) // true
944
+ * isFile({ path: 'a.ts' }) // false (missing content / state / size / lines)
945
+ * ```
946
+ */
947
+ function isFile(value) {
948
+ if (!isRecord(value)) return false;
949
+ if (!isString(value.path) || !isString(value.state)) return false;
950
+ if (!isNumber(value.size) || !isNumber(value.lines)) return false;
951
+ if (!isRecord(value.content)) return false;
952
+ const text = isString(value.content.text) && isString(value.content.language);
953
+ const binary = isString(value.content.data) && isString(value.content.mime);
954
+ return text || binary;
955
+ }
956
+ /**
957
+ * Narrow an `unknown` to a {@link WorkspaceSnapshot} — the AGENTS §14 boundary guard for an
958
+ * UNTRUSTED snapshot read (a storage row a
959
+ * {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} reads back
960
+ * from its opaque JSON column, a snapshot loaded from disk).
961
+ *
962
+ * @remarks
963
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
964
+ * SHAPE: a `string` `id` and a `files` array EVERY element of which is a valid {@link FileInterface}
965
+ * record ({@link isFile}) — enough to safely impose the {@link WorkspaceSnapshot} type at a storage
966
+ * boundary WITHOUT a cast. The structural twin of
967
+ * the analogous `isWorkflowSnapshot` in `@orkestrel/workflow`. A malformed blob (a non-record, a
968
+ * missing / non-string `id`, a non-array `files`, or any malformed file element) resolves `false`,
969
+ * so a {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} read
970
+ * yields `undefined` rather than a broken workspace.
971
+ *
972
+ * @param value - The value to test (an opaque storage read)
973
+ * @returns `true` when `value` has the structural shape of a {@link WorkspaceSnapshot}
974
+ *
975
+ * @example
976
+ * ```ts
977
+ * isWorkspaceSnapshot({ id: 'w1', files: [] }) // true
978
+ * isWorkspaceSnapshot({ id: 'w1', files: 'nope' }) // false
979
+ * isWorkspaceSnapshot({ files: [] }) // false (missing id)
980
+ * ```
981
+ */
982
+ function isWorkspaceSnapshot(value) {
983
+ return isRecord(value) && isString(value.id) && isArray(value.files) && value.files.every(isFile);
984
+ }
985
+ /**
986
+ * Whether an `unknown` is structurally a {@link ToolCall} record — the per-call step of the
987
+ * {@link isMessage} read-boundary narrow (AGENTS §14: narrow an untrusted storage read via a
988
+ * guard, never an `as`).
989
+ *
990
+ * @remarks
991
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the call's
992
+ * SHAPE: a record with a `string` `id`, a `string` `name`, and a record `arguments` — the real
993
+ * {@link ToolCall} shape a restored assistant turn replays to a provider. This is the ASI06
994
+ * fail-CLOSED element check: a tampered persisted call (`null`, a bare string, a non-record
995
+ * `arguments`) fails the guard, so {@link isMessage} — and through it
996
+ * {@link isConversationSnapshot} — rejects the whole snapshot and a poisoned store row reads
997
+ * back as ABSENT instead of replaying a malformed call into a chat template.
998
+ *
999
+ * @param value - The value to test (one element of an assistant message's `calls`)
1000
+ * @returns `true` when `value` has the structural shape of a {@link ToolCall}
1001
+ *
1002
+ * @example
1003
+ * ```ts
1004
+ * isToolCall({ id: 'c1', name: 'search', arguments: { q: 'acme' } }) // true
1005
+ * isToolCall({ id: 'c1', name: 'search' }) // false (missing arguments)
1006
+ * isToolCall({ id: 'c1', name: 123, arguments: {} }) // false (non-string name)
1007
+ * ```
1008
+ */
1009
+ function isToolCall(value) {
1010
+ if (!isRecord(value)) return false;
1011
+ return isString(value.id) && isString(value.name) && isRecord(value.arguments);
1012
+ }
1013
+ /**
1014
+ * Project a {@link ToolResult} into the MCP `CallToolResult` shape — a top-level `error`
1015
+ * maps to a single `isError: true` text block (the failure reason), otherwise the `value`
1016
+ * is JSON-stringified into a single text block with no `isError`.
1017
+ *
1018
+ * @param result - The {@link ToolResult} to project (as returned by {@link import('./tools/ToolManager.js').ToolManager.execute})
1019
+ * @returns The equivalent {@link ToolCallResult}
1020
+ *
1021
+ * @example
1022
+ * ```ts
1023
+ * buildToolResult({ id: 'c1', name: 'search', error: 'max depth' })
1024
+ * // { content: [{ type: 'text', text: 'max depth' }], isError: true }
1025
+ * buildToolResult({ id: 'c1', name: 'search', value: { count: 1 } })
1026
+ * // { content: [{ type: 'text', text: '{"count":1}' }] }
1027
+ * ```
1028
+ */
1029
+ function buildToolResult(result) {
1030
+ if (result.error !== void 0) return {
1031
+ content: [{
1032
+ type: "text",
1033
+ text: result.error
1034
+ }],
1035
+ isError: true
1036
+ };
1037
+ return { content: [{
1038
+ type: "text",
1039
+ text: JSON.stringify(result.value)
1040
+ }] };
1041
+ }
1042
+ /**
1043
+ * Whether an `unknown` is structurally a {@link MessageInterface} record — the per-message step of
1044
+ * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
1045
+ * read via a guard, never an `as`). The conversation analogue of {@link isFile}.
1046
+ *
1047
+ * @remarks
1048
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
1049
+ * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
1050
+ * — a `calls` array EVERY element of which is a valid {@link ToolCall} ({@link isToolCall} — the
1051
+ * ASI06 fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
1052
+ * reads back as absent rather than replaying a malformed call) and an `images` that is an array
1053
+ * (an absent optional passes). The `role` is left as a broad `string` here (an open
1054
+ * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
1055
+ * defensively rather than rejected against the current literal set) — exactly as {@link isFile}
1056
+ * accepts a broad `state` / `mime`. Enough to safely impose the {@link MessageInterface} type at
1057
+ * a storage boundary WITHOUT a cast.
1058
+ *
1059
+ * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
1060
+ * @returns `true` when `value` has the structural shape of a {@link MessageInterface}
1061
+ *
1062
+ * @example
1063
+ * ```ts
1064
+ * isMessage({ id: '1', role: 'user', content: 'hi' }) // true
1065
+ * isMessage({ id: '1', role: 'assistant', content: '', calls: [] }) // true
1066
+ * isMessage({ id: '1', role: 'user' }) // false (missing content)
1067
+ * isMessage({ id: '1', role: 'assistant', content: '', calls: [null] }) // false (malformed call)
1068
+ * ```
1069
+ */
1070
+ function isMessage(value) {
1071
+ if (!isRecord(value)) return false;
1072
+ if (!isString(value.id) || !isString(value.role) || !isString(value.content)) return false;
1073
+ if (value.calls !== void 0 && !(isArray(value.calls) && value.calls.every(isToolCall))) return false;
1074
+ return value.images === void 0 || isArray(value.images);
1075
+ }
1076
+ /**
1077
+ * Whether an `unknown` is structurally a {@link SectionInterface} record — the per-section step of
1078
+ * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
1079
+ * read via a guard, never an `as`).
1080
+ *
1081
+ * @remarks
1082
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the section's
1083
+ * SHAPE: a record with a `string` `id`, a `string` `summary`, and a `messages` array EVERY element
1084
+ * of which is a valid {@link MessageInterface} record ({@link isMessage}). Enough to safely impose
1085
+ * the {@link SectionInterface} type at a storage boundary WITHOUT a cast.
1086
+ *
1087
+ * @param value - The value to test (one element of a snapshot's `sections` array)
1088
+ * @returns `true` when `value` has the structural shape of a {@link SectionInterface}
1089
+ *
1090
+ * @example
1091
+ * ```ts
1092
+ * isSection({ id: 's', summary: 'recap', messages: [{ id: '1', role: 'user', content: 'hi' }] }) // true
1093
+ * isSection({ id: 's', summary: 'recap', messages: 'nope' }) // false
1094
+ * isSection({ id: 's', messages: [] }) // false (missing summary)
1095
+ * ```
1096
+ */
1097
+ function isSection(value) {
1098
+ if (!isRecord(value)) return false;
1099
+ if (!isString(value.id) || !isString(value.summary)) return false;
1100
+ return isArray(value.messages) && value.messages.every(isMessage);
1101
+ }
1102
+ /**
1103
+ * Narrow an `unknown` to a {@link ConversationSnapshot} — the AGENTS §14 boundary guard for an
1104
+ * UNTRUSTED snapshot read (a storage row a
1105
+ * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
1106
+ * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
1107
+ * {@link isWorkspaceSnapshot}.
1108
+ *
1109
+ * @remarks
1110
+ * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
1111
+ * SHAPE: a `string` `id`, an OPTIONAL `string` `summary` (present-or-absent — the rollup is
1112
+ * `undefined` until the first compaction), a `sections` array EVERY element of which is a valid
1113
+ * {@link SectionInterface} ({@link isSection}), and a `messages` array EVERY element of which is a
1114
+ * valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
1115
+ * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
1116
+ * {@link isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
1117
+ * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
1118
+ * element) resolves `false`, so a
1119
+ * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
1120
+ * read yields `undefined` rather than a broken conversation.
1121
+ *
1122
+ * @param value - The value to test (an opaque storage read)
1123
+ * @returns `true` when `value` has the structural shape of a {@link ConversationSnapshot}
1124
+ *
1125
+ * @example
1126
+ * ```ts
1127
+ * isConversationSnapshot({ id: 'c1', sections: [], messages: [] }) // true
1128
+ * isConversationSnapshot({ id: 'c1', summary: 'recap', sections: [], messages: [] }) // true
1129
+ * isConversationSnapshot({ id: 'c1', sections: 'nope', messages: [] }) // false
1130
+ * isConversationSnapshot({ sections: [], messages: [] }) // false (missing id)
1131
+ * ```
1132
+ */
1133
+ function isConversationSnapshot(value) {
1134
+ if (!isRecord(value)) return false;
1135
+ if (!isString(value.id)) return false;
1136
+ if (value.summary !== void 0 && !isString(value.summary)) return false;
1137
+ if (!isArray(value.sections) || !value.sections.every(isSection)) return false;
1138
+ return isArray(value.messages) && value.messages.every(isMessage);
1139
+ }
1140
+ /**
1141
+ * Compute the byte size of a {@link FileContent} — the `size` a {@link FileInterface} derives
1142
+ * once when built.
1143
+ *
1144
+ * @remarks
1145
+ * Dispatches on {@link isText}: a text arm is its UTF-8 byte length (via `new
1146
+ * TextEncoder().encode(text).length` — the standard Web/Node API, so a multi-byte
1147
+ * character like `'é'` or `'😀'` counts as its real encoded bytes, NOT its character
1148
+ * count); a binary arm is the decoded payload byte length of its base64 `data` (via
1149
+ * {@link decodedSize}, computed arithmetically — no `atob` / `Buffer`). Total — never throws.
1150
+ *
1151
+ * @param content - The file content to size
1152
+ * @returns The byte size (UTF-8 bytes for text; decoded payload bytes for binary)
1153
+ *
1154
+ * @example
1155
+ * ```ts
1156
+ * computeSize({ text: 'café', language: 'text' }) // 5 (é is two bytes)
1157
+ * computeSize({ data: 'AAAA', mime: 'image/png' }) // 3
1158
+ * ```
1159
+ */
1160
+ function computeSize(content) {
1161
+ if (isText(content)) return new TextEncoder().encode(content.text).length;
1162
+ return decodedSize(content.data);
1163
+ }
1164
+ /**
1165
+ * Count the lines of a {@link FileContent} — the `lines` a {@link FileInterface} derives once
1166
+ * when built.
1167
+ *
1168
+ * @remarks
1169
+ * A text arm's line count is `0` for the empty string, otherwise one more than the
1170
+ * number of newline (`\n`) separators it contains — so `'a'` is one line, `'a\nb'` is two,
1171
+ * and a trailing newline `'a\n'` counts the empty final line as two. A binary arm has no
1172
+ * lines and returns `0`. Total — never throws.
1173
+ *
1174
+ * @param content - The file content to count lines for
1175
+ * @returns The line count (text line count; `0` for a binary arm or empty text)
1176
+ *
1177
+ * @example
1178
+ * ```ts
1179
+ * countLines({ text: '', language: 'text' }) // 0
1180
+ * countLines({ text: 'a\nb\nc', language: 'text' }) // 3
1181
+ * countLines({ data: 'AAAA', mime: 'image/png' }) // 0
1182
+ * ```
1183
+ */
1184
+ function countLines(content) {
1185
+ if (!isText(content)) return 0;
1186
+ if (content.text.length === 0) return 0;
1187
+ let count = 1;
1188
+ for (const character of content.text) if (character === "\n") count += 1;
1189
+ return count;
1190
+ }
1191
+ /**
1192
+ * Compute the decoded byte length of a base64 string ARITHMETICALLY — the image-payload
1193
+ * sizing primitive {@link computeSize} uses, with no `atob` / `Buffer` decode.
1194
+ *
1195
+ * @remarks
1196
+ * A base64 string encodes each group of 3 input bytes as 4 characters, so a well-formed
1197
+ * string's length is a multiple of 4 and its decoded length is `(length / 4) * 3` MINUS
1198
+ * the trailing `=` padding (one `=` ⇒ the last group held 2 bytes, two `==` ⇒ 1 byte).
1199
+ * The padding is counted from the trailing `=` characters, so both the `=` and `==` cases
1200
+ * are handled. The empty string decodes to `0`. Total — never throws (a malformed,
1201
+ * non-multiple-of-4 length still yields a defined non-negative estimate via `floor`).
1202
+ *
1203
+ * @param base64 - The base64-encoded string (e.g. an image's `data`)
1204
+ * @returns The decoded payload's byte length
1205
+ *
1206
+ * @example
1207
+ * ```ts
1208
+ * decodedSize('') // 0
1209
+ * decodedSize('AAAA') // 3 (no padding)
1210
+ * decodedSize('AAA=') // 2 (one '=')
1211
+ * decodedSize('AA==') // 1 (two '=')
1212
+ * ```
1213
+ */
1214
+ function decodedSize(base64) {
1215
+ if (base64.length === 0) return 0;
1216
+ let padding = 0;
1217
+ if (base64.endsWith("==")) padding = 2;
1218
+ else if (base64.endsWith("=")) padding = 1;
1219
+ return Math.floor(base64.length / 4) * 3 - padding;
1220
+ }
1221
+ /**
1222
+ * Whether a {@link Range} is STRUCTURALLY valid — the predicate a ranged `write` checks
1223
+ * before applying (a `false` here is the `RANGE` throw).
1224
+ *
1225
+ * @remarks
1226
+ * Structural validity is independent of any content: every component must be `>= 1`
1227
+ * (1-based lines and columns), and `start` must not come after `end` (an inverted range
1228
+ * is invalid). A range that is structurally valid but reaches PAST the end of a specific
1229
+ * text is still valid — it is {@link clampRange}d to the bounds when applied, not rejected.
1230
+ * Total — never throws.
1231
+ *
1232
+ * @param range - The range to validate
1233
+ * @returns `true` when every component is `>= 1` and `start` is at or before `end`
1234
+ *
1235
+ * @example
1236
+ * ```ts
1237
+ * isValidRange({ start: { line: 1, column: 1 }, end: { line: 2, column: 1 } }) // true
1238
+ * isValidRange({ start: { line: 2, column: 1 }, end: { line: 1, column: 1 } }) // false (inverted)
1239
+ * isValidRange({ start: { line: 0, column: 1 }, end: { line: 1, column: 1 } }) // false (sub-1 line)
1240
+ * ```
1241
+ */
1242
+ function isValidRange(range) {
1243
+ if (range.start.line < 1 || range.start.column < 1) return false;
1244
+ if (range.end.line < 1 || range.end.column < 1) return false;
1245
+ if (range.start.line > range.end.line) return false;
1246
+ return !(range.start.line === range.end.line && range.start.column > range.end.column);
1247
+ }
1248
+ /**
1249
+ * Clamp a 1-based {@link Position} to the bounds of `text` — every component pinned into a
1250
+ * caret that actually exists in the content.
1251
+ *
1252
+ * @remarks
1253
+ * `line` is pinned to `[1, lineCount]` and `column` to `[1, lineLength + 1]` of the
1254
+ * resolved line (column `lineLength + 1` is the caret just past the line's last
1255
+ * character). So a position beyond the end of the text resolves to the end rather than
1256
+ * overflowing. Total — never throws.
1257
+ *
1258
+ * @param text - The text the position addresses
1259
+ * @param position - The 1-based position to clamp
1260
+ * @returns The clamped {@link Position}
1261
+ *
1262
+ * @example
1263
+ * ```ts
1264
+ * clampPosition('ab\ncd', { line: 9, column: 9 }) // { line: 2, column: 3 } (end of 'cd')
1265
+ * ```
1266
+ */
1267
+ function clampPosition(text, position) {
1268
+ const lines = text.split("\n");
1269
+ const line = Math.max(1, Math.min(position.line, lines.length));
1270
+ const lineText = lines[line - 1] ?? "";
1271
+ return {
1272
+ line,
1273
+ column: Math.max(1, Math.min(position.column, lineText.length + 1))
1274
+ };
1275
+ }
1276
+ /**
1277
+ * Clamp both ends of a {@link Range} to the bounds of `text` — the actual span a ranged
1278
+ * read / write applies (and the `range` a {@link import('./types.js').ReadResult} reports).
1279
+ *
1280
+ * @remarks
1281
+ * Clamps `start` and `end` independently via {@link clampPosition}, so a range reaching
1282
+ * past the end of the content is trimmed to the content's end. Total — never throws.
1283
+ *
1284
+ * @param text - The text the range addresses
1285
+ * @param range - The 1-based range to clamp
1286
+ * @returns The clamped {@link Range}
1287
+ *
1288
+ * @example
1289
+ * ```ts
1290
+ * clampRange('ab\ncd', { start: { line: 1, column: 1 }, end: { line: 9, column: 9 } })
1291
+ * // { start: { line: 1, column: 1 }, end: { line: 2, column: 3 } }
1292
+ * ```
1293
+ */
1294
+ function clampRange(text, range) {
1295
+ return {
1296
+ start: clampPosition(text, range.start),
1297
+ end: clampPosition(text, range.end)
1298
+ };
1299
+ }
1300
+ /**
1301
+ * Convert a 1-based {@link Position} to a 0-based string offset into `text` — the indexing
1302
+ * primitive {@link sliceRange} / {@link spliceRange} use, clamped to `text.length`.
1303
+ *
1304
+ * @remarks
1305
+ * Sums each preceding line's length plus its `\n` separator, then adds the `column - 1`
1306
+ * within the target line, capped at `text.length`. Total — never throws (an out-of-bounds
1307
+ * position yields a defined in-range offset).
1308
+ *
1309
+ * @param text - The text to index into
1310
+ * @param position - The 1-based position to resolve
1311
+ * @returns The 0-based offset (in `[0, text.length]`)
1312
+ *
1313
+ * @example
1314
+ * ```ts
1315
+ * offsetAt('ab\ncd', { line: 2, column: 1 }) // 3 (just after 'ab\n')
1316
+ * ```
1317
+ */
1318
+ function offsetAt(text, position) {
1319
+ const lines = text.split("\n");
1320
+ let offset = 0;
1321
+ for (let index = 0; index < position.line - 1 && index < lines.length; index += 1) offset += (lines[index]?.length ?? 0) + 1;
1322
+ offset += position.column - 1;
1323
+ return Math.min(offset, text.length);
1324
+ }
1325
+ /**
1326
+ * Slice the substring of `text` spanned by a {@link Range} (start INCLUSIVE, end
1327
+ * EXCLUSIVE), clamping the range to the text's bounds first — the read half of the ranged
1328
+ * edit surface.
1329
+ *
1330
+ * @remarks
1331
+ * Clamps via {@link clampRange}, resolves each end to an offset via {@link offsetAt}, and
1332
+ * returns `text.slice(startOffset, endOffset)`. Total — never throws.
1333
+ *
1334
+ * @param text - The text to slice
1335
+ * @param range - The 1-based range to extract
1336
+ * @returns The spanned substring (empty when the clamped span is empty)
1337
+ *
1338
+ * @example
1339
+ * ```ts
1340
+ * sliceRange('hello\nworld', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }) // 'hello'
1341
+ * ```
1342
+ */
1343
+ function sliceRange(text, range) {
1344
+ const clamped = clampRange(text, range);
1345
+ return text.slice(offsetAt(text, clamped.start), offsetAt(text, clamped.end));
1346
+ }
1347
+ /**
1348
+ * Replace the span of `text` covered by a {@link Range} with `replacement` (start
1349
+ * INCLUSIVE, end EXCLUSIVE), clamping the range to the text's bounds first — the write
1350
+ * half of the ranged edit surface.
1351
+ *
1352
+ * @remarks
1353
+ * Clamps via {@link clampRange}, then stitches `before + replacement + after` around the
1354
+ * resolved offsets. An empty span (`start === end`) becomes a pure insertion. Total —
1355
+ * never throws.
1356
+ *
1357
+ * @param text - The original text
1358
+ * @param range - The 1-based range to overwrite
1359
+ * @param replacement - The text to splice in place of the spanned range
1360
+ * @returns The text with the spanned range replaced by `replacement`
1361
+ *
1362
+ * @example
1363
+ * ```ts
1364
+ * spliceRange('hello', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }, 'bye') // 'bye'
1365
+ * ```
1366
+ */
1367
+ function spliceRange(text, range, replacement) {
1368
+ const clamped = clampRange(text, range);
1369
+ const start = offsetAt(text, clamped.start);
1370
+ const end = offsetAt(text, clamped.end);
1371
+ return text.slice(0, start) + replacement + text.slice(end);
1372
+ }
1373
+ /**
1374
+ * Assemble a 1-based nested {@link Range} from the four FLAT caret integers of the workspace tool's
1375
+ * `'splice'` operation — the bridge from the small-model FLAT surface
1376
+ * ({@link import('./types.js').WorkspaceOperation}) back to the nested {@link Range} the
1377
+ * {@link import('./workspaces/Workspace.js').Workspace} edit surface speaks.
1378
+ *
1379
+ * @remarks
1380
+ * Pairs `(fromLine, fromColumn)` into `start` and `(toLine, toColumn)` into `end` verbatim — a pure
1381
+ * structural lift, no validation (a structurally invalid range is rejected downstream by the ranged
1382
+ * `write` it feeds, via {@link isValidRange}). Total — never throws. Zero-Node.
1383
+ *
1384
+ * @param fromLine - The 1-based start line
1385
+ * @param fromColumn - The 1-based start column
1386
+ * @param toLine - The 1-based end line
1387
+ * @param toColumn - The 1-based end column
1388
+ * @returns The `{ start, end }` {@link Range}
1389
+ *
1390
+ * @example
1391
+ * ```ts
1392
+ * rangeOf(1, 11, 1, 12) // { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }
1393
+ * ```
1394
+ */
1395
+ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
1396
+ return {
1397
+ start: {
1398
+ line: fromLine,
1399
+ column: fromColumn
1400
+ },
1401
+ end: {
1402
+ line: toLine,
1403
+ column: toColumn
1404
+ }
1405
+ };
1406
+ }
1407
+ /**
1408
+ * Render a path-addressed text body as a fenced reference block — the framing an
1409
+ * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
1410
+ * active workspace is the SOLE document/image context).
1411
+ *
1412
+ * @remarks
1413
+ * Produces `` File: <path>\n```<language>\n<content>\n``` `` — the `File:` label line, then a
1414
+ * fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
1415
+ * total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
1416
+ * frames an active workspace's text files with it (each carries its own `language` on its
1417
+ * {@link FileContent} text arm).
1418
+ *
1419
+ * @param path - The file path shown on the `File:` label line
1420
+ * @param language - The fenced-code language tag (e.g. `'typescript'`)
1421
+ * @param content - The file body rendered verbatim inside the fence
1422
+ * @returns The fenced reference block
1423
+ *
1424
+ * @example
1425
+ * ```ts
1426
+ * import { fencedFile } from '@src/core'
1427
+ *
1428
+ * fencedFile('src/main.ts', 'typescript', 'const x = 1')
1429
+ * // 'File: src/main.ts\n```typescript\nconst x = 1\n```'
1430
+ * ```
1431
+ */
1432
+ function fencedFile(path, language, content) {
1433
+ return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
1434
+ }
1435
+ /**
1436
+ * Escape a string's regex-special characters so it matches LITERALLY inside a `RegExp` — the
1437
+ * primitive a {@link import('./workspaces/Workspace.js').Workspace} search builds a literal-text
1438
+ * search pattern through (as opposed to a caller-supplied regex pattern).
1439
+ *
1440
+ * @remarks
1441
+ * Prefixes every character in the class `. * + ? ^ $ { } ( ) | [ ] \` with a backslash, so the
1442
+ * escaped string, when compiled into a `RegExp`, matches only its own literal characters — no
1443
+ * character acts as a quantifier, anchor, group, or class. Pure string assembly, total — never
1444
+ * throws.
1445
+ *
1446
+ * @param value - The text to escape for literal use inside a `RegExp` pattern
1447
+ * @returns `value` with every regex-special character backslash-escaped
1448
+ *
1449
+ * @example
1450
+ * ```ts
1451
+ * escapeRegExp('a.b*c') // 'a\\.b\\*c'
1452
+ * new RegExp(escapeRegExp('a.b*c')).test('a.b*c') // true
1453
+ * ```
1454
+ */
1455
+ function escapeRegExp(value) {
1456
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1457
+ }
1458
+ //#endregion
1459
+ //#region src/core/instructions/Instruction.ts
1460
+ /**
1461
+ * An immutable named directive — a {@link InstructionInterface} assembled once from its
1462
+ * input (`name` / `content`, an optional `priority` defaulting to `0`), the `id` minted
1463
+ * at construction.
1464
+ *
1465
+ * @remarks
1466
+ * A thin immutable value object (mirroring {@link import('../tools/Tool.js').Tool}): the
1467
+ * constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
1468
+ * `content`, resolves `priority` to the input's value or `0`, and carries the input's
1469
+ * per-item `format` override ONLY when supplied (assigned just when present, mirroring a
1470
+ * message's `images` / `calls` present-when-given convention — kept absent otherwise).
1471
+ * Never mutated after construction. An
1472
+ * {@link import('./InstructionManager.js').InstructionManager} keys it by `name` and
1473
+ * renders it (highest `priority` first) under its section header.
1474
+ *
1475
+ * @example
1476
+ * ```ts
1477
+ * const instruction = new Instruction({ name: 'tone', content: 'Be concise.', priority: 5 })
1478
+ * instruction.priority // 5
1479
+ * ```
1480
+ */
1481
+ var Instruction = class {
1482
+ id;
1483
+ name;
1484
+ content;
1485
+ priority;
1486
+ format;
1487
+ constructor(input) {
1488
+ this.id = crypto.randomUUID();
1489
+ this.name = input.name;
1490
+ this.content = input.content;
1491
+ this.priority = input.priority ?? 0;
1492
+ if (input.format !== void 0) this.format = input.format;
1493
+ }
1494
+ };
1495
+ //#endregion
1496
+ //#region src/core/instructions/InstructionManager.ts
1497
+ /**
1498
+ * The instruction registry a richer context assembles a directives block from —
1499
+ * immutable {@link Instruction}s keyed by `name`, listed by descending `priority`.
1500
+ *
1501
+ * @remarks
1502
+ * - **Registry.** Instructions live in an insertion-ordered `Map` keyed by `name`;
1503
+ * `add` takes one {@link InstructionInput} or a batch (§9.2), MINTS each instruction's
1504
+ * `id`, and a re-`add` of the same name OVERWRITES it (last write wins). `count` is the
1505
+ * map size, `instruction(name)` looks one up, and `instructions()` lists them SORTED by
1506
+ * descending `priority` (a stable sort, so equal priorities keep insertion order).
1507
+ * - **Build contract (with the manager-options override).** `description` is the section
1508
+ * header a context renders the instructions under; `format(instruction)` renders one
1509
+ * instruction (its `content`). Each ENCAPSULATES the cascade's `[options-override →
1510
+ * built-in]` half: when `InstructionManagerOptions.format` supplies an `open` /
1511
+ * `render`, `description` / `format` return IT, else the built-in — so a richer context
1512
+ * reads one consistent pair and layers the provider default + per-item override on top
1513
+ * (see {@link import('../AgentContext.js').AgentContext}). The per-item
1514
+ * {@link InstructionInput.format} is round-tripped onto the stored instruction.
1515
+ * - **Removal.** `remove` drops one by name, or a batch (§9.2) — `true` when any was
1516
+ * removed; `clear` empties the registry.
1517
+ * - **Observable (§13).** The owned {@link emitter} ({@link InstructionManagerEventMap})
1518
+ * carries `add` (the created instruction) / `remove` (the name) / `clear` for
1519
+ * fire-and-forget observers. Every event is emitted directly, strictly AFTER the map
1520
+ * mutation completes; the emitter isolates a listener throw and routes it to its `error`
1521
+ * handler (the `error` option), so a buggy observer can never corrupt a mutation.
1522
+ *
1523
+ * @example
1524
+ * ```ts
1525
+ * const manager = new InstructionManager()
1526
+ * manager.add([
1527
+ * { name: 'tone', content: 'Be concise.', priority: 1 },
1528
+ * { name: 'safety', content: 'Refuse unsafe requests.', priority: 10 },
1529
+ * ])
1530
+ * manager.instructions().map((one) => one.name) // ['safety', 'tone'] — highest priority first
1531
+ * ```
1532
+ */
1533
+ var InstructionManager = class {
1534
+ #instructions = /* @__PURE__ */ new Map();
1535
+ #emitter;
1536
+ #format;
1537
+ constructor(options) {
1538
+ this.#emitter = new Emitter({
1539
+ on: options?.on,
1540
+ error: options?.error
1541
+ });
1542
+ this.#format = options?.format;
1543
+ }
1544
+ get emitter() {
1545
+ return this.#emitter;
1546
+ }
1547
+ get count() {
1548
+ return this.#instructions.size;
1549
+ }
1550
+ get description() {
1551
+ return this.#format?.open ?? "## Instructions";
1552
+ }
1553
+ get framing() {
1554
+ return this.#format;
1555
+ }
1556
+ add(input) {
1557
+ if (isArray(input)) return input.map((one) => this.#create(one));
1558
+ return this.#create(input);
1559
+ }
1560
+ instruction(name) {
1561
+ return this.#instructions.get(name);
1562
+ }
1563
+ instructions() {
1564
+ return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
1565
+ }
1566
+ format(instruction) {
1567
+ return this.#format?.render?.(instruction) ?? instruction.content;
1568
+ }
1569
+ remove(names) {
1570
+ if (isArray(names)) {
1571
+ let removed = false;
1572
+ for (const name of names) if (this.#delete(name)) removed = true;
1573
+ return removed;
1574
+ }
1575
+ return this.#delete(names);
1576
+ }
1577
+ clear() {
1578
+ this.#instructions.clear();
1579
+ this.#emitter.emit("clear");
1580
+ }
1581
+ #create(input) {
1582
+ const instruction = new Instruction(input);
1583
+ this.#instructions.set(instruction.name, instruction);
1584
+ this.#emitter.emit("add", instruction);
1585
+ return instruction;
1586
+ }
1587
+ #delete(name) {
1588
+ const removed = this.#instructions.delete(name);
1589
+ if (removed) this.#emitter.emit("remove", name);
1590
+ return removed;
1591
+ }
1592
+ };
1593
+ //#endregion
1594
+ //#region src/core/tools/ToolManager.ts
1595
+ /**
1596
+ * The tool registry the agent loop dispatches model tool-calls through — resolves
1597
+ * names, lists {@link ToolDefinition}s for the provider, and executes calls with
1598
+ * per-call error isolation.
1599
+ *
1600
+ * @remarks
1601
+ * - **Registry.** Tools live in an insertion-ordered `Map` keyed by `tool.name`;
1602
+ * `add` takes one or a batch (§9.2) and a re-`add` of the same name OVERWRITES it
1603
+ * (last write wins). `count` is the map size, `tool(name)` looks one up, `tools()`
1604
+ * lists them in insertion order, and `definitions()` maps each to a plain
1605
+ * {@link ToolDefinition} (`name` / `description?` / `parameters?`, the `execute`
1606
+ * handler stripped) — exactly what a provider advertises to the model.
1607
+ * - **Per-call error isolation (the load-bearing part).** `execute` resolves a
1608
+ * {@link ToolCall}'s tool by name and ALWAYS resolves a {@link ToolResult}: an
1609
+ * unknown name → `{ id, name, error: 'tool not found: <name>' }`; a successful run →
1610
+ * `{ id, name, value }`; a handler throw is CAUGHT into `{ id, name, error }` (an
1611
+ * `Error`'s message, else the stringified throw). A tool throw never escapes — it
1612
+ * becomes a result the model can react to.
1613
+ * - **Batch never fails as a whole.** `execute(calls)` runs every call via
1614
+ * `Promise.all(calls.map(...))` and resolves the results correlated by `id` in the
1615
+ * input order — a mix of success, throw, and not-found all resolve; one bad call
1616
+ * does not reject the batch.
1617
+ * - **Event-free.** A purely functional registry — no Emitter, no events.
1618
+ *
1619
+ * @example
1620
+ * ```ts
1621
+ * const manager = new ToolManager()
1622
+ * manager.add(new Tool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
1623
+ * const result = await manager.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
1624
+ * result.value // 3
1625
+ * ```
1626
+ */
1627
+ var ToolManager = class {
1628
+ #tools = /* @__PURE__ */ new Map();
1629
+ get count() {
1630
+ return this.#tools.size;
1631
+ }
1632
+ add(tools) {
1633
+ if (isArray(tools)) {
1634
+ for (const tool of tools) this.#tools.set(tool.name, tool);
1635
+ return;
1636
+ }
1637
+ this.#tools.set(tools.name, tools);
1638
+ }
1639
+ tool(name) {
1640
+ return this.#tools.get(name);
1641
+ }
1642
+ tools() {
1643
+ return [...this.#tools.values()];
1644
+ }
1645
+ definitions() {
1646
+ return [...this.#tools.values()].map((tool) => this.#definition(tool));
1647
+ }
1648
+ execute(call) {
1649
+ if (isArray(call)) return Promise.all(call.map((one) => this.#run(one)));
1650
+ return this.#run(call);
1651
+ }
1652
+ remove(names) {
1653
+ if (isArray(names)) {
1654
+ let removed = false;
1655
+ for (const name of names) if (this.#tools.delete(name)) removed = true;
1656
+ return removed;
1657
+ }
1658
+ return this.#tools.delete(names);
1659
+ }
1660
+ clear() {
1661
+ this.#tools.clear();
1662
+ }
1663
+ async #run(call) {
1664
+ const tool = this.#tools.get(call.name);
1665
+ if (tool === void 0) return {
1666
+ id: call.id,
1667
+ name: call.name,
1668
+ error: `tool not found: ${call.name}`
1669
+ };
1670
+ try {
1671
+ const value = await tool.execute(call.arguments);
1672
+ return {
1673
+ id: call.id,
1674
+ name: call.name,
1675
+ value
1676
+ };
1677
+ } catch (error) {
1678
+ return {
1679
+ id: call.id,
1680
+ name: call.name,
1681
+ error: error instanceof Error ? error.message : String(error)
1682
+ };
1683
+ }
1684
+ }
1685
+ #definition(tool) {
1686
+ const definition = { name: tool.name };
1687
+ if (tool.description !== void 0) definition.description = tool.description;
1688
+ if (tool.parameters !== void 0) definition.parameters = tool.parameters;
1689
+ return definition;
1690
+ }
1691
+ };
1692
+ //#endregion
1693
+ //#region src/core/workspaces/Workspace.ts
1694
+ /**
1695
+ * A mutable, `path`-keyed working set of immutable {@link FileInterface}s — the
1696
+ * in-memory editing surface over the file primitive.
1697
+ *
1698
+ * @remarks
1699
+ * - **Registry.** Files live in an insertion-ordered `Map` keyed by `path`; `count` is the
1700
+ * map size, `file(path)` looks one up, and `files()` lists them in insertion order.
1701
+ * - **Write replaces the File (§11 immutability).** Every edit MINTS a replacement
1702
+ * {@link FileInterface} (via {@link import('../factories.js').createFile}) rather than
1703
+ * mutating in place, transitioning `state` to `'created'` for a brand-new path or
1704
+ * `'modified'` for an existing one. A whole-file string write preserves an existing text
1705
+ * file's `language`, else infers it from the `path` (`inferLanguage`); writing a string
1706
+ * onto an existing BINARY path replaces it with a text file (a deliberate retype). A
1707
+ * ranged `write` splices a `Range` of an existing text file.
1708
+ * - **Modality rules.** Text-only ops on a binary file are rejected: a ranged `read` /
1709
+ * `write`, `prepend`, and `append` throw `MODALITY`; a plain `read(path)` of a binary
1710
+ * file returns `undefined`; `search` / `replace` skip binary files (0 matches).
1711
+ * - **In-memory removal.** `remove(path)` / `remove(paths)` drop files outright (no
1712
+ * tombstone — the `'loaded'` / `'deleted'` states are reserved for a future FileStore);
1713
+ * `remove()` and `clear()` both empty the registry and emit the single `clear` signal.
1714
+ * The disk/sync lifecycle (`load` / `revert` / `accept` / `purge` / `dirty`) is NOT part
1715
+ * of this surface.
1716
+ * - **Observable (§13).** The owned {@link emitter} ({@link WorkspaceEventMap}) carries
1717
+ * `write` (the resulting file) / `remove` (the path) / `move` (`{ from, to }`) / `clear`.
1718
+ * Every event is emitted directly, strictly AFTER the map mutation completes; the
1719
+ * emitter isolates a listener throw and routes it to its `error` handler, so a buggy
1720
+ * observer can never corrupt a mutation.
1721
+ *
1722
+ * @example
1723
+ * ```ts
1724
+ * const workspace = new Workspace()
1725
+ * workspace.write('src/main.ts', 'const x = 1')
1726
+ * workspace.file('src/main.ts')?.state // 'created'
1727
+ * workspace.append('src/main.ts', '\nconst y = 2')
1728
+ * workspace.file('src/main.ts')?.state // 'modified'
1729
+ * workspace.read('src/main.ts') // 'const x = 1\nconst y = 2'
1730
+ * ```
1731
+ */
1732
+ var Workspace = class {
1733
+ #id;
1734
+ #files = /* @__PURE__ */ new Map();
1735
+ #emitter;
1736
+ constructor(options, seed) {
1737
+ this.#id = options?.id ?? crypto.randomUUID();
1738
+ this.#emitter = new Emitter({
1739
+ on: options?.on,
1740
+ error: options?.error
1741
+ });
1742
+ if (seed) for (const [path, file] of seed) this.#files.set(path, file);
1743
+ }
1744
+ get id() {
1745
+ return this.#id;
1746
+ }
1747
+ get emitter() {
1748
+ return this.#emitter;
1749
+ }
1750
+ get count() {
1751
+ return this.#files.size;
1752
+ }
1753
+ file(path) {
1754
+ return this.#files.get(path);
1755
+ }
1756
+ files() {
1757
+ return [...this.#files.values()];
1758
+ }
1759
+ read(path, range) {
1760
+ if (isArray(path)) {
1761
+ const result = {};
1762
+ for (const one of path) {
1763
+ const file = this.#files.get(one);
1764
+ if (file && isText(file.content)) result[one] = file.content.text;
1765
+ }
1766
+ return result;
1767
+ }
1768
+ const file = this.#files.get(path);
1769
+ if (!file) return void 0;
1770
+ if (!range) return isText(file.content) ? file.content.text : void 0;
1771
+ if (!isText(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
1772
+ return {
1773
+ content: sliceRange(file.content.text, range),
1774
+ range: clampRange(file.content.text, range)
1775
+ };
1776
+ }
1777
+ has(path) {
1778
+ if (isArray(path)) return path.some((one) => this.#files.has(one));
1779
+ return this.#files.has(path);
1780
+ }
1781
+ search(query, options) {
1782
+ const pattern = this.#pattern(query, options);
1783
+ const limit = options?.limit;
1784
+ const matches = [];
1785
+ for (const file of this.#files.values()) {
1786
+ if (limit !== void 0 && matches.length >= limit) break;
1787
+ if (!isText(file.content)) continue;
1788
+ const lines = file.content.text.split("\n");
1789
+ for (let index = 0; index < lines.length; index += 1) {
1790
+ if (limit !== void 0 && matches.length >= limit) break;
1791
+ const lineText = lines[index] ?? "";
1792
+ pattern.lastIndex = 0;
1793
+ let hit = pattern.exec(lineText);
1794
+ while (hit !== null) {
1795
+ if (limit !== void 0 && matches.length >= limit) break;
1796
+ matches.push({
1797
+ path: file.path,
1798
+ line: index + 1,
1799
+ column: hit.index + 1,
1800
+ length: hit[0].length,
1801
+ content: lineText
1802
+ });
1803
+ if (hit[0].length === 0) pattern.lastIndex += 1;
1804
+ hit = pattern.exec(lineText);
1805
+ }
1806
+ }
1807
+ }
1808
+ return matches;
1809
+ }
1810
+ replace(query, replacement, options) {
1811
+ const pattern = this.#pattern(query, options);
1812
+ const limit = options?.limit;
1813
+ let replaced = 0;
1814
+ let files = 0;
1815
+ for (const [path, file] of this.#files) {
1816
+ if (limit !== void 0 && replaced >= limit) break;
1817
+ if (!isText(file.content)) continue;
1818
+ const remaining = limit === void 0 ? void 0 : limit - replaced;
1819
+ let count = 0;
1820
+ pattern.lastIndex = 0;
1821
+ const next = file.content.text.replace(pattern, (match) => {
1822
+ if (remaining !== void 0 && count >= remaining) return match;
1823
+ count += 1;
1824
+ return replacement;
1825
+ });
1826
+ if (count > 0) {
1827
+ replaced += count;
1828
+ files += 1;
1829
+ this.write(path, next);
1830
+ }
1831
+ }
1832
+ return {
1833
+ query,
1834
+ replaced,
1835
+ files
1836
+ };
1837
+ }
1838
+ write(path, content, range) {
1839
+ if (isRecord(path)) {
1840
+ for (const [one, text] of Object.entries(path)) this.#write(one, text);
1841
+ return;
1842
+ }
1843
+ const text = content ?? "";
1844
+ if (!range) {
1845
+ this.#write(path, text);
1846
+ return;
1847
+ }
1848
+ this.#splice(path, text, range);
1849
+ }
1850
+ prepend(path, content) {
1851
+ if (isRecord(path)) {
1852
+ for (const [one, text] of Object.entries(path)) this.#prepend(one, text);
1853
+ return;
1854
+ }
1855
+ this.#prepend(path, content ?? "");
1856
+ }
1857
+ append(path, content) {
1858
+ if (isRecord(path)) {
1859
+ for (const [one, text] of Object.entries(path)) this.#append(one, text);
1860
+ return;
1861
+ }
1862
+ this.#append(path, content ?? "");
1863
+ }
1864
+ move(from, to) {
1865
+ if (isRecord(from)) {
1866
+ let moved = false;
1867
+ for (const [one, target] of Object.entries(from)) if (this.#move(one, target)) moved = true;
1868
+ return moved;
1869
+ }
1870
+ return this.#move(from, to ?? "");
1871
+ }
1872
+ remove(path) {
1873
+ if (path === void 0) {
1874
+ this.#files.clear();
1875
+ this.#emitter.emit("clear");
1876
+ return;
1877
+ }
1878
+ if (isArray(path)) {
1879
+ let removed = false;
1880
+ for (const one of path) if (this.#remove(one)) removed = true;
1881
+ return removed;
1882
+ }
1883
+ return this.#remove(path);
1884
+ }
1885
+ clear() {
1886
+ this.#files.clear();
1887
+ this.#emitter.emit("clear");
1888
+ }
1889
+ snapshot() {
1890
+ return {
1891
+ id: this.#id,
1892
+ files: this.files()
1893
+ };
1894
+ }
1895
+ #write(path, content) {
1896
+ const existing = this.#files.get(path);
1897
+ const file = createFile({
1898
+ path,
1899
+ content: {
1900
+ text: content,
1901
+ language: existing && isText(existing.content) ? existing.content.language : inferLanguage(path)
1902
+ },
1903
+ state: existing ? "modified" : "created"
1904
+ });
1905
+ this.#files.set(path, file);
1906
+ this.#emitter.emit("write", file);
1907
+ }
1908
+ #splice(path, content, range) {
1909
+ const existing = this.#files.get(path);
1910
+ if (!existing || !isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a non-text file: ${path}`, { path });
1911
+ if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
1912
+ path,
1913
+ range
1914
+ });
1915
+ const file = createFile({
1916
+ path,
1917
+ content: {
1918
+ text: spliceRange(existing.content.text, range, content),
1919
+ language: existing.content.language
1920
+ },
1921
+ state: "modified"
1922
+ });
1923
+ this.#files.set(path, file);
1924
+ this.#emitter.emit("write", file);
1925
+ }
1926
+ #prepend(path, content) {
1927
+ this.#write(path, content + this.#text(path, "prepend"));
1928
+ }
1929
+ #append(path, content) {
1930
+ this.#write(path, this.#text(path, "append") + content);
1931
+ }
1932
+ #text(path, operation) {
1933
+ const existing = this.#files.get(path);
1934
+ if (!existing) return "";
1935
+ if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot ${operation} text to a binary file: ${path}`, { path });
1936
+ return existing.content.text;
1937
+ }
1938
+ #move(from, to) {
1939
+ const file = this.#files.get(from);
1940
+ if (!file) return false;
1941
+ const moved = createFile({
1942
+ path: to,
1943
+ content: file.content,
1944
+ state: "modified"
1945
+ });
1946
+ this.#files.delete(from);
1947
+ this.#files.set(to, moved);
1948
+ this.#emitter.emit("move", {
1949
+ from,
1950
+ to
1951
+ });
1952
+ return true;
1953
+ }
1954
+ #remove(path) {
1955
+ const removed = this.#files.delete(path);
1956
+ if (removed) this.#emitter.emit("remove", path);
1957
+ return removed;
1958
+ }
1959
+ #pattern(query, options) {
1960
+ const source = options?.regex === true ? query : escapeRegExp(query);
1961
+ const flags = options?.exact === false ? "gi" : "g";
1962
+ try {
1963
+ return new RegExp(source, flags);
1964
+ } catch {
1965
+ throw new WorkspaceError("PATTERN", `Invalid search pattern: ${query}`, { query });
1966
+ }
1967
+ }
1968
+ };
1969
+ //#endregion
1970
+ //#region src/core/workspaces/WorkspaceManager.ts
1971
+ /**
1972
+ * The registry of {@link Workspace}s keyed by `id`, in insertion order, WITH an active pointer
1973
+ * — the §9 store over the workspace layer PLUS the `active` / `switch` seam the context renders.
1974
+ * Event-free (a registry, like {@link ConversationManager}); the observability lives on each
1975
+ * {@link Workspace}.
1976
+ *
1977
+ * @remarks
1978
+ * - **Registry.** Workspaces live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
1979
+ * mints a {@link Workspace} (its `id` from `input` or `crypto.randomUUID()`), flowing the
1980
+ * manager's default `on` / `error` in unless the `input` OVERRIDES them, and stores it (an
1981
+ * already-present `id` OVERWRITES — last write wins). `count` is the map size, `workspace(id)`
1982
+ * looks one up, `workspaces()` lists them in insertion order.
1983
+ * - **Active pointer.** `active` is the active workspace (what the context renders), `undefined`
1984
+ * until the FIRST `add` (which auto-activates it — a registry with workspaces always has one
1985
+ * active). A subsequent `add` leaves `active` unchanged. `switch(id)` re-points `active` to the
1986
+ * workspace with `id` and returns it; an unknown `id` returns `undefined` and leaves `active`
1987
+ * unchanged (the lenient lookup style — never throws, no new error code).
1988
+ * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
1989
+ * removing the ACTIVE workspace sets `active` to `undefined`. `clear` empties the registry and
1990
+ * sets `active` to `undefined`.
1991
+ * - **Durable open / save (the optional `store`).** With a {@link WorkspaceStoreInterface} supplied
1992
+ * (the `store` option), `open(id)` HYDRATES a workspace on a registry miss — it `store.get(id)`s
1993
+ * the snapshot and rebuilds a fresh {@link Workspace} through the constructor `seed`
1994
+ * (`snapshot.files` → path → File), then activates it — and `save(id)` PERSISTS a registered
1995
+ * workspace's `snapshot()`. Both are LENIENT without a store (open resolves only registered ids,
1996
+ * save is a no-op `false`), consistent with the lenient `switch`.
1997
+ * - **Event-free.** A purely registry store — no Emitter, no events (each workspace owns its own
1998
+ * observable `emitter`).
1999
+ *
2000
+ * @example
2001
+ * ```ts
2002
+ * const manager = new WorkspaceManager()
2003
+ * const first = manager.add() // auto-activates — active === first
2004
+ * manager.add({ id: 'scratch' }) // leaves active unchanged
2005
+ * manager.switch('scratch') // re-points active to the 'scratch' workspace
2006
+ * manager.count // 2
2007
+ * ```
2008
+ */
2009
+ var WorkspaceManager = class {
2010
+ #workspaces = /* @__PURE__ */ new Map();
2011
+ #active;
2012
+ #on;
2013
+ #error;
2014
+ #store;
2015
+ constructor(options) {
2016
+ this.#on = options?.on;
2017
+ this.#error = options?.error;
2018
+ this.#store = options?.store;
2019
+ }
2020
+ get count() {
2021
+ return this.#workspaces.size;
2022
+ }
2023
+ get active() {
2024
+ return this.#active === void 0 ? void 0 : this.#workspaces.get(this.#active);
2025
+ }
2026
+ workspace(id) {
2027
+ return this.#workspaces.get(id);
2028
+ }
2029
+ workspaces() {
2030
+ return [...this.#workspaces.values()];
2031
+ }
2032
+ add(input) {
2033
+ const workspace = new Workspace({
2034
+ ...input?.id === void 0 ? {} : { id: input.id },
2035
+ on: input?.on ?? this.#on,
2036
+ error: input?.error ?? this.#error
2037
+ }, input?.seed);
2038
+ this.#workspaces.set(workspace.id, workspace);
2039
+ if (this.#active === void 0) this.#active = workspace.id;
2040
+ return workspace;
2041
+ }
2042
+ switch(id) {
2043
+ const workspace = this.#workspaces.get(id);
2044
+ if (workspace === void 0) return void 0;
2045
+ this.#active = id;
2046
+ return workspace;
2047
+ }
2048
+ async open(id) {
2049
+ const existing = this.#workspaces.get(id);
2050
+ if (existing !== void 0) {
2051
+ this.#active = id;
2052
+ return existing;
2053
+ }
2054
+ if (this.#store === void 0) return void 0;
2055
+ const snapshot = await this.#store.get(id);
2056
+ if (snapshot === void 0) return void 0;
2057
+ const workspace = this.add({
2058
+ id,
2059
+ seed: snapshot.files.map((file) => [file.path, file])
2060
+ });
2061
+ this.#active = workspace.id;
2062
+ return workspace;
2063
+ }
2064
+ async save(id) {
2065
+ const workspace = this.#workspaces.get(id);
2066
+ if (this.#store === void 0 || workspace === void 0) return false;
2067
+ await this.#store.set(workspace.snapshot());
2068
+ return true;
2069
+ }
2070
+ remove(ids) {
2071
+ if (isArray(ids)) {
2072
+ let removed = false;
2073
+ for (const id of ids) if (this.#drop(id)) removed = true;
2074
+ return removed;
2075
+ }
2076
+ return this.#drop(ids);
2077
+ }
2078
+ clear() {
2079
+ this.#workspaces.clear();
2080
+ this.#active = void 0;
2081
+ }
2082
+ #drop(id) {
2083
+ const removed = this.#workspaces.delete(id);
2084
+ if (removed && this.#active === id) this.#active = void 0;
2085
+ return removed;
2086
+ }
2087
+ };
2088
+ //#endregion
2089
+ //#region src/core/AgentContext.ts
2090
+ /**
2091
+ * The richer turn context the agent loop assembles a provider request from — the optional
2092
+ * system prompt, the observable context managers (instructions / workspaces), the
2093
+ * {@link ConversationManagerInterface} message source (whose active conversation IS `messages`),
2094
+ * the {@link ToolManagerInterface} registry, and a mutable active {@link ScopeInterface}.
2095
+ *
2096
+ * @remarks
2097
+ * - **Composition.** `system` is the optional system prompt; `instructions` / `tools` /
2098
+ * `workspaces` / `conversations` are the registries passed in `options` (bring your own), or
2099
+ * fresh empty ones when omitted (so `workspaces` is ALWAYS present); `messages` is the ACTIVE
2100
+ * conversation's live tail (ALWAYS defined — see below). `scope` is the active filter —
2101
+ * `undefined` (the default) ⇒ no filtering; settable afterwards. `workspaces` / `conversations`
2102
+ * are likewise SETTABLE (swap the whole registry between runs).
2103
+ * - **The message source — the conversation registry's ACTIVE conversation.** `conversations` is a
2104
+ * {@link ConversationManagerInterface}; the context ENSURES it always has an active conversation
2105
+ * (at construction it `add`s a default when the manager has none), so the DYNAMIC `messages`
2106
+ * getter — `this.#conversations.active` — is ALWAYS defined. `messages` returns the active
2107
+ * conversation ITSELF (it owns the live tail + the message verbs directly, satisfying
2108
+ * {@link MessageManagerInterface} structurally — the same reference, no duplication), and
2109
+ * `build()` folds that conversation's `view()` (its per-section summaries + live tail) as the
2110
+ * AUTHORITATIVE message inclusion — the scope does NOT filter the conversation (it owns inclusion
2111
+ * via compaction; scope filters only instructions / tools / workspace files). Because `messages`
2112
+ * is read dynamically, an agent SWITCHES the active
2113
+ * conversation BETWEEN runs (`conversations.switch(id)`) to serve MANY threads (the real
2114
+ * multi-conversation pattern); switch between runs, not during a run, and use separate agents
2115
+ * for concurrent threads.
2116
+ * - **`build(format?)` — the scoped assembly + the format cascade.** It folds, in order,
2117
+ * the system prompt then the scope-filtered instructions → the ACTIVE workspace's text files
2118
+ * (each as a block: the section's resolved `open` text, each item's resolved rendering, then
2119
+ * any resolved `close` text) into ONE leading `system` message (prepended only when at least
2120
+ * one part exists), then appends the ACTIVE conversation's `view()` (the conversation owns
2121
+ * message inclusion via compaction — the scope does NOT filter the conversation). Each
2122
+ * `open` / item / `close`
2123
+ * resolves INDEPENDENTLY, MOST-SPECIFIC-FIRST — `build()`'s optional `format` (a provider's
2124
+ * per-section default) is the PROVIDER level: `open` = manager-options-override > provider >
2125
+ * built-in; per item = item-override > manager-options-override > provider > built-in; `close` =
2126
+ * manager-options-override > provider (NO built-in ⇒ no closing line when unset) (see
2127
+ * {@link AgentContextInterface.build}). Passing NO `format` (and with no overrides / no per-item
2128
+ * format) reproduces the built-in framing byte-for-byte (each section is its built-in header +
2129
+ * items, no closing line). The active workspace's scoped-in image files' `data` is attached to
2130
+ * the LAST user message (a vision provider reads images off a user turn); when no user message
2131
+ * exists the attachment is skipped. Built fresh each call (recomputed, never cached), so it
2132
+ * always reflects the current managers / messages / scope / active workspace; it never mutates a
2133
+ * manager or the stored messages.
2134
+ * - **The ACTIVE workspace, rendered BY CARRIER — the SOLE document/image context.**
2135
+ * `workspaces.active` (when set) has its {@link FileInterface}s scope-filtered by `scope.files`,
2136
+ * then split: TEXT files fold into a dedicated `## Workspace` system section (fenced reference
2137
+ * blocks — placed right after the instructions section), and IMAGE files' base64 `data` attaches
2138
+ * to the last user message. ACTIVE-ONLY — never the other registered workspaces; with no active
2139
+ * workspace nothing renders for workspaces. `build()` OWNS this render (a `Workspace` /
2140
+ * `WorkspaceManager` stays file-focused).
2141
+ * - **Tools are structural, not in the prompt.** The registry is advertised to the provider
2142
+ * via `tools.definitions()` (scope-filtered by the loop), NEVER serialized into the
2143
+ * message array — so `build()`'s output carries no tool content, scoped or not.
2144
+ * - **Event-free context; observable managers.** The context itself owns no Emitter; the
2145
+ * context managers each carry their own (the §13 observation surface).
2146
+ *
2147
+ * @example
2148
+ * ```ts
2149
+ * const context = new AgentContext({ system: 'You are concise.' })
2150
+ * context.instructions.add({ name: 'tone', content: 'Be terse.' })
2151
+ * context.messages.add({ role: 'user', content: 'Hi' })
2152
+ * context.build() // [{ role: 'system', content: 'You are concise.\n\n## Instructions\n\nBe terse.' }, { role: 'user', content: 'Hi' }]
2153
+ * ```
2154
+ */
2155
+ var AgentContext = class {
2156
+ #system;
2157
+ #instructions;
2158
+ #workspaces;
2159
+ #conversations;
2160
+ #tools;
2161
+ #scope;
2162
+ constructor(options) {
2163
+ this.#system = options?.system;
2164
+ this.#instructions = options?.instructions ?? new InstructionManager();
2165
+ this.#workspaces = options?.workspaces ?? new WorkspaceManager();
2166
+ this.#conversations = options?.conversations ?? new ConversationManager();
2167
+ if (this.#conversations.active === void 0) this.#conversations.add();
2168
+ this.#tools = options?.tools ?? new ToolManager();
2169
+ this.#scope = options?.scope;
2170
+ }
2171
+ get system() {
2172
+ return this.#system;
2173
+ }
2174
+ get instructions() {
2175
+ return this.#instructions;
2176
+ }
2177
+ get workspaces() {
2178
+ return this.#workspaces;
2179
+ }
2180
+ set workspaces(value) {
2181
+ this.#workspaces = value;
2182
+ }
2183
+ get messages() {
2184
+ return this.#conversations.active ?? this.#ensure();
2185
+ }
2186
+ get conversations() {
2187
+ return this.#conversations;
2188
+ }
2189
+ set conversations(value) {
2190
+ this.#conversations = value;
2191
+ if (this.#conversations.active === void 0) this.#conversations.add();
2192
+ }
2193
+ get tools() {
2194
+ return this.#tools;
2195
+ }
2196
+ get scope() {
2197
+ return this.#scope;
2198
+ }
2199
+ set scope(value) {
2200
+ this.#scope = value;
2201
+ }
2202
+ build(format) {
2203
+ const scope = this.#scope;
2204
+ const parts = [];
2205
+ if (this.#system !== void 0) parts.push(this.#system);
2206
+ const instructions = filterAllowList(scope?.instructions, this.#instructions.instructions(), (one) => one.name);
2207
+ this.#section(parts, this.#header(this.#instructions, format?.instructions), instructions, (one) => this.#render(this.#instructions, format?.instructions, one), this.#footer(this.#instructions, format?.instructions));
2208
+ const files = filterAllowList(scope?.files, this.#workspaces.active?.files() ?? [], (one) => one.path);
2209
+ const workspaceTexts = files.filter((file) => isText(file.content));
2210
+ this.#section(parts, WORKSPACE_SECTION_HEADER, workspaceTexts, (file) => this.#fenced(file), void 0);
2211
+ const conversation = (this.#conversations.active ?? this.#ensure()).view();
2212
+ const tail = this.#attach(conversation, this.#workspaceImages(files));
2213
+ if (parts.length === 0) return tail;
2214
+ return [{
2215
+ id: crypto.randomUUID(),
2216
+ role: "system",
2217
+ content: parts.join("\n\n")
2218
+ }, ...tail];
2219
+ }
2220
+ #section(parts, open, items, format, close) {
2221
+ if (items.length === 0) return;
2222
+ const lines = [open, ...items.map(format)];
2223
+ if (close !== void 0) lines.push(close);
2224
+ parts.push(lines.join("\n\n"));
2225
+ }
2226
+ #header(manager, provider) {
2227
+ return manager.framing?.open ?? provider?.open ?? manager.description;
2228
+ }
2229
+ #footer(manager, provider) {
2230
+ return manager.framing?.close ?? provider?.close;
2231
+ }
2232
+ #render(manager, provider, item) {
2233
+ return item.format ?? manager.framing?.render?.(item) ?? provider?.render?.(item) ?? manager.format(item);
2234
+ }
2235
+ #attach(conversation, data) {
2236
+ if (data.length === 0) return conversation;
2237
+ let target = -1;
2238
+ for (let index = conversation.length - 1; index >= 0; index -= 1) if (conversation[index]?.role === "user") {
2239
+ target = index;
2240
+ break;
2241
+ }
2242
+ if (target === -1) return conversation;
2243
+ return conversation.map((message, index) => index === target ? this.#withImages(message, data) : message);
2244
+ }
2245
+ #withImages(message, data) {
2246
+ const images = [...message.images ?? [], ...data];
2247
+ return message.calls === void 0 ? {
2248
+ id: message.id,
2249
+ role: message.role,
2250
+ content: message.content,
2251
+ images
2252
+ } : {
2253
+ id: message.id,
2254
+ role: message.role,
2255
+ content: message.content,
2256
+ calls: message.calls,
2257
+ images
2258
+ };
2259
+ }
2260
+ #fenced(file) {
2261
+ if (isText(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
2262
+ return fencedFile(file.path, "text", "");
2263
+ }
2264
+ #workspaceImages(files) {
2265
+ const data = [];
2266
+ for (const file of files) if (isBinary(file.content) && isImage(file.content)) data.push(file.content.data);
2267
+ return data;
2268
+ }
2269
+ #ensure() {
2270
+ const conversation = this.#conversations.add();
2271
+ return this.#conversations.active ?? conversation;
2272
+ }
2273
+ };
2274
+ //#endregion
2275
+ //#region src/core/Channel.ts
2276
+ /**
2277
+ * A minimal unbounded async channel — the eager pump WRITES chunks into it (`push`)
2278
+ * and ends it (`close` / `fail`) regardless of consumption; a consumer READS them back
2279
+ * live via the `drain` async-iterator. Decoupling write from read is what lets a
2280
+ * producer make progress without a consumer pulling.
2281
+ *
2282
+ * @remarks
2283
+ * The standard resolver-swap: a waiting `drain` parks on `#wake` (a void resolver);
2284
+ * `push` / `close` / `fail` enqueue/flag, then fire `#wake` so the parked reader wakes,
2285
+ * re-reads the buffer, and either yields the next chunk, returns (on `close`), or throws
2286
+ * (on `fail`). Event-free, no `!` / `as` / `any`.
2287
+ */
2288
+ var Channel = class {
2289
+ #buffer = [];
2290
+ #wake;
2291
+ #closed = false;
2292
+ #failure;
2293
+ push(value) {
2294
+ this.#buffer.push(value);
2295
+ this.#signal();
2296
+ }
2297
+ close() {
2298
+ this.#closed = true;
2299
+ this.#signal();
2300
+ }
2301
+ fail(error) {
2302
+ if (this.#failure === void 0) this.#failure = { error };
2303
+ this.#closed = true;
2304
+ this.#signal();
2305
+ }
2306
+ async *drain() {
2307
+ for (;;) {
2308
+ while (this.#buffer.length > 0) {
2309
+ const next = this.#buffer.shift();
2310
+ if (next !== void 0) yield next;
2311
+ }
2312
+ if (this.#failure !== void 0) throw this.#failure.error;
2313
+ if (this.#closed) return;
2314
+ await this.#parked();
2315
+ }
2316
+ }
2317
+ #signal() {
2318
+ const wake = this.#wake;
2319
+ this.#wake = void 0;
2320
+ wake?.();
2321
+ }
2322
+ #parked() {
2323
+ return new Promise((resolve) => {
2324
+ this.#wake = resolve;
2325
+ });
2326
+ }
2327
+ };
2328
+ //#endregion
2329
+ //#region src/core/Agent.ts
2330
+ /**
2331
+ * The agent loop — composes a {@link ProviderInterface}, an {@link AgentContext}, and
2332
+ * a {@link ToolManagerInterface} into a bounded context → provider → tools → repeat
2333
+ * turn, exposed as both a one-shot `generate` and a live `stream`.
2334
+ *
2335
+ * @remarks
2336
+ * - **One loop, two faces.** A single private async generator (`#run`) drives the
2337
+ * whole turn. `stream` kicks off an eager pump that iterates `#run` into a private
2338
+ * {@link Channel}, settling `result` from the run's outcome — so `result` settles
2339
+ * whether or not the live `events` are drained; `generate` simply awaits that same
2340
+ * settled `result` — so the two can never diverge.
2341
+ * - **The turn.** `#run` builds the provider input once (`context.build()` into a
2342
+ * working array) then loops up to `limit`: drive `provider.stream(...)` accumulating
2343
+ * + yielding each content delta as a `token` chunk; fold the turn's usage into the
2344
+ * running total + the `budget` and yield a `usage` chunk; if the model requested
2345
+ * tools, append the assistant turn, `execute` them, yield a `tool` chunk per call,
2346
+ * append each tool result message, and continue; otherwise append the final
2347
+ * assistant message and stop.
2348
+ * - **Bounded.** Each run arms one cancel via `createAbort({ signal: AbortSignal.any([
2349
+ * …]) })` folding the external `signal`, the `timeout` deadline, and the `budget`
2350
+ * signal; `abort()` fires it. Any trip stops the loop and commits a PARTIAL result
2351
+ * (the `result` promise RESOLVES, never rejects, on a cancel) — only a genuine
2352
+ * provider / tool error rejects.
2353
+ * - **Paced + capped.** The `scheduler` (when given) `yield`s between turns; tool
2354
+ * iteration is capped at `limit`.
2355
+ * - **Two observation surfaces.** The PULL {@link AgentChunk} stream carries per-token
2356
+ * deltas (+ usage/tool chunks); the PUSH {@link emitter} ({@link AgentEventMap}) carries
2357
+ * lifecycle + usage/tool/deny moments for fire-and-forget observers. Every event is
2358
+ * emitted directly, AFTER the relevant state transition / settle; the emitter isolates a
2359
+ * listener throw and routes it to its `error` handler (the `error` option), so a buggy
2360
+ * observer can never escape into / reorder / corrupt the settle-once loop — observation is
2361
+ * purely a side-channel.
2362
+ *
2363
+ * @example
2364
+ * ```ts
2365
+ * const agent = new Agent(provider, { system: 'You are concise.' })
2366
+ * agent.context.messages.add({ role: 'user', content: 'Say hi.' })
2367
+ * const result = await agent.generate()
2368
+ * ```
2369
+ */
2370
+ var Agent = class {
2371
+ #provider;
2372
+ #context;
2373
+ #limit;
2374
+ #timeoutMs;
2375
+ #budget;
2376
+ #scheduler;
2377
+ #signal;
2378
+ #authority;
2379
+ #window;
2380
+ #emitter;
2381
+ id = crypto.randomUUID();
2382
+ #status = "idle";
2383
+ #runs = /* @__PURE__ */ new Set();
2384
+ constructor(provider, options) {
2385
+ this.#provider = provider;
2386
+ this.#context = new AgentContext({
2387
+ system: options?.system,
2388
+ tools: options?.tools,
2389
+ conversations: options?.conversations
2390
+ });
2391
+ this.#limit = options?.limit ?? 10;
2392
+ this.#timeoutMs = options?.timeout;
2393
+ this.#budget = options?.budget;
2394
+ this.#scheduler = options?.scheduler;
2395
+ this.#signal = options?.signal;
2396
+ this.#authority = options?.authority;
2397
+ this.#window = options?.window;
2398
+ this.#emitter = new Emitter({
2399
+ on: options?.on,
2400
+ error: options?.error
2401
+ });
2402
+ }
2403
+ get emitter() {
2404
+ return this.#emitter;
2405
+ }
2406
+ get status() {
2407
+ return this.#status;
2408
+ }
2409
+ get context() {
2410
+ return this.#context;
2411
+ }
2412
+ generate(options) {
2413
+ return this.stream(options).result;
2414
+ }
2415
+ stream(options) {
2416
+ const timeout = this.#timeoutMs === void 0 ? void 0 : createTimeout({ ms: this.#timeoutMs });
2417
+ timeout?.start();
2418
+ this.#budget?.start();
2419
+ const abort = createAbort({ signal: this.#parents(timeout) });
2420
+ this.#runs.add(abort);
2421
+ this.#status = "running";
2422
+ this.#emitter.emit("start", this.id);
2423
+ const outcome = {
2424
+ content: "",
2425
+ thinking: void 0,
2426
+ usage: void 0,
2427
+ partial: false
2428
+ };
2429
+ const channel = new Channel();
2430
+ const settled = createDeferred();
2431
+ this.#pump(abort, outcome, timeout, channel, settled, options?.think);
2432
+ settled.promise.catch(() => {});
2433
+ return {
2434
+ events: this.#events(channel, abort),
2435
+ result: settled.promise,
2436
+ abort: (reason) => {
2437
+ abort.abort(reason);
2438
+ }
2439
+ };
2440
+ }
2441
+ abort(reason) {
2442
+ for (const abort of [...this.#runs]) abort.abort(reason);
2443
+ }
2444
+ async #pump(abort, outcome, timeout, channel, settled, think) {
2445
+ let failure;
2446
+ try {
2447
+ for await (const chunk of this.#run(abort, outcome, think)) channel.push(chunk);
2448
+ } catch (error) {
2449
+ failure = { error };
2450
+ } finally {
2451
+ timeout?.clear();
2452
+ this.#runs.delete(abort);
2453
+ if (failure === void 0) {
2454
+ this.#status = "done";
2455
+ channel.close();
2456
+ const result = this.#result(outcome);
2457
+ settled.resolve(result);
2458
+ if (outcome.partial) this.#emitter.emit("abort", abort.signal.reason);
2459
+ this.#emitter.emit("finish", result);
2460
+ } else {
2461
+ this.#status = "error";
2462
+ channel.fail(failure.error);
2463
+ settled.reject(failure.error);
2464
+ this.#emitter.emit("error", failure.error);
2465
+ }
2466
+ }
2467
+ }
2468
+ async *#events(channel, abort) {
2469
+ try {
2470
+ yield* channel.drain();
2471
+ } finally {
2472
+ abort.abort();
2473
+ }
2474
+ }
2475
+ async *#run(abort, outcome, think) {
2476
+ const messages = [...this.#context.build(this.#provider.format)];
2477
+ const tools = this.#context.tools;
2478
+ let content = "";
2479
+ let thinking;
2480
+ let usage;
2481
+ const compaction = { futile: false };
2482
+ const compacting = this.#window !== void 0 && this.#context.conversations.active?.summarizable === true;
2483
+ if (compacting) {
2484
+ this.#window?.clear();
2485
+ if (!abort.signal.aborted) await this.#trim(messages, compaction, false);
2486
+ }
2487
+ for (let turn = 0; turn < this.#limit; turn += 1) {
2488
+ this.#emitter.emit("turn", turn);
2489
+ if (turn > 0) try {
2490
+ await this.#scheduler?.yield({ signal: abort.signal });
2491
+ } catch (error) {
2492
+ if (abort.signal.aborted) {
2493
+ outcome.partial = true;
2494
+ break;
2495
+ }
2496
+ throw error;
2497
+ }
2498
+ if (abort.signal.aborted) {
2499
+ outcome.partial = true;
2500
+ break;
2501
+ }
2502
+ const advertised = filterAllowList(this.#context.scope?.tools, tools.definitions(), (definition) => definition.name);
2503
+ const definitions = advertised.length > 0 ? advertised : void 0;
2504
+ let result;
2505
+ try {
2506
+ result = yield* this.#provide(messages, abort.signal, definitions, think, (delta) => {
2507
+ content += delta;
2508
+ });
2509
+ } catch (error) {
2510
+ if (abort.signal.aborted) {
2511
+ if (isProviderAbortError(error) && error.partial.thinking !== void 0) thinking = this.#thought(thinking, error.partial.thinking);
2512
+ outcome.partial = true;
2513
+ break;
2514
+ }
2515
+ throw error;
2516
+ }
2517
+ if (result.thinking !== void 0 && result.thinking.length > 0) thinking = this.#thought(thinking, result.thinking);
2518
+ if (result.usage !== void 0) {
2519
+ this.#budget?.consume(result.usage);
2520
+ usage = this.#sum(usage, result.usage);
2521
+ this.#emitter.emit("usage", result.usage);
2522
+ yield {
2523
+ type: "usage",
2524
+ usage: result.usage
2525
+ };
2526
+ }
2527
+ if (result.tools !== void 0 && result.tools.length > 0) {
2528
+ const assistant = this.#context.messages.add({
2529
+ role: "assistant",
2530
+ content: result.content,
2531
+ calls: result.tools
2532
+ });
2533
+ messages.push(assistant);
2534
+ const results = await this.#authorize(tools, result.tools);
2535
+ for (let index = 0; index < result.tools.length; index += 1) {
2536
+ const call = result.tools[index];
2537
+ const outcomeResult = results[index];
2538
+ if (call === void 0 || outcomeResult === void 0) continue;
2539
+ this.#emitter.emit("tool", call, outcomeResult);
2540
+ yield {
2541
+ type: "tool",
2542
+ call,
2543
+ result: outcomeResult
2544
+ };
2545
+ const toolMessage = this.#context.messages.add({
2546
+ role: "tool",
2547
+ content: JSON.stringify(outcomeResult.value ?? outcomeResult.error)
2548
+ });
2549
+ messages.push(toolMessage);
2550
+ }
2551
+ if (compacting) await this.#trim(messages, compaction, true);
2552
+ continue;
2553
+ }
2554
+ messages.push(this.#context.messages.add({
2555
+ role: "assistant",
2556
+ content: result.content
2557
+ }));
2558
+ content = result.content;
2559
+ break;
2560
+ }
2561
+ outcome.content = content;
2562
+ outcome.thinking = thinking;
2563
+ outcome.usage = usage;
2564
+ }
2565
+ async #trim(messages, compaction, latchFutile) {
2566
+ const conversation = this.#context.conversations.active;
2567
+ if (this.#window === void 0 || conversation?.summarizable !== true || compaction.futile) return;
2568
+ this.#window.clear();
2569
+ this.#window.consume(messages);
2570
+ if (!this.#window.exhausted) return;
2571
+ let section;
2572
+ try {
2573
+ section = await conversation.compact();
2574
+ } catch (error) {
2575
+ this.#emitter.emit("compactError", error);
2576
+ return;
2577
+ }
2578
+ if (section === void 0) {
2579
+ if (latchFutile) compaction.futile = true;
2580
+ return;
2581
+ }
2582
+ messages.splice(0, messages.length, ...this.#context.build(this.#provider.format));
2583
+ }
2584
+ async #authorize(tools, calls) {
2585
+ if (this.#authority === void 0) return tools.execute(calls);
2586
+ const authority = this.#authority;
2587
+ const allowed = [];
2588
+ const denials = /* @__PURE__ */ new Map();
2589
+ for (const call of calls) {
2590
+ let decision;
2591
+ try {
2592
+ decision = authority.evaluate({ call });
2593
+ } catch (error) {
2594
+ const reason = this.#reason(error);
2595
+ denials.set(call.id, this.#denial(call, reason));
2596
+ this.#emitter.emit("deny", call, reason);
2597
+ continue;
2598
+ }
2599
+ if (decision.allowed) allowed.push(call);
2600
+ else {
2601
+ denials.set(call.id, this.#denial(call, decision.reason));
2602
+ this.#emitter.emit("deny", call, decision.reason);
2603
+ }
2604
+ }
2605
+ const executed = allowed.length > 0 ? await tools.execute(allowed) : [];
2606
+ const byId = new Map(denials);
2607
+ for (const result of executed) byId.set(result.id, result);
2608
+ return calls.map((call) => byId.get(call.id) ?? this.#denial(call, void 0));
2609
+ }
2610
+ #denial(call, reason) {
2611
+ return {
2612
+ id: call.id,
2613
+ name: call.name,
2614
+ error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
2615
+ };
2616
+ }
2617
+ #reason(error) {
2618
+ return error instanceof Error ? error.message : String(error);
2619
+ }
2620
+ async *#provide(messages, signal, definitions, think, onDelta) {
2621
+ const generator = this.#provider.stream(messages, signal, definitions, think === void 0 ? void 0 : { think });
2622
+ let next = await generator.next();
2623
+ while (!next.done) {
2624
+ const delta = next.value;
2625
+ if (delta.type === "content") {
2626
+ onDelta(delta.text);
2627
+ yield {
2628
+ type: "token",
2629
+ content: delta.text
2630
+ };
2631
+ } else yield {
2632
+ type: "think",
2633
+ content: delta.text
2634
+ };
2635
+ next = await generator.next();
2636
+ }
2637
+ return next.value;
2638
+ }
2639
+ #parents(timeout) {
2640
+ const signals = [];
2641
+ if (this.#signal !== void 0) signals.push(this.#signal);
2642
+ if (timeout !== void 0) signals.push(timeout.signal);
2643
+ if (this.#budget !== void 0) signals.push(this.#budget.signal);
2644
+ if (signals.length === 0) return void 0;
2645
+ if (signals.length === 1) return signals[0];
2646
+ return AbortSignal.any(signals);
2647
+ }
2648
+ #thought(running, next) {
2649
+ return running === void 0 ? next : `${running}\n\n${next}`;
2650
+ }
2651
+ #sum(running, next) {
2652
+ if (running === void 0) return next;
2653
+ return {
2654
+ prompt: running.prompt + next.prompt,
2655
+ completion: running.completion + next.completion,
2656
+ total: running.total + next.total
2657
+ };
2658
+ }
2659
+ #result(outcome) {
2660
+ const result = {
2661
+ content: outcome.content,
2662
+ partial: outcome.partial
2663
+ };
2664
+ if (outcome.thinking !== void 0) result.thinking = outcome.thinking;
2665
+ if (outcome.usage !== void 0) result.usage = outcome.usage;
2666
+ return result;
2667
+ }
2668
+ };
2669
+ //#endregion
2670
+ //#region src/core/AgentRegistry.ts
2671
+ /**
2672
+ * The bridge that makes a durable, JSON-serializable {@link AgentJobInput} runnable —
2673
+ * it holds the named pools of live, non-serializable pieces (providers, tools,
2674
+ * authorities, schedulers) and rehydrates a seeded, signal-wired {@link Agent} from a
2675
+ * job's names + data.
2676
+ *
2677
+ * @remarks
2678
+ * - **Why it exists.** An `AgentJobInput` is serializable so it can survive a crash in a
2679
+ * Queue's store; the live objects it needs (a provider with sockets, tools / rules /
2680
+ * schedulers carrying functions) cannot serialize. The registry closes that gap:
2681
+ * construct it once with the live pools, then a queue / runner handler calls `build`
2682
+ * on each (possibly restored) job to get a ready agent.
2683
+ * - **Accessors throw on a miss (§9.1 + §12).** `provider` / `tool` / `authority` /
2684
+ * `scheduler` resolve a name against their pool and THROW `unknown <category>: <name>`
2685
+ * when it is absent — an unknown name in a rehydrated job is a programmer / config
2686
+ * error that must fail LOUDLY at build time, never silently resolve to `undefined` and
2687
+ * run an agent missing a dependency.
2688
+ * - **`build` rehydrates.** Resolve the job's `provider`; assemble a fresh
2689
+ * {@link ToolManager} from the `tools` names; rebuild the token `budget` from its
2690
+ * ceiling (`createTokenBudget({ max })`); resolve the optional `authority` /
2691
+ * `scheduler` names; construct the {@link Agent} with `system` / `limit` / `timeout` /
2692
+ * the threaded `signal`; seed its context with the job's `messages`; return it. The
2693
+ * `signal` is the queue attempt's / runner unit's cancel, so a bounded abort propagates
2694
+ * into the agent (which commits a partial — the job's `allowPartial` policy then
2695
+ * decides success vs. retry).
2696
+ * - **Event-free.** A pure resolver — no Emitter, no events.
2697
+ *
2698
+ * @example
2699
+ * ```ts
2700
+ * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
2701
+ * const registry = new AgentRegistry({ providers: { main: provider } })
2702
+ * const agent = registry.build({ provider: 'main', messages: [{ role: 'user', content: 'Say ok.' }] })
2703
+ * const result = await agent.generate()
2704
+ * ```
2705
+ */
2706
+ var AgentRegistry = class {
2707
+ #providers;
2708
+ #tools;
2709
+ #authorities;
2710
+ #schedulers;
2711
+ constructor(options) {
2712
+ this.#providers = new Map(Object.entries(options.providers));
2713
+ this.#tools = new Map(Object.entries(options.tools ?? {}));
2714
+ this.#authorities = new Map(Object.entries(options.authorities ?? {}));
2715
+ this.#schedulers = new Map(Object.entries(options.schedulers ?? {}));
2716
+ }
2717
+ provider(name) {
2718
+ return this.#resolve(this.#providers, "provider", name);
2719
+ }
2720
+ tool(name) {
2721
+ return this.#resolve(this.#tools, "tool", name);
2722
+ }
2723
+ authority(name) {
2724
+ return this.#resolve(this.#authorities, "authority", name);
2725
+ }
2726
+ scheduler(name) {
2727
+ return this.#resolve(this.#schedulers, "scheduler", name);
2728
+ }
2729
+ build(input, signal) {
2730
+ const agent = new Agent(this.provider(input.provider), this.#options(input, signal));
2731
+ for (const message of input.messages) agent.context.messages.add(message);
2732
+ return agent;
2733
+ }
2734
+ #options(input, signal) {
2735
+ return {
2736
+ system: input.system,
2737
+ tools: this.#manager(input.tools),
2738
+ limit: input.limit,
2739
+ timeout: input.timeout,
2740
+ budget: this.#budget(input.budget),
2741
+ authority: input.authority === void 0 ? void 0 : this.authority(input.authority),
2742
+ scheduler: input.scheduler === void 0 ? void 0 : this.scheduler(input.scheduler),
2743
+ signal
2744
+ };
2745
+ }
2746
+ #manager(names) {
2747
+ const manager = new ToolManager();
2748
+ if (names !== void 0) for (const name of names) manager.add(this.tool(name));
2749
+ return manager;
2750
+ }
2751
+ #budget(max) {
2752
+ return max === void 0 ? void 0 : createTokenBudget({ max });
2753
+ }
2754
+ #resolve(pool, category, name) {
2755
+ const value = pool.get(name);
2756
+ if (value === void 0) throw new Error(`unknown ${category}: ${name}`);
2757
+ return value;
2758
+ }
2759
+ };
2760
+ //#endregion
2761
+ //#region src/core/Authority.ts
2762
+ /**
2763
+ * The synchronous policy gate the agent loop consults before each tool call runs —
2764
+ * it turns one {@link AuthorityContextInterface} into an {@link AuthorityDecision}.
2765
+ *
2766
+ * @remarks
2767
+ * - **Ordered, first-match-wins.** `evaluate` walks the configured rules in order and
2768
+ * returns the FIRST whose `match(context)` is true as
2769
+ * `{ zone, allowed: rule.allowed ?? true, reason }` — a matched rule ALLOWS by
2770
+ * default and DENIES only when its `allowed` is explicitly `false`.
2771
+ * - **Fallback.** When no rule matches, `evaluate` returns the configured `fallback`.
2772
+ * It defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }` (allow-unmatched),
2773
+ * so a rules list of denials behaves as a DENYLIST. To make the gate deny-by-default
2774
+ * (an ALLOWLIST — only matched rules that allow get through), pass an `allowed: false`
2775
+ * `fallback`.
2776
+ * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
2777
+ * every {@link import('./types.js').ToolCall} the model emits; a denied call is fed
2778
+ * back to the model as a denial {@link import('./types.js').ToolResult} (a `tool`
2779
+ * chunk + a tool message) instead of being executed — no tool run, no budget cost —
2780
+ * so the model sees the denial and can react.
2781
+ * - **Synchronous now.** The async human-approval handshake (request / grant / deny) is
2782
+ * deferred to a later chunk; `evaluate` returns a verdict directly.
2783
+ * - **Event-free.** A purely functional gate — no Emitter, no events.
2784
+ *
2785
+ * @example
2786
+ * ```ts
2787
+ * // A denylist: deny the `delete` tool, allow everything else (default fallback).
2788
+ * const authority = new Authority({
2789
+ * rules: [{ match: (c) => c.call.name === 'delete', zone: 'restricted', allowed: false }],
2790
+ * })
2791
+ * authority.evaluate({ call: { id: '1', name: 'delete', arguments: {} } }) // { zone: 'restricted', allowed: false }
2792
+ * authority.evaluate({ call: { id: '2', name: 'add', arguments: {} } }) // { zone: 'default', allowed: true }
2793
+ * ```
2794
+ */
2795
+ var Authority = class {
2796
+ #rules;
2797
+ #fallback;
2798
+ constructor(options) {
2799
+ this.#rules = options?.rules ?? [];
2800
+ this.#fallback = options?.fallback ?? {
2801
+ zone: "default",
2802
+ allowed: true
2803
+ };
2804
+ }
2805
+ evaluate(context) {
2806
+ for (const rule of this.#rules) if (rule.match(context)) return {
2807
+ zone: rule.zone,
2808
+ allowed: rule.allowed ?? true,
2809
+ reason: rule.reason
2810
+ };
2811
+ return this.#fallback;
2812
+ }
2813
+ };
2814
+ //#endregion
2815
+ //#region src/core/conversations/stores/DatabaseConversationStore.ts
2816
+ /**
2817
+ * A {@link ConversationStoreInterface} backed by one table of the `databases` layer — a
2818
+ * conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
2819
+ * / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
2820
+ * {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
2821
+ * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}.
2822
+ *
2823
+ * @remarks
2824
+ * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
2825
+ * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
2826
+ * IndexedDB backend swaps in WITHOUT touching the
2827
+ * {@link import('../ConversationManager.js').ConversationManager} or the
2828
+ * {@link import('../Conversation.js').Conversation} — the same seam as
2829
+ * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}. The
2830
+ * driver defaults to memory ({@link import('../../factories.js').createDatabaseConversationStore}
2831
+ * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
2832
+ * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
2833
+ *
2834
+ * The {@link ConversationSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
2835
+ * `{ id; snapshot }` ({@link ConversationSnapshotRow}), the snapshot the whole JSON blob (a
2836
+ * `rawShape` column the factory builds) — exactly as `DatabaseWorkspaceStore` stores its snapshot.
2837
+ * The snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is
2838
+ * lossless AND keeps the row type flat (`snapshot` reads back as `unknown`).
2839
+ *
2840
+ * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
2841
+ * the row `{ id: snapshot.id, snapshot }`.
2842
+ * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
2843
+ * a {@link ConversationSnapshot} ({@link import('../../helpers.js').isConversationSnapshot} — the
2844
+ * AGENTS §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
2845
+ * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
2846
+ *
2847
+ * UNLIKE a session store there is NO idle-TTL / eviction — a persisted conversation lives until an
2848
+ * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
2849
+ * §22 method bijection with {@link ConversationStoreInterface}). Hydration stays a caller concern: a
2850
+ * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
2851
+ * the live conversation through the constructor `seed` (its `open` / `save`).
2852
+ *
2853
+ * @example
2854
+ * ```ts
2855
+ * import { createConversation, createDatabaseConversationStore, createMemoryDriver } from '@src/core'
2856
+ *
2857
+ * const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
2858
+ * const conversation = createConversation()
2859
+ * conversation.add({ role: 'user', content: 'hello' })
2860
+ * await store.set(conversation.snapshot()) // persist the conversation (one JSON column)
2861
+ * const snapshot = await store.get(conversation.id)
2862
+ * await store.delete(conversation.id) // drop it
2863
+ * ```
2864
+ */
2865
+ var DatabaseConversationStore = class {
2866
+ #table;
2867
+ /**
2868
+ * Wrap a table as a conversation store.
2869
+ *
2870
+ * @param table - The {@link TableInterface} holding the snapshots — its row is the
2871
+ * {@link ConversationSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
2872
+ */
2873
+ constructor(table) {
2874
+ this.#table = table;
2875
+ }
2876
+ /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `ConversationSnapshot`. */
2877
+ async get(id) {
2878
+ const row = await this.#table.get(id);
2879
+ if (row === void 0) return void 0;
2880
+ return isConversationSnapshot(row.snapshot) ? row.snapshot : void 0;
2881
+ }
2882
+ /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
2883
+ async set(snapshot) {
2884
+ await this.#table.set({
2885
+ id: snapshot.id,
2886
+ snapshot
2887
+ });
2888
+ }
2889
+ /** Drop a snapshot by id; an absent id is a no-op (no throw). */
2890
+ async delete(id) {
2891
+ await this.#table.remove(id);
2892
+ }
2893
+ };
2894
+ //#endregion
2895
+ //#region src/core/conversations/stores/MemoryConversationStore.ts
2896
+ /**
2897
+ * The in-memory {@link ConversationStoreInterface} — a process-lifetime `Map` of
2898
+ * {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
2899
+ * {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
2900
+ * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
2901
+ *
2902
+ * @remarks
2903
+ * A plain `Map<string, ConversationSnapshot>` (AGENTS §21 — the snapshot is already pure,
2904
+ * self-contained JSON, so no encoding is needed for the memory tier). Like the
2905
+ * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore} it twins,
2906
+ * there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
2907
+ * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
2908
+ * the {@link import('../ConversationManager.js').ConversationManager} or the
2909
+ * {@link import('../Conversation.js').Conversation} — its driver-pluggable twin is
2910
+ * {@link import('./DatabaseConversationStore.js').DatabaseConversationStore} (the snapshot as one
2911
+ * opaque JSON column).
2912
+ *
2913
+ * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
2914
+ * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
2915
+ * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
2916
+ *
2917
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
2918
+ * bijection with {@link ConversationStoreInterface}). Hydration is a caller concern: a
2919
+ * {@link import('../ConversationManager.js').ConversationManager} reads a snapshot back and rebuilds
2920
+ * the live conversation through the constructor `seed` (its `open` / `save`).
2921
+ *
2922
+ * @example
2923
+ * ```ts
2924
+ * import { createConversation, createMemoryConversationStore } from '@src/core'
2925
+ *
2926
+ * const store = createMemoryConversationStore()
2927
+ * const conversation = createConversation()
2928
+ * conversation.add({ role: 'user', content: 'hello' })
2929
+ * await store.set(conversation.snapshot()) // persist the conversation
2930
+ * const snapshot = await store.get(conversation.id)
2931
+ * await store.delete(conversation.id) // drop it
2932
+ * ```
2933
+ */
2934
+ var MemoryConversationStore = class {
2935
+ #snapshots = /* @__PURE__ */ new Map();
2936
+ get(id) {
2937
+ return Promise.resolve(this.#snapshots.get(id));
2938
+ }
2939
+ set(snapshot) {
2940
+ this.#snapshots.set(snapshot.id, snapshot);
2941
+ return Promise.resolve();
2942
+ }
2943
+ delete(id) {
2944
+ this.#snapshots.delete(id);
2945
+ return Promise.resolve();
2946
+ }
2947
+ };
2948
+ //#endregion
2949
+ //#region src/core/scopes/Scope.ts
2950
+ /**
2951
+ * A named, immutable filter over a richer context's items — three optional allow-lists,
2952
+ * one per category (`instructions` / `tools` / `files`), each keyed by that category's
2953
+ * identity (an instruction's `name`, a tool's `name`, a workspace file's `path`).
2954
+ *
2955
+ * @remarks
2956
+ * - **A category list is three-way.** `undefined` ⇒ NO constraint on that category (all
2957
+ * pass); `[]` ⇒ NONE pass; a non-empty list ⇒ only the listed keys pass. The build
2958
+ * step / loop apply this via `filterAllowList`.
2959
+ * - **Immutable.** The `id` is minted at construction; every supplied list is COPIED in
2960
+ * (so a later mutation of the caller's array can't leak in), and the lists are
2961
+ * `readonly`. A `Scope` is never mutated after construction — `narrow` returns a NEW
2962
+ * one rather than altering this one.
2963
+ * - **`narrow` is set-INTERSECTION (immutable composition).** A child scope's visible set
2964
+ * per category is the intersection of THIS scope's list and the config's list — but
2965
+ * `undefined` means "no constraint", so it acts as the universal set: intersecting
2966
+ * `undefined` with a list yields the list, and `undefined` with `undefined` stays
2967
+ * `undefined`. Narrowing can only TIGHTEN, never widen — a key excluded by a parent
2968
+ * can never be re-admitted by a child.
2969
+ *
2970
+ * @example
2971
+ * ```ts
2972
+ * const scope = new Scope({ name: 'reader', tools: ['search', 'read'] })
2973
+ * // narrow intersects: tools ∩ ['read', 'write'] = ['read'] (write was never in the parent).
2974
+ * const tighter = scope.narrow({ tools: ['read', 'write'] })
2975
+ * tighter.tools // ['read']
2976
+ * // instructions had no parent constraint (undefined) → the child's list passes through.
2977
+ * tighter.narrow({ instructions: ['safety'] }).instructions // ['safety']
2978
+ * ```
2979
+ */
2980
+ var Scope = class Scope {
2981
+ id = crypto.randomUUID();
2982
+ name;
2983
+ instructions;
2984
+ tools;
2985
+ files;
2986
+ constructor(input) {
2987
+ this.name = input.name;
2988
+ this.instructions = input.instructions === void 0 ? void 0 : [...input.instructions];
2989
+ this.tools = input.tools === void 0 ? void 0 : [...input.tools];
2990
+ this.files = input.files === void 0 ? void 0 : [...input.files];
2991
+ }
2992
+ narrow(config) {
2993
+ return new Scope({
2994
+ name: this.name,
2995
+ instructions: Scope.#intersect(this.instructions, config.instructions),
2996
+ tools: Scope.#intersect(this.tools, config.tools),
2997
+ files: Scope.#intersect(this.files, config.files)
2998
+ });
2999
+ }
3000
+ static #intersect(parent, child) {
3001
+ if (parent === void 0) return child === void 0 ? void 0 : [...child];
3002
+ if (child === void 0) return [...parent];
3003
+ const allowed = new Set(parent);
3004
+ return child.filter((key) => allowed.has(key));
3005
+ }
3006
+ };
3007
+ //#endregion
3008
+ //#region src/core/scopes/ScopeManager.ts
3009
+ /**
3010
+ * The scope registry a richer context reuses named filters from — immutable {@link Scope}s
3011
+ * keyed by their minted `id`, in insertion order.
3012
+ *
3013
+ * @remarks
3014
+ * - **Registry.** Scopes live in an insertion-ordered `Map` keyed by their minted `id`;
3015
+ * `create` mints a {@link Scope} from a {@link ScopeInput} (an `id` plus the three
3016
+ * allow-lists), stores it, and returns it. `count` is the map size, `scope(id)` looks
3017
+ * one up, and `scopes()` lists them in insertion order. (Unlike the name-keyed
3018
+ * instruction registry, a scope's key is its minted `id`, so two scopes may share a
3019
+ * `name`; `create` therefore always adds — it never overwrites.)
3020
+ * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was
3021
+ * removed; `clear` empties the registry.
3022
+ * - **Observable (§13).** The owned {@link emitter} ({@link ScopeManagerEventMap}) carries
3023
+ * `create` (the created scope) / `remove` (the id) / `clear`. Every event is emitted
3024
+ * directly, strictly AFTER the map mutation completes; the emitter isolates a listener
3025
+ * throw and routes it to its `error` handler (the `error` option), so a buggy observer can
3026
+ * never corrupt a mutation.
3027
+ *
3028
+ * @example
3029
+ * ```ts
3030
+ * const manager = new ScopeManager()
3031
+ * const reader = manager.create({ name: 'reader', tools: ['search', 'read'] })
3032
+ * manager.scope(reader.id) // the same scope
3033
+ * manager.count // 1
3034
+ * ```
3035
+ */
3036
+ var ScopeManager = class {
3037
+ #scopes = /* @__PURE__ */ new Map();
3038
+ #emitter;
3039
+ constructor(on, error) {
3040
+ this.#emitter = new Emitter({
3041
+ on,
3042
+ error
3043
+ });
3044
+ }
3045
+ get emitter() {
3046
+ return this.#emitter;
3047
+ }
3048
+ get count() {
3049
+ return this.#scopes.size;
3050
+ }
3051
+ create(input) {
3052
+ const scope = new Scope(input);
3053
+ this.#scopes.set(scope.id, scope);
3054
+ this.#emitter.emit("create", scope);
3055
+ return scope;
3056
+ }
3057
+ scope(id) {
3058
+ return this.#scopes.get(id);
3059
+ }
3060
+ scopes() {
3061
+ return [...this.#scopes.values()];
3062
+ }
3063
+ remove(ids) {
3064
+ if (isArray(ids)) {
3065
+ let removed = false;
3066
+ for (const id of ids) if (this.#delete(id)) removed = true;
3067
+ return removed;
3068
+ }
3069
+ return this.#delete(ids);
3070
+ }
3071
+ clear() {
3072
+ this.#scopes.clear();
3073
+ this.#emitter.emit("clear");
3074
+ }
3075
+ #delete(id) {
3076
+ const removed = this.#scopes.delete(id);
3077
+ if (removed) this.#emitter.emit("remove", id);
3078
+ return removed;
3079
+ }
3080
+ };
3081
+ //#endregion
3082
+ //#region src/core/ThinkSplitter.ts
3083
+ /**
3084
+ * The stream-stateful `<think>` separator — feeds raw content deltas through a tiny
3085
+ * state machine that routes everything inside a `<think>…</think>` span to `thinking`
3086
+ * and returns everything outside it as clean content, so a provider yields ONLY the
3087
+ * answer and surfaces the reasoning as {@link import('./types.js').ProviderResult.thinking}.
3088
+ *
3089
+ * @remarks
3090
+ * - **Cross-chunk tags.** A tag may arrive split across wire deltas (`'<thi'` then
3091
+ * `'nk>'`): any suffix of the pending text that is a strict PREFIX of a tag being
3092
+ * scanned for is HELD BACK (neither surfaced nor routed) until the next delta — or
3093
+ * `flush()` — disambiguates it. A held tag prefix that never completes is real
3094
+ * content; a held close-tag prefix inside a span is thinking.
3095
+ * - **The IMPLICIT leading open (the qwen3-template shape).** Some chat templates
3096
+ * PRE-SEED `<think>` into the prompt scaffold, so the wire stream begins
3097
+ * MID-REASONING and only a bare `</think>` appears. Before any tag event, a bare
3098
+ * close therefore RECLASSIFIES everything surfaced so far (plus the pre-close
3099
+ * pending) as thinking — `content` is corrected retroactively (the already-returned
3100
+ * prefix cannot be recalled, so `content` is the authoritative accumulation). The
3101
+ * rule is ONE-SHOT: after any tag event a bare `</think>` is plain text.
3102
+ * - **Multiple spans** accumulate onto `thinking` in stream order. A nested-looking
3103
+ * `<think>` inside an open span is just thinking text (no nesting is tracked — the
3104
+ * first `</think>` closes the span), matching how the models emit it.
3105
+ * - **Unclosed span at stream end.** `flush()` routes the open span's tail (including
3106
+ * any held partial close tag) to `thinking` — a cut-off model was still reasoning.
3107
+ * - **One splitter, one stream.** State is per-stream; create a fresh instance per
3108
+ * provider call ({@link import('./factories.js').createThinkSplitter}).
3109
+ *
3110
+ * @example
3111
+ * ```ts
3112
+ * const splitter = new ThinkSplitter()
3113
+ * splitter.split('<thi') // '' (held — ambiguous)
3114
+ * splitter.split('nk>plan</think>ok') // 'ok'
3115
+ * splitter.thinking // 'plan'
3116
+ * splitter.content // 'ok'
3117
+ * splitter.flush() // '' (nothing held)
3118
+ * ```
3119
+ */
3120
+ var ThinkSplitter = class {
3121
+ #pending = "";
3122
+ #inside = false;
3123
+ #opened = false;
3124
+ #content = "";
3125
+ #thinking = "";
3126
+ get content() {
3127
+ return this.#content;
3128
+ }
3129
+ get thinking() {
3130
+ return this.#thinking;
3131
+ }
3132
+ split(delta) {
3133
+ const out = this.#scan(delta);
3134
+ this.#content += out;
3135
+ return out;
3136
+ }
3137
+ flush() {
3138
+ const pending = this.#pending;
3139
+ this.#pending = "";
3140
+ if (this.#inside) {
3141
+ this.#thinking += pending;
3142
+ this.#inside = false;
3143
+ return "";
3144
+ }
3145
+ this.#content += pending;
3146
+ return pending;
3147
+ }
3148
+ #scan(delta) {
3149
+ this.#pending += delta;
3150
+ let content = "";
3151
+ for (;;) {
3152
+ if (this.#inside) {
3153
+ const close = this.#pending.indexOf(THINK_CLOSE);
3154
+ if (close === -1) {
3155
+ this.#thinking += this.#hold([THINK_CLOSE]);
3156
+ return content;
3157
+ }
3158
+ this.#thinking += this.#pending.slice(0, close);
3159
+ this.#pending = this.#pending.slice(close + THINK_CLOSE.length);
3160
+ this.#inside = false;
3161
+ continue;
3162
+ }
3163
+ const open = this.#pending.indexOf(THINK_OPEN);
3164
+ if (!this.#opened) {
3165
+ const close = this.#pending.indexOf(THINK_CLOSE);
3166
+ if (close !== -1 && (open === -1 || close < open)) {
3167
+ this.#thinking += this.#content + content + this.#pending.slice(0, close);
3168
+ this.#content = "";
3169
+ content = "";
3170
+ this.#pending = this.#pending.slice(close + THINK_CLOSE.length);
3171
+ this.#opened = true;
3172
+ continue;
3173
+ }
3174
+ }
3175
+ if (open === -1) {
3176
+ const tags = this.#opened ? [THINK_OPEN] : [THINK_OPEN, THINK_CLOSE];
3177
+ content += this.#hold(tags);
3178
+ return content;
3179
+ }
3180
+ this.#opened = true;
3181
+ content += this.#pending.slice(0, open);
3182
+ this.#pending = this.#pending.slice(open + THINK_OPEN.length);
3183
+ this.#inside = true;
3184
+ }
3185
+ }
3186
+ #hold(tags) {
3187
+ const keep = Math.max(...tags.map((tag) => this.#overlap(tag)));
3188
+ const cut = this.#pending.length - keep;
3189
+ const settled = this.#pending.slice(0, cut);
3190
+ this.#pending = this.#pending.slice(cut);
3191
+ return settled;
3192
+ }
3193
+ #overlap(tag) {
3194
+ const max = Math.min(this.#pending.length, tag.length - 1);
3195
+ for (let length = max; length > 0; length -= 1) if (this.#pending.endsWith(tag.slice(0, length))) return length;
3196
+ return 0;
3197
+ }
3198
+ };
3199
+ //#endregion
3200
+ //#region src/core/tools/Tool.ts
3201
+ /**
3202
+ * A registered tool — its {@link import('../types.js').ToolDefinition} schema (the
3203
+ * `name` / `description` / `parameters` the model sees) bound to the `execute`
3204
+ * handler that runs a call.
3205
+ *
3206
+ * @remarks
3207
+ * A thin value object: the constructor stores the schema fields and the handler, and
3208
+ * `execute` delegates to the handler verbatim. `parameters` is kept by reference (the
3209
+ * open JSON-Schema the provider forwards as-is) — not cloned. The handler's `args` is
3210
+ * the model-supplied `unknown` arguments record, narrowed inside the handler (§14);
3211
+ * isolating a throw is the {@link ToolManager}'s job, not this class's. Event-free —
3212
+ * no Emitter, no events.
3213
+ *
3214
+ * @example
3215
+ * ```ts
3216
+ * const tool = new Tool({
3217
+ * name: 'add',
3218
+ * description: 'Add two numbers',
3219
+ * parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } } },
3220
+ * execute: (args) => Number(args.a) + Number(args.b),
3221
+ * })
3222
+ * ```
3223
+ */
3224
+ var Tool = class {
3225
+ name;
3226
+ description;
3227
+ parameters;
3228
+ #execute;
3229
+ constructor(options) {
3230
+ this.name = options.name;
3231
+ this.description = options.description;
3232
+ this.parameters = options.parameters;
3233
+ this.#execute = options.execute;
3234
+ }
3235
+ execute(args) {
3236
+ return this.#execute(args);
3237
+ }
3238
+ };
3239
+ //#endregion
3240
+ //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
3241
+ /**
3242
+ * A {@link WorkspaceStoreInterface} backed by one table of the `databases` layer — a workspace's
3243
+ * durable state IS a row, so persistence reduces to keyed point-access (`get` / `set` / `delete`)
3244
+ * over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
3245
+ * {@link import('./MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
3246
+ *
3247
+ * @remarks
3248
+ * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
3249
+ * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
3250
+ * IndexedDB backend swaps in WITHOUT touching the
3251
+ * {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3252
+ * {@link import('../Workspace.js').Workspace} — the same seam as
3253
+ * the analogous `DatabaseWorkflowStore` in `@orkestrel/workflow`. The
3254
+ * driver defaults to memory ({@link import('../../factories.js').createDatabaseWorkspaceStore}
3255
+ * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
3256
+ * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
3257
+ *
3258
+ * The {@link WorkspaceSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
3259
+ * `{ id; snapshot }` ({@link WorkspaceSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
3260
+ * column the factory builds) — exactly as `DatabaseWorkflowStore` stores its snapshot. The snapshot
3261
+ * is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
3262
+ * keeps the row type flat (`snapshot` reads back as `unknown`).
3263
+ *
3264
+ * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
3265
+ * the row `{ id: snapshot.id, snapshot }`.
3266
+ * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
3267
+ * a {@link WorkspaceSnapshot} ({@link import('../../helpers.js').isWorkspaceSnapshot} — the AGENTS
3268
+ * §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
3269
+ * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
3270
+ *
3271
+ * UNLIKE a session store there is NO idle-TTL / eviction — a persisted workspace lives until an
3272
+ * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
3273
+ * §22 method bijection with {@link WorkspaceStoreInterface}). Hydration stays a caller concern: a
3274
+ * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3275
+ * live workspace through the constructor `seed` (its `open` / `save`).
3276
+ *
3277
+ * @example
3278
+ * ```ts
3279
+ * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspace } from '@src/core'
3280
+ *
3281
+ * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
3282
+ * const workspace = createWorkspace()
3283
+ * workspace.write('notes.txt', 'hello')
3284
+ * await store.set(workspace.snapshot()) // persist the workspace (one JSON column)
3285
+ * const snapshot = await store.get(workspace.id)
3286
+ * await store.delete(workspace.id) // drop it
3287
+ * ```
3288
+ */
3289
+ var DatabaseWorkspaceStore = class {
3290
+ #table;
3291
+ /**
3292
+ * Wrap a table as a workspace store.
3293
+ *
3294
+ * @param table - The {@link TableInterface} holding the snapshots — its row is the
3295
+ * {@link WorkspaceSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
3296
+ */
3297
+ constructor(table) {
3298
+ this.#table = table;
3299
+ }
3300
+ /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkspaceSnapshot`. */
3301
+ async get(id) {
3302
+ const row = await this.#table.get(id);
3303
+ if (row === void 0) return void 0;
3304
+ return isWorkspaceSnapshot(row.snapshot) ? row.snapshot : void 0;
3305
+ }
3306
+ /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
3307
+ async set(snapshot) {
3308
+ await this.#table.set({
3309
+ id: snapshot.id,
3310
+ snapshot
3311
+ });
3312
+ }
3313
+ /** Drop a snapshot by id; an absent id is a no-op (no throw). */
3314
+ async delete(id) {
3315
+ await this.#table.remove(id);
3316
+ }
3317
+ };
3318
+ //#endregion
3319
+ //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
3320
+ /**
3321
+ * The in-memory {@link WorkspaceStoreInterface} — a process-lifetime `Map` of
3322
+ * {@link WorkspaceSnapshot}s keyed by workspace id, the DEFAULT store
3323
+ * {@link import('../../factories.js').createMemoryWorkspaceStore} builds.
3324
+ *
3325
+ * @remarks
3326
+ * A plain `Map<string, WorkspaceSnapshot>` (AGENTS §21 — the snapshot is already pure,
3327
+ * self-contained JSON, so no encoding is needed for the memory tier). Like the
3328
+ * the analogous `MemoryWorkflowStore` in `@orkestrel/workflow` it twins,
3329
+ * there is NO idle-TTL and NO eviction: a persisted workspace lives until an explicit `delete`. A
3330
+ * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
3331
+ * the {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3332
+ * {@link import('../Workspace.js').Workspace} — its driver-pluggable twin is
3333
+ * {@link import('./DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} (the snapshot as one opaque
3334
+ * JSON column).
3335
+ *
3336
+ * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
3337
+ * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
3338
+ * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
3339
+ *
3340
+ * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
3341
+ * bijection with {@link WorkspaceStoreInterface}). Hydration is a caller concern: a
3342
+ * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3343
+ * live workspace through the constructor `seed` (its `open` / `save`).
3344
+ *
3345
+ * @example
3346
+ * ```ts
3347
+ * import { createMemoryWorkspaceStore, createWorkspace } from '@src/core'
3348
+ *
3349
+ * const store = createMemoryWorkspaceStore()
3350
+ * const workspace = createWorkspace()
3351
+ * workspace.write('notes.txt', 'hello')
3352
+ * await store.set(workspace.snapshot()) // persist the workspace
3353
+ * const snapshot = await store.get(workspace.id)
3354
+ * await store.delete(workspace.id) // drop it
3355
+ * ```
3356
+ */
3357
+ var MemoryWorkspaceStore = class {
3358
+ #snapshots = /* @__PURE__ */ new Map();
3359
+ get(id) {
3360
+ return Promise.resolve(this.#snapshots.get(id));
3361
+ }
3362
+ set(snapshot) {
3363
+ this.#snapshots.set(snapshot.id, snapshot);
3364
+ return Promise.resolve();
3365
+ }
3366
+ delete(id) {
3367
+ this.#snapshots.delete(id);
3368
+ return Promise.resolve();
3369
+ }
3370
+ };
3371
+ //#endregion
3372
+ //#region src/core/factories.ts
3373
+ /**
3374
+ * Create a tool — a {@link ToolInterface} binding a {@link ToolDefinition} schema
3375
+ * (the `name` / `description` / `parameters` the model sees) to the `execute` handler
3376
+ * that runs a call.
3377
+ *
3378
+ * @remarks
3379
+ * Only `name` is required (it keys the tool in a {@link ToolManagerInterface} and is
3380
+ * what the model calls); `description` / `parameters` are the optional JSON-Schema the
3381
+ * provider advertises (forwarded verbatim). The handler's `args` is the model-supplied
3382
+ * `unknown` arguments record — narrow it inside (§14); a `createToolManager` registry
3383
+ * isolates a throw into a `ToolResult.error`.
3384
+ *
3385
+ * @param options - `name` (required), optional `description` / `parameters`, and the
3386
+ * `execute` handler (see {@link ToolOptions})
3387
+ * @returns A working {@link ToolInterface}
3388
+ *
3389
+ * @example
3390
+ * ```ts
3391
+ * import { createTool } from '@src/core'
3392
+ *
3393
+ * const add = createTool({
3394
+ * name: 'add',
3395
+ * description: 'Add two numbers',
3396
+ * execute: (args) => Number(args.a) + Number(args.b),
3397
+ * })
3398
+ * ```
3399
+ */
3400
+ function createTool(options) {
3401
+ return new Tool(options);
3402
+ }
3403
+ /**
3404
+ * Create a tool registry — a {@link ToolManagerInterface} that resolves tool names,
3405
+ * lists {@link ToolDefinition}s for the provider, and executes calls with per-call
3406
+ * error isolation.
3407
+ *
3408
+ * @remarks
3409
+ * Starts empty; `add` registers one tool or a batch (§9.2), `definitions()` yields the
3410
+ * schemas to hand a provider, and `execute` runs a {@link ToolCall} (or a batch),
3411
+ * ALWAYS resolving a {@link ToolResult} — a handler throw becomes an `error` result and
3412
+ * an unknown name a not-found `error`, so a tool throw never escapes and a batch never
3413
+ * fails as a whole.
3414
+ *
3415
+ * @returns An empty {@link ToolManagerInterface}
3416
+ *
3417
+ * @example
3418
+ * ```ts
3419
+ * import { createTool, createToolManager } from '@src/core'
3420
+ *
3421
+ * const tools = createToolManager()
3422
+ * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
3423
+ * const result = await tools.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
3424
+ * ```
3425
+ */
3426
+ function createToolManager() {
3427
+ return new ToolManager();
3428
+ }
3429
+ /**
3430
+ * Create a conversation — a {@link ConversationInterface} grouping messages above a flat
3431
+ * message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
3432
+ * rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
3433
+ * provider-agnostic {@link ConversationSummarizer} seam.
3434
+ *
3435
+ * @remarks
3436
+ * Append turns through the conversation's own `add` (the live tail it owns); `view()` is the model input
3437
+ * (each section as a summary message, then the live tail). `compact()` folds the older live
3438
+ * messages into a summarized {@link SectionInterface} and regenerates the rollup — it REQUIRES
3439
+ * a `summarize` (omitted ⇒ `compact()` throws a `ConversationError`); `keep` retains a recent
3440
+ * tail (default `DEFAULT_CONVERSATION_KEEP` — fold ALL). `rehydrate(id)` / `search(query)` read
3441
+ * the retained originals. Observable (`emitter` — `compact` / `summary` / `rehydrate`), wired
3442
+ * via the reserved `on` option (§8); the emitter isolates a listener throw and routes it to
3443
+ * its `error` handler (the `error` option, §13), so it can never corrupt a compaction.
3444
+ *
3445
+ * @param options - Optional `id` / `on` hooks + the `summarize` seam + `keep` (see {@link ConversationOptions})
3446
+ * @returns A working {@link ConversationInterface}
3447
+ *
3448
+ * @example
3449
+ * ```ts
3450
+ * import type { ProviderInterface } from '@src/core'
3451
+ * import { createConversation } from '@src/core'
3452
+ *
3453
+ * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3454
+ * const conversation = createConversation({
3455
+ * // Append the instruction as the FINAL user turn — a chat model emits nothing when the
3456
+ * // prompt ends on an assistant turn, so a leading-system instruction is unreliable.
3457
+ * summarize: async (messages) =>
3458
+ * (await provider.generate([...messages, { id: 's', role: 'user', content: 'Summarize the conversation so far concisely.' }], AbortSignal.timeout(30_000))).content,
3459
+ * })
3460
+ * conversation.add({ role: 'user', content: 'Hello' })
3461
+ * await conversation.compact() // folds the live tail into a summarized section
3462
+ * ```
3463
+ */
3464
+ function createConversation(options) {
3465
+ return new Conversation(options);
3466
+ }
3467
+ /**
3468
+ * Create a conversation registry — a {@link ConversationManagerInterface} holding
3469
+ * {@link ConversationInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
3470
+ * the §9 store over the conversation layer plus the `active` / `switch` seam the context renders.
3471
+ *
3472
+ * @remarks
3473
+ * Starts empty; `add(input?)` mints a {@link ConversationInterface} (its `id` from the input
3474
+ * or a random UUID), flowing the manager's default `summarize` / `keep` in unless the input
3475
+ * overrides them, and stores it (an already-present `id` overwrites — last write wins) — and
3476
+ * AUTO-ACTIVATES the FIRST one (a registry with conversations always has one `active`); a later
3477
+ * `add` leaves `active` unchanged. `switch(id)` re-points `active` (an unknown `id` returns
3478
+ * `undefined`, leaving `active` unchanged — lenient, never throws); `conversation(id)` /
3479
+ * `conversations()` look up; `remove` (one or a batch, §9.2) reports whether any was removed AND
3480
+ * clears `active` if it was the removed one; `clear` empties it and clears `active`. Event-free
3481
+ * (each conversation owns its own observable `emitter`). A conversation created with NEITHER a
3482
+ * manager default nor a per-`add` `summarize` cannot `compact` (it throws a `ConversationError`).
3483
+ *
3484
+ * @param options - Optional default `summarize` / `keep` (see {@link ConversationManagerOptions})
3485
+ * @returns An empty {@link ConversationManagerInterface}
3486
+ *
3487
+ * @example
3488
+ * ```ts
3489
+ * import { createConversationManager } from '@src/core'
3490
+ *
3491
+ * const conversations = createConversationManager({ summarize: async (m) => `recap of ${m.length}` })
3492
+ * const chat = conversations.add() // auto-activates — conversations.active === chat
3493
+ * chat.add({ role: 'user', content: 'Hello' })
3494
+ * ```
3495
+ */
3496
+ function createConversationManager(options) {
3497
+ return new ConversationManager(options);
3498
+ }
3499
+ /**
3500
+ * Create the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
3501
+ * process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
3502
+ * id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
3503
+ * {@link ConversationManagerInterface.save} seam. The EXACT twin of {@link createMemoryWorkspaceStore}.
3504
+ *
3505
+ * @remarks
3506
+ * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
3507
+ * the structural twin of {@link createMemoryWorkspaceStore}. `get` / `set` / `delete` are async (the
3508
+ * same shape a durable backend fits); UNLIKE a session store there is NO idle-TTL / eviction — a
3509
+ * persisted conversation lives until an explicit `delete`. Its driver-pluggable twin is
3510
+ * {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
3511
+ * table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB driver, and it swaps in WITHOUT
3512
+ * touching the manager or the conversation. Hydration stays a manager concern: read a snapshot back
3513
+ * and rebuild the live conversation through the constructor `seed` (re-supplying the live
3514
+ * `summarize` / `keep`).
3515
+ *
3516
+ * @returns A memory-backed {@link ConversationStoreInterface}
3517
+ *
3518
+ * @example
3519
+ * ```ts
3520
+ * import { createConversationManager, createMemoryConversationStore } from '@src/core'
3521
+ *
3522
+ * const store = createMemoryConversationStore()
3523
+ * const manager = createConversationManager({ store })
3524
+ * const conversation = manager.add()
3525
+ * conversation.add({ role: 'user', content: 'hello' })
3526
+ * await manager.save(conversation.id) // persist the conversation
3527
+ * ```
3528
+ */
3529
+ function createMemoryConversationStore() {
3530
+ return new MemoryConversationStore();
3531
+ }
3532
+ /**
3533
+ * Create a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
3534
+ * driver-pluggable backing for the conversation persistence seam, the opt-in twin of
3535
+ * {@link createMemoryConversationStore}. The EXACT twin of {@link createDatabaseWorkspaceStore}.
3536
+ *
3537
+ * @remarks
3538
+ * Builds a one-table database (`conversations`, keyed by `id`) over the supplied driver, the snapshot
3539
+ * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
3540
+ * `rawShape` (a JSON blob), exactly as {@link createDatabaseWorkspaceStore} stores its snapshot. The
3541
+ * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
3542
+ * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
3543
+ * {@link import('./helpers.js').isConversationSnapshot}). The `driver` DEFAULTS to
3544
+ * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
3545
+ * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
3546
+ * the durability is the driver's job, the store engine is shared. It swaps in behind
3547
+ * {@link ConversationStoreInterface} WITHOUT touching the manager or the conversation.
3548
+ *
3549
+ * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
3550
+ * @returns A {@link ConversationStoreInterface} over the driver
3551
+ *
3552
+ * @example
3553
+ * ```ts
3554
+ * import { createConversationManager, createDatabaseConversationStore, createMemoryDriver } from '@src/core'
3555
+ *
3556
+ * const store = createDatabaseConversationStore(createMemoryDriver()) // a durable driver swaps in here
3557
+ * const manager = createConversationManager({ store })
3558
+ * const conversation = manager.add()
3559
+ * conversation.add({ role: 'user', content: 'hello' })
3560
+ * await manager.save(conversation.id) // persist the conversation (one JSON column)
3561
+ * ```
3562
+ */
3563
+ function createDatabaseConversationStore(driver = createMemoryDriver()) {
3564
+ return new DatabaseConversationStore(createDatabase({
3565
+ driver,
3566
+ tables: { conversations: {
3567
+ id: stringShape(),
3568
+ snapshot: rawShape({})
3569
+ } }
3570
+ }).table("conversations"));
3571
+ }
3572
+ /**
3573
+ * Create an instruction — an immutable {@link InstructionInterface} (a named directive)
3574
+ * from its `name` / `content` and optional `priority`, the `id` minted at construction.
3575
+ *
3576
+ * @remarks
3577
+ * Only `name` / `content` are required; `priority` orders the instruction in an
3578
+ * {@link InstructionManagerInterface}'s rendered list (higher first) and defaults to `0`.
3579
+ * Stored immutable — never mutated after creation.
3580
+ *
3581
+ * @param input - `name` / `content` (required) and an optional `priority` (see
3582
+ * {@link InstructionInput})
3583
+ * @returns A working {@link InstructionInterface}
3584
+ *
3585
+ * @example
3586
+ * ```ts
3587
+ * import { createInstruction } from '@src/core'
3588
+ *
3589
+ * const instruction = createInstruction({ name: 'tone', content: 'Be concise.', priority: 5 })
3590
+ * ```
3591
+ */
3592
+ function createInstruction(input) {
3593
+ return new Instruction(input);
3594
+ }
3595
+ /**
3596
+ * Create an instruction registry — an {@link InstructionManagerInterface} holding
3597
+ * immutable instructions keyed by `name`, listed by descending `priority`.
3598
+ *
3599
+ * @remarks
3600
+ * Starts empty; `add` (one or a batch, §9.2) MINTS each `id` and OVERWRITES a same-name
3601
+ * instruction (last write wins); `instructions()` lists them sorted by descending
3602
+ * `priority` (stable for ties); `format` / `description` are the build contract a richer
3603
+ * context renders an instructions block with; `remove` (one or a batch) reports whether
3604
+ * any was removed; `clear` empties it. Carries an observable `emitter`
3605
+ * ({@link import('./types.js').InstructionManagerEventMap}) wired via the reserved `on`
3606
+ * option (§8); the emitter isolates a listener throw and routes it to its `error` handler
3607
+ * (the `error` option, §13), so it can never corrupt a mutation. An optional `format`
3608
+ * override is the manager-options level of the `AgentContext` build cascade (consulted by
3609
+ * `description` / `format`, beating the provider default + built-in; a per-item
3610
+ * `InstructionInput.format` still beats it).
3611
+ *
3612
+ * @param options - Optional `on` hooks + a `format` override (see {@link InstructionManagerOptions})
3613
+ * @returns An empty {@link InstructionManagerInterface}
3614
+ *
3615
+ * @example
3616
+ * ```ts
3617
+ * import { createInstructionManager } from '@src/core'
3618
+ *
3619
+ * const instructions = createInstructionManager()
3620
+ * instructions.add({ name: 'tone', content: 'Be concise.', priority: 5 })
3621
+ * ```
3622
+ */
3623
+ function createInstructionManager(options) {
3624
+ return new InstructionManager(options);
3625
+ }
3626
+ /**
3627
+ * Create a named scope — an immutable {@link ScopeInterface} from its `name` and the four
3628
+ * optional per-category allow-lists, the `id` minted at construction.
3629
+ *
3630
+ * @remarks
3631
+ * Each list is THREE-WAY: `undefined` ⇒ NO constraint on that category (all pass), `[]` ⇒
3632
+ * NONE pass, a non-empty list ⇒ only the listed keys pass. `narrow(config)` composes a
3633
+ * tighter child by set-INTERSECTION (an `undefined` side imposing no constraint). Stored
3634
+ * immutable — never mutated after creation (`narrow` returns a new scope).
3635
+ *
3636
+ * @param input - `name` (required) and the optional `instructions` / `tools` / `messages` /
3637
+ * `files` allow-lists (see {@link ScopeInput})
3638
+ * @returns A working {@link ScopeInterface}
3639
+ *
3640
+ * @example
3641
+ * ```ts
3642
+ * import { createScope } from '@src/core'
3643
+ *
3644
+ * const reader = createScope({ name: 'reader', tools: ['search', 'read'] })
3645
+ * reader.narrow({ tools: ['read', 'write'] }).tools // ['read'] — intersection tightens
3646
+ * ```
3647
+ */
3648
+ function createScope(input) {
3649
+ return new Scope(input);
3650
+ }
3651
+ /**
3652
+ * Create a scope registry — a {@link ScopeManagerInterface} holding immutable scopes keyed
3653
+ * by their minted `id`, in insertion order.
3654
+ *
3655
+ * @remarks
3656
+ * Starts empty; `create` mints each scope's `id` and stores it (keyed by `id`, so it
3657
+ * always adds — two scopes may share a `name`); `scopes()` lists them in insertion order;
3658
+ * `remove` (one or a batch, §9.2) reports whether any was removed; `clear` empties it.
3659
+ * Carries an observable `emitter` ({@link import('./types.js').ScopeManagerEventMap}) wired
3660
+ * via the reserved `on` option (§8); the emitter isolates a listener throw and routes it to
3661
+ * its `error` handler (the `error` option, §13), so it can never corrupt a mutation.
3662
+ *
3663
+ * @param options - Optional `on` hooks (see {@link ScopeManagerOptions})
3664
+ * @returns An empty {@link ScopeManagerInterface}
3665
+ *
3666
+ * @example
3667
+ * ```ts
3668
+ * import { createScopeManager } from '@src/core'
3669
+ *
3670
+ * const scopes = createScopeManager()
3671
+ * const reader = scopes.create({ name: 'reader', tools: ['search'] })
3672
+ * ```
3673
+ */
3674
+ function createScopeManager(options) {
3675
+ return new ScopeManager(options?.on, options?.error);
3676
+ }
3677
+ /**
3678
+ * Create a richer turn context — an {@link AgentContextInterface} assembling a provider
3679
+ * request from the optional system prompt, the instruction registry, the workspace registry,
3680
+ * the conversation store, the tool registry, and the active scope.
3681
+ *
3682
+ * @remarks
3683
+ * `system` is the optional system prompt; `tools` / `instructions` / `workspaces` are pre-built
3684
+ * managers to reuse (empty ones are created when omitted, so `context.workspaces` is ALWAYS
3685
+ * present); `scope` is the initial active filter (`undefined` ⇒ no filtering, mutable afterwards
3686
+ * via `context.scope`). The `messages` store is always fresh. `build()` folds the scoped
3687
+ * instructions — PLUS the ACTIVE workspace's scope-filtered text files (fenced) — into ONE leading
3688
+ * `system` message and appends the scoped conversation (attaching the active workspace's
3689
+ * scope-filtered image files' `data` to the last user message), built fresh each call; the active
3690
+ * workspace is the SOLE document/image context. Tools are advertised STRUCTURALLY (via
3691
+ * `tools.definitions()`, scope-filtered by the loop), never serialized into the prompt.
3692
+ *
3693
+ * @param options - Optional `system` / `tools` / `instructions` / `workspaces` / `scope`
3694
+ * (see {@link AgentContextOptions})
3695
+ * @returns A working {@link AgentContextInterface}
3696
+ *
3697
+ * @example
3698
+ * ```ts
3699
+ * import { createAgentContext } from '@src/core'
3700
+ *
3701
+ * const context = createAgentContext({ system: 'You are concise.' })
3702
+ * context.instructions.add({ name: 'tone', content: 'Be terse.' })
3703
+ * context.messages.add({ role: 'user', content: 'Hi' })
3704
+ * context.build() // [{ role: 'system', content: 'You are concise.\n\n## Instructions\n\nBe terse.' }, { role: 'user', content: 'Hi' }]
3705
+ * ```
3706
+ */
3707
+ function createAgentContext(options) {
3708
+ return new AgentContext(options);
3709
+ }
3710
+ /**
3711
+ * Create an agent loop — an {@link AgentInterface} composing a
3712
+ * {@link ProviderInterface}, its {@link AgentContextInterface}, and a tool registry
3713
+ * into a bounded context → provider → tools → repeat turn, exposed as a one-shot
3714
+ * `generate` and a live `stream`.
3715
+ *
3716
+ * @remarks
3717
+ * One private loop drives the turn; `generate` DRAINS the same stream `stream`
3718
+ * exposes, so they can never diverge. Each turn is bounded by one cancel folded from
3719
+ * `signal` + `timeout` + `budget` (via `AbortSignal.any`) — any trip (or `abort()`)
3720
+ * commits a PARTIAL result (the stream's `result` RESOLVES on a cancel, rejects only
3721
+ * on a genuine provider / tool error). The `scheduler` paces between turns; tool
3722
+ * iteration is capped at `limit` (default `DEFAULT_AGENT_LIMIT`). Tools are advertised
3723
+ * structurally via `context.tools.definitions()`. Two observation surfaces: the
3724
+ * {@link AgentChunk} stream (pull — per-token content) and a typed `emitter` (push —
3725
+ * lifecycle + `usage` / `tool` / `deny` for fire-and-forget observers).
3726
+ *
3727
+ * @param provider - The {@link ProviderInterface} the loop drives each turn
3728
+ * @param options - Optional `system` / `tools` / `limit` / `timeout` / `budget` /
3729
+ * `scheduler` / `signal` (see {@link AgentOptions})
3730
+ * @returns A working {@link AgentInterface}
3731
+ *
3732
+ * @example
3733
+ * ```ts
3734
+ * import type { ProviderInterface } from '@src/core'
3735
+ * import { createAgent, createTokenBudget } from '@src/core'
3736
+ *
3737
+ * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3738
+ * const agent = createAgent(provider, {
3739
+ * system: 'You are concise.',
3740
+ * budget: createTokenBudget({ max: 50_000, scope: 'total' }),
3741
+ * })
3742
+ * agent.context.messages.add({ role: 'user', content: 'Say hi.' })
3743
+ *
3744
+ * const stream = agent.stream()
3745
+ * for await (const chunk of stream.events) {
3746
+ * if (chunk.type === 'token') process.stdout.write(chunk.content)
3747
+ * }
3748
+ * const result = await stream.result // { content, usage?, partial }
3749
+ * ```
3750
+ */
3751
+ function createAgent(provider, options) {
3752
+ return new Agent(provider, options);
3753
+ }
3754
+ /**
3755
+ * Create a stream-stateful `<think>` separator — a {@link ThinkSplitterInterface} that
3756
+ * splits a thinking model's in-content `<think>…</think>` reasoning spans away from
3757
+ * the answer, delta by delta, so a provider yields ONLY clean content and surfaces the
3758
+ * accumulated reasoning as {@link import('./types.js').ProviderResult.thinking}.
3759
+ *
3760
+ * @remarks
3761
+ * Feed each raw wire delta through `split(delta)` (it returns the clean content to
3762
+ * surface — possibly `''` mid-think) and settle the stream end with `flush()` (a held
3763
+ * partial open tag that never completed returns as final content; an UNCLOSED think
3764
+ * span lands on `thinking`). Tags split ACROSS deltas are held back until
3765
+ * disambiguated, multiple spans accumulate in order, and a nested-looking `<think>`
3766
+ * inside an open span is just thinking text. One splitter serves ONE stream — create
3767
+ * a fresh one per provider call.
3768
+ *
3769
+ * @returns A fresh {@link ThinkSplitterInterface} (state empty, outside any span)
3770
+ *
3771
+ * @example
3772
+ * ```ts
3773
+ * import { createThinkSplitter } from '@src/core'
3774
+ *
3775
+ * const splitter = createThinkSplitter()
3776
+ * const clean = splitter.split('<think>plan the answer</think>Here it is.')
3777
+ * clean // 'Here it is.'
3778
+ * splitter.thinking // 'plan the answer'
3779
+ * ```
3780
+ */
3781
+ function createThinkSplitter() {
3782
+ return new ThinkSplitter();
3783
+ }
3784
+ /**
3785
+ * Create a policy gate — an {@link AuthorityInterface} the agent loop consults before
3786
+ * each tool call runs, evaluating the ordered rules first-match-wins and falling back
3787
+ * to the configured default when none match.
3788
+ *
3789
+ * @remarks
3790
+ * `rules` are evaluated in order — the FIRST whose `match` is true decides (a matched
3791
+ * rule ALLOWS unless its `allowed` is explicitly `false`). When no rule matches, the
3792
+ * `fallback` decides; it defaults to `{ zone: DEFAULT_AUTHORITY_ZONE, allowed: true }`
3793
+ * (allow-unmatched — a rules list of denials acts as a DENYLIST). Pass an
3794
+ * `allowed: false` `fallback` to flip the gate to deny-by-default (an ALLOWLIST). Wire
3795
+ * the result into `createAgent` via `AgentOptions.authority`: a denied call is fed back
3796
+ * to the model as a denial `ToolResult` (not executed, no budget cost), so the model
3797
+ * can react. Synchronous now — the async human-approval handshake is deferred.
3798
+ *
3799
+ * @param options - Optional `rules` (ordered) and `fallback` (see {@link AuthorityOptions})
3800
+ * @returns A working {@link AuthorityInterface}
3801
+ *
3802
+ * @example
3803
+ * ```ts
3804
+ * import { createAgent, createAuthority } from '@src/core'
3805
+ *
3806
+ * // Deny the `delete` tool, allow everything else.
3807
+ * const authority = createAuthority({
3808
+ * rules: [{ match: (c) => c.call.name === 'delete', zone: 'restricted', allowed: false }],
3809
+ * })
3810
+ * const agent = createAgent(provider, { tools, authority })
3811
+ * ```
3812
+ */
3813
+ function createAuthority(options) {
3814
+ return new Authority(options);
3815
+ }
3816
+ /**
3817
+ * Create an agent registry — an {@link AgentRegistryInterface} holding the named pools of
3818
+ * live, non-serializable pieces (providers, tools, authorities, schedulers) that a
3819
+ * serializable {@link AgentJobInput}'s names resolve against, and `build`ing a seeded,
3820
+ * signal-wired {@link AgentInterface} from a job.
3821
+ *
3822
+ * @remarks
3823
+ * `providers` is required; `tools` / `authorities` / `schedulers` are optional pools.
3824
+ * The accessors (`provider` / `tool` / `authority` / `scheduler`) THROW `unknown <category>:
3825
+ * <name>` on an unregistered name (§9.1 + §12) — a misconfigured or crash-restored job
3826
+ * fails loudly rather than running with a missing dependency. `build(input, signal)`
3827
+ * resolves the names, rebuilds the token budget from its ceiling, seeds the agent's
3828
+ * context with the job's messages, and threads `signal` so a queue / runner abort
3829
+ * propagates. This is the bridge that makes durable, serializable agent jobs runnable.
3830
+ *
3831
+ * @param options - The named pools (see {@link AgentRegistryOptions})
3832
+ * @returns A working {@link AgentRegistryInterface}
3833
+ *
3834
+ * @example
3835
+ * ```ts
3836
+ * import type { ProviderInterface } from '@src/core'
3837
+ * import { createAgentRegistry, createTool } from '@src/core'
3838
+ *
3839
+ * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3840
+ * const registry = createAgentRegistry({
3841
+ * providers: { main: provider },
3842
+ * tools: { add: createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }) },
3843
+ * })
3844
+ * const agent = registry.build({ provider: 'main', messages: [{ role: 'user', content: 'Hi.' }] })
3845
+ * ```
3846
+ */
3847
+ function createAgentRegistry(options) {
3848
+ return new AgentRegistry(options);
3849
+ }
3850
+ /**
3851
+ * Create a durable, bounded-concurrency agent-job queue — a {@link QueueInterface} over
3852
+ * serializable {@link AgentJobInput}s that COMPOSES `createQueue`: each job is rehydrated
3853
+ * through the `registry` into a live {@link AgentInterface}, run to its {@link AgentResult},
3854
+ * and subjected to the partial-as-configurable-failure policy.
3855
+ *
3856
+ * @remarks
3857
+ * - **Composes the substrate (no new engine).** The handler is the only new logic;
3858
+ * bounded `concurrency`, `retries`, the per-attempt `timeout`, and durable persistence
3859
+ * via `store` (+ `restore()` after a crash) are all the backing Queue's. `enqueue`
3860
+ * returns a per-job promise.
3861
+ * - **Durable + serializable.** Because `AgentJobInput` is JSON-serializable, a `store`
3862
+ * (e.g. `createMemoryQueueStore` / `createDatabaseQueueStore`) persists outstanding
3863
+ * jobs; `restore()` re-enqueues them after a restart and the `registry` rehydrates the
3864
+ * live pieces from the names — so a job survives a crash.
3865
+ * - **Partial policy.** A partial result THROWS an
3866
+ * {@link import('./errors.js').AgentJobError} by default, so a job cancelled by its
3867
+ * attempt deadline / a queue abort RETRIES while attempts remain; `allowPartial: true`
3868
+ * resolves the partial as success instead.
3869
+ * - **Cancellation threads through.** The handler passes `execution.signal` into
3870
+ * `registry.build`, so a queue `abort()` or a per-attempt timeout cancels the in-flight
3871
+ * agent (which commits a partial → throws → retries / fails per policy).
3872
+ *
3873
+ * @param options - The `registry`, the `allowPartial` policy, and the substrate knobs
3874
+ * (`concurrency` / `retries` / `timeout` / `store`) (see {@link AgentQueueOptions})
3875
+ * @returns A {@link QueueInterface} of {@link AgentJobInput} → {@link AgentResult}
3876
+ *
3877
+ * @example
3878
+ * ```ts
3879
+ * import { createAgentQueue, createAgentRegistry, createMemoryQueueStore } from '@src/core'
3880
+ *
3881
+ * const registry = createAgentRegistry({ providers: { main: provider } })
3882
+ * const store = createMemoryQueueStore(agentJobShape) // survives a restart via restore()
3883
+ * const queue = createAgentQueue({ registry, concurrency: 2, retries: 1, store })
3884
+ * const result = await queue.enqueue({ provider: 'main', messages: [{ role: 'user', content: 'ok?' }] })
3885
+ * ```
3886
+ */
3887
+ function createAgentQueue(options) {
3888
+ const { registry, allowPartial = false, concurrency, retries, timeout, store } = options;
3889
+ return createQueue({
3890
+ concurrency,
3891
+ retries,
3892
+ timeout,
3893
+ store,
3894
+ handler: (input, execution) => settleAgentJob(registry.build(input, execution.signal), allowPartial)
3895
+ });
3896
+ }
3897
+ /**
3898
+ * Create an agent-job runner — a {@link RunnerInterface} over serializable
3899
+ * {@link AgentJobInput}s that COMPOSES `createRunner` (one-shot, ordered, fail-fast), each
3900
+ * unit rehydrated through the `registry` and subjected to the partial policy. The runner
3901
+ * enables **sub-agent fan-out**: a parent job's handler can `controller.spawn(childJob)`.
3902
+ *
3903
+ * @remarks
3904
+ * - **Composes the substrate (no new engine).** Bounded `concurrency`, `retries`, the
3905
+ * per-attempt `timeout`, ordered results, and fail-fast are all the backing Runner's;
3906
+ * the handler adds only rehydration + the partial policy.
3907
+ * - **Sub-agent fan-out.** Each unit's handler receives a `ControllerInterface` whose
3908
+ * `spawn(childJob)` launches a CHILD agent job through the same bounded queue (the
3909
+ * child's result joins the run after the declared units, in spawn order). On a bounded
3910
+ * runner, FAN OUT and return — do NOT inline-`await` a spawn from within the handler (a
3911
+ * slot-holding handler awaiting its own spawn can deadlock; see `ControllerInterface`).
3912
+ * - **Partial policy + cancellation.** Same as `createAgentQueue`: a partial result
3913
+ * THROWS by default (the run's fail-fast engages), `allowPartial: true` resolves it; the
3914
+ * handler threads `controller.signal` into `registry.build`, so a runner abort / a
3915
+ * per-attempt timeout cancels the agent.
3916
+ *
3917
+ * @param options - The `registry`, the `allowPartial` policy, and the substrate knobs
3918
+ * (`concurrency` / `retries` / `timeout`) (see {@link AgentRunnerOptions})
3919
+ * @returns A {@link RunnerInterface} of {@link AgentJobInput} → {@link AgentResult}
3920
+ *
3921
+ * @example
3922
+ * ```ts
3923
+ * import { createAgentRunner, createAgentRegistry } from '@src/core'
3924
+ *
3925
+ * const registry = createAgentRegistry({ providers: { main: provider } })
3926
+ * const runner = createAgentRunner({ registry, concurrency: 2 })
3927
+ * // Run two jobs; the first fans out a child sub-agent then returns.
3928
+ * const child = { provider: 'main', messages: [{ role: 'user', content: 'child' }] }
3929
+ * const parent = { provider: 'main', messages: [{ role: 'user', content: 'parent' }] }
3930
+ * const results = await runner.execute([parent, child]) // declared first, then any spawns
3931
+ * ```
3932
+ */
3933
+ function createAgentRunner(options) {
3934
+ const { registry, allowPartial = false, concurrency, retries, timeout } = options;
3935
+ return createRunner({
3936
+ concurrency,
3937
+ retries,
3938
+ timeout,
3939
+ handler: (controller) => {
3940
+ const children = controller.input.children;
3941
+ if (children !== void 0) for (const child of children) controller.spawn(child);
3942
+ return settleAgentJob(registry.build(controller.input, controller.signal), allowPartial);
3943
+ }
3944
+ });
3945
+ }
3946
+ /**
3947
+ * Create a file — an immutable {@link FileInterface} from its `path` + {@link FileContent}
3948
+ * and optional {@link import('./types.js').FileState}, with `size` / `lines` DERIVED from the
3949
+ * content.
3950
+ *
3951
+ * @remarks
3952
+ * Returns a PLAIN `Object.freeze`d record (NOT a class instance) — the `path` IS its identity
3953
+ * (there is no `id`). Only `path` / `content` are required; `state` defaults to `'created'`
3954
+ * when omitted. `size` (via {@link computeSize}) and `lines` (via {@link countLines}) are
3955
+ * computed from the content here (so they never drift from it). Frozen + plain, so it
3956
+ * `structuredClone`s losslessly and is never mutated after creation.
3957
+ *
3958
+ * @param input - `path` / `content` (required) and an optional `state` (see {@link FileInput})
3959
+ * @returns A frozen {@link FileInterface} record
3960
+ *
3961
+ * @example
3962
+ * ```ts
3963
+ * import { createFile, createTextContent } from '@src/core'
3964
+ *
3965
+ * const file = createFile({ path: 'src/main.ts', content: createTextContent('const x = 1', 'typescript') })
3966
+ * file.lines // 1
3967
+ * ```
3968
+ */
3969
+ function createFile(input) {
3970
+ return Object.freeze({
3971
+ path: input.path,
3972
+ content: input.content,
3973
+ state: input.state ?? "created",
3974
+ size: computeSize(input.content),
3975
+ lines: countLines(input.content)
3976
+ });
3977
+ }
3978
+ /**
3979
+ * Build the TEXT {@link FileContent} arm — the §4.2.3 split constructor for text (a separate
3980
+ * function per arm, not one constructor dispatching on a discriminator parameter).
3981
+ *
3982
+ * @param text - The literal text body
3983
+ * @param language - The fenced-code language the text renders as (e.g. `'typescript'`)
3984
+ * @returns A `{ text; language }` content arm
3985
+ *
3986
+ * @example
3987
+ * ```ts
3988
+ * import { createTextContent } from '@src/core'
3989
+ *
3990
+ * createTextContent('const x = 1', 'typescript') // { text: 'const x = 1', language: 'typescript' }
3991
+ * ```
3992
+ */
3993
+ function createTextContent(text, language) {
3994
+ return {
3995
+ text,
3996
+ language
3997
+ };
3998
+ }
3999
+ /**
4000
+ * Build the BINARY {@link FileContent} arm — the §4.2.3 split constructor for binary (a
4001
+ * separate function per arm, not one constructor dispatching on a discriminator parameter).
4002
+ * An image is just a binary with an image {@link BinaryMIME}.
4003
+ *
4004
+ * @param data - The base64-encoded binary payload
4005
+ * @param mime - The {@link BinaryMIME} that labels the payload
4006
+ * @returns A `{ data; mime }` content arm
4007
+ *
4008
+ * @example
4009
+ * ```ts
4010
+ * import { createBinaryContent } from '@src/core'
4011
+ *
4012
+ * createBinaryContent('<base64>', 'image/png') // { data: '<base64>', mime: 'image/png' }
4013
+ * ```
4014
+ */
4015
+ function createBinaryContent(data, mime) {
4016
+ return {
4017
+ data,
4018
+ mime
4019
+ };
4020
+ }
4021
+ /**
4022
+ * Create a workspace — a mutable, `path`-keyed working set of immutable
4023
+ * {@link FileInterface}s with the in-memory edit surface (read / write / search / replace /
4024
+ * move / remove), observable through its `EmitterInterface` (from `@orkestrel/emitter`).
4025
+ *
4026
+ * @param options - Optional initial {@link import('./types.js').WorkspaceEventMap} listeners (`on`) and the emitter's `error` handler (see {@link WorkspaceOptions})
4027
+ * @returns A working {@link WorkspaceInterface}
4028
+ *
4029
+ * @example
4030
+ * ```ts
4031
+ * import { createWorkspace } from '@src/core'
4032
+ *
4033
+ * const workspace = createWorkspace({ on: { write: (file) => console.log(file.path) } })
4034
+ * workspace.write('src/main.ts', 'const x = 1')
4035
+ * workspace.file('src/main.ts')?.state // 'created'
4036
+ * ```
4037
+ */
4038
+ function createWorkspace(options) {
4039
+ return new Workspace(options);
4040
+ }
4041
+ /**
4042
+ * Create the in-memory workspace store — a {@link WorkspaceStoreInterface} backed by a
4043
+ * process-lifetime `Map` of {@link import('./types.js').WorkspaceSnapshot}s keyed by workspace id,
4044
+ * the DEFAULT backing for the durable {@link WorkspaceManagerInterface.open} /
4045
+ * {@link WorkspaceManagerInterface.save} seam.
4046
+ *
4047
+ * @remarks
4048
+ * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
4049
+ * the structural twin of the analogous `createMemoryWorkflowStore` in `@orkestrel/workflow`.
4050
+ * `get` / `set` / `delete` are async (the same shape a durable backend fits); UNLIKE a session
4051
+ * store there is NO idle-TTL / eviction — a persisted workspace lives until an explicit `delete`.
4052
+ * Its driver-pluggable twin is {@link createDatabaseWorkspaceStore} (the snapshot as one opaque
4053
+ * JSON column over a `databases` table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB
4054
+ * driver, and it swaps in WITHOUT touching the manager or the workspace. Hydration stays a manager
4055
+ * concern: read a snapshot back and rebuild the live workspace through the constructor `seed`.
4056
+ *
4057
+ * @returns A memory-backed {@link WorkspaceStoreInterface}
4058
+ *
4059
+ * @example
4060
+ * ```ts
4061
+ * import { createMemoryWorkspaceStore, createWorkspaceManager } from '@src/core'
4062
+ *
4063
+ * const store = createMemoryWorkspaceStore()
4064
+ * const manager = createWorkspaceManager({ store })
4065
+ * const workspace = manager.add()
4066
+ * workspace.write('notes.txt', 'hello')
4067
+ * await manager.save(workspace.id) // persist the workspace
4068
+ * ```
4069
+ */
4070
+ function createMemoryWorkspaceStore() {
4071
+ return new MemoryWorkspaceStore();
4072
+ }
4073
+ /**
4074
+ * Create a {@link DatabaseWorkspaceStore} over any {@link DriverInterface} — the durable,
4075
+ * driver-pluggable backing for the workspace persistence seam, the opt-in twin of
4076
+ * {@link createMemoryWorkspaceStore}.
4077
+ *
4078
+ * @remarks
4079
+ * Builds a one-table database (`workspaces`, keyed by `id`) over the supplied driver, the snapshot
4080
+ * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
4081
+ * `rawShape` (a JSON blob), exactly as
4082
+ * the analogous `createDatabaseWorkflowStore` in `@orkestrel/workflow` stores its snapshot. The
4083
+ * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
4084
+ * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
4085
+ * {@link import('./helpers.js').isWorkspaceSnapshot}). The `driver` DEFAULTS to
4086
+ * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
4087
+ * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4088
+ * the durability is the driver's job, the store engine is shared. It swaps in behind
4089
+ * {@link WorkspaceStoreInterface} WITHOUT touching the manager or the workspace.
4090
+ *
4091
+ * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
4092
+ * @returns A {@link WorkspaceStoreInterface} over the driver
4093
+ *
4094
+ * @example
4095
+ * ```ts
4096
+ * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspaceManager } from '@src/core'
4097
+ *
4098
+ * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
4099
+ * const manager = createWorkspaceManager({ store })
4100
+ * const workspace = manager.add()
4101
+ * workspace.write('notes.txt', 'hello')
4102
+ * await manager.save(workspace.id) // persist the workspace (one JSON column)
4103
+ * ```
4104
+ */
4105
+ function createDatabaseWorkspaceStore(driver = createMemoryDriver()) {
4106
+ return new DatabaseWorkspaceStore(createDatabase({
4107
+ driver,
4108
+ tables: { workspaces: {
4109
+ id: stringShape(),
4110
+ snapshot: rawShape({})
4111
+ } }
4112
+ }).table("workspaces"));
4113
+ }
4114
+ /**
4115
+ * Create a workspace registry — a {@link WorkspaceManagerInterface} holding
4116
+ * {@link WorkspaceInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
4117
+ * the §9 store over the workspace layer plus the `active` / `switch` seam the context renders.
4118
+ *
4119
+ * @remarks
4120
+ * Starts empty; `add(input?)` mints a {@link WorkspaceInterface} (its `id` from the input or a
4121
+ * random UUID), flowing the manager's default `on` / `error` in unless the input overrides them,
4122
+ * and stores it (an already-present `id` overwrites — last write wins) — and AUTO-ACTIVATES the
4123
+ * FIRST one (a registry with workspaces always has one `active`); a later `add` leaves `active`
4124
+ * unchanged. `switch(id)` re-points `active` (an unknown `id` returns `undefined`, leaving
4125
+ * `active` unchanged — lenient, never throws); `workspace(id)` / `workspaces()` look up;
4126
+ * `remove` (one or a batch, §9.2) reports whether any was removed AND clears `active` if it was
4127
+ * the removed one; `clear` empties it and clears `active`. Event-free (each workspace owns its
4128
+ * own observable `emitter`).
4129
+ *
4130
+ * With the optional `store` ({@link WorkspaceStoreInterface}, e.g. `createMemoryWorkspaceStore` /
4131
+ * `createDatabaseWorkspaceStore`), `open(id)` HYDRATES a workspace on a registry miss (rebuilding it
4132
+ * through the constructor `seed` from the snapshot's `files`, then activating it) and `save(id)`
4133
+ * PERSISTS a registered workspace's `snapshot()`. Both are LENIENT without a store (open resolves
4134
+ * only registered ids, save is a no-op `false`).
4135
+ *
4136
+ * @param options - Optional default `on` / `error` for created workspaces + the durable `store`
4137
+ * (see {@link WorkspaceManagerOptions})
4138
+ * @returns An empty {@link WorkspaceManagerInterface}
4139
+ *
4140
+ * @example
4141
+ * ```ts
4142
+ * import { createWorkspaceManager } from '@src/core'
4143
+ *
4144
+ * const workspaces = createWorkspaceManager()
4145
+ * const scratch = workspaces.add() // auto-activates — workspaces.active === scratch
4146
+ * scratch.write('notes.txt', 'hello')
4147
+ * ```
4148
+ */
4149
+ function createWorkspaceManager(options) {
4150
+ return new WorkspaceManager(options);
4151
+ }
4152
+ /**
4153
+ * Wrap a {@link WorkspaceManagerInterface} as an LLM-callable {@link ToolInterface} — it ADVERTISES
4154
+ * the `operation`-discriminated 13-op union ({@link import('./shapers.js').workspaceToolShape}) as
4155
+ * its `parameters`, and its handler PARSES the model-supplied args against that contract and
4156
+ * DISPATCHES the matched operation against the manager's ACTIVE workspace (the registry ops drive
4157
+ * the manager itself), returning the plain result (throwing a typed {@link WorkspaceError} on
4158
+ * failure).
4159
+ *
4160
+ * @remarks
4161
+ * MANAGER-DRIVEN: every edit / read op (read / list / has / search / replace / write / splice /
4162
+ * prepend / append / move / remove) targets `manager.active`, so the model edits whichever workspace
4163
+ * is active and a host can re-point it ({@link WorkspaceManagerInterface.switch}) between turns —
4164
+ * wire the agent's registry in via `createWorkspaceTool(agent.context.workspaces)`. Two REGISTRY ops
4165
+ * make the model self-sufficient: `workspaces` LISTS the registered workspaces (each
4166
+ * `{ id, files, active }`) so it can discover an id, and `switch` re-points the active workspace by
4167
+ * id (lenient — an unknown id is a no-op reporting `switched: false`, never a throw).
4168
+ *
4169
+ * NO-ACTIVE RULE (the ergonomic seam): a WRITING op (write / splice / prepend / append / move /
4170
+ * remove / replace) run when `manager.active` is `undefined` AUTO-CREATES + activates a default
4171
+ * workspace (`manager.add()`) so the model can just start writing; a pure-READ op (read / list / has
4172
+ * / search) against no active workspace returns the EMPTY result (`undefined` / `[]` / `false`),
4173
+ * never creating one and never throwing.
4174
+ *
4175
+ * A plain {@link ToolManagerInterface}-compatible tool (so `createMCPServer` / `createMCPRoutes`
4176
+ * expose it over MCP for free — nothing MCP is wired here), built as a factory + dispatch closure
4177
+ * exactly like the analogous `createWorkflowTool` in `@orkestrel/workflow` (NOT a class). The
4178
+ * contract is compiled ONCE and its JSON Schema is narrowed to the open
4179
+ * `Readonly<Record<string, unknown>>` a tool advertises via the shared
4180
+ * {@link schemaToParameters} contracts helper, never an assertion
4181
+ * (§14) — a compiled contract schema is always a record, so it passes; the `undefined` fallback only
4182
+ * satisfies the type's optionality.
4183
+ *
4184
+ * The handler conforms to the universal tool-handler contract (AGENTS §14): it `contract.parse`s the
4185
+ * args, THROWS a `TOOL` {@link WorkspaceError} when no operation arm matched (a malformed / unknown
4186
+ * operation), else `switch`es on `op.operation` and RETURNS the plain result — letting a
4187
+ * `WorkspaceError` raised by the live workspace (`MODALITY` / `PATTERN` / `RANGE`) PROPAGATE
4188
+ * unCAUGHT. The {@link import('./tools/ToolManager.js').ToolManager} performs the ONE
4189
+ * canonical wrap (`{ id, name, value }` on a return; `{ id, name, error }` on a throw, ISOLATED so
4190
+ * nothing escapes the run), so the outcome appears EXACTLY ONCE over BOTH the agent loop and MCP (a
4191
+ * throw → MCP `isError: true`). There is no `{ ok, output, duration }` envelope. The range edit is
4192
+ * the FLAT `'splice'` op: its four flat caret integers are reassembled into a {@link import('./types.js').Range}
4193
+ * by {@link rangeOf} and fed to the workspace's ranged `write`.
4194
+ *
4195
+ * @param manager - The workspace registry the tool reads + edits (its ACTIVE workspace; closed over by the handler)
4196
+ * @param options - Optional advertised `name` / `description` overrides (see {@link WorkspaceToolOptions})
4197
+ * @returns A {@link ToolInterface} (named {@link import('./constants.js').WORKSPACE_TOOL_NAME} by
4198
+ * default) whose `parameters` advertise the `operation`-discriminated union schema
4199
+ *
4200
+ * @example
4201
+ * ```ts
4202
+ * import { createWorkspaceManager, createWorkspaceTool, createToolManager } from '@src/core'
4203
+ *
4204
+ * const manager = createWorkspaceManager()
4205
+ * const tool = createWorkspaceTool(manager) // edits the manager's ACTIVE workspace
4206
+ * const tools = createToolManager()
4207
+ * tools.add(tool) // a model can now read + edit the active workspace by calling `workspace`
4208
+ * ```
4209
+ */
4210
+ function createWorkspaceTool(manager, options) {
4211
+ const contract = createContract(workspaceToolShape);
4212
+ const parameters = schemaToParameters(contract.schema);
4213
+ return createTool({
4214
+ name: options?.name ?? "workspace",
4215
+ description: options?.description ?? WORKSPACE_TOOL_DESCRIPTION,
4216
+ parameters,
4217
+ execute: (args) => {
4218
+ const op = contract.parse(args);
4219
+ if (op === void 0) throw new WorkspaceError("TOOL", `unknown or malformed operation`, { args });
4220
+ if (op.operation === "workspaces") {
4221
+ const activeId = manager.active?.id;
4222
+ return manager.workspaces().map((workspace) => ({
4223
+ id: workspace.id,
4224
+ files: workspace.count,
4225
+ active: workspace.id === activeId
4226
+ }));
4227
+ }
4228
+ if (op.operation === "switch") {
4229
+ const switched = manager.switch(op.id);
4230
+ return switched === void 0 ? {
4231
+ id: op.id,
4232
+ switched: false
4233
+ } : {
4234
+ id: switched.id,
4235
+ switched: true,
4236
+ files: switched.count
4237
+ };
4238
+ }
4239
+ const active = manager.active;
4240
+ switch (op.operation) {
4241
+ case "read": return active?.read(op.path);
4242
+ case "list": return (active?.files() ?? []).map((file) => ({
4243
+ path: file.path,
4244
+ state: file.state,
4245
+ size: file.size,
4246
+ lines: file.lines,
4247
+ kind: isText(file.content) ? "text" : "binary"
4248
+ }));
4249
+ case "has": return active?.has(op.path) ?? false;
4250
+ case "search": return active?.search(op.query, {
4251
+ regex: op.regex,
4252
+ exact: op.exact,
4253
+ limit: op.limit
4254
+ }) ?? [];
4255
+ case "replace": return (active ?? manager.add()).replace(op.query, op.replacement, {
4256
+ regex: op.regex,
4257
+ exact: op.exact,
4258
+ limit: op.limit
4259
+ });
4260
+ case "write": {
4261
+ const workspace = active ?? manager.add();
4262
+ workspace.write(op.path, op.content);
4263
+ return {
4264
+ path: op.path,
4265
+ state: workspace.file(op.path)?.state
4266
+ };
4267
+ }
4268
+ case "splice": {
4269
+ const workspace = active ?? manager.add();
4270
+ workspace.write(op.path, op.content, rangeOf(op.fromLine, op.fromColumn, op.toLine, op.toColumn));
4271
+ return {
4272
+ path: op.path,
4273
+ state: workspace.file(op.path)?.state
4274
+ };
4275
+ }
4276
+ case "prepend": {
4277
+ const workspace = active ?? manager.add();
4278
+ workspace.prepend(op.path, op.content);
4279
+ return {
4280
+ path: op.path,
4281
+ state: workspace.file(op.path)?.state
4282
+ };
4283
+ }
4284
+ case "append": {
4285
+ const workspace = active ?? manager.add();
4286
+ workspace.append(op.path, op.content);
4287
+ return {
4288
+ path: op.path,
4289
+ state: workspace.file(op.path)?.state
4290
+ };
4291
+ }
4292
+ case "move": {
4293
+ const workspace = active ?? manager.add();
4294
+ return {
4295
+ from: op.from,
4296
+ to: op.to,
4297
+ moved: workspace.move(op.from, op.to)
4298
+ };
4299
+ }
4300
+ case "remove": {
4301
+ const workspace = active ?? manager.add();
4302
+ return {
4303
+ path: op.path,
4304
+ removed: workspace.remove(op.path)
4305
+ };
4306
+ }
4307
+ }
4308
+ }
4309
+ });
4310
+ }
4311
+ //#endregion
4312
+ export { Agent, AgentContext, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, DatabaseWorkspaceStore, EXTENSION_TO_LANGUAGE, Instruction, InstructionManager, MemoryConversationStore, MemoryWorkspaceStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, Tool, ToolManager, WORKSPACE_SECTION_HEADER, WORKSPACE_TOOL_DESCRIPTION, WORKSPACE_TOOL_EXAMPLE, WORKSPACE_TOOL_NAME, Workspace, WorkspaceError, WorkspaceManager, buildToolResult, clampPosition, clampRange, computeSize, countLines, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createBinaryContent, createConversation, createConversationManager, createDatabaseConversationStore, createDatabaseWorkspaceStore, createFile, createInstruction, createInstructionManager, createMemoryConversationStore, createMemoryWorkspaceStore, createScope, createScopeManager, createTextContent, createThinkSplitter, createTool, createToolManager, createWorkspace, createWorkspaceManager, createWorkspaceTool, decodedSize, escapeRegExp, estimateMessages, estimateTokens, fencedFile, filterAllowList, inferLanguage, isAgentJobError, isBinary, isConversationError, isConversationSnapshot, isFile, isImage, isMessage, isProviderAbortError, isSection, isText, isToolCall, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, settleAgentJob, sliceRange, spliceRange, workspaceToolShape };
4313
+
4314
+ //# sourceMappingURL=index.js.map