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