@orkestrel/agent 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
- import { isArray, isNumber, isRecord, isString, rawShape, stringShape } from "@orkestrel/contract";
1
+ import { attempt, isArray, isBoolean, isFiniteNumber, isObject, isRecord, isString, parseJSONValue, rawShape, stringShape } from "@orkestrel/contract";
2
2
  import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
3
  import { createQueue } from "@orkestrel/queue";
4
4
  import { createDeferred, createRunner } from "@orkestrel/workflow";
5
5
  import { createAbort } from "@orkestrel/abort";
6
6
  import { createTimeout } from "@orkestrel/timeout";
7
7
  import { Emitter } from "@orkestrel/emitter";
8
+ import { ToolManager, isToolCall } from "@orkestrel/tool";
9
+ import { WorkspaceManager, isBinary, isText } from "@orkestrel/workspace";
8
10
  import { createTokenBudget } from "@orkestrel/budget";
9
11
  //#region src/core/constants.ts
10
12
  /**
@@ -90,53 +92,6 @@ var MESSAGE_TOKEN_OVERHEAD = 4;
90
92
  * exact count.
91
93
  */
92
94
  var IMAGE_TOKEN_ESTIMATE = 512;
93
- /**
94
- * The extension→language table {@link import('./helpers.js').inferLanguage} reads to
95
- * map a file path's extension to a fenced-code language tag. Covers the common source /
96
- * markup / data extensions; an unlisted extension falls back to `'text'`.
97
- */
98
- var EXTENSION_TO_LANGUAGE = Object.freeze({
99
- ts: "typescript",
100
- tsx: "typescript",
101
- js: "javascript",
102
- jsx: "javascript",
103
- mjs: "javascript",
104
- cjs: "javascript",
105
- json: "json",
106
- md: "markdown",
107
- html: "html",
108
- htm: "html",
109
- css: "css",
110
- scss: "scss",
111
- sass: "sass",
112
- less: "less",
113
- vue: "vue",
114
- svelte: "svelte",
115
- py: "python",
116
- rb: "ruby",
117
- rs: "rust",
118
- go: "go",
119
- java: "java",
120
- kt: "kotlin",
121
- swift: "swift",
122
- c: "c",
123
- cpp: "cpp",
124
- h: "c",
125
- hpp: "cpp",
126
- cs: "csharp",
127
- php: "php",
128
- sql: "sql",
129
- sh: "bash",
130
- bash: "bash",
131
- zsh: "bash",
132
- ps1: "powershell",
133
- yaml: "yaml",
134
- yml: "yaml",
135
- toml: "toml",
136
- xml: "xml",
137
- svg: "xml",
138
- txt: "text"
139
- });
140
95
  //#endregion
141
96
  //#region src/core/errors.ts
142
97
  /**
@@ -260,45 +215,6 @@ function isConversationError(value) {
260
215
  return value instanceof ConversationError;
261
216
  }
262
217
  /**
263
- * An error thrown by the in-memory {@link import('./workspaces/Workspace.js').Workspace} edit
264
- * surface.
265
- *
266
- * @remarks
267
- * Carries a {@link WorkspaceErrorCode} and an optional `context` bag naming the offending
268
- * path / range. Thrown for a text-only operation aimed at an image file (`MODALITY` — a
269
- * ranged read / write, `prepend`, or `append`), an invalid `search` / `replace` regular
270
- * expression (`PATTERN`), and a structurally invalid ranged-write {@link import('./types.js').Range}
271
- * (`RANGE` — inverted, or a sub-1 line / column).
272
- */
273
- var WorkspaceError = class extends Error {
274
- code;
275
- context;
276
- constructor(code, message, context) {
277
- super(message);
278
- this.name = "WorkspaceError";
279
- this.code = code;
280
- if (context !== void 0) this.context = context;
281
- }
282
- };
283
- /**
284
- * Narrow an unknown caught value to a {@link WorkspaceError}.
285
- *
286
- * @param value - The value to test (typically a `catch` binding)
287
- * @returns `true` when `value` is a {@link WorkspaceError}
288
- *
289
- * @example
290
- * ```ts
291
- * try {
292
- * workspace.prepend('icon.png', '// header')
293
- * } catch (error) {
294
- * if (isWorkspaceError(error) && error.code === 'MODALITY') skipBinary()
295
- * }
296
- * ```
297
- */
298
- function isWorkspaceError(value) {
299
- return value instanceof WorkspaceError;
300
- }
301
- /**
302
218
  * Thrown synchronously by an {@link AgentInterface}'s `stream()` when a concurrent run would
303
219
  * corrupt SHARED per-agent accounting — carries a machine-readable `code`.
304
220
  *
@@ -551,7 +467,7 @@ var Conversation = class {
551
467
  * The registry of {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
552
468
  * — the §9 store over the conversation layer PLUS the `active` / `switch` seam the
553
469
  * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
554
- * {@link import('../workspaces/WorkspaceManager.js').WorkspaceManager}); the observability lives
470
+ * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
555
471
  * on each {@link Conversation}.
556
472
  *
557
473
  * @remarks
@@ -666,28 +582,59 @@ var ConversationManager = class {
666
582
  //#endregion
667
583
  //#region src/core/helpers.ts
668
584
  /**
669
- * Infer a fenced-code language tag from a file path's extension — what a workspace text file
670
- * (or the {@link fencedFile} renderer) renders its content block as.
671
- *
672
- * @remarks
673
- * Reads the extension after the last `.` (case-insensitive) and maps it through a
674
- * fixed extension→language table. An unknown extension, or a path with no extension,
675
- * falls back to `'text'` (a safe, language-agnostic fence). Total — never throws.
676
- *
677
- * @param path - The document's file path (e.g. `'src/main.ts'`)
678
- * @returns The inferred language tag (e.g. `'typescript'`), or `'text'` when unknown
679
- *
680
- * @example
681
- * ```ts
682
- * inferLanguage('src/main.ts') // 'typescript'
683
- * inferLanguage('README.md') // 'markdown'
684
- * inferLanguage('LICENSE') // 'text'
685
- * ```
686
- */
687
- function inferLanguage(path) {
688
- const dot = path.lastIndexOf(".");
689
- if (dot === -1) return "text";
690
- return EXTENSION_TO_LANGUAGE[path.slice(dot + 1).toLowerCase()] ?? "text";
585
+ * Project an unknown value onto the canonical JSON representation of an
586
+ * {@link AgentResult}.
587
+ *
588
+ * @remarks
589
+ * This is a total hostile-boundary projection. Each structural field is captured once
590
+ * through Contract's sanctioned exception boundary, so conforming accessors and inherited
591
+ * properties are supported while a throwing getter or revoked proxy returns `undefined`.
592
+ * Present usage counts must be finite numbers; negative and fractional values are preserved,
593
+ * not normalized. Extra input properties are dropped while a fresh exact plain object is rebuilt
594
+ * and deep-gated through
595
+ * {@link import('@orkestrel/contract').parseJSONValue}.
596
+ *
597
+ * @param value - The unknown value to project
598
+ * @returns A fresh JSON value containing only AgentResult fields, or `undefined` when invalid
599
+ *
600
+ * @example
601
+ * ```ts
602
+ * import { agentResultToJSON } from '@orkestrel/agent'
603
+ *
604
+ * agentResultToJSON({ content: 'done', usage: { prompt: 2, completion: 1, total: 3 }, partial: false })
605
+ * // { content: 'done', usage: { prompt: 2, completion: 1, total: 3 }, partial: false }
606
+ * ```
607
+ */
608
+ function agentResultToJSON(value) {
609
+ const captured = attempt(() => {
610
+ if (!isObject(value)) return void 0;
611
+ const content = Reflect.get(value, "content");
612
+ const thinking = Reflect.get(value, "thinking");
613
+ const usage = Reflect.get(value, "usage");
614
+ const partial = Reflect.get(value, "partial");
615
+ if (!isString(content) || !isBoolean(partial)) return void 0;
616
+ if (thinking !== void 0 && !isString(thinking)) return void 0;
617
+ let projectedUsage;
618
+ if (usage !== void 0) {
619
+ if (!isObject(usage)) return void 0;
620
+ const prompt = Reflect.get(usage, "prompt");
621
+ const completion = Reflect.get(usage, "completion");
622
+ const total = Reflect.get(usage, "total");
623
+ if (!isFiniteNumber(prompt) || !isFiniteNumber(completion) || !isFiniteNumber(total)) return;
624
+ projectedUsage = {
625
+ prompt,
626
+ completion,
627
+ total
628
+ };
629
+ }
630
+ return {
631
+ content,
632
+ ...thinking === void 0 ? {} : { thinking },
633
+ ...projectedUsage === void 0 ? {} : { usage: projectedUsage },
634
+ partial
635
+ };
636
+ });
637
+ return captured.success ? parseJSONValue(captured.value) : void 0;
691
638
  }
692
639
  /**
693
640
  * Filter a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
@@ -850,201 +797,22 @@ function handleAgentRunnerJob(registry, allowPartial, controller) {
850
797
  return settleAgentJob(registry.build(controller.input, controller.signal), allowPartial);
851
798
  }
852
799
  /**
853
- * Whether a {@link FileContent} is the TEXT arm — the narrowing guard for the text-vs-binary
854
- * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
855
- *
856
- * @remarks
857
- * Tests `'text' in content` structurally — there is no `modality` discriminant. A `true`
858
- * narrows `content` to `{ text: string; language: string }`, unlocking `.text` / `.language`.
859
- * Total — never throws.
860
- *
861
- * @param content - The file content to test
862
- * @returns `true` when `content` is the text arm (carries `text` + `language`)
863
- *
864
- * @example
865
- * ```ts
866
- * if (isText(file.content)) file.content.text // the literal text + .language
867
- * ```
868
- */
869
- function isText(content) {
870
- return "text" in content;
871
- }
872
- /**
873
- * Whether a {@link FileContent} is the BINARY arm — the narrowing guard for the text-vs-binary
874
- * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
875
- *
876
- * @remarks
877
- * Tests `'data' in content` structurally — there is no `modality` discriminant. A `true`
878
- * narrows `content` to `{ data: string; mime: BinaryMIME }`, unlocking `.data` / `.mime`. An
879
- * image is a binary whose `mime` starts with `image/` ({@link isImage}). Total — never throws.
880
- *
881
- * @param content - The file content to test
882
- * @returns `true` when `content` is the binary arm (carries base64 `data` + `mime`)
883
- *
884
- * @example
885
- * ```ts
886
- * if (isBinary(file.content)) file.content.data // the base64 payload + .mime
887
- * ```
888
- */
889
- function isBinary(content) {
890
- return "data" in content;
891
- }
892
- /**
893
- * Whether a {@link FileContent} is an IMAGE — a {@link isBinary} arm whose `mime` is an
894
- * `image/*` type (an image is just a binary with an image MIME).
895
- *
896
- * @remarks
897
- * Narrows to the binary arm first, then checks the `image/` MIME prefix — so a future
898
- * non-image binary (a PDF, `'application/pdf'`) is binary but NOT an image. Total — never
899
- * throws.
900
- *
901
- * @param content - The file content to test
902
- * @returns `true` when `content` is a binary arm with an `image/*` MIME
903
- *
904
- * @example
905
- * ```ts
906
- * isImage({ data: '<base64>', mime: 'image/png' }) // true
907
- * isImage({ text: 'hi', language: 'text' }) // false
908
- * ```
909
- */
910
- function isImage(content) {
911
- return isBinary(content) && content.mime.startsWith("image/");
912
- }
913
- /**
914
- * Whether an `unknown` is structurally a {@link FileInterface} record — the per-file step of the
915
- * {@link isWorkspaceSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage read
916
- * via a guard, never an `as`).
917
- *
918
- * @remarks
919
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the file's SHAPE:
920
- * a record with a `string` `path`, a `string` `state`, `number` `size` / `lines`, and a `content`
921
- * that is EITHER the TEXT arm (`{ text: string; language: string }`) OR the BINARY arm
922
- * (`{ data: string; mime: string }`) — the tagless {@link FileContent} union, told apart
923
- * structurally exactly as {@link isText} / {@link isBinary} do (no `modality` discriminant). Enough
924
- * to safely impose the {@link FileInterface} type at a storage boundary WITHOUT a cast; the `mime`
925
- * is left as a broad `string` here (an open {@link BinaryMIME}, so any storage-read MIME string is
926
- * accepted defensively rather than rejected against the current literal set).
927
- *
928
- * @param value - The value to test (one element of a snapshot's opaque `files` array)
929
- * @returns `true` when `value` has the structural shape of a {@link FileInterface}
930
- *
931
- * @example
932
- * ```ts
933
- * isFile({ path: 'a.ts', content: { text: 'x', language: 'typescript' }, state: 'created', size: 1, lines: 1 }) // true
934
- * isFile({ path: 'a.png', content: { data: 'AAAA', mime: 'image/png' }, state: 'created', size: 3, lines: 0 }) // true
935
- * isFile({ path: 'a.ts' }) // false (missing content / state / size / lines)
936
- * ```
937
- */
938
- function isFile(value) {
939
- if (!isRecord(value)) return false;
940
- if (!isString(value.path) || !isString(value.state)) return false;
941
- if (!isNumber(value.size) || !isNumber(value.lines)) return false;
942
- if (!isRecord(value.content)) return false;
943
- const text = isString(value.content.text) && isString(value.content.language);
944
- const binary = isString(value.content.data) && isString(value.content.mime);
945
- return text || binary;
946
- }
947
- /**
948
- * Narrow an `unknown` to a {@link WorkspaceSnapshot} — the AGENTS §14 boundary guard for an
949
- * UNTRUSTED snapshot read (a storage row a
950
- * {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} reads back
951
- * from its opaque JSON column, a snapshot loaded from disk).
952
- *
953
- * @remarks
954
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
955
- * SHAPE: a `string` `id` and a `files` array EVERY element of which is a valid {@link FileInterface}
956
- * record ({@link isFile}) — enough to safely impose the {@link WorkspaceSnapshot} type at a storage
957
- * boundary WITHOUT a cast. The structural twin of
958
- * the analogous `isWorkflowSnapshot` in `@orkestrel/workflow`. A malformed blob (a non-record, a
959
- * missing / non-string `id`, a non-array `files`, or any malformed file element) resolves `false`,
960
- * so a {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} read
961
- * yields `undefined` rather than a broken workspace.
962
- *
963
- * @param value - The value to test (an opaque storage read)
964
- * @returns `true` when `value` has the structural shape of a {@link WorkspaceSnapshot}
965
- *
966
- * @example
967
- * ```ts
968
- * isWorkspaceSnapshot({ id: 'w1', files: [] }) // true
969
- * isWorkspaceSnapshot({ id: 'w1', files: 'nope' }) // false
970
- * isWorkspaceSnapshot({ files: [] }) // false (missing id)
971
- * ```
972
- */
973
- function isWorkspaceSnapshot(value) {
974
- return isRecord(value) && isString(value.id) && isArray(value.files) && value.files.every(isFile);
975
- }
976
- /**
977
- * Whether an `unknown` is structurally a {@link ToolCall} record — the per-call step of the
978
- * {@link isMessage} read-boundary narrow (AGENTS §14: narrow an untrusted storage read via a
979
- * guard, never an `as`).
980
- *
981
- * @remarks
982
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the call's
983
- * SHAPE: a record with a `string` `id`, a `string` `name`, and a record `arguments` — the real
984
- * {@link ToolCall} shape a restored assistant turn replays to a provider. This is the ASI06
985
- * fail-CLOSED element check: a tampered persisted call (`null`, a bare string, a non-record
986
- * `arguments`) fails the guard, so {@link isMessage} — and through it
987
- * {@link isConversationSnapshot} — rejects the whole snapshot and a poisoned store row reads
988
- * back as ABSENT instead of replaying a malformed call into a chat template.
989
- *
990
- * @param value - The value to test (one element of an assistant message's `calls`)
991
- * @returns `true` when `value` has the structural shape of a {@link ToolCall}
992
- *
993
- * @example
994
- * ```ts
995
- * isToolCall({ id: 'c1', name: 'search', arguments: { q: 'acme' } }) // true
996
- * isToolCall({ id: 'c1', name: 'search' }) // false (missing arguments)
997
- * isToolCall({ id: 'c1', name: 123, arguments: {} }) // false (non-string name)
998
- * ```
999
- */
1000
- function isToolCall(value) {
1001
- if (!isRecord(value)) return false;
1002
- return isString(value.id) && isString(value.name) && isRecord(value.arguments);
1003
- }
1004
- /**
1005
- * Project a {@link ToolResult} into the MCP `CallToolResult` shape — a top-level `error`
1006
- * maps to a single `isError: true` text block (the failure reason), otherwise the `value`
1007
- * is JSON-stringified into a single text block with no `isError`.
1008
- *
1009
- * @param result - The {@link ToolResult} to project (as returned by {@link import('./tools/ToolManager.js').ToolManager.execute})
1010
- * @returns The equivalent {@link ToolCallResult}
1011
- *
1012
- * @example
1013
- * ```ts
1014
- * buildToolResult({ id: 'c1', name: 'search', error: 'max depth' })
1015
- * // { content: [{ type: 'text', text: 'max depth' }], isError: true }
1016
- * buildToolResult({ id: 'c1', name: 'search', value: { count: 1 } })
1017
- * // { content: [{ type: 'text', text: '{"count":1}' }] }
1018
- * ```
1019
- */
1020
- function buildToolResult(result) {
1021
- if (result.error !== void 0) return {
1022
- content: [{
1023
- type: "text",
1024
- text: result.error
1025
- }],
1026
- isError: true
1027
- };
1028
- return { content: [{
1029
- type: "text",
1030
- text: JSON.stringify(result.value)
1031
- }] };
1032
- }
1033
- /**
1034
800
  * Whether an `unknown` is structurally a {@link MessageInterface} record — the per-message step of
1035
801
  * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
1036
- * read via a guard, never an `as`). The conversation analogue of {@link isFile}.
802
+ * read via a guard, never an `as`). The conversation analogue of
803
+ * {@link import('@orkestrel/workspace').isFile}.
1037
804
  *
1038
805
  * @remarks
1039
806
  * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
1040
807
  * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
1041
- * — a `calls` array EVERY element of which is a valid {@link ToolCall} ({@link isToolCall} — the
808
+ * — a `calls` array EVERY element of which is a valid
809
+ * {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
1042
810
  * ASI06 fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
1043
811
  * reads back as absent rather than replaying a malformed call) and an `images` that is an array
1044
812
  * (an absent optional passes). The `role` is left as a broad `string` here (an open
1045
813
  * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
1046
- * defensively rather than rejected against the current literal set) — exactly as {@link isFile}
1047
- * accepts a broad `state` / `mime`. Enough to safely impose the {@link MessageInterface} type at
814
+ * defensively rather than rejected against the current literal set). Enough to safely impose the
815
+ * {@link MessageInterface} type at
1048
816
  * a storage boundary WITHOUT a cast.
1049
817
  *
1050
818
  * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
@@ -1095,7 +863,7 @@ function isSection(value) {
1095
863
  * UNTRUSTED snapshot read (a storage row a
1096
864
  * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
1097
865
  * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
1098
- * {@link isWorkspaceSnapshot}.
866
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
1099
867
  *
1100
868
  * @remarks
1101
869
  * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
@@ -1104,7 +872,7 @@ function isSection(value) {
1104
872
  * {@link SectionInterface} ({@link isSection}), and a `messages` array EVERY element of which is a
1105
873
  * valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
1106
874
  * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
1107
- * {@link isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
875
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
1108
876
  * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
1109
877
  * element) resolves `false`, so a
1110
878
  * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
@@ -1129,273 +897,6 @@ function isConversationSnapshot(value) {
1129
897
  return isArray(value.messages) && value.messages.every(isMessage);
1130
898
  }
1131
899
  /**
1132
- * Compute the byte size of a {@link FileContent} — the `size` a {@link FileInterface} derives
1133
- * once when built.
1134
- *
1135
- * @remarks
1136
- * Dispatches on {@link isText}: a text arm is its UTF-8 byte length (via `new
1137
- * TextEncoder().encode(text).length` — the standard Web/Node API, so a multi-byte
1138
- * character like `'é'` or `'😀'` counts as its real encoded bytes, NOT its character
1139
- * count); a binary arm is the decoded payload byte length of its base64 `data` (via
1140
- * {@link decodedSize}, computed arithmetically — no `atob` / `Buffer`). Total — never throws.
1141
- *
1142
- * @param content - The file content to size
1143
- * @returns The byte size (UTF-8 bytes for text; decoded payload bytes for binary)
1144
- *
1145
- * @example
1146
- * ```ts
1147
- * computeSize({ text: 'café', language: 'text' }) // 5 (é is two bytes)
1148
- * computeSize({ data: 'AAAA', mime: 'image/png' }) // 3
1149
- * ```
1150
- */
1151
- function computeSize(content) {
1152
- if (isText(content)) return new TextEncoder().encode(content.text).length;
1153
- return decodedSize(content.data);
1154
- }
1155
- /**
1156
- * Count the lines of a {@link FileContent} — the `lines` a {@link FileInterface} derives once
1157
- * when built.
1158
- *
1159
- * @remarks
1160
- * A text arm's line count is `0` for the empty string, otherwise one more than the
1161
- * number of newline (`\n`) separators it contains — so `'a'` is one line, `'a\nb'` is two,
1162
- * and a trailing newline `'a\n'` counts the empty final line as two. A binary arm has no
1163
- * lines and returns `0`. Total — never throws.
1164
- *
1165
- * @param content - The file content to count lines for
1166
- * @returns The line count (text line count; `0` for a binary arm or empty text)
1167
- *
1168
- * @example
1169
- * ```ts
1170
- * countLines({ text: '', language: 'text' }) // 0
1171
- * countLines({ text: 'a\nb\nc', language: 'text' }) // 3
1172
- * countLines({ data: 'AAAA', mime: 'image/png' }) // 0
1173
- * ```
1174
- */
1175
- function countLines(content) {
1176
- if (!isText(content)) return 0;
1177
- if (content.text.length === 0) return 0;
1178
- let count = 1;
1179
- for (const character of content.text) if (character === "\n") count += 1;
1180
- return count;
1181
- }
1182
- /**
1183
- * Compute the decoded byte length of a base64 string ARITHMETICALLY — the image-payload
1184
- * sizing primitive {@link computeSize} uses, with no `atob` / `Buffer` decode.
1185
- *
1186
- * @remarks
1187
- * A base64 string encodes each group of 3 input bytes as 4 characters, so a well-formed
1188
- * string's length is a multiple of 4 and its decoded length is `(length / 4) * 3` MINUS
1189
- * the trailing `=` padding (one `=` ⇒ the last group held 2 bytes, two `==` ⇒ 1 byte).
1190
- * The padding is counted from the trailing `=` characters, so both the `=` and `==` cases
1191
- * are handled. The empty string decodes to `0`. Total — never throws (a malformed,
1192
- * non-multiple-of-4 length still yields a defined non-negative estimate via `floor`).
1193
- *
1194
- * @param base64 - The base64-encoded string (e.g. an image's `data`)
1195
- * @returns The decoded payload's byte length
1196
- *
1197
- * @example
1198
- * ```ts
1199
- * decodedSize('') // 0
1200
- * decodedSize('AAAA') // 3 (no padding)
1201
- * decodedSize('AAA=') // 2 (one '=')
1202
- * decodedSize('AA==') // 1 (two '=')
1203
- * ```
1204
- */
1205
- function decodedSize(base64) {
1206
- if (base64.length === 0) return 0;
1207
- let padding = 0;
1208
- if (base64.endsWith("==")) padding = 2;
1209
- else if (base64.endsWith("=")) padding = 1;
1210
- return Math.floor(base64.length / 4) * 3 - padding;
1211
- }
1212
- /**
1213
- * Whether a {@link Range} is STRUCTURALLY valid — the predicate a ranged `write` checks
1214
- * before applying (a `false` here is the `RANGE` throw).
1215
- *
1216
- * @remarks
1217
- * Structural validity is independent of any content: every component must be `>= 1`
1218
- * (1-based lines and columns), and `start` must not come after `end` (an inverted range
1219
- * is invalid). A range that is structurally valid but reaches PAST the end of a specific
1220
- * text is still valid — it is {@link clampRange}d to the bounds when applied, not rejected.
1221
- * Total — never throws.
1222
- *
1223
- * @param range - The range to validate
1224
- * @returns `true` when every component is `>= 1` and `start` is at or before `end`
1225
- *
1226
- * @example
1227
- * ```ts
1228
- * isValidRange({ start: { line: 1, column: 1 }, end: { line: 2, column: 1 } }) // true
1229
- * isValidRange({ start: { line: 2, column: 1 }, end: { line: 1, column: 1 } }) // false (inverted)
1230
- * isValidRange({ start: { line: 0, column: 1 }, end: { line: 1, column: 1 } }) // false (sub-1 line)
1231
- * ```
1232
- */
1233
- function isValidRange(range) {
1234
- if (range.start.line < 1 || range.start.column < 1) return false;
1235
- if (range.end.line < 1 || range.end.column < 1) return false;
1236
- if (range.start.line > range.end.line) return false;
1237
- return !(range.start.line === range.end.line && range.start.column > range.end.column);
1238
- }
1239
- /**
1240
- * Clamp a 1-based {@link Position} to the bounds of `text` — every component pinned into a
1241
- * caret that actually exists in the content.
1242
- *
1243
- * @remarks
1244
- * `line` is pinned to `[1, lineCount]` and `column` to `[1, lineLength + 1]` of the
1245
- * resolved line (column `lineLength + 1` is the caret just past the line's last
1246
- * character). So a position beyond the end of the text resolves to the end rather than
1247
- * overflowing. Total — never throws.
1248
- *
1249
- * @param text - The text the position addresses
1250
- * @param position - The 1-based position to clamp
1251
- * @returns The clamped {@link Position}
1252
- *
1253
- * @example
1254
- * ```ts
1255
- * clampPosition('ab\ncd', { line: 9, column: 9 }) // { line: 2, column: 3 } (end of 'cd')
1256
- * ```
1257
- */
1258
- function clampPosition(text, position) {
1259
- const lines = text.split("\n");
1260
- const line = Math.max(1, Math.min(position.line, lines.length));
1261
- const lineText = lines[line - 1] ?? "";
1262
- return {
1263
- line,
1264
- column: Math.max(1, Math.min(position.column, lineText.length + 1))
1265
- };
1266
- }
1267
- /**
1268
- * Clamp both ends of a {@link Range} to the bounds of `text` — the actual span a ranged
1269
- * read / write applies (and the `range` a {@link import('./types.js').ReadResult} reports).
1270
- *
1271
- * @remarks
1272
- * Clamps `start` and `end` independently via {@link clampPosition}, so a range reaching
1273
- * past the end of the content is trimmed to the content's end. Total — never throws.
1274
- *
1275
- * @param text - The text the range addresses
1276
- * @param range - The 1-based range to clamp
1277
- * @returns The clamped {@link Range}
1278
- *
1279
- * @example
1280
- * ```ts
1281
- * clampRange('ab\ncd', { start: { line: 1, column: 1 }, end: { line: 9, column: 9 } })
1282
- * // { start: { line: 1, column: 1 }, end: { line: 2, column: 3 } }
1283
- * ```
1284
- */
1285
- function clampRange(text, range) {
1286
- return {
1287
- start: clampPosition(text, range.start),
1288
- end: clampPosition(text, range.end)
1289
- };
1290
- }
1291
- /**
1292
- * Convert a 1-based {@link Position} to a 0-based string offset into `text` — the indexing
1293
- * primitive {@link sliceRange} / {@link spliceRange} use, clamped to `text.length`.
1294
- *
1295
- * @remarks
1296
- * Sums each preceding line's length plus its `\n` separator, then adds the `column - 1`
1297
- * within the target line, capped at `text.length`. Total — never throws (an out-of-bounds
1298
- * position yields a defined in-range offset).
1299
- *
1300
- * @param text - The text to index into
1301
- * @param position - The 1-based position to resolve
1302
- * @returns The 0-based offset (in `[0, text.length]`)
1303
- *
1304
- * @example
1305
- * ```ts
1306
- * offsetAt('ab\ncd', { line: 2, column: 1 }) // 3 (just after 'ab\n')
1307
- * ```
1308
- */
1309
- function offsetAt(text, position) {
1310
- const lines = text.split("\n");
1311
- let offset = 0;
1312
- for (let index = 0; index < position.line - 1 && index < lines.length; index += 1) offset += (lines[index]?.length ?? 0) + 1;
1313
- offset += position.column - 1;
1314
- return Math.min(offset, text.length);
1315
- }
1316
- /**
1317
- * Slice the substring of `text` spanned by a {@link Range} (start INCLUSIVE, end
1318
- * EXCLUSIVE), clamping the range to the text's bounds first — the read half of the ranged
1319
- * edit surface.
1320
- *
1321
- * @remarks
1322
- * Clamps via {@link clampRange}, resolves each end to an offset via {@link offsetAt}, and
1323
- * returns `text.slice(startOffset, endOffset)`. Total — never throws.
1324
- *
1325
- * @param text - The text to slice
1326
- * @param range - The 1-based range to extract
1327
- * @returns The spanned substring (empty when the clamped span is empty)
1328
- *
1329
- * @example
1330
- * ```ts
1331
- * sliceRange('hello\nworld', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }) // 'hello'
1332
- * ```
1333
- */
1334
- function sliceRange(text, range) {
1335
- const clamped = clampRange(text, range);
1336
- return text.slice(offsetAt(text, clamped.start), offsetAt(text, clamped.end));
1337
- }
1338
- /**
1339
- * Replace the span of `text` covered by a {@link Range} with `replacement` (start
1340
- * INCLUSIVE, end EXCLUSIVE), clamping the range to the text's bounds first — the write
1341
- * half of the ranged edit surface.
1342
- *
1343
- * @remarks
1344
- * Clamps via {@link clampRange}, then stitches `before + replacement + after` around the
1345
- * resolved offsets. An empty span (`start === end`) becomes a pure insertion. Total —
1346
- * never throws.
1347
- *
1348
- * @param text - The original text
1349
- * @param range - The 1-based range to overwrite
1350
- * @param replacement - The text to splice in place of the spanned range
1351
- * @returns The text with the spanned range replaced by `replacement`
1352
- *
1353
- * @example
1354
- * ```ts
1355
- * spliceRange('hello', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }, 'bye') // 'bye'
1356
- * ```
1357
- */
1358
- function spliceRange(text, range, replacement) {
1359
- const clamped = clampRange(text, range);
1360
- const start = offsetAt(text, clamped.start);
1361
- const end = offsetAt(text, clamped.end);
1362
- return text.slice(0, start) + replacement + text.slice(end);
1363
- }
1364
- /**
1365
- * Assemble a 1-based nested {@link Range} from four FLAT caret integers — the bridge a
1366
- * workspace-editing tool (see `@orkestrel/tool`) uses from its small-model FLAT surface back to the
1367
- * nested {@link Range} the {@link import('./workspaces/Workspace.js').Workspace} edit surface
1368
- * speaks. Published as a public helper for that consumer.
1369
- *
1370
- * @remarks
1371
- * Pairs `(fromLine, fromColumn)` into `start` and `(toLine, toColumn)` into `end` verbatim — a pure
1372
- * structural lift, no validation (a structurally invalid range is rejected downstream by the ranged
1373
- * `write` it feeds, via {@link isValidRange}). Total — never throws. Zero-Node.
1374
- *
1375
- * @param fromLine - The 1-based start line
1376
- * @param fromColumn - The 1-based start column
1377
- * @param toLine - The 1-based end line
1378
- * @param toColumn - The 1-based end column
1379
- * @returns The `{ start, end }` {@link Range}
1380
- *
1381
- * @example
1382
- * ```ts
1383
- * rangeOf(1, 11, 1, 12) // { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }
1384
- * ```
1385
- */
1386
- function rangeOf(fromLine, fromColumn, toLine, toColumn) {
1387
- return {
1388
- start: {
1389
- line: fromLine,
1390
- column: fromColumn
1391
- },
1392
- end: {
1393
- line: toLine,
1394
- column: toColumn
1395
- }
1396
- };
1397
- }
1398
- /**
1399
900
  * Render a path-addressed text body as a fenced reference block — the framing an
1400
901
  * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
1401
902
  * active workspace is the SOLE document/image context).
@@ -1405,7 +906,7 @@ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
1405
906
  * fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
1406
907
  * total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
1407
908
  * frames an active workspace's text files with it (each carries its own `language` on its
1408
- * {@link FileContent} text arm).
909
+ * {@link import('@orkestrel/workspace').FileContent} text arm).
1409
910
  *
1410
911
  * @param path - The file path shown on the `File:` label line
1411
912
  * @param language - The fenced-code language tag (e.g. `'typescript'`)
@@ -1424,29 +925,6 @@ function fencedFile(path, language, content) {
1424
925
  return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
1425
926
  }
1426
927
  /**
1427
- * Escape a string's regex-special characters so it matches LITERALLY inside a `RegExp` — the
1428
- * primitive a {@link import('./workspaces/Workspace.js').Workspace} search builds a literal-text
1429
- * search pattern through (as opposed to a caller-supplied regex pattern).
1430
- *
1431
- * @remarks
1432
- * Prefixes every character in the class `. * + ? ^ $ { } ( ) | [ ] \` with a backslash, so the
1433
- * escaped string, when compiled into a `RegExp`, matches only its own literal characters — no
1434
- * character acts as a quantifier, anchor, group, or class. Pure string assembly, total — never
1435
- * throws.
1436
- *
1437
- * @param value - The text to escape for literal use inside a `RegExp` pattern
1438
- * @returns `value` with every regex-special character backslash-escaped
1439
- *
1440
- * @example
1441
- * ```ts
1442
- * escapeRegExp('a.b*c') // 'a\\.b\\*c'
1443
- * new RegExp(escapeRegExp('a.b*c')).test('a.b*c') // true
1444
- * ```
1445
- */
1446
- function escapeRegExp(value) {
1447
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1448
- }
1449
- /**
1450
928
  * Sanitize one reported token count into a safe non-negative integer.
1451
929
  *
1452
930
  * @param value - The token count to sanitize
@@ -1488,7 +966,7 @@ function sanitizeUsage(usage) {
1488
966
  * at construction.
1489
967
  *
1490
968
  * @remarks
1491
- * A thin immutable value object (mirroring {@link import('../tools/Tool.js').Tool}): the
969
+ * A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
1492
970
  * constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
1493
971
  * `content`, resolves `priority` to the input's value or `0`, and carries the input's
1494
972
  * per-item `format` override ONLY when supplied (assigned just when present, mirroring a
@@ -1566,552 +1044,52 @@ var InstructionManager = class {
1566
1044
  });
1567
1045
  this.#format = options?.format;
1568
1046
  }
1569
- get emitter() {
1570
- return this.#emitter;
1571
- }
1572
- get count() {
1573
- return this.#instructions.size;
1574
- }
1575
- get description() {
1576
- return this.#format?.open ?? "## Instructions";
1577
- }
1578
- get framing() {
1579
- return this.#format;
1580
- }
1581
- add(input) {
1582
- if (isArray(input)) return input.map((one) => this.#create(one));
1583
- return this.#create(input);
1584
- }
1585
- instruction(name) {
1586
- return this.#instructions.get(name);
1587
- }
1588
- instructions() {
1589
- return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
1590
- }
1591
- format(instruction) {
1592
- return this.#format?.render?.(instruction) ?? instruction.content;
1593
- }
1594
- remove(names) {
1595
- if (isArray(names)) {
1596
- let removed = false;
1597
- for (const name of names) if (this.#delete(name)) removed = true;
1598
- return removed;
1599
- }
1600
- return this.#delete(names);
1601
- }
1602
- clear() {
1603
- this.#instructions.clear();
1604
- this.#emitter.emit("clear");
1605
- }
1606
- #create(input) {
1607
- const instruction = new Instruction(input);
1608
- this.#instructions.set(instruction.name, instruction);
1609
- this.#emitter.emit("add", instruction);
1610
- return instruction;
1611
- }
1612
- #delete(name) {
1613
- const removed = this.#instructions.delete(name);
1614
- if (removed) this.#emitter.emit("remove", name);
1615
- return removed;
1616
- }
1617
- };
1618
- //#endregion
1619
- //#region src/core/tools/ToolManager.ts
1620
- /**
1621
- * The tool registry the agent loop dispatches model tool-calls through — resolves
1622
- * names, lists {@link ToolDefinition}s for the provider, and executes calls with
1623
- * per-call error isolation.
1624
- *
1625
- * @remarks
1626
- * - **Registry.** Tools live in an insertion-ordered `Map` keyed by `tool.name`;
1627
- * `add` takes one or a batch (§9.2) and a re-`add` of the same name OVERWRITES it
1628
- * (last write wins). `count` is the map size, `tool(name)` looks one up, `tools()`
1629
- * lists them in insertion order, and `definitions()` maps each to a plain
1630
- * {@link ToolDefinition} (`name` / `description?` / `parameters?`, the `execute`
1631
- * handler stripped) — exactly what a provider advertises to the model, with
1632
- * `tool.summary ?? tool.description` advertised as `description` (a lean `summary`
1633
- * stands in for the full text when set).
1634
- * - **Per-call error isolation (the load-bearing part).** `execute` resolves a
1635
- * {@link ToolCall}'s tool by name and ALWAYS resolves a {@link ToolResult}: an
1636
- * unknown name → `{ id, name, error: 'tool not found: <name>' }`; a successful run →
1637
- * `{ id, name, value }`; a handler throw is CAUGHT into `{ id, name, error }` (an
1638
- * `Error`'s message, else the stringified throw). A tool throw never escapes — it
1639
- * becomes a result the model can react to.
1640
- * - **Batch never fails as a whole.** `execute(calls)` runs every call via
1641
- * `Promise.all(calls.map(...))` and resolves the results correlated by `id` in the
1642
- * input order — a mix of success, throw, and not-found all resolve; one bad call
1643
- * does not reject the batch.
1644
- * - **Event-free.** A purely functional registry — no Emitter, no events.
1645
- *
1646
- * @example
1647
- * ```ts
1648
- * const manager = new ToolManager()
1649
- * manager.add(new Tool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
1650
- * const result = await manager.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
1651
- * result.value // 3
1652
- * ```
1653
- */
1654
- var ToolManager = class {
1655
- #tools = /* @__PURE__ */ new Map();
1656
- get count() {
1657
- return this.#tools.size;
1658
- }
1659
- add(tools) {
1660
- if (isArray(tools)) {
1661
- for (const tool of tools) this.#tools.set(tool.name, tool);
1662
- return;
1663
- }
1664
- this.#tools.set(tools.name, tools);
1665
- }
1666
- tool(name) {
1667
- return this.#tools.get(name);
1668
- }
1669
- tools() {
1670
- return [...this.#tools.values()];
1671
- }
1672
- definitions() {
1673
- return [...this.#tools.values()].map((tool) => this.#definition(tool));
1674
- }
1675
- execute(call) {
1676
- if (isArray(call)) return Promise.all(call.map((one) => this.#run(one)));
1677
- return this.#run(call);
1678
- }
1679
- remove(names) {
1680
- if (isArray(names)) {
1681
- let removed = false;
1682
- for (const name of names) if (this.#tools.delete(name)) removed = true;
1683
- return removed;
1684
- }
1685
- return this.#tools.delete(names);
1686
- }
1687
- clear() {
1688
- this.#tools.clear();
1689
- }
1690
- async #run(call) {
1691
- const tool = this.#tools.get(call.name);
1692
- if (tool === void 0) return {
1693
- id: call.id,
1694
- name: call.name,
1695
- error: `tool not found: ${call.name}`
1696
- };
1697
- try {
1698
- const value = await tool.execute(call.arguments);
1699
- return {
1700
- id: call.id,
1701
- name: call.name,
1702
- value
1703
- };
1704
- } catch (error) {
1705
- return {
1706
- id: call.id,
1707
- name: call.name,
1708
- error: error instanceof Error ? error.message : String(error)
1709
- };
1710
- }
1711
- }
1712
- #definition(tool) {
1713
- const definition = { name: tool.name };
1714
- const description = tool.summary ?? tool.description;
1715
- if (description !== void 0) definition.description = description;
1716
- if (tool.parameters !== void 0) definition.parameters = tool.parameters;
1717
- return definition;
1718
- }
1719
- };
1720
- //#endregion
1721
- //#region src/core/workspaces/Workspace.ts
1722
- /**
1723
- * A mutable, `path`-keyed working set of immutable {@link FileInterface}s — the
1724
- * in-memory editing surface over the file primitive.
1725
- *
1726
- * @remarks
1727
- * - **Registry.** Files live in an insertion-ordered `Map` keyed by `path`; `count` is the
1728
- * map size, `file(path)` looks one up, and `files()` lists them in insertion order.
1729
- * - **Write replaces the File (§11 immutability).** Every edit MINTS a replacement
1730
- * {@link FileInterface} (via {@link import('../factories.js').createFile}) rather than
1731
- * mutating in place, transitioning `state` to `'created'` for a brand-new path or
1732
- * `'modified'` for an existing one. A whole-file string write preserves an existing text
1733
- * file's `language`, else infers it from the `path` (`inferLanguage`); writing a string
1734
- * onto an existing BINARY path replaces it with a text file (a deliberate retype). A
1735
- * ranged `write` splices a `Range` of an existing text file.
1736
- * - **Modality rules.** Text-only ops on a binary file are rejected: a ranged `read` /
1737
- * `write`, `prepend`, and `append` throw `MODALITY`; a plain `read(path)` of a binary
1738
- * file returns `undefined`; `search` / `replace` skip binary files (0 matches).
1739
- * - **In-memory removal.** `remove(path)` / `remove(paths)` drop files outright (no
1740
- * tombstone — the `'loaded'` / `'deleted'` states are reserved for a future FileStore);
1741
- * `remove()` and `clear()` both empty the registry and emit the single `clear` signal.
1742
- * The disk/sync lifecycle (`load` / `revert` / `accept` / `purge` / `dirty`) is NOT part
1743
- * of this surface.
1744
- * - **Observable (§13).** The owned {@link emitter} ({@link WorkspaceEventMap}) carries
1745
- * `write` (the resulting file) / `remove` (the path) / `move` (`{ from, to }`) / `clear`.
1746
- * Every event is emitted directly, strictly AFTER the map mutation completes; the
1747
- * emitter isolates a listener throw and routes it to its `error` handler, so a buggy
1748
- * observer can never corrupt a mutation.
1749
- *
1750
- * @example
1751
- * ```ts
1752
- * const workspace = new Workspace()
1753
- * workspace.write('src/main.ts', 'const x = 1')
1754
- * workspace.file('src/main.ts')?.state // 'created'
1755
- * workspace.append('src/main.ts', '\nconst y = 2')
1756
- * workspace.file('src/main.ts')?.state // 'modified'
1757
- * workspace.read('src/main.ts') // 'const x = 1\nconst y = 2'
1758
- * ```
1759
- */
1760
- var Workspace = class {
1761
- #id;
1762
- #files = /* @__PURE__ */ new Map();
1763
- #emitter;
1764
- constructor(options, seed) {
1765
- this.#id = options?.id ?? crypto.randomUUID();
1766
- this.#emitter = new Emitter({
1767
- ...options?.on === void 0 ? {} : { on: options.on },
1768
- ...options?.error === void 0 ? {} : { error: options.error }
1769
- });
1770
- if (seed) for (const [path, file] of seed) this.#files.set(path, file);
1771
- }
1772
- get id() {
1773
- return this.#id;
1774
- }
1775
- get emitter() {
1776
- return this.#emitter;
1777
- }
1778
- get count() {
1779
- return this.#files.size;
1780
- }
1781
- file(path) {
1782
- return this.#files.get(path);
1783
- }
1784
- files() {
1785
- return [...this.#files.values()];
1786
- }
1787
- read(path, range) {
1788
- if (isArray(path)) {
1789
- const result = {};
1790
- for (const one of path) {
1791
- const file = this.#files.get(one);
1792
- if (file && isText(file.content)) result[one] = file.content.text;
1793
- }
1794
- return result;
1795
- }
1796
- const file = this.#files.get(path);
1797
- if (!file) return void 0;
1798
- if (!range) return isText(file.content) ? file.content.text : void 0;
1799
- if (!isText(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
1800
- return {
1801
- content: sliceRange(file.content.text, range),
1802
- range: clampRange(file.content.text, range)
1803
- };
1804
- }
1805
- has(path) {
1806
- if (isArray(path)) return path.some((one) => this.#files.has(one));
1807
- return this.#files.has(path);
1808
- }
1809
- search(query, options) {
1810
- const pattern = this.#pattern(query, options);
1811
- const limit = options?.limit;
1812
- const matches = [];
1813
- for (const file of this.#files.values()) {
1814
- if (limit !== void 0 && matches.length >= limit) break;
1815
- if (!isText(file.content)) continue;
1816
- const lines = file.content.text.split("\n");
1817
- for (let index = 0; index < lines.length; index += 1) {
1818
- if (limit !== void 0 && matches.length >= limit) break;
1819
- const lineText = lines[index] ?? "";
1820
- pattern.lastIndex = 0;
1821
- let hit = pattern.exec(lineText);
1822
- while (hit !== null) {
1823
- if (limit !== void 0 && matches.length >= limit) break;
1824
- matches.push({
1825
- path: file.path,
1826
- line: index + 1,
1827
- column: hit.index + 1,
1828
- length: hit[0].length,
1829
- content: lineText
1830
- });
1831
- if (hit[0].length === 0) pattern.lastIndex += 1;
1832
- hit = pattern.exec(lineText);
1833
- }
1834
- }
1835
- }
1836
- return matches;
1837
- }
1838
- replace(query, replacement, options) {
1839
- const pattern = this.#pattern(query, options);
1840
- const limit = options?.limit;
1841
- let replaced = 0;
1842
- let files = 0;
1843
- for (const [path, file] of this.#files) {
1844
- if (limit !== void 0 && replaced >= limit) break;
1845
- if (!isText(file.content)) continue;
1846
- const remaining = limit === void 0 ? void 0 : limit - replaced;
1847
- let count = 0;
1848
- pattern.lastIndex = 0;
1849
- const next = file.content.text.replace(pattern, (match) => {
1850
- if (remaining !== void 0 && count >= remaining) return match;
1851
- count += 1;
1852
- return replacement;
1853
- });
1854
- if (count > 0) {
1855
- replaced += count;
1856
- files += 1;
1857
- this.write(path, next);
1858
- }
1859
- }
1860
- return {
1861
- query,
1862
- replaced,
1863
- files
1864
- };
1865
- }
1866
- write(path, content, range) {
1867
- if (isRecord(path)) {
1868
- for (const [one, text] of Object.entries(path)) this.#write(one, text);
1869
- return;
1870
- }
1871
- const text = content ?? "";
1872
- if (!range) {
1873
- this.#write(path, text);
1874
- return;
1875
- }
1876
- this.#splice(path, text, range);
1877
- }
1878
- prepend(path, content) {
1879
- if (isRecord(path)) {
1880
- for (const [one, text] of Object.entries(path)) this.#prepend(one, text);
1881
- return;
1882
- }
1883
- this.#prepend(path, content ?? "");
1884
- }
1885
- append(path, content) {
1886
- if (isRecord(path)) {
1887
- for (const [one, text] of Object.entries(path)) this.#append(one, text);
1888
- return;
1889
- }
1890
- this.#append(path, content ?? "");
1891
- }
1892
- move(from, to) {
1893
- if (isRecord(from)) {
1894
- let moved = false;
1895
- for (const [one, target] of Object.entries(from)) if (this.#move(one, target)) moved = true;
1896
- return moved;
1897
- }
1898
- return this.#move(from, to ?? "");
1899
- }
1900
- remove(path) {
1901
- if (path === void 0) {
1902
- this.#files.clear();
1903
- this.#emitter.emit("clear");
1904
- return;
1905
- }
1906
- if (isArray(path)) {
1907
- let removed = false;
1908
- for (const one of path) if (this.#remove(one)) removed = true;
1909
- return removed;
1910
- }
1911
- return this.#remove(path);
1912
- }
1913
- clear() {
1914
- this.#files.clear();
1915
- this.#emitter.emit("clear");
1916
- }
1917
- snapshot() {
1918
- return {
1919
- id: this.#id,
1920
- files: this.files()
1921
- };
1922
- }
1923
- #write(path, content) {
1924
- const existing = this.#files.get(path);
1925
- const file = createFile({
1926
- path,
1927
- content: {
1928
- text: content,
1929
- language: existing && isText(existing.content) ? existing.content.language : inferLanguage(path)
1930
- },
1931
- state: existing ? "modified" : "created"
1932
- });
1933
- this.#files.set(path, file);
1934
- this.#emitter.emit("write", file);
1935
- }
1936
- #splice(path, content, range) {
1937
- const existing = this.#files.get(path);
1938
- if (!existing || !isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a non-text file: ${path}`, { path });
1939
- if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
1940
- path,
1941
- range
1942
- });
1943
- const file = createFile({
1944
- path,
1945
- content: {
1946
- text: spliceRange(existing.content.text, range, content),
1947
- language: existing.content.language
1948
- },
1949
- state: "modified"
1950
- });
1951
- this.#files.set(path, file);
1952
- this.#emitter.emit("write", file);
1953
- }
1954
- #prepend(path, content) {
1955
- this.#write(path, content + this.#text(path, "prepend"));
1956
- }
1957
- #append(path, content) {
1958
- this.#write(path, this.#text(path, "append") + content);
1959
- }
1960
- #text(path, operation) {
1961
- const existing = this.#files.get(path);
1962
- if (!existing) return "";
1963
- if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot ${operation} text to a binary file: ${path}`, { path });
1964
- return existing.content.text;
1965
- }
1966
- #move(from, to) {
1967
- const file = this.#files.get(from);
1968
- if (!file) return false;
1969
- const moved = createFile({
1970
- path: to,
1971
- content: file.content,
1972
- state: "modified"
1973
- });
1974
- this.#files.delete(from);
1975
- this.#files.set(to, moved);
1976
- this.#emitter.emit("move", {
1977
- from,
1978
- to
1979
- });
1980
- return true;
1981
- }
1982
- #remove(path) {
1983
- const removed = this.#files.delete(path);
1984
- if (removed) this.#emitter.emit("remove", path);
1985
- return removed;
1986
- }
1987
- #pattern(query, options) {
1988
- const source = options?.regex === true ? query : escapeRegExp(query);
1989
- const flags = options?.exact === false ? "gi" : "g";
1990
- try {
1991
- return new RegExp(source, flags);
1992
- } catch {
1993
- throw new WorkspaceError("PATTERN", `Invalid search pattern: ${query}`, { query });
1994
- }
1995
- }
1996
- };
1997
- //#endregion
1998
- //#region src/core/workspaces/WorkspaceManager.ts
1999
- /**
2000
- * The registry of {@link Workspace}s keyed by `id`, in insertion order, WITH an active pointer
2001
- * — the §9 store over the workspace layer PLUS the `active` / `switch` seam the context renders.
2002
- * Event-free (a registry, like {@link ConversationManager}); the observability lives on each
2003
- * {@link Workspace}.
2004
- *
2005
- * @remarks
2006
- * - **Registry.** Workspaces live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
2007
- * mints a {@link Workspace} (its `id` from `input` or `crypto.randomUUID()`), flowing the
2008
- * manager's default `on` / `error` in unless the `input` OVERRIDES them, and stores it (an
2009
- * already-present `id` OVERWRITES — last write wins). `count` is the map size, `workspace(id)`
2010
- * looks one up, `workspaces()` lists them in insertion order.
2011
- * - **Active pointer.** `active` is the active workspace (what the context renders), `undefined`
2012
- * until the FIRST `add` (which auto-activates it — a registry with workspaces always has one
2013
- * active). A subsequent `add` leaves `active` unchanged. `switch(id)` re-points `active` to the
2014
- * workspace with `id` and returns it; an unknown `id` returns `undefined` and leaves `active`
2015
- * unchanged (the lenient lookup style — never throws, no new error code).
2016
- * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
2017
- * removing the ACTIVE workspace sets `active` to `undefined`. `clear` empties the registry and
2018
- * sets `active` to `undefined`.
2019
- * - **Durable open / save (the optional `store`).** With a {@link WorkspaceStoreInterface} supplied
2020
- * (the `store` option), `open(id)` HYDRATES a workspace on a registry miss — it `store.get(id)`s
2021
- * the snapshot and rebuilds a fresh {@link Workspace} through the constructor `seed`
2022
- * (`snapshot.files` → path → File), then activates it — and `save(id)` PERSISTS a registered
2023
- * workspace's `snapshot()`. Both are LENIENT without a store (open resolves only registered ids,
2024
- * save is a no-op `false`), consistent with the lenient `switch`.
2025
- * - **Event-free.** A purely registry store — no Emitter, no events (each workspace owns its own
2026
- * observable `emitter`).
2027
- *
2028
- * @example
2029
- * ```ts
2030
- * const manager = new WorkspaceManager()
2031
- * const first = manager.add() // auto-activates — active === first
2032
- * manager.add({ id: 'scratch' }) // leaves active unchanged
2033
- * manager.switch('scratch') // re-points active to the 'scratch' workspace
2034
- * manager.count // 2
2035
- * ```
2036
- */
2037
- var WorkspaceManager = class {
2038
- #workspaces = /* @__PURE__ */ new Map();
2039
- #active;
2040
- #on;
2041
- #error;
2042
- #store;
2043
- constructor(options) {
2044
- this.#on = options?.on;
2045
- this.#error = options?.error;
2046
- this.#store = options?.store;
1047
+ get emitter() {
1048
+ return this.#emitter;
2047
1049
  }
2048
1050
  get count() {
2049
- return this.#workspaces.size;
2050
- }
2051
- get active() {
2052
- return this.#active === void 0 ? void 0 : this.#workspaces.get(this.#active);
1051
+ return this.#instructions.size;
2053
1052
  }
2054
- workspace(id) {
2055
- return this.#workspaces.get(id);
1053
+ get description() {
1054
+ return this.#format?.open ?? "## Instructions";
2056
1055
  }
2057
- workspaces() {
2058
- return [...this.#workspaces.values()];
1056
+ get framing() {
1057
+ return this.#format;
2059
1058
  }
2060
1059
  add(input) {
2061
- const on = input?.on ?? this.#on;
2062
- const error = input?.error ?? this.#error;
2063
- const workspace = new Workspace({
2064
- ...input?.id === void 0 ? {} : { id: input.id },
2065
- ...on === void 0 ? {} : { on },
2066
- ...error === void 0 ? {} : { error }
2067
- }, input?.seed);
2068
- this.#workspaces.set(workspace.id, workspace);
2069
- if (this.#active === void 0) this.#active = workspace.id;
2070
- return workspace;
1060
+ if (isArray(input)) return input.map((one) => this.#create(one));
1061
+ return this.#create(input);
2071
1062
  }
2072
- switch(id) {
2073
- const workspace = this.#workspaces.get(id);
2074
- if (workspace === void 0) return void 0;
2075
- this.#active = id;
2076
- return workspace;
1063
+ instruction(name) {
1064
+ return this.#instructions.get(name);
2077
1065
  }
2078
- async open(id) {
2079
- const existing = this.#workspaces.get(id);
2080
- if (existing !== void 0) {
2081
- this.#active = id;
2082
- return existing;
2083
- }
2084
- if (this.#store === void 0) return void 0;
2085
- const snapshot = await this.#store.get(id);
2086
- if (snapshot === void 0) return void 0;
2087
- const workspace = this.add({
2088
- id,
2089
- seed: snapshot.files.map((file) => [file.path, file])
2090
- });
2091
- this.#active = workspace.id;
2092
- return workspace;
1066
+ instructions() {
1067
+ return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
2093
1068
  }
2094
- async save(id) {
2095
- const workspace = this.#workspaces.get(id);
2096
- if (this.#store === void 0 || workspace === void 0) return false;
2097
- await this.#store.set(workspace.snapshot());
2098
- return true;
1069
+ format(instruction) {
1070
+ return this.#format?.render?.(instruction) ?? instruction.content;
2099
1071
  }
2100
- remove(ids) {
2101
- if (isArray(ids)) {
1072
+ remove(names) {
1073
+ if (isArray(names)) {
2102
1074
  let removed = false;
2103
- for (const id of ids) if (this.#drop(id)) removed = true;
1075
+ for (const name of names) if (this.#delete(name)) removed = true;
2104
1076
  return removed;
2105
1077
  }
2106
- return this.#drop(ids);
1078
+ return this.#delete(names);
2107
1079
  }
2108
1080
  clear() {
2109
- this.#workspaces.clear();
2110
- this.#active = void 0;
1081
+ this.#instructions.clear();
1082
+ this.#emitter.emit("clear");
2111
1083
  }
2112
- #drop(id) {
2113
- const removed = this.#workspaces.delete(id);
2114
- if (removed && this.#active === id) this.#active = void 0;
1084
+ #create(input) {
1085
+ const instruction = new Instruction(input);
1086
+ this.#instructions.set(instruction.name, instruction);
1087
+ this.#emitter.emit("add", instruction);
1088
+ return instruction;
1089
+ }
1090
+ #delete(name) {
1091
+ const removed = this.#instructions.delete(name);
1092
+ if (removed) this.#emitter.emit("remove", name);
2115
1093
  return removed;
2116
1094
  }
2117
1095
  };
@@ -2288,7 +1266,7 @@ var AgentContext = class {
2288
1266
  }
2289
1267
  #workspaceImages(files) {
2290
1268
  const data = [];
2291
- for (const file of files) if (isBinary(file.content) && isImage(file.content)) data.push(file.content.data);
1269
+ for (const file of files) if (isBinary(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.data);
2292
1270
  return data;
2293
1271
  }
2294
1272
  #ensure() {
@@ -2622,7 +1600,7 @@ var Agent = class {
2622
1600
  };
2623
1601
  const toolMessage = this.#context.messages.add({
2624
1602
  role: "tool",
2625
- content: JSON.stringify(outcomeResult.value ?? outcomeResult.error)
1603
+ content: outcomeResult.success ? JSON.stringify(outcomeResult.value) : outcomeResult.error
2626
1604
  });
2627
1605
  messages.push(toolMessage);
2628
1606
  }
@@ -2699,6 +1677,7 @@ var Agent = class {
2699
1677
  }
2700
1678
  #denial(call, reason) {
2701
1679
  return {
1680
+ success: false,
2702
1681
  id: call.id,
2703
1682
  name: call.name,
2704
1683
  error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
@@ -2872,8 +1851,8 @@ var AgentRegistry = class {
2872
1851
  * (an ALLOWLIST — only matched rules that allow get through), pass an `allowed: false`
2873
1852
  * `fallback`.
2874
1853
  * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
2875
- * every {@link import('./types.js').ToolCall} the model emits; a denied call is fed
2876
- * back to the model as a denial {@link import('./types.js').ToolResult} (a `tool`
1854
+ * every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
1855
+ * back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
2877
1856
  * chunk + a tool message) instead of being executed — no tool run, no budget cost —
2878
1857
  * so the model sees the denial and can react.
2879
1858
  * - **Synchronous now.** The async human-approval handshake (request / grant / deny) is
@@ -2916,7 +1895,7 @@ var Authority = class {
2916
1895
  * conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
2917
1896
  * / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
2918
1897
  * {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
2919
- * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}.
1898
+ * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}.
2920
1899
  *
2921
1900
  * @remarks
2922
1901
  * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
@@ -2924,7 +1903,7 @@ var Authority = class {
2924
1903
  * IndexedDB backend swaps in WITHOUT touching the
2925
1904
  * {@link import('../ConversationManager.js').ConversationManager} or the
2926
1905
  * {@link import('../Conversation.js').Conversation} — the same seam as
2927
- * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}. The
1906
+ * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}. The
2928
1907
  * driver defaults to memory ({@link import('../../factories.js').createDatabaseConversationStore}
2929
1908
  * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
2930
1909
  * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
@@ -2995,12 +1974,12 @@ var DatabaseConversationStore = class {
2995
1974
  * The in-memory {@link ConversationStoreInterface} — a process-lifetime `Map` of
2996
1975
  * {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
2997
1976
  * {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
2998
- * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
1977
+ * {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
2999
1978
  *
3000
1979
  * @remarks
3001
1980
  * A plain `Map<string, ConversationSnapshot>` (AGENTS §21 — the snapshot is already pure,
3002
1981
  * self-contained JSON, so no encoding is needed for the memory tier). Like the
3003
- * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore} it twins,
1982
+ * {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
3004
1983
  * there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
3005
1984
  * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
3006
1985
  * the {@link import('../ConversationManager.js').ConversationManager} or the
@@ -3298,238 +2277,8 @@ var ThinkSplitter = class {
3298
2277
  }
3299
2278
  };
3300
2279
  //#endregion
3301
- //#region src/core/tools/Tool.ts
3302
- /**
3303
- * A registered tool — its {@link import('../types.js').ToolDefinition} schema (the
3304
- * `name` / `description` / `parameters` the model sees) bound to the `execute`
3305
- * handler that runs a call.
3306
- *
3307
- * @remarks
3308
- * A thin value object: the constructor stores the schema fields and the handler, and
3309
- * `execute` delegates to the handler verbatim. `parameters` is kept by reference (the
3310
- * open JSON-Schema the provider forwards as-is) — not cloned. The handler's `args` is
3311
- * the model-supplied `unknown` arguments record, narrowed inside the handler (§14);
3312
- * isolating a throw is the {@link ToolManager}'s job, not this class's. Event-free —
3313
- * no Emitter, no events.
3314
- *
3315
- * @example
3316
- * ```ts
3317
- * const tool = new Tool({
3318
- * name: 'add',
3319
- * description: 'Add two numbers',
3320
- * parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } } },
3321
- * execute: (args) => Number(args.a) + Number(args.b),
3322
- * })
3323
- * ```
3324
- */
3325
- var Tool = class {
3326
- name;
3327
- description;
3328
- summary;
3329
- parameters;
3330
- #execute;
3331
- constructor(options) {
3332
- this.name = options.name;
3333
- if (options.description !== void 0) this.description = options.description;
3334
- if (options.summary !== void 0) this.summary = options.summary;
3335
- if (options.parameters !== void 0) this.parameters = options.parameters;
3336
- this.#execute = options.execute;
3337
- }
3338
- execute(args) {
3339
- return this.#execute(args);
3340
- }
3341
- };
3342
- //#endregion
3343
- //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
3344
- /**
3345
- * A {@link WorkspaceStoreInterface} backed by one table of the `databases` layer — a workspace's
3346
- * durable state IS a row, so persistence reduces to keyed point-access (`get` / `set` / `delete`)
3347
- * over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
3348
- * {@link import('./MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
3349
- *
3350
- * @remarks
3351
- * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
3352
- * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
3353
- * IndexedDB backend swaps in WITHOUT touching the
3354
- * {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3355
- * {@link import('../Workspace.js').Workspace} — the same seam as
3356
- * the analogous `DatabaseWorkflowStore` in `@orkestrel/workflow`. The
3357
- * driver defaults to memory ({@link import('../../factories.js').createDatabaseWorkspaceStore}
3358
- * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
3359
- * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
3360
- *
3361
- * The {@link WorkspaceSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
3362
- * `{ id; snapshot }` ({@link WorkspaceSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
3363
- * column the factory builds) — exactly as `DatabaseWorkflowStore` stores its snapshot. The snapshot
3364
- * is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
3365
- * keeps the row type flat (`snapshot` reads back as `unknown`).
3366
- *
3367
- * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
3368
- * the row `{ id: snapshot.id, snapshot }`.
3369
- * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
3370
- * a {@link WorkspaceSnapshot} ({@link import('../../helpers.js').isWorkspaceSnapshot} — the AGENTS
3371
- * §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
3372
- * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
3373
- *
3374
- * UNLIKE a session store there is NO idle-TTL / eviction — a persisted workspace lives until an
3375
- * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
3376
- * §22 method bijection with {@link WorkspaceStoreInterface}). Hydration stays a caller concern: a
3377
- * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3378
- * live workspace through the constructor `seed` (its `open` / `save`).
3379
- *
3380
- * @example
3381
- * ```ts
3382
- * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspace } from '@src/core'
3383
- *
3384
- * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
3385
- * const workspace = createWorkspace()
3386
- * workspace.write('notes.txt', 'hello')
3387
- * await store.set(workspace.snapshot()) // persist the workspace (one JSON column)
3388
- * const snapshot = await store.get(workspace.id)
3389
- * await store.delete(workspace.id) // drop it
3390
- * ```
3391
- */
3392
- var DatabaseWorkspaceStore = class {
3393
- #table;
3394
- /**
3395
- * Wrap a table as a workspace store.
3396
- *
3397
- * @param table - The {@link TableInterface} holding the snapshots — its row is the
3398
- * {@link WorkspaceSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
3399
- */
3400
- constructor(table) {
3401
- this.#table = table;
3402
- }
3403
- /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkspaceSnapshot`. */
3404
- async get(id) {
3405
- const row = await this.#table.get(id);
3406
- if (row === void 0) return void 0;
3407
- return isWorkspaceSnapshot(row.snapshot) ? row.snapshot : void 0;
3408
- }
3409
- /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
3410
- async set(snapshot) {
3411
- await this.#table.set({
3412
- id: snapshot.id,
3413
- snapshot
3414
- });
3415
- }
3416
- /** Drop a snapshot by id; an absent id is a no-op (no throw). */
3417
- async delete(id) {
3418
- await this.#table.remove(id);
3419
- }
3420
- };
3421
- //#endregion
3422
- //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
3423
- /**
3424
- * The in-memory {@link WorkspaceStoreInterface} — a process-lifetime `Map` of
3425
- * {@link WorkspaceSnapshot}s keyed by workspace id, the DEFAULT store
3426
- * {@link import('../../factories.js').createMemoryWorkspaceStore} builds.
3427
- *
3428
- * @remarks
3429
- * A plain `Map<string, WorkspaceSnapshot>` (AGENTS §21 — the snapshot is already pure,
3430
- * self-contained JSON, so no encoding is needed for the memory tier). Like the
3431
- * the analogous `MemoryWorkflowStore` in `@orkestrel/workflow` it twins,
3432
- * there is NO idle-TTL and NO eviction: a persisted workspace lives until an explicit `delete`. A
3433
- * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
3434
- * the {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3435
- * {@link import('../Workspace.js').Workspace} — its driver-pluggable twin is
3436
- * {@link import('./DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} (the snapshot as one opaque
3437
- * JSON column).
3438
- *
3439
- * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
3440
- * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
3441
- * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
3442
- *
3443
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
3444
- * bijection with {@link WorkspaceStoreInterface}). Hydration is a caller concern: a
3445
- * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3446
- * live workspace through the constructor `seed` (its `open` / `save`).
3447
- *
3448
- * @example
3449
- * ```ts
3450
- * import { createMemoryWorkspaceStore, createWorkspace } from '@src/core'
3451
- *
3452
- * const store = createMemoryWorkspaceStore()
3453
- * const workspace = createWorkspace()
3454
- * workspace.write('notes.txt', 'hello')
3455
- * await store.set(workspace.snapshot()) // persist the workspace
3456
- * const snapshot = await store.get(workspace.id)
3457
- * await store.delete(workspace.id) // drop it
3458
- * ```
3459
- */
3460
- var MemoryWorkspaceStore = class {
3461
- #snapshots = /* @__PURE__ */ new Map();
3462
- get(id) {
3463
- return Promise.resolve(this.#snapshots.get(id));
3464
- }
3465
- set(snapshot) {
3466
- this.#snapshots.set(snapshot.id, snapshot);
3467
- return Promise.resolve();
3468
- }
3469
- delete(id) {
3470
- this.#snapshots.delete(id);
3471
- return Promise.resolve();
3472
- }
3473
- };
3474
- //#endregion
3475
2280
  //#region src/core/factories.ts
3476
2281
  /**
3477
- * Create a tool — a {@link ToolInterface} binding a {@link ToolDefinition} schema
3478
- * (the `name` / `description` / `parameters` the model sees) to the `execute` handler
3479
- * that runs a call.
3480
- *
3481
- * @remarks
3482
- * Only `name` is required (it keys the tool in a {@link ToolManagerInterface} and is
3483
- * what the model calls); `description` / `parameters` are the optional JSON-Schema the
3484
- * provider advertises (forwarded verbatim). The handler's `args` is the model-supplied
3485
- * `unknown` arguments record — narrow it inside (§14); a `createToolManager` registry
3486
- * isolates a throw into a `ToolResult.error`.
3487
- *
3488
- * @param options - `name` (required), optional `description` / `parameters`, and the
3489
- * `execute` handler (see {@link ToolOptions})
3490
- * @returns A working {@link ToolInterface}
3491
- *
3492
- * @example
3493
- * ```ts
3494
- * import { createTool } from '@src/core'
3495
- *
3496
- * const add = createTool({
3497
- * name: 'add',
3498
- * description: 'Add two numbers',
3499
- * execute: (args) => Number(args.a) + Number(args.b),
3500
- * })
3501
- * ```
3502
- */
3503
- function createTool(options) {
3504
- return new Tool(options);
3505
- }
3506
- /**
3507
- * Create a tool registry — a {@link ToolManagerInterface} that resolves tool names,
3508
- * lists {@link ToolDefinition}s for the provider, and executes calls with per-call
3509
- * error isolation.
3510
- *
3511
- * @remarks
3512
- * Starts empty; `add` registers one tool or a batch (§9.2), `definitions()` yields the
3513
- * schemas to hand a provider, and `execute` runs a {@link ToolCall} (or a batch),
3514
- * ALWAYS resolving a {@link ToolResult} — a handler throw becomes an `error` result and
3515
- * an unknown name a not-found `error`, so a tool throw never escapes and a batch never
3516
- * fails as a whole.
3517
- *
3518
- * @returns An empty {@link ToolManagerInterface}
3519
- *
3520
- * @example
3521
- * ```ts
3522
- * import { createTool, createToolManager } from '@src/core'
3523
- *
3524
- * const tools = createToolManager()
3525
- * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
3526
- * const result = await tools.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
3527
- * ```
3528
- */
3529
- function createToolManager() {
3530
- return new ToolManager();
3531
- }
3532
- /**
3533
2282
  * Create a conversation — a {@link ConversationInterface} grouping messages above a flat
3534
2283
  * message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
3535
2284
  * rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
@@ -3603,11 +2352,13 @@ function createConversationManager(options) {
3603
2352
  * Create the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
3604
2353
  * process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
3605
2354
  * id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
3606
- * {@link ConversationManagerInterface.save} seam. The EXACT twin of {@link createMemoryWorkspaceStore}.
2355
+ * {@link ConversationManagerInterface.save} seam. The exact twin of
2356
+ * {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
3607
2357
  *
3608
2358
  * @remarks
3609
2359
  * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
3610
- * the structural twin of {@link createMemoryWorkspaceStore}. `get` / `set` / `delete` are async (the
2360
+ * the structural twin of {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
2361
+ * `get` / `set` / `delete` are async (the
3611
2362
  * same shape a durable backend fits); UNLIKE a session store there is NO idle-TTL / eviction — a
3612
2363
  * persisted conversation lives until an explicit `delete`. Its driver-pluggable twin is
3613
2364
  * {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
@@ -3635,12 +2386,14 @@ function createMemoryConversationStore() {
3635
2386
  /**
3636
2387
  * Create a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
3637
2388
  * driver-pluggable backing for the conversation persistence seam, the opt-in twin of
3638
- * {@link createMemoryConversationStore}. The EXACT twin of {@link createDatabaseWorkspaceStore}.
2389
+ * {@link createMemoryConversationStore}. The exact twin of
2390
+ * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
3639
2391
  *
3640
2392
  * @remarks
3641
2393
  * Builds a one-table database (`conversations`, keyed by `id`) over the supplied driver, the snapshot
3642
2394
  * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
3643
- * `rawShape` (a JSON blob), exactly as {@link createDatabaseWorkspaceStore} stores its snapshot. The
2395
+ * `rawShape` (a JSON blob), exactly as
2396
+ * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
3644
2397
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
3645
2398
  * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
3646
2399
  * {@link import('./helpers.js').isConversationSnapshot}). The `driver` DEFAULTS to
@@ -3664,12 +2417,13 @@ function createMemoryConversationStore() {
3664
2417
  * ```
3665
2418
  */
3666
2419
  function createDatabaseConversationStore(driver = createMemoryDriver()) {
2420
+ const columns = {
2421
+ id: stringShape(),
2422
+ snapshot: rawShape({})
2423
+ };
3667
2424
  return new DatabaseConversationStore(createDatabase({
3668
2425
  driver,
3669
- tables: { conversations: {
3670
- id: stringShape(),
3671
- snapshot: rawShape({})
3672
- } }
2426
+ tables: { conversations: columns }
3673
2427
  }).table("conversations"));
3674
2428
  }
3675
2429
  /**
@@ -3936,8 +2690,9 @@ function createAuthority(options) {
3936
2690
  *
3937
2691
  * @example
3938
2692
  * ```ts
2693
+ * import { createAgentRegistry } from '@src/core'
3939
2694
  * import type { ProviderInterface } from '@src/core'
3940
- * import { createAgentRegistry, createTool } from '@src/core'
2695
+ * import { createTool } from '@orkestrel/tool'
3941
2696
  *
3942
2697
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3943
2698
  * const registry = createAgentRegistry({
@@ -4042,213 +2797,7 @@ function createAgentRunner(options) {
4042
2797
  handler: handleAgentRunnerJob.bind(void 0, registry, allowPartial)
4043
2798
  });
4044
2799
  }
4045
- /**
4046
- * Create a file — an immutable {@link FileInterface} from its `path` + {@link FileContent}
4047
- * and optional {@link import('./types.js').FileState}, with `size` / `lines` DERIVED from the
4048
- * content.
4049
- *
4050
- * @remarks
4051
- * Returns a PLAIN `Object.freeze`d record (NOT a class instance) — the `path` IS its identity
4052
- * (there is no `id`). Only `path` / `content` are required; `state` defaults to `'created'`
4053
- * when omitted. `size` (via {@link computeSize}) and `lines` (via {@link countLines}) are
4054
- * computed from the content here (so they never drift from it). Frozen + plain, so it
4055
- * `structuredClone`s losslessly and is never mutated after creation.
4056
- *
4057
- * @param input - `path` / `content` (required) and an optional `state` (see {@link FileInput})
4058
- * @returns A frozen {@link FileInterface} record
4059
- *
4060
- * @example
4061
- * ```ts
4062
- * import { createFile, createTextContent } from '@src/core'
4063
- *
4064
- * const file = createFile({ path: 'src/main.ts', content: createTextContent('const x = 1', 'typescript') })
4065
- * file.lines // 1
4066
- * ```
4067
- */
4068
- function createFile(input) {
4069
- return Object.freeze({
4070
- path: input.path,
4071
- content: input.content,
4072
- state: input.state ?? "created",
4073
- size: computeSize(input.content),
4074
- lines: countLines(input.content)
4075
- });
4076
- }
4077
- /**
4078
- * Build the TEXT {@link FileContent} arm — the §4.2.3 split constructor for text (a separate
4079
- * function per arm, not one constructor dispatching on a discriminator parameter).
4080
- *
4081
- * @param text - The literal text body
4082
- * @param language - The fenced-code language the text renders as (e.g. `'typescript'`)
4083
- * @returns A `{ text; language }` content arm
4084
- *
4085
- * @example
4086
- * ```ts
4087
- * import { createTextContent } from '@src/core'
4088
- *
4089
- * createTextContent('const x = 1', 'typescript') // { text: 'const x = 1', language: 'typescript' }
4090
- * ```
4091
- */
4092
- function createTextContent(text, language) {
4093
- return {
4094
- text,
4095
- language
4096
- };
4097
- }
4098
- /**
4099
- * Build the BINARY {@link FileContent} arm — the §4.2.3 split constructor for binary (a
4100
- * separate function per arm, not one constructor dispatching on a discriminator parameter).
4101
- * An image is just a binary with an image {@link BinaryMIME}.
4102
- *
4103
- * @param data - The base64-encoded binary payload
4104
- * @param mime - The {@link BinaryMIME} that labels the payload
4105
- * @returns A `{ data; mime }` content arm
4106
- *
4107
- * @example
4108
- * ```ts
4109
- * import { createBinaryContent } from '@src/core'
4110
- *
4111
- * createBinaryContent('<base64>', 'image/png') // { data: '<base64>', mime: 'image/png' }
4112
- * ```
4113
- */
4114
- function createBinaryContent(data, mime) {
4115
- return {
4116
- data,
4117
- mime
4118
- };
4119
- }
4120
- /**
4121
- * Create a workspace — a mutable, `path`-keyed working set of immutable
4122
- * {@link FileInterface}s with the in-memory edit surface (read / write / search / replace /
4123
- * move / remove), observable through its `EmitterInterface` (from `@orkestrel/emitter`).
4124
- *
4125
- * @param options - Optional initial {@link import('./types.js').WorkspaceEventMap} listeners (`on`) and the emitter's `error` handler (see {@link WorkspaceOptions})
4126
- * @returns A working {@link WorkspaceInterface}
4127
- *
4128
- * @example
4129
- * ```ts
4130
- * import { createWorkspace } from '@src/core'
4131
- *
4132
- * const workspace = createWorkspace({ on: { write: (file) => console.log(file.path) } })
4133
- * workspace.write('src/main.ts', 'const x = 1')
4134
- * workspace.file('src/main.ts')?.state // 'created'
4135
- * ```
4136
- */
4137
- function createWorkspace(options) {
4138
- return new Workspace(options);
4139
- }
4140
- /**
4141
- * Create the in-memory workspace store — a {@link WorkspaceStoreInterface} backed by a
4142
- * process-lifetime `Map` of {@link import('./types.js').WorkspaceSnapshot}s keyed by workspace id,
4143
- * the DEFAULT backing for the durable {@link WorkspaceManagerInterface.open} /
4144
- * {@link WorkspaceManagerInterface.save} seam.
4145
- *
4146
- * @remarks
4147
- * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
4148
- * the structural twin of the analogous `createMemoryWorkflowStore` in `@orkestrel/workflow`.
4149
- * `get` / `set` / `delete` are async (the same shape a durable backend fits); UNLIKE a session
4150
- * store there is NO idle-TTL / eviction — a persisted workspace lives until an explicit `delete`.
4151
- * Its driver-pluggable twin is {@link createDatabaseWorkspaceStore} (the snapshot as one opaque
4152
- * JSON column over a `databases` table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB
4153
- * driver, and it swaps in WITHOUT touching the manager or the workspace. Hydration stays a manager
4154
- * concern: read a snapshot back and rebuild the live workspace through the constructor `seed`.
4155
- *
4156
- * @returns A memory-backed {@link WorkspaceStoreInterface}
4157
- *
4158
- * @example
4159
- * ```ts
4160
- * import { createMemoryWorkspaceStore, createWorkspaceManager } from '@src/core'
4161
- *
4162
- * const store = createMemoryWorkspaceStore()
4163
- * const manager = createWorkspaceManager({ store })
4164
- * const workspace = manager.add()
4165
- * workspace.write('notes.txt', 'hello')
4166
- * await manager.save(workspace.id) // persist the workspace
4167
- * ```
4168
- */
4169
- function createMemoryWorkspaceStore() {
4170
- return new MemoryWorkspaceStore();
4171
- }
4172
- /**
4173
- * Create a {@link DatabaseWorkspaceStore} over any {@link DriverInterface} — the durable,
4174
- * driver-pluggable backing for the workspace persistence seam, the opt-in twin of
4175
- * {@link createMemoryWorkspaceStore}.
4176
- *
4177
- * @remarks
4178
- * Builds a one-table database (`workspaces`, keyed by `id`) over the supplied driver, the snapshot
4179
- * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
4180
- * `rawShape` (a JSON blob), exactly as
4181
- * the analogous `createDatabaseWorkflowStore` in `@orkestrel/workflow` stores its snapshot. The
4182
- * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
4183
- * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
4184
- * {@link import('./helpers.js').isWorkspaceSnapshot}). The `driver` DEFAULTS to
4185
- * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
4186
- * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4187
- * the durability is the driver's job, the store engine is shared. It swaps in behind
4188
- * {@link WorkspaceStoreInterface} WITHOUT touching the manager or the workspace.
4189
- *
4190
- * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
4191
- * @returns A {@link WorkspaceStoreInterface} over the driver
4192
- *
4193
- * @example
4194
- * ```ts
4195
- * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspaceManager } from '@src/core'
4196
- *
4197
- * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
4198
- * const manager = createWorkspaceManager({ store })
4199
- * const workspace = manager.add()
4200
- * workspace.write('notes.txt', 'hello')
4201
- * await manager.save(workspace.id) // persist the workspace (one JSON column)
4202
- * ```
4203
- */
4204
- function createDatabaseWorkspaceStore(driver = createMemoryDriver()) {
4205
- return new DatabaseWorkspaceStore(createDatabase({
4206
- driver,
4207
- tables: { workspaces: {
4208
- id: stringShape(),
4209
- snapshot: rawShape({})
4210
- } }
4211
- }).table("workspaces"));
4212
- }
4213
- /**
4214
- * Create a workspace registry — a {@link WorkspaceManagerInterface} holding
4215
- * {@link WorkspaceInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
4216
- * the §9 store over the workspace layer plus the `active` / `switch` seam the context renders.
4217
- *
4218
- * @remarks
4219
- * Starts empty; `add(input?)` mints a {@link WorkspaceInterface} (its `id` from the input or a
4220
- * random UUID), flowing the manager's default `on` / `error` in unless the input overrides them,
4221
- * and stores it (an already-present `id` overwrites — last write wins) — and AUTO-ACTIVATES the
4222
- * FIRST one (a registry with workspaces always has one `active`); a later `add` leaves `active`
4223
- * unchanged. `switch(id)` re-points `active` (an unknown `id` returns `undefined`, leaving
4224
- * `active` unchanged — lenient, never throws); `workspace(id)` / `workspaces()` look up;
4225
- * `remove` (one or a batch, §9.2) reports whether any was removed AND clears `active` if it was
4226
- * the removed one; `clear` empties it and clears `active`. Event-free (each workspace owns its
4227
- * own observable `emitter`).
4228
- *
4229
- * With the optional `store` ({@link WorkspaceStoreInterface}, e.g. `createMemoryWorkspaceStore` /
4230
- * `createDatabaseWorkspaceStore`), `open(id)` HYDRATES a workspace on a registry miss (rebuilding it
4231
- * through the constructor `seed` from the snapshot's `files`, then activating it) and `save(id)`
4232
- * PERSISTS a registered workspace's `snapshot()`. Both are LENIENT without a store (open resolves
4233
- * only registered ids, save is a no-op `false`).
4234
- *
4235
- * @param options - Optional default `on` / `error` for created workspaces + the durable `store`
4236
- * (see {@link WorkspaceManagerOptions})
4237
- * @returns An empty {@link WorkspaceManagerInterface}
4238
- *
4239
- * @example
4240
- * ```ts
4241
- * import { createWorkspaceManager } from '@src/core'
4242
- *
4243
- * const workspaces = createWorkspaceManager()
4244
- * const scratch = workspaces.add() // auto-activates — workspaces.active === scratch
4245
- * scratch.write('notes.txt', 'hello')
4246
- * ```
4247
- */
4248
- function createWorkspaceManager(options) {
4249
- return new WorkspaceManager(options);
4250
- }
4251
2800
  //#endregion
4252
- export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, DatabaseWorkspaceStore, EXTENSION_TO_LANGUAGE, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, MemoryWorkspaceStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, Tool, ToolManager, WORKSPACE_SECTION_HEADER, Workspace, WorkspaceError, WorkspaceManager, buildToolResult, clampPosition, clampRange, computeSize, countLines, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createBinaryContent, createConversation, createConversationManager, createDatabaseConversationStore, createDatabaseWorkspaceStore, createFile, createInstruction, createInstructionManager, createMemoryConversationStore, createMemoryWorkspaceStore, createScope, createScopeManager, createTextContent, createThinkSplitter, createTool, createToolManager, createWorkspace, createWorkspaceManager, decodedSize, escapeRegExp, estimateMessages, estimateTokens, fencedFile, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, inferLanguage, isAgentError, isAgentJobError, isBinary, isConversationError, isConversationSnapshot, isFile, isImage, isMessage, isProviderAbortError, isSection, isText, isToolCall, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, sanitizeToken, sanitizeUsage, settleAgentJob, sliceRange, spliceRange };
2801
+ export { Agent, AgentContext, AgentError, AgentJobError, AgentRegistry, Authority, CONVERSATION_RECAP_PREFIX, Channel, Conversation, ConversationError, ConversationManager, DEFAULT_AGENT_LIMIT, DEFAULT_AUTHORITY_ZONE, DEFAULT_CONVERSATION_KEEP, DatabaseConversationStore, IMAGE_TOKEN_ESTIMATE, Instruction, InstructionManager, MESSAGE_TOKEN_OVERHEAD, MemoryConversationStore, ProviderAbortError, Scope, ScopeManager, THINK_CLOSE, THINK_OPEN, ThinkSplitter, WORKSPACE_SECTION_HEADER, agentResultToJSON, createAgent, createAgentContext, createAgentQueue, createAgentRegistry, createAgentRunner, createAuthority, createConversation, createConversationManager, createDatabaseConversationStore, createInstruction, createInstructionManager, createMemoryConversationStore, createScope, createScopeManager, createThinkSplitter, estimateMessages, estimateTokens, fencedFile, filterAllowList, handleAgentQueueJob, handleAgentRunnerJob, isAgentError, isAgentJobError, isConversationError, isConversationSnapshot, isMessage, isProviderAbortError, isSection, sanitizeToken, sanitizeUsage, settleAgentJob };
4253
2802
 
4254
2803
  //# sourceMappingURL=index.js.map