@orkestrel/agent 0.0.10 → 0.0.12

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.
@@ -6,6 +6,8 @@ let _orkestrel_workflow = require("@orkestrel/workflow");
6
6
  let _orkestrel_abort = require("@orkestrel/abort");
7
7
  let _orkestrel_timeout = require("@orkestrel/timeout");
8
8
  let _orkestrel_emitter = require("@orkestrel/emitter");
9
+ let _orkestrel_tool = require("@orkestrel/tool");
10
+ let _orkestrel_workspace = require("@orkestrel/workspace");
9
11
  let _orkestrel_budget = require("@orkestrel/budget");
10
12
  //#region src/core/constants.ts
11
13
  /**
@@ -91,53 +93,6 @@ var MESSAGE_TOKEN_OVERHEAD = 4;
91
93
  * exact count.
92
94
  */
93
95
  var IMAGE_TOKEN_ESTIMATE = 512;
94
- /**
95
- * The extension→language table {@link import('./helpers.js').inferLanguage} reads to
96
- * map a file path's extension to a fenced-code language tag. Covers the common source /
97
- * markup / data extensions; an unlisted extension falls back to `'text'`.
98
- */
99
- var EXTENSION_TO_LANGUAGE = Object.freeze({
100
- ts: "typescript",
101
- tsx: "typescript",
102
- js: "javascript",
103
- jsx: "javascript",
104
- mjs: "javascript",
105
- cjs: "javascript",
106
- json: "json",
107
- md: "markdown",
108
- html: "html",
109
- htm: "html",
110
- css: "css",
111
- scss: "scss",
112
- sass: "sass",
113
- less: "less",
114
- vue: "vue",
115
- svelte: "svelte",
116
- py: "python",
117
- rb: "ruby",
118
- rs: "rust",
119
- go: "go",
120
- java: "java",
121
- kt: "kotlin",
122
- swift: "swift",
123
- c: "c",
124
- cpp: "cpp",
125
- h: "c",
126
- hpp: "cpp",
127
- cs: "csharp",
128
- php: "php",
129
- sql: "sql",
130
- sh: "bash",
131
- bash: "bash",
132
- zsh: "bash",
133
- ps1: "powershell",
134
- yaml: "yaml",
135
- yml: "yaml",
136
- toml: "toml",
137
- xml: "xml",
138
- svg: "xml",
139
- txt: "text"
140
- });
141
96
  //#endregion
142
97
  //#region src/core/errors.ts
143
98
  /**
@@ -261,45 +216,6 @@ function isConversationError(value) {
261
216
  return value instanceof ConversationError;
262
217
  }
263
218
  /**
264
- * An error thrown by the in-memory {@link import('./workspaces/Workspace.js').Workspace} edit
265
- * surface.
266
- *
267
- * @remarks
268
- * Carries a {@link WorkspaceErrorCode} and an optional `context` bag naming the offending
269
- * path / range. Thrown for a text-only operation aimed at an image file (`MODALITY` — a
270
- * ranged read / write, `prepend`, or `append`), an invalid `search` / `replace` regular
271
- * expression (`PATTERN`), and a structurally invalid ranged-write {@link import('./types.js').Range}
272
- * (`RANGE` — inverted, or a sub-1 line / column).
273
- */
274
- var WorkspaceError = class extends Error {
275
- code;
276
- context;
277
- constructor(code, message, context) {
278
- super(message);
279
- this.name = "WorkspaceError";
280
- this.code = code;
281
- if (context !== void 0) this.context = context;
282
- }
283
- };
284
- /**
285
- * Narrow an unknown caught value to a {@link WorkspaceError}.
286
- *
287
- * @param value - The value to test (typically a `catch` binding)
288
- * @returns `true` when `value` is a {@link WorkspaceError}
289
- *
290
- * @example
291
- * ```ts
292
- * try {
293
- * workspace.prepend('icon.png', '// header')
294
- * } catch (error) {
295
- * if (isWorkspaceError(error) && error.code === 'MODALITY') skipBinary()
296
- * }
297
- * ```
298
- */
299
- function isWorkspaceError(value) {
300
- return value instanceof WorkspaceError;
301
- }
302
- /**
303
219
  * Thrown synchronously by an {@link AgentInterface}'s `stream()` when a concurrent run would
304
220
  * corrupt SHARED per-agent accounting — carries a machine-readable `code`.
305
221
  *
@@ -552,7 +468,7 @@ var Conversation = class {
552
468
  * The registry of {@link Conversation}s keyed by `id`, in insertion order, WITH an active pointer
553
469
  * — the §9 store over the conversation layer PLUS the `active` / `switch` seam the
554
470
  * {@link import('../AgentContext.js').AgentContext} renders. Event-free (a registry, like
555
- * {@link import('../workspaces/WorkspaceManager.js').WorkspaceManager}); the observability lives
471
+ * {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
556
472
  * on each {@link Conversation}.
557
473
  *
558
474
  * @remarks
@@ -667,30 +583,6 @@ var ConversationManager = class {
667
583
  //#endregion
668
584
  //#region src/core/helpers.ts
669
585
  /**
670
- * Infer a fenced-code language tag from a file path's extension — what a workspace text file
671
- * (or the {@link fencedFile} renderer) renders its content block as.
672
- *
673
- * @remarks
674
- * Reads the extension after the last `.` (case-insensitive) and maps it through a
675
- * fixed extension→language table. An unknown extension, or a path with no extension,
676
- * falls back to `'text'` (a safe, language-agnostic fence). Total — never throws.
677
- *
678
- * @param path - The document's file path (e.g. `'src/main.ts'`)
679
- * @returns The inferred language tag (e.g. `'typescript'`), or `'text'` when unknown
680
- *
681
- * @example
682
- * ```ts
683
- * inferLanguage('src/main.ts') // 'typescript'
684
- * inferLanguage('README.md') // 'markdown'
685
- * inferLanguage('LICENSE') // 'text'
686
- * ```
687
- */
688
- function inferLanguage(path) {
689
- const dot = path.lastIndexOf(".");
690
- if (dot === -1) return "text";
691
- return EXTENSION_TO_LANGUAGE[path.slice(dot + 1).toLowerCase()] ?? "text";
692
- }
693
- /**
694
586
  * Filter a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
695
587
  * keys — the pure, total set-membership primitive the context's build step and the agent
696
588
  * loop's tool-advertise step apply a scope through.
@@ -851,201 +743,22 @@ function handleAgentRunnerJob(registry, allowPartial, controller) {
851
743
  return settleAgentJob(registry.build(controller.input, controller.signal), allowPartial);
852
744
  }
853
745
  /**
854
- * Whether a {@link FileContent} is the TEXT arm — the narrowing guard for the text-vs-binary
855
- * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
856
- *
857
- * @remarks
858
- * Tests `'text' in content` structurally — there is no `modality` discriminant. A `true`
859
- * narrows `content` to `{ text: string; language: string }`, unlocking `.text` / `.language`.
860
- * Total — never throws.
861
- *
862
- * @param content - The file content to test
863
- * @returns `true` when `content` is the text arm (carries `text` + `language`)
864
- *
865
- * @example
866
- * ```ts
867
- * if (isText(file.content)) file.content.text // the literal text + .language
868
- * ```
869
- */
870
- function isText(content) {
871
- return "text" in content;
872
- }
873
- /**
874
- * Whether a {@link FileContent} is the BINARY arm — the narrowing guard for the text-vs-binary
875
- * union (AGENTS §14: narrow an untyped arm via a guard, never an `as`).
876
- *
877
- * @remarks
878
- * Tests `'data' in content` structurally — there is no `modality` discriminant. A `true`
879
- * narrows `content` to `{ data: string; mime: BinaryMIME }`, unlocking `.data` / `.mime`. An
880
- * image is a binary whose `mime` starts with `image/` ({@link isImage}). Total — never throws.
881
- *
882
- * @param content - The file content to test
883
- * @returns `true` when `content` is the binary arm (carries base64 `data` + `mime`)
884
- *
885
- * @example
886
- * ```ts
887
- * if (isBinary(file.content)) file.content.data // the base64 payload + .mime
888
- * ```
889
- */
890
- function isBinary(content) {
891
- return "data" in content;
892
- }
893
- /**
894
- * Whether a {@link FileContent} is an IMAGE — a {@link isBinary} arm whose `mime` is an
895
- * `image/*` type (an image is just a binary with an image MIME).
896
- *
897
- * @remarks
898
- * Narrows to the binary arm first, then checks the `image/` MIME prefix — so a future
899
- * non-image binary (a PDF, `'application/pdf'`) is binary but NOT an image. Total — never
900
- * throws.
901
- *
902
- * @param content - The file content to test
903
- * @returns `true` when `content` is a binary arm with an `image/*` MIME
904
- *
905
- * @example
906
- * ```ts
907
- * isImage({ data: '<base64>', mime: 'image/png' }) // true
908
- * isImage({ text: 'hi', language: 'text' }) // false
909
- * ```
910
- */
911
- function isImage(content) {
912
- return isBinary(content) && content.mime.startsWith("image/");
913
- }
914
- /**
915
- * Whether an `unknown` is structurally a {@link FileInterface} record — the per-file step of the
916
- * {@link isWorkspaceSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage read
917
- * via a guard, never an `as`).
918
- *
919
- * @remarks
920
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the file's SHAPE:
921
- * a record with a `string` `path`, a `string` `state`, `number` `size` / `lines`, and a `content`
922
- * that is EITHER the TEXT arm (`{ text: string; language: string }`) OR the BINARY arm
923
- * (`{ data: string; mime: string }`) — the tagless {@link FileContent} union, told apart
924
- * structurally exactly as {@link isText} / {@link isBinary} do (no `modality` discriminant). Enough
925
- * to safely impose the {@link FileInterface} type at a storage boundary WITHOUT a cast; the `mime`
926
- * is left as a broad `string` here (an open {@link BinaryMIME}, so any storage-read MIME string is
927
- * accepted defensively rather than rejected against the current literal set).
928
- *
929
- * @param value - The value to test (one element of a snapshot's opaque `files` array)
930
- * @returns `true` when `value` has the structural shape of a {@link FileInterface}
931
- *
932
- * @example
933
- * ```ts
934
- * isFile({ path: 'a.ts', content: { text: 'x', language: 'typescript' }, state: 'created', size: 1, lines: 1 }) // true
935
- * isFile({ path: 'a.png', content: { data: 'AAAA', mime: 'image/png' }, state: 'created', size: 3, lines: 0 }) // true
936
- * isFile({ path: 'a.ts' }) // false (missing content / state / size / lines)
937
- * ```
938
- */
939
- function isFile(value) {
940
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
941
- if (!(0, _orkestrel_contract.isString)(value.path) || !(0, _orkestrel_contract.isString)(value.state)) return false;
942
- if (!(0, _orkestrel_contract.isNumber)(value.size) || !(0, _orkestrel_contract.isNumber)(value.lines)) return false;
943
- if (!(0, _orkestrel_contract.isRecord)(value.content)) return false;
944
- const text = (0, _orkestrel_contract.isString)(value.content.text) && (0, _orkestrel_contract.isString)(value.content.language);
945
- const binary = (0, _orkestrel_contract.isString)(value.content.data) && (0, _orkestrel_contract.isString)(value.content.mime);
946
- return text || binary;
947
- }
948
- /**
949
- * Narrow an `unknown` to a {@link WorkspaceSnapshot} — the AGENTS §14 boundary guard for an
950
- * UNTRUSTED snapshot read (a storage row a
951
- * {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} reads back
952
- * from its opaque JSON column, a snapshot loaded from disk).
953
- *
954
- * @remarks
955
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
956
- * SHAPE: a `string` `id` and a `files` array EVERY element of which is a valid {@link FileInterface}
957
- * record ({@link isFile}) — enough to safely impose the {@link WorkspaceSnapshot} type at a storage
958
- * boundary WITHOUT a cast. The structural twin of
959
- * the analogous `isWorkflowSnapshot` in `@orkestrel/workflow`. A malformed blob (a non-record, a
960
- * missing / non-string `id`, a non-array `files`, or any malformed file element) resolves `false`,
961
- * so a {@link import('./workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} read
962
- * yields `undefined` rather than a broken workspace.
963
- *
964
- * @param value - The value to test (an opaque storage read)
965
- * @returns `true` when `value` has the structural shape of a {@link WorkspaceSnapshot}
966
- *
967
- * @example
968
- * ```ts
969
- * isWorkspaceSnapshot({ id: 'w1', files: [] }) // true
970
- * isWorkspaceSnapshot({ id: 'w1', files: 'nope' }) // false
971
- * isWorkspaceSnapshot({ files: [] }) // false (missing id)
972
- * ```
973
- */
974
- function isWorkspaceSnapshot(value) {
975
- return (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.isArray)(value.files) && value.files.every(isFile);
976
- }
977
- /**
978
- * Whether an `unknown` is structurally a {@link ToolCall} record — the per-call step of the
979
- * {@link isMessage} read-boundary narrow (AGENTS §14: narrow an untrusted storage read via a
980
- * guard, never an `as`).
981
- *
982
- * @remarks
983
- * A total guard (it NEVER throws — adversarial input returns `false`). It checks the call's
984
- * SHAPE: a record with a `string` `id`, a `string` `name`, and a record `arguments` — the real
985
- * {@link ToolCall} shape a restored assistant turn replays to a provider. This is the ASI06
986
- * fail-CLOSED element check: a tampered persisted call (`null`, a bare string, a non-record
987
- * `arguments`) fails the guard, so {@link isMessage} — and through it
988
- * {@link isConversationSnapshot} — rejects the whole snapshot and a poisoned store row reads
989
- * back as ABSENT instead of replaying a malformed call into a chat template.
990
- *
991
- * @param value - The value to test (one element of an assistant message's `calls`)
992
- * @returns `true` when `value` has the structural shape of a {@link ToolCall}
993
- *
994
- * @example
995
- * ```ts
996
- * isToolCall({ id: 'c1', name: 'search', arguments: { q: 'acme' } }) // true
997
- * isToolCall({ id: 'c1', name: 'search' }) // false (missing arguments)
998
- * isToolCall({ id: 'c1', name: 123, arguments: {} }) // false (non-string name)
999
- * ```
1000
- */
1001
- function isToolCall(value) {
1002
- if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1003
- return (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.isString)(value.name) && (0, _orkestrel_contract.isRecord)(value.arguments);
1004
- }
1005
- /**
1006
- * Project a {@link ToolResult} into the MCP `CallToolResult` shape — a top-level `error`
1007
- * maps to a single `isError: true` text block (the failure reason), otherwise the `value`
1008
- * is JSON-stringified into a single text block with no `isError`.
1009
- *
1010
- * @param result - The {@link ToolResult} to project (as returned by {@link import('./tools/ToolManager.js').ToolManager.execute})
1011
- * @returns The equivalent {@link ToolCallResult}
1012
- *
1013
- * @example
1014
- * ```ts
1015
- * buildToolResult({ id: 'c1', name: 'search', error: 'max depth' })
1016
- * // { content: [{ type: 'text', text: 'max depth' }], isError: true }
1017
- * buildToolResult({ id: 'c1', name: 'search', value: { count: 1 } })
1018
- * // { content: [{ type: 'text', text: '{"count":1}' }] }
1019
- * ```
1020
- */
1021
- function buildToolResult(result) {
1022
- if (result.error !== void 0) return {
1023
- content: [{
1024
- type: "text",
1025
- text: result.error
1026
- }],
1027
- isError: true
1028
- };
1029
- return { content: [{
1030
- type: "text",
1031
- text: JSON.stringify(result.value)
1032
- }] };
1033
- }
1034
- /**
1035
746
  * Whether an `unknown` is structurally a {@link MessageInterface} record — the per-message step of
1036
747
  * the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
1037
- * read via a guard, never an `as`). The conversation analogue of {@link isFile}.
748
+ * read via a guard, never an `as`). The conversation analogue of
749
+ * {@link import('@orkestrel/workspace').isFile}.
1038
750
  *
1039
751
  * @remarks
1040
752
  * A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
1041
753
  * SHAPE: a record with a `string` `id`, a `string` `role`, a `string` `content`, and — WHEN present
1042
- * — a `calls` array EVERY element of which is a valid {@link ToolCall} ({@link isToolCall} — the
754
+ * — a `calls` array EVERY element of which is a valid
755
+ * {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
1043
756
  * ASI06 fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
1044
757
  * reads back as absent rather than replaying a malformed call) and an `images` that is an array
1045
758
  * (an absent optional passes). The `role` is left as a broad `string` here (an open
1046
759
  * {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
1047
- * defensively rather than rejected against the current literal set) — exactly as {@link isFile}
1048
- * accepts a broad `state` / `mime`. Enough to safely impose the {@link MessageInterface} type at
760
+ * defensively rather than rejected against the current literal set). Enough to safely impose the
761
+ * {@link MessageInterface} type at
1049
762
  * a storage boundary WITHOUT a cast.
1050
763
  *
1051
764
  * @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
@@ -1062,7 +775,7 @@ function buildToolResult(result) {
1062
775
  function isMessage(value) {
1063
776
  if (!(0, _orkestrel_contract.isRecord)(value)) return false;
1064
777
  if (!(0, _orkestrel_contract.isString)(value.id) || !(0, _orkestrel_contract.isString)(value.role) || !(0, _orkestrel_contract.isString)(value.content)) return false;
1065
- if (value.calls !== void 0 && !((0, _orkestrel_contract.isArray)(value.calls) && value.calls.every(isToolCall))) return false;
778
+ if (value.calls !== void 0 && !((0, _orkestrel_contract.isArray)(value.calls) && value.calls.every(_orkestrel_tool.isToolCall))) return false;
1066
779
  return value.images === void 0 || (0, _orkestrel_contract.isArray)(value.images);
1067
780
  }
1068
781
  /**
@@ -1096,7 +809,7 @@ function isSection(value) {
1096
809
  * UNTRUSTED snapshot read (a storage row a
1097
810
  * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
1098
811
  * reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
1099
- * {@link isWorkspaceSnapshot}.
812
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
1100
813
  *
1101
814
  * @remarks
1102
815
  * A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
@@ -1105,7 +818,7 @@ function isSection(value) {
1105
818
  * {@link SectionInterface} ({@link isSection}), and a `messages` array EVERY element of which is a
1106
819
  * valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
1107
820
  * {@link ConversationSnapshot} type at a storage boundary WITHOUT a cast. The structural twin of
1108
- * {@link isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
821
+ * {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
1109
822
  * non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
1110
823
  * element) resolves `false`, so a
1111
824
  * {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
@@ -1130,273 +843,6 @@ function isConversationSnapshot(value) {
1130
843
  return (0, _orkestrel_contract.isArray)(value.messages) && value.messages.every(isMessage);
1131
844
  }
1132
845
  /**
1133
- * Compute the byte size of a {@link FileContent} — the `size` a {@link FileInterface} derives
1134
- * once when built.
1135
- *
1136
- * @remarks
1137
- * Dispatches on {@link isText}: a text arm is its UTF-8 byte length (via `new
1138
- * TextEncoder().encode(text).length` — the standard Web/Node API, so a multi-byte
1139
- * character like `'é'` or `'😀'` counts as its real encoded bytes, NOT its character
1140
- * count); a binary arm is the decoded payload byte length of its base64 `data` (via
1141
- * {@link decodedSize}, computed arithmetically — no `atob` / `Buffer`). Total — never throws.
1142
- *
1143
- * @param content - The file content to size
1144
- * @returns The byte size (UTF-8 bytes for text; decoded payload bytes for binary)
1145
- *
1146
- * @example
1147
- * ```ts
1148
- * computeSize({ text: 'café', language: 'text' }) // 5 (é is two bytes)
1149
- * computeSize({ data: 'AAAA', mime: 'image/png' }) // 3
1150
- * ```
1151
- */
1152
- function computeSize(content) {
1153
- if (isText(content)) return new TextEncoder().encode(content.text).length;
1154
- return decodedSize(content.data);
1155
- }
1156
- /**
1157
- * Count the lines of a {@link FileContent} — the `lines` a {@link FileInterface} derives once
1158
- * when built.
1159
- *
1160
- * @remarks
1161
- * A text arm's line count is `0` for the empty string, otherwise one more than the
1162
- * number of newline (`\n`) separators it contains — so `'a'` is one line, `'a\nb'` is two,
1163
- * and a trailing newline `'a\n'` counts the empty final line as two. A binary arm has no
1164
- * lines and returns `0`. Total — never throws.
1165
- *
1166
- * @param content - The file content to count lines for
1167
- * @returns The line count (text line count; `0` for a binary arm or empty text)
1168
- *
1169
- * @example
1170
- * ```ts
1171
- * countLines({ text: '', language: 'text' }) // 0
1172
- * countLines({ text: 'a\nb\nc', language: 'text' }) // 3
1173
- * countLines({ data: 'AAAA', mime: 'image/png' }) // 0
1174
- * ```
1175
- */
1176
- function countLines(content) {
1177
- if (!isText(content)) return 0;
1178
- if (content.text.length === 0) return 0;
1179
- let count = 1;
1180
- for (const character of content.text) if (character === "\n") count += 1;
1181
- return count;
1182
- }
1183
- /**
1184
- * Compute the decoded byte length of a base64 string ARITHMETICALLY — the image-payload
1185
- * sizing primitive {@link computeSize} uses, with no `atob` / `Buffer` decode.
1186
- *
1187
- * @remarks
1188
- * A base64 string encodes each group of 3 input bytes as 4 characters, so a well-formed
1189
- * string's length is a multiple of 4 and its decoded length is `(length / 4) * 3` MINUS
1190
- * the trailing `=` padding (one `=` ⇒ the last group held 2 bytes, two `==` ⇒ 1 byte).
1191
- * The padding is counted from the trailing `=` characters, so both the `=` and `==` cases
1192
- * are handled. The empty string decodes to `0`. Total — never throws (a malformed,
1193
- * non-multiple-of-4 length still yields a defined non-negative estimate via `floor`).
1194
- *
1195
- * @param base64 - The base64-encoded string (e.g. an image's `data`)
1196
- * @returns The decoded payload's byte length
1197
- *
1198
- * @example
1199
- * ```ts
1200
- * decodedSize('') // 0
1201
- * decodedSize('AAAA') // 3 (no padding)
1202
- * decodedSize('AAA=') // 2 (one '=')
1203
- * decodedSize('AA==') // 1 (two '=')
1204
- * ```
1205
- */
1206
- function decodedSize(base64) {
1207
- if (base64.length === 0) return 0;
1208
- let padding = 0;
1209
- if (base64.endsWith("==")) padding = 2;
1210
- else if (base64.endsWith("=")) padding = 1;
1211
- return Math.floor(base64.length / 4) * 3 - padding;
1212
- }
1213
- /**
1214
- * Whether a {@link Range} is STRUCTURALLY valid — the predicate a ranged `write` checks
1215
- * before applying (a `false` here is the `RANGE` throw).
1216
- *
1217
- * @remarks
1218
- * Structural validity is independent of any content: every component must be `>= 1`
1219
- * (1-based lines and columns), and `start` must not come after `end` (an inverted range
1220
- * is invalid). A range that is structurally valid but reaches PAST the end of a specific
1221
- * text is still valid — it is {@link clampRange}d to the bounds when applied, not rejected.
1222
- * Total — never throws.
1223
- *
1224
- * @param range - The range to validate
1225
- * @returns `true` when every component is `>= 1` and `start` is at or before `end`
1226
- *
1227
- * @example
1228
- * ```ts
1229
- * isValidRange({ start: { line: 1, column: 1 }, end: { line: 2, column: 1 } }) // true
1230
- * isValidRange({ start: { line: 2, column: 1 }, end: { line: 1, column: 1 } }) // false (inverted)
1231
- * isValidRange({ start: { line: 0, column: 1 }, end: { line: 1, column: 1 } }) // false (sub-1 line)
1232
- * ```
1233
- */
1234
- function isValidRange(range) {
1235
- if (range.start.line < 1 || range.start.column < 1) return false;
1236
- if (range.end.line < 1 || range.end.column < 1) return false;
1237
- if (range.start.line > range.end.line) return false;
1238
- return !(range.start.line === range.end.line && range.start.column > range.end.column);
1239
- }
1240
- /**
1241
- * Clamp a 1-based {@link Position} to the bounds of `text` — every component pinned into a
1242
- * caret that actually exists in the content.
1243
- *
1244
- * @remarks
1245
- * `line` is pinned to `[1, lineCount]` and `column` to `[1, lineLength + 1]` of the
1246
- * resolved line (column `lineLength + 1` is the caret just past the line's last
1247
- * character). So a position beyond the end of the text resolves to the end rather than
1248
- * overflowing. Total — never throws.
1249
- *
1250
- * @param text - The text the position addresses
1251
- * @param position - The 1-based position to clamp
1252
- * @returns The clamped {@link Position}
1253
- *
1254
- * @example
1255
- * ```ts
1256
- * clampPosition('ab\ncd', { line: 9, column: 9 }) // { line: 2, column: 3 } (end of 'cd')
1257
- * ```
1258
- */
1259
- function clampPosition(text, position) {
1260
- const lines = text.split("\n");
1261
- const line = Math.max(1, Math.min(position.line, lines.length));
1262
- const lineText = lines[line - 1] ?? "";
1263
- return {
1264
- line,
1265
- column: Math.max(1, Math.min(position.column, lineText.length + 1))
1266
- };
1267
- }
1268
- /**
1269
- * Clamp both ends of a {@link Range} to the bounds of `text` — the actual span a ranged
1270
- * read / write applies (and the `range` a {@link import('./types.js').ReadResult} reports).
1271
- *
1272
- * @remarks
1273
- * Clamps `start` and `end` independently via {@link clampPosition}, so a range reaching
1274
- * past the end of the content is trimmed to the content's end. Total — never throws.
1275
- *
1276
- * @param text - The text the range addresses
1277
- * @param range - The 1-based range to clamp
1278
- * @returns The clamped {@link Range}
1279
- *
1280
- * @example
1281
- * ```ts
1282
- * clampRange('ab\ncd', { start: { line: 1, column: 1 }, end: { line: 9, column: 9 } })
1283
- * // { start: { line: 1, column: 1 }, end: { line: 2, column: 3 } }
1284
- * ```
1285
- */
1286
- function clampRange(text, range) {
1287
- return {
1288
- start: clampPosition(text, range.start),
1289
- end: clampPosition(text, range.end)
1290
- };
1291
- }
1292
- /**
1293
- * Convert a 1-based {@link Position} to a 0-based string offset into `text` — the indexing
1294
- * primitive {@link sliceRange} / {@link spliceRange} use, clamped to `text.length`.
1295
- *
1296
- * @remarks
1297
- * Sums each preceding line's length plus its `\n` separator, then adds the `column - 1`
1298
- * within the target line, capped at `text.length`. Total — never throws (an out-of-bounds
1299
- * position yields a defined in-range offset).
1300
- *
1301
- * @param text - The text to index into
1302
- * @param position - The 1-based position to resolve
1303
- * @returns The 0-based offset (in `[0, text.length]`)
1304
- *
1305
- * @example
1306
- * ```ts
1307
- * offsetAt('ab\ncd', { line: 2, column: 1 }) // 3 (just after 'ab\n')
1308
- * ```
1309
- */
1310
- function offsetAt(text, position) {
1311
- const lines = text.split("\n");
1312
- let offset = 0;
1313
- for (let index = 0; index < position.line - 1 && index < lines.length; index += 1) offset += (lines[index]?.length ?? 0) + 1;
1314
- offset += position.column - 1;
1315
- return Math.min(offset, text.length);
1316
- }
1317
- /**
1318
- * Slice the substring of `text` spanned by a {@link Range} (start INCLUSIVE, end
1319
- * EXCLUSIVE), clamping the range to the text's bounds first — the read half of the ranged
1320
- * edit surface.
1321
- *
1322
- * @remarks
1323
- * Clamps via {@link clampRange}, resolves each end to an offset via {@link offsetAt}, and
1324
- * returns `text.slice(startOffset, endOffset)`. Total — never throws.
1325
- *
1326
- * @param text - The text to slice
1327
- * @param range - The 1-based range to extract
1328
- * @returns The spanned substring (empty when the clamped span is empty)
1329
- *
1330
- * @example
1331
- * ```ts
1332
- * sliceRange('hello\nworld', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }) // 'hello'
1333
- * ```
1334
- */
1335
- function sliceRange(text, range) {
1336
- const clamped = clampRange(text, range);
1337
- return text.slice(offsetAt(text, clamped.start), offsetAt(text, clamped.end));
1338
- }
1339
- /**
1340
- * Replace the span of `text` covered by a {@link Range} with `replacement` (start
1341
- * INCLUSIVE, end EXCLUSIVE), clamping the range to the text's bounds first — the write
1342
- * half of the ranged edit surface.
1343
- *
1344
- * @remarks
1345
- * Clamps via {@link clampRange}, then stitches `before + replacement + after` around the
1346
- * resolved offsets. An empty span (`start === end`) becomes a pure insertion. Total —
1347
- * never throws.
1348
- *
1349
- * @param text - The original text
1350
- * @param range - The 1-based range to overwrite
1351
- * @param replacement - The text to splice in place of the spanned range
1352
- * @returns The text with the spanned range replaced by `replacement`
1353
- *
1354
- * @example
1355
- * ```ts
1356
- * spliceRange('hello', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }, 'bye') // 'bye'
1357
- * ```
1358
- */
1359
- function spliceRange(text, range, replacement) {
1360
- const clamped = clampRange(text, range);
1361
- const start = offsetAt(text, clamped.start);
1362
- const end = offsetAt(text, clamped.end);
1363
- return text.slice(0, start) + replacement + text.slice(end);
1364
- }
1365
- /**
1366
- * Assemble a 1-based nested {@link Range} from four FLAT caret integers — the bridge a
1367
- * workspace-editing tool (see `@orkestrel/tool`) uses from its small-model FLAT surface back to the
1368
- * nested {@link Range} the {@link import('./workspaces/Workspace.js').Workspace} edit surface
1369
- * speaks. Published as a public helper for that consumer.
1370
- *
1371
- * @remarks
1372
- * Pairs `(fromLine, fromColumn)` into `start` and `(toLine, toColumn)` into `end` verbatim — a pure
1373
- * structural lift, no validation (a structurally invalid range is rejected downstream by the ranged
1374
- * `write` it feeds, via {@link isValidRange}). Total — never throws. Zero-Node.
1375
- *
1376
- * @param fromLine - The 1-based start line
1377
- * @param fromColumn - The 1-based start column
1378
- * @param toLine - The 1-based end line
1379
- * @param toColumn - The 1-based end column
1380
- * @returns The `{ start, end }` {@link Range}
1381
- *
1382
- * @example
1383
- * ```ts
1384
- * rangeOf(1, 11, 1, 12) // { start: { line: 1, column: 11 }, end: { line: 1, column: 12 } }
1385
- * ```
1386
- */
1387
- function rangeOf(fromLine, fromColumn, toLine, toColumn) {
1388
- return {
1389
- start: {
1390
- line: fromLine,
1391
- column: fromColumn
1392
- },
1393
- end: {
1394
- line: toLine,
1395
- column: toColumn
1396
- }
1397
- };
1398
- }
1399
- /**
1400
846
  * Render a path-addressed text body as a fenced reference block — the framing an
1401
847
  * {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
1402
848
  * active workspace is the SOLE document/image context).
@@ -1406,7 +852,7 @@ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
1406
852
  * fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
1407
853
  * total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
1408
854
  * frames an active workspace's text files with it (each carries its own `language` on its
1409
- * {@link FileContent} text arm).
855
+ * {@link import('@orkestrel/workspace').FileContent} text arm).
1410
856
  *
1411
857
  * @param path - The file path shown on the `File:` label line
1412
858
  * @param language - The fenced-code language tag (e.g. `'typescript'`)
@@ -1425,29 +871,6 @@ function fencedFile(path, language, content) {
1425
871
  return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
1426
872
  }
1427
873
  /**
1428
- * Escape a string's regex-special characters so it matches LITERALLY inside a `RegExp` — the
1429
- * primitive a {@link import('./workspaces/Workspace.js').Workspace} search builds a literal-text
1430
- * search pattern through (as opposed to a caller-supplied regex pattern).
1431
- *
1432
- * @remarks
1433
- * Prefixes every character in the class `. * + ? ^ $ { } ( ) | [ ] \` with a backslash, so the
1434
- * escaped string, when compiled into a `RegExp`, matches only its own literal characters — no
1435
- * character acts as a quantifier, anchor, group, or class. Pure string assembly, total — never
1436
- * throws.
1437
- *
1438
- * @param value - The text to escape for literal use inside a `RegExp` pattern
1439
- * @returns `value` with every regex-special character backslash-escaped
1440
- *
1441
- * @example
1442
- * ```ts
1443
- * escapeRegExp('a.b*c') // 'a\\.b\\*c'
1444
- * new RegExp(escapeRegExp('a.b*c')).test('a.b*c') // true
1445
- * ```
1446
- */
1447
- function escapeRegExp(value) {
1448
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1449
- }
1450
- /**
1451
874
  * Sanitize one reported token count into a safe non-negative integer.
1452
875
  *
1453
876
  * @param value - The token count to sanitize
@@ -1489,7 +912,7 @@ function sanitizeUsage(usage) {
1489
912
  * at construction.
1490
913
  *
1491
914
  * @remarks
1492
- * A thin immutable value object (mirroring {@link import('../tools/Tool.js').Tool}): the
915
+ * A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
1493
916
  * constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
1494
917
  * `content`, resolves `priority` to the input's value or `0`, and carries the input's
1495
918
  * per-item `format` override ONLY when supplied (assigned just when present, mirroring a
@@ -1570,549 +993,49 @@ var InstructionManager = class {
1570
993
  get emitter() {
1571
994
  return this.#emitter;
1572
995
  }
1573
- get count() {
1574
- return this.#instructions.size;
1575
- }
1576
- get description() {
1577
- return this.#format?.open ?? "## Instructions";
1578
- }
1579
- get framing() {
1580
- return this.#format;
1581
- }
1582
- add(input) {
1583
- if ((0, _orkestrel_contract.isArray)(input)) return input.map((one) => this.#create(one));
1584
- return this.#create(input);
1585
- }
1586
- instruction(name) {
1587
- return this.#instructions.get(name);
1588
- }
1589
- instructions() {
1590
- return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
1591
- }
1592
- format(instruction) {
1593
- return this.#format?.render?.(instruction) ?? instruction.content;
1594
- }
1595
- remove(names) {
1596
- if ((0, _orkestrel_contract.isArray)(names)) {
1597
- let removed = false;
1598
- for (const name of names) if (this.#delete(name)) removed = true;
1599
- return removed;
1600
- }
1601
- return this.#delete(names);
1602
- }
1603
- clear() {
1604
- this.#instructions.clear();
1605
- this.#emitter.emit("clear");
1606
- }
1607
- #create(input) {
1608
- const instruction = new Instruction(input);
1609
- this.#instructions.set(instruction.name, instruction);
1610
- this.#emitter.emit("add", instruction);
1611
- return instruction;
1612
- }
1613
- #delete(name) {
1614
- const removed = this.#instructions.delete(name);
1615
- if (removed) this.#emitter.emit("remove", name);
1616
- return removed;
1617
- }
1618
- };
1619
- //#endregion
1620
- //#region src/core/tools/ToolManager.ts
1621
- /**
1622
- * The tool registry the agent loop dispatches model tool-calls through — resolves
1623
- * names, lists {@link ToolDefinition}s for the provider, and executes calls with
1624
- * per-call error isolation.
1625
- *
1626
- * @remarks
1627
- * - **Registry.** Tools live in an insertion-ordered `Map` keyed by `tool.name`;
1628
- * `add` takes one or a batch (§9.2) and a re-`add` of the same name OVERWRITES it
1629
- * (last write wins). `count` is the map size, `tool(name)` looks one up, `tools()`
1630
- * lists them in insertion order, and `definitions()` maps each to a plain
1631
- * {@link ToolDefinition} (`name` / `description?` / `parameters?`, the `execute`
1632
- * handler stripped) — exactly what a provider advertises to the model, with
1633
- * `tool.summary ?? tool.description` advertised as `description` (a lean `summary`
1634
- * stands in for the full text when set).
1635
- * - **Per-call error isolation (the load-bearing part).** `execute` resolves a
1636
- * {@link ToolCall}'s tool by name and ALWAYS resolves a {@link ToolResult}: an
1637
- * unknown name → `{ id, name, error: 'tool not found: <name>' }`; a successful run →
1638
- * `{ id, name, value }`; a handler throw is CAUGHT into `{ id, name, error }` (an
1639
- * `Error`'s message, else the stringified throw). A tool throw never escapes — it
1640
- * becomes a result the model can react to.
1641
- * - **Batch never fails as a whole.** `execute(calls)` runs every call via
1642
- * `Promise.all(calls.map(...))` and resolves the results correlated by `id` in the
1643
- * input order — a mix of success, throw, and not-found all resolve; one bad call
1644
- * does not reject the batch.
1645
- * - **Event-free.** A purely functional registry — no Emitter, no events.
1646
- *
1647
- * @example
1648
- * ```ts
1649
- * const manager = new ToolManager()
1650
- * manager.add(new Tool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
1651
- * const result = await manager.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
1652
- * result.value // 3
1653
- * ```
1654
- */
1655
- var ToolManager = class {
1656
- #tools = /* @__PURE__ */ new Map();
1657
- get count() {
1658
- return this.#tools.size;
1659
- }
1660
- add(tools) {
1661
- if ((0, _orkestrel_contract.isArray)(tools)) {
1662
- for (const tool of tools) this.#tools.set(tool.name, tool);
1663
- return;
1664
- }
1665
- this.#tools.set(tools.name, tools);
1666
- }
1667
- tool(name) {
1668
- return this.#tools.get(name);
1669
- }
1670
- tools() {
1671
- return [...this.#tools.values()];
1672
- }
1673
- definitions() {
1674
- return [...this.#tools.values()].map((tool) => this.#definition(tool));
1675
- }
1676
- execute(call) {
1677
- if ((0, _orkestrel_contract.isArray)(call)) return Promise.all(call.map((one) => this.#run(one)));
1678
- return this.#run(call);
1679
- }
1680
- remove(names) {
1681
- if ((0, _orkestrel_contract.isArray)(names)) {
1682
- let removed = false;
1683
- for (const name of names) if (this.#tools.delete(name)) removed = true;
1684
- return removed;
1685
- }
1686
- return this.#tools.delete(names);
1687
- }
1688
- clear() {
1689
- this.#tools.clear();
1690
- }
1691
- async #run(call) {
1692
- const tool = this.#tools.get(call.name);
1693
- if (tool === void 0) return {
1694
- id: call.id,
1695
- name: call.name,
1696
- error: `tool not found: ${call.name}`
1697
- };
1698
- try {
1699
- const value = await tool.execute(call.arguments);
1700
- return {
1701
- id: call.id,
1702
- name: call.name,
1703
- value
1704
- };
1705
- } catch (error) {
1706
- return {
1707
- id: call.id,
1708
- name: call.name,
1709
- error: error instanceof Error ? error.message : String(error)
1710
- };
1711
- }
1712
- }
1713
- #definition(tool) {
1714
- const definition = { name: tool.name };
1715
- const description = tool.summary ?? tool.description;
1716
- if (description !== void 0) definition.description = description;
1717
- if (tool.parameters !== void 0) definition.parameters = tool.parameters;
1718
- return definition;
1719
- }
1720
- };
1721
- //#endregion
1722
- //#region src/core/workspaces/Workspace.ts
1723
- /**
1724
- * A mutable, `path`-keyed working set of immutable {@link FileInterface}s — the
1725
- * in-memory editing surface over the file primitive.
1726
- *
1727
- * @remarks
1728
- * - **Registry.** Files live in an insertion-ordered `Map` keyed by `path`; `count` is the
1729
- * map size, `file(path)` looks one up, and `files()` lists them in insertion order.
1730
- * - **Write replaces the File (§11 immutability).** Every edit MINTS a replacement
1731
- * {@link FileInterface} (via {@link import('../factories.js').createFile}) rather than
1732
- * mutating in place, transitioning `state` to `'created'` for a brand-new path or
1733
- * `'modified'` for an existing one. A whole-file string write preserves an existing text
1734
- * file's `language`, else infers it from the `path` (`inferLanguage`); writing a string
1735
- * onto an existing BINARY path replaces it with a text file (a deliberate retype). A
1736
- * ranged `write` splices a `Range` of an existing text file.
1737
- * - **Modality rules.** Text-only ops on a binary file are rejected: a ranged `read` /
1738
- * `write`, `prepend`, and `append` throw `MODALITY`; a plain `read(path)` of a binary
1739
- * file returns `undefined`; `search` / `replace` skip binary files (0 matches).
1740
- * - **In-memory removal.** `remove(path)` / `remove(paths)` drop files outright (no
1741
- * tombstone — the `'loaded'` / `'deleted'` states are reserved for a future FileStore);
1742
- * `remove()` and `clear()` both empty the registry and emit the single `clear` signal.
1743
- * The disk/sync lifecycle (`load` / `revert` / `accept` / `purge` / `dirty`) is NOT part
1744
- * of this surface.
1745
- * - **Observable (§13).** The owned {@link emitter} ({@link WorkspaceEventMap}) carries
1746
- * `write` (the resulting file) / `remove` (the path) / `move` (`{ from, to }`) / `clear`.
1747
- * Every event is emitted directly, strictly AFTER the map mutation completes; the
1748
- * emitter isolates a listener throw and routes it to its `error` handler, so a buggy
1749
- * observer can never corrupt a mutation.
1750
- *
1751
- * @example
1752
- * ```ts
1753
- * const workspace = new Workspace()
1754
- * workspace.write('src/main.ts', 'const x = 1')
1755
- * workspace.file('src/main.ts')?.state // 'created'
1756
- * workspace.append('src/main.ts', '\nconst y = 2')
1757
- * workspace.file('src/main.ts')?.state // 'modified'
1758
- * workspace.read('src/main.ts') // 'const x = 1\nconst y = 2'
1759
- * ```
1760
- */
1761
- var Workspace = class {
1762
- #id;
1763
- #files = /* @__PURE__ */ new Map();
1764
- #emitter;
1765
- constructor(options, seed) {
1766
- this.#id = options?.id ?? crypto.randomUUID();
1767
- this.#emitter = new _orkestrel_emitter.Emitter({
1768
- ...options?.on === void 0 ? {} : { on: options.on },
1769
- ...options?.error === void 0 ? {} : { error: options.error }
1770
- });
1771
- if (seed) for (const [path, file] of seed) this.#files.set(path, file);
1772
- }
1773
- get id() {
1774
- return this.#id;
1775
- }
1776
- get emitter() {
1777
- return this.#emitter;
1778
- }
1779
- get count() {
1780
- return this.#files.size;
1781
- }
1782
- file(path) {
1783
- return this.#files.get(path);
1784
- }
1785
- files() {
1786
- return [...this.#files.values()];
1787
- }
1788
- read(path, range) {
1789
- if ((0, _orkestrel_contract.isArray)(path)) {
1790
- const result = {};
1791
- for (const one of path) {
1792
- const file = this.#files.get(one);
1793
- if (file && isText(file.content)) result[one] = file.content.text;
1794
- }
1795
- return result;
1796
- }
1797
- const file = this.#files.get(path);
1798
- if (!file) return void 0;
1799
- if (!range) return isText(file.content) ? file.content.text : void 0;
1800
- if (!isText(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
1801
- return {
1802
- content: sliceRange(file.content.text, range),
1803
- range: clampRange(file.content.text, range)
1804
- };
1805
- }
1806
- has(path) {
1807
- if ((0, _orkestrel_contract.isArray)(path)) return path.some((one) => this.#files.has(one));
1808
- return this.#files.has(path);
1809
- }
1810
- search(query, options) {
1811
- const pattern = this.#pattern(query, options);
1812
- const limit = options?.limit;
1813
- const matches = [];
1814
- for (const file of this.#files.values()) {
1815
- if (limit !== void 0 && matches.length >= limit) break;
1816
- if (!isText(file.content)) continue;
1817
- const lines = file.content.text.split("\n");
1818
- for (let index = 0; index < lines.length; index += 1) {
1819
- if (limit !== void 0 && matches.length >= limit) break;
1820
- const lineText = lines[index] ?? "";
1821
- pattern.lastIndex = 0;
1822
- let hit = pattern.exec(lineText);
1823
- while (hit !== null) {
1824
- if (limit !== void 0 && matches.length >= limit) break;
1825
- matches.push({
1826
- path: file.path,
1827
- line: index + 1,
1828
- column: hit.index + 1,
1829
- length: hit[0].length,
1830
- content: lineText
1831
- });
1832
- if (hit[0].length === 0) pattern.lastIndex += 1;
1833
- hit = pattern.exec(lineText);
1834
- }
1835
- }
1836
- }
1837
- return matches;
1838
- }
1839
- replace(query, replacement, options) {
1840
- const pattern = this.#pattern(query, options);
1841
- const limit = options?.limit;
1842
- let replaced = 0;
1843
- let files = 0;
1844
- for (const [path, file] of this.#files) {
1845
- if (limit !== void 0 && replaced >= limit) break;
1846
- if (!isText(file.content)) continue;
1847
- const remaining = limit === void 0 ? void 0 : limit - replaced;
1848
- let count = 0;
1849
- pattern.lastIndex = 0;
1850
- const next = file.content.text.replace(pattern, (match) => {
1851
- if (remaining !== void 0 && count >= remaining) return match;
1852
- count += 1;
1853
- return replacement;
1854
- });
1855
- if (count > 0) {
1856
- replaced += count;
1857
- files += 1;
1858
- this.write(path, next);
1859
- }
1860
- }
1861
- return {
1862
- query,
1863
- replaced,
1864
- files
1865
- };
1866
- }
1867
- write(path, content, range) {
1868
- if ((0, _orkestrel_contract.isRecord)(path)) {
1869
- for (const [one, text] of Object.entries(path)) this.#write(one, text);
1870
- return;
1871
- }
1872
- const text = content ?? "";
1873
- if (!range) {
1874
- this.#write(path, text);
1875
- return;
1876
- }
1877
- this.#splice(path, text, range);
1878
- }
1879
- prepend(path, content) {
1880
- if ((0, _orkestrel_contract.isRecord)(path)) {
1881
- for (const [one, text] of Object.entries(path)) this.#prepend(one, text);
1882
- return;
1883
- }
1884
- this.#prepend(path, content ?? "");
1885
- }
1886
- append(path, content) {
1887
- if ((0, _orkestrel_contract.isRecord)(path)) {
1888
- for (const [one, text] of Object.entries(path)) this.#append(one, text);
1889
- return;
1890
- }
1891
- this.#append(path, content ?? "");
1892
- }
1893
- move(from, to) {
1894
- if ((0, _orkestrel_contract.isRecord)(from)) {
1895
- let moved = false;
1896
- for (const [one, target] of Object.entries(from)) if (this.#move(one, target)) moved = true;
1897
- return moved;
1898
- }
1899
- return this.#move(from, to ?? "");
1900
- }
1901
- remove(path) {
1902
- if (path === void 0) {
1903
- this.#files.clear();
1904
- this.#emitter.emit("clear");
1905
- return;
1906
- }
1907
- if ((0, _orkestrel_contract.isArray)(path)) {
1908
- let removed = false;
1909
- for (const one of path) if (this.#remove(one)) removed = true;
1910
- return removed;
1911
- }
1912
- return this.#remove(path);
1913
- }
1914
- clear() {
1915
- this.#files.clear();
1916
- this.#emitter.emit("clear");
1917
- }
1918
- snapshot() {
1919
- return {
1920
- id: this.#id,
1921
- files: this.files()
1922
- };
1923
- }
1924
- #write(path, content) {
1925
- const existing = this.#files.get(path);
1926
- const file = createFile({
1927
- path,
1928
- content: {
1929
- text: content,
1930
- language: existing && isText(existing.content) ? existing.content.language : inferLanguage(path)
1931
- },
1932
- state: existing ? "modified" : "created"
1933
- });
1934
- this.#files.set(path, file);
1935
- this.#emitter.emit("write", file);
1936
- }
1937
- #splice(path, content, range) {
1938
- const existing = this.#files.get(path);
1939
- if (!existing || !isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a non-text file: ${path}`, { path });
1940
- if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
1941
- path,
1942
- range
1943
- });
1944
- const file = createFile({
1945
- path,
1946
- content: {
1947
- text: spliceRange(existing.content.text, range, content),
1948
- language: existing.content.language
1949
- },
1950
- state: "modified"
1951
- });
1952
- this.#files.set(path, file);
1953
- this.#emitter.emit("write", file);
1954
- }
1955
- #prepend(path, content) {
1956
- this.#write(path, content + this.#text(path, "prepend"));
1957
- }
1958
- #append(path, content) {
1959
- this.#write(path, this.#text(path, "append") + content);
1960
- }
1961
- #text(path, operation) {
1962
- const existing = this.#files.get(path);
1963
- if (!existing) return "";
1964
- if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot ${operation} text to a binary file: ${path}`, { path });
1965
- return existing.content.text;
1966
- }
1967
- #move(from, to) {
1968
- const file = this.#files.get(from);
1969
- if (!file) return false;
1970
- const moved = createFile({
1971
- path: to,
1972
- content: file.content,
1973
- state: "modified"
1974
- });
1975
- this.#files.delete(from);
1976
- this.#files.set(to, moved);
1977
- this.#emitter.emit("move", {
1978
- from,
1979
- to
1980
- });
1981
- return true;
1982
- }
1983
- #remove(path) {
1984
- const removed = this.#files.delete(path);
1985
- if (removed) this.#emitter.emit("remove", path);
1986
- return removed;
1987
- }
1988
- #pattern(query, options) {
1989
- const source = options?.regex === true ? query : escapeRegExp(query);
1990
- const flags = options?.exact === false ? "gi" : "g";
1991
- try {
1992
- return new RegExp(source, flags);
1993
- } catch {
1994
- throw new WorkspaceError("PATTERN", `Invalid search pattern: ${query}`, { query });
1995
- }
1996
- }
1997
- };
1998
- //#endregion
1999
- //#region src/core/workspaces/WorkspaceManager.ts
2000
- /**
2001
- * The registry of {@link Workspace}s keyed by `id`, in insertion order, WITH an active pointer
2002
- * — the §9 store over the workspace layer PLUS the `active` / `switch` seam the context renders.
2003
- * Event-free (a registry, like {@link ConversationManager}); the observability lives on each
2004
- * {@link Workspace}.
2005
- *
2006
- * @remarks
2007
- * - **Registry.** Workspaces live in an insertion-ordered `Map` keyed by `id`. `add(input?)`
2008
- * mints a {@link Workspace} (its `id` from `input` or `crypto.randomUUID()`), flowing the
2009
- * manager's default `on` / `error` in unless the `input` OVERRIDES them, and stores it (an
2010
- * already-present `id` OVERWRITES — last write wins). `count` is the map size, `workspace(id)`
2011
- * looks one up, `workspaces()` lists them in insertion order.
2012
- * - **Active pointer.** `active` is the active workspace (what the context renders), `undefined`
2013
- * until the FIRST `add` (which auto-activates it — a registry with workspaces always has one
2014
- * active). A subsequent `add` leaves `active` unchanged. `switch(id)` re-points `active` to the
2015
- * workspace with `id` and returns it; an unknown `id` returns `undefined` and leaves `active`
2016
- * unchanged (the lenient lookup style — never throws, no new error code).
2017
- * - **Removal.** `remove` drops one by id, or a batch (§9.2) — `true` when any was removed;
2018
- * removing the ACTIVE workspace sets `active` to `undefined`. `clear` empties the registry and
2019
- * sets `active` to `undefined`.
2020
- * - **Durable open / save (the optional `store`).** With a {@link WorkspaceStoreInterface} supplied
2021
- * (the `store` option), `open(id)` HYDRATES a workspace on a registry miss — it `store.get(id)`s
2022
- * the snapshot and rebuilds a fresh {@link Workspace} through the constructor `seed`
2023
- * (`snapshot.files` → path → File), then activates it — and `save(id)` PERSISTS a registered
2024
- * workspace's `snapshot()`. Both are LENIENT without a store (open resolves only registered ids,
2025
- * save is a no-op `false`), consistent with the lenient `switch`.
2026
- * - **Event-free.** A purely registry store — no Emitter, no events (each workspace owns its own
2027
- * observable `emitter`).
2028
- *
2029
- * @example
2030
- * ```ts
2031
- * const manager = new WorkspaceManager()
2032
- * const first = manager.add() // auto-activates — active === first
2033
- * manager.add({ id: 'scratch' }) // leaves active unchanged
2034
- * manager.switch('scratch') // re-points active to the 'scratch' workspace
2035
- * manager.count // 2
2036
- * ```
2037
- */
2038
- var WorkspaceManager = class {
2039
- #workspaces = /* @__PURE__ */ new Map();
2040
- #active;
2041
- #on;
2042
- #error;
2043
- #store;
2044
- constructor(options) {
2045
- this.#on = options?.on;
2046
- this.#error = options?.error;
2047
- this.#store = options?.store;
2048
- }
2049
- get count() {
2050
- return this.#workspaces.size;
2051
- }
2052
- get active() {
2053
- return this.#active === void 0 ? void 0 : this.#workspaces.get(this.#active);
996
+ get count() {
997
+ return this.#instructions.size;
2054
998
  }
2055
- workspace(id) {
2056
- return this.#workspaces.get(id);
999
+ get description() {
1000
+ return this.#format?.open ?? "## Instructions";
2057
1001
  }
2058
- workspaces() {
2059
- return [...this.#workspaces.values()];
1002
+ get framing() {
1003
+ return this.#format;
2060
1004
  }
2061
1005
  add(input) {
2062
- const on = input?.on ?? this.#on;
2063
- const error = input?.error ?? this.#error;
2064
- const workspace = new Workspace({
2065
- ...input?.id === void 0 ? {} : { id: input.id },
2066
- ...on === void 0 ? {} : { on },
2067
- ...error === void 0 ? {} : { error }
2068
- }, input?.seed);
2069
- this.#workspaces.set(workspace.id, workspace);
2070
- if (this.#active === void 0) this.#active = workspace.id;
2071
- return workspace;
1006
+ if ((0, _orkestrel_contract.isArray)(input)) return input.map((one) => this.#create(one));
1007
+ return this.#create(input);
2072
1008
  }
2073
- switch(id) {
2074
- const workspace = this.#workspaces.get(id);
2075
- if (workspace === void 0) return void 0;
2076
- this.#active = id;
2077
- return workspace;
1009
+ instruction(name) {
1010
+ return this.#instructions.get(name);
2078
1011
  }
2079
- async open(id) {
2080
- const existing = this.#workspaces.get(id);
2081
- if (existing !== void 0) {
2082
- this.#active = id;
2083
- return existing;
2084
- }
2085
- if (this.#store === void 0) return void 0;
2086
- const snapshot = await this.#store.get(id);
2087
- if (snapshot === void 0) return void 0;
2088
- const workspace = this.add({
2089
- id,
2090
- seed: snapshot.files.map((file) => [file.path, file])
2091
- });
2092
- this.#active = workspace.id;
2093
- return workspace;
1012
+ instructions() {
1013
+ return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
2094
1014
  }
2095
- async save(id) {
2096
- const workspace = this.#workspaces.get(id);
2097
- if (this.#store === void 0 || workspace === void 0) return false;
2098
- await this.#store.set(workspace.snapshot());
2099
- return true;
1015
+ format(instruction) {
1016
+ return this.#format?.render?.(instruction) ?? instruction.content;
2100
1017
  }
2101
- remove(ids) {
2102
- if ((0, _orkestrel_contract.isArray)(ids)) {
1018
+ remove(names) {
1019
+ if ((0, _orkestrel_contract.isArray)(names)) {
2103
1020
  let removed = false;
2104
- for (const id of ids) if (this.#drop(id)) removed = true;
1021
+ for (const name of names) if (this.#delete(name)) removed = true;
2105
1022
  return removed;
2106
1023
  }
2107
- return this.#drop(ids);
1024
+ return this.#delete(names);
2108
1025
  }
2109
1026
  clear() {
2110
- this.#workspaces.clear();
2111
- this.#active = void 0;
1027
+ this.#instructions.clear();
1028
+ this.#emitter.emit("clear");
2112
1029
  }
2113
- #drop(id) {
2114
- const removed = this.#workspaces.delete(id);
2115
- if (removed && this.#active === id) this.#active = void 0;
1030
+ #create(input) {
1031
+ const instruction = new Instruction(input);
1032
+ this.#instructions.set(instruction.name, instruction);
1033
+ this.#emitter.emit("add", instruction);
1034
+ return instruction;
1035
+ }
1036
+ #delete(name) {
1037
+ const removed = this.#instructions.delete(name);
1038
+ if (removed) this.#emitter.emit("remove", name);
2116
1039
  return removed;
2117
1040
  }
2118
1041
  };
@@ -2122,15 +1045,17 @@ var WorkspaceManager = class {
2122
1045
  * The richer turn context the agent loop assembles a provider request from — the optional
2123
1046
  * system prompt, the observable context managers (instructions / workspaces), the
2124
1047
  * {@link ConversationManagerInterface} message source (whose active conversation IS `messages`),
2125
- * the {@link ToolManagerInterface} registry, and a mutable active {@link ScopeInterface}.
1048
+ * the {@link ToolManagerInterface} registry, and an active {@link ScopeInterface} changed through
1049
+ * {@link AgentContextInterface.apply}.
2126
1050
  *
2127
1051
  * @remarks
2128
1052
  * - **Composition.** `system` is the optional system prompt; `instructions` / `tools` /
2129
1053
  * `workspaces` / `conversations` are the registries passed in `options` (bring your own), or
2130
1054
  * fresh empty ones when omitted (so `workspaces` is ALWAYS present); `messages` is the ACTIVE
2131
1055
  * conversation's live tail (ALWAYS defined — see below). `scope` is the active filter —
2132
- * `undefined` (the default) ⇒ no filtering; settable afterwards. `workspaces` / `conversations`
2133
- * are likewise SETTABLE (swap the whole registry between runs).
1056
+ * `undefined` (the default) ⇒ no filtering; change it through `apply(scope)`. The structural
1057
+ * `workspaces` / `conversations` registries are fixed at construction; switch their active
1058
+ * members through their own `switch(id)` methods.
2134
1059
  * - **The message source — the conversation registry's ACTIVE conversation.** `conversations` is a
2135
1060
  * {@link ConversationManagerInterface}; the context ENSURES it always has an active conversation
2136
1061
  * (at construction it `add`s a default when the manager has none), so the DYNAMIC `messages`
@@ -2193,10 +1118,10 @@ var AgentContext = class {
2193
1118
  constructor(options) {
2194
1119
  this.#system = options?.system;
2195
1120
  this.#instructions = options?.instructions ?? new InstructionManager();
2196
- this.#workspaces = options?.workspaces ?? new WorkspaceManager();
1121
+ this.#workspaces = options?.workspaces ?? new _orkestrel_workspace.WorkspaceManager();
2197
1122
  this.#conversations = options?.conversations ?? new ConversationManager();
2198
1123
  if (this.#conversations.active === void 0) this.#conversations.add();
2199
- this.#tools = options?.tools ?? new ToolManager();
1124
+ this.#tools = options?.tools ?? new _orkestrel_tool.ToolManager();
2200
1125
  this.#scope = options?.scope;
2201
1126
  }
2202
1127
  get system() {
@@ -2208,27 +1133,20 @@ var AgentContext = class {
2208
1133
  get workspaces() {
2209
1134
  return this.#workspaces;
2210
1135
  }
2211
- set workspaces(value) {
2212
- this.#workspaces = value;
2213
- }
2214
1136
  get messages() {
2215
1137
  return this.#conversations.active ?? this.#ensure();
2216
1138
  }
2217
1139
  get conversations() {
2218
1140
  return this.#conversations;
2219
1141
  }
2220
- set conversations(value) {
2221
- this.#conversations = value;
2222
- if (this.#conversations.active === void 0) this.#conversations.add();
2223
- }
2224
1142
  get tools() {
2225
1143
  return this.#tools;
2226
1144
  }
2227
1145
  get scope() {
2228
1146
  return this.#scope;
2229
1147
  }
2230
- set scope(value) {
2231
- this.#scope = value;
1148
+ apply(scope) {
1149
+ this.#scope = scope;
2232
1150
  }
2233
1151
  build(format) {
2234
1152
  const scope = this.#scope;
@@ -2237,7 +1155,7 @@ var AgentContext = class {
2237
1155
  const instructions = filterAllowList(scope?.instructions, this.#instructions.instructions(), (one) => one.name);
2238
1156
  this.#section(parts, this.#header(this.#instructions, format?.instructions), instructions, (one) => this.#render(this.#instructions, format?.instructions, one), this.#footer(this.#instructions, format?.instructions));
2239
1157
  const files = filterAllowList(scope?.files, this.#workspaces.active?.files() ?? [], (one) => one.path);
2240
- const workspaceTexts = files.filter((file) => isText(file.content));
1158
+ const workspaceTexts = files.filter((file) => (0, _orkestrel_workspace.isText)(file.content));
2241
1159
  this.#section(parts, WORKSPACE_SECTION_HEADER, workspaceTexts, (file) => this.#fenced(file), void 0);
2242
1160
  const conversation = (this.#conversations.active ?? this.#ensure()).view();
2243
1161
  const tail = this.#attach(conversation, this.#workspaceImages(files));
@@ -2289,12 +1207,12 @@ var AgentContext = class {
2289
1207
  };
2290
1208
  }
2291
1209
  #fenced(file) {
2292
- if (isText(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
1210
+ if ((0, _orkestrel_workspace.isText)(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
2293
1211
  return fencedFile(file.path, "text", "");
2294
1212
  }
2295
1213
  #workspaceImages(files) {
2296
1214
  const data = [];
2297
- for (const file of files) if (isBinary(file.content) && isImage(file.content)) data.push(file.content.data);
1215
+ for (const file of files) if ((0, _orkestrel_workspace.isBinary)(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.data);
2298
1216
  return data;
2299
1217
  }
2300
1218
  #ensure() {
@@ -2628,7 +1546,7 @@ var Agent = class {
2628
1546
  };
2629
1547
  const toolMessage = this.#context.messages.add({
2630
1548
  role: "tool",
2631
- content: JSON.stringify(outcomeResult.value ?? outcomeResult.error)
1549
+ content: outcomeResult.success ? JSON.stringify(outcomeResult.value) : outcomeResult.error
2632
1550
  });
2633
1551
  messages.push(toolMessage);
2634
1552
  }
@@ -2705,6 +1623,7 @@ var Agent = class {
2705
1623
  }
2706
1624
  #denial(call, reason) {
2707
1625
  return {
1626
+ success: false,
2708
1627
  id: call.id,
2709
1628
  name: call.name,
2710
1629
  error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
@@ -2848,7 +1767,7 @@ var AgentRegistry = class {
2848
1767
  };
2849
1768
  }
2850
1769
  #manager(names) {
2851
- const manager = new ToolManager();
1770
+ const manager = new _orkestrel_tool.ToolManager();
2852
1771
  if (names !== void 0) for (const name of names) manager.add(this.tool(name));
2853
1772
  return manager;
2854
1773
  }
@@ -2878,8 +1797,8 @@ var AgentRegistry = class {
2878
1797
  * (an ALLOWLIST — only matched rules that allow get through), pass an `allowed: false`
2879
1798
  * `fallback`.
2880
1799
  * - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
2881
- * every {@link import('./types.js').ToolCall} the model emits; a denied call is fed
2882
- * back to the model as a denial {@link import('./types.js').ToolResult} (a `tool`
1800
+ * every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
1801
+ * back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
2883
1802
  * chunk + a tool message) instead of being executed — no tool run, no budget cost —
2884
1803
  * so the model sees the denial and can react.
2885
1804
  * - **Synchronous now.** The async human-approval handshake (request / grant / deny) is
@@ -2922,7 +1841,7 @@ var Authority = class {
2922
1841
  * conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
2923
1842
  * / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
2924
1843
  * {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
2925
- * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}.
1844
+ * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}.
2926
1845
  *
2927
1846
  * @remarks
2928
1847
  * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
@@ -2930,7 +1849,7 @@ var Authority = class {
2930
1849
  * IndexedDB backend swaps in WITHOUT touching the
2931
1850
  * {@link import('../ConversationManager.js').ConversationManager} or the
2932
1851
  * {@link import('../Conversation.js').Conversation} — the same seam as
2933
- * {@link import('../../workspaces/stores/DatabaseWorkspaceStore.js').DatabaseWorkspaceStore}. The
1852
+ * {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}. The
2934
1853
  * driver defaults to memory ({@link import('../../factories.js').createDatabaseConversationStore}
2935
1854
  * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
2936
1855
  * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
@@ -3001,12 +1920,12 @@ var DatabaseConversationStore = class {
3001
1920
  * The in-memory {@link ConversationStoreInterface} — a process-lifetime `Map` of
3002
1921
  * {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
3003
1922
  * {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
3004
- * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
1923
+ * {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
3005
1924
  *
3006
1925
  * @remarks
3007
1926
  * A plain `Map<string, ConversationSnapshot>` (AGENTS §21 — the snapshot is already pure,
3008
1927
  * self-contained JSON, so no encoding is needed for the memory tier). Like the
3009
- * {@link import('../../workspaces/stores/MemoryWorkspaceStore.js').MemoryWorkspaceStore} it twins,
1928
+ * {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
3010
1929
  * there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
3011
1930
  * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
3012
1931
  * the {@link import('../ConversationManager.js').ConversationManager} or the
@@ -3304,238 +2223,8 @@ var ThinkSplitter = class {
3304
2223
  }
3305
2224
  };
3306
2225
  //#endregion
3307
- //#region src/core/tools/Tool.ts
3308
- /**
3309
- * A registered tool — its {@link import('../types.js').ToolDefinition} schema (the
3310
- * `name` / `description` / `parameters` the model sees) bound to the `execute`
3311
- * handler that runs a call.
3312
- *
3313
- * @remarks
3314
- * A thin value object: the constructor stores the schema fields and the handler, and
3315
- * `execute` delegates to the handler verbatim. `parameters` is kept by reference (the
3316
- * open JSON-Schema the provider forwards as-is) — not cloned. The handler's `args` is
3317
- * the model-supplied `unknown` arguments record, narrowed inside the handler (§14);
3318
- * isolating a throw is the {@link ToolManager}'s job, not this class's. Event-free —
3319
- * no Emitter, no events.
3320
- *
3321
- * @example
3322
- * ```ts
3323
- * const tool = new Tool({
3324
- * name: 'add',
3325
- * description: 'Add two numbers',
3326
- * parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } } },
3327
- * execute: (args) => Number(args.a) + Number(args.b),
3328
- * })
3329
- * ```
3330
- */
3331
- var Tool = class {
3332
- name;
3333
- description;
3334
- summary;
3335
- parameters;
3336
- #execute;
3337
- constructor(options) {
3338
- this.name = options.name;
3339
- if (options.description !== void 0) this.description = options.description;
3340
- if (options.summary !== void 0) this.summary = options.summary;
3341
- if (options.parameters !== void 0) this.parameters = options.parameters;
3342
- this.#execute = options.execute;
3343
- }
3344
- execute(args) {
3345
- return this.#execute(args);
3346
- }
3347
- };
3348
- //#endregion
3349
- //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
3350
- /**
3351
- * A {@link WorkspaceStoreInterface} backed by one table of the `databases` layer — a workspace's
3352
- * durable state IS a row, so persistence reduces to keyed point-access (`get` / `set` / `delete`)
3353
- * over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
3354
- * {@link import('./MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
3355
- *
3356
- * @remarks
3357
- * The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
3358
- * JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
3359
- * IndexedDB backend swaps in WITHOUT touching the
3360
- * {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3361
- * {@link import('../Workspace.js').Workspace} — the same seam as
3362
- * the analogous `DatabaseWorkflowStore` in `@orkestrel/workflow`. The
3363
- * driver defaults to memory ({@link import('../../factories.js').createDatabaseWorkspaceStore}
3364
- * passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
3365
- * durable plumbing by passing a JSON / SQLite / IndexedDB driver.
3366
- *
3367
- * The {@link WorkspaceSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
3368
- * `{ id; snapshot }` ({@link WorkspaceSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
3369
- * column the factory builds) — exactly as `DatabaseWorkflowStore` stores its snapshot. The snapshot
3370
- * is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
3371
- * keeps the row type flat (`snapshot` reads back as `unknown`).
3372
- *
3373
- * - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
3374
- * the row `{ id: snapshot.id, snapshot }`.
3375
- * - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
3376
- * a {@link WorkspaceSnapshot} ({@link import('../../helpers.js').isWorkspaceSnapshot} — the AGENTS
3377
- * §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
3378
- * - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
3379
- *
3380
- * UNLIKE a session store there is NO idle-TTL / eviction — a persisted workspace lives until an
3381
- * explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
3382
- * §22 method bijection with {@link WorkspaceStoreInterface}). Hydration stays a caller concern: a
3383
- * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3384
- * live workspace through the constructor `seed` (its `open` / `save`).
3385
- *
3386
- * @example
3387
- * ```ts
3388
- * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspace } from '@src/core'
3389
- *
3390
- * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
3391
- * const workspace = createWorkspace()
3392
- * workspace.write('notes.txt', 'hello')
3393
- * await store.set(workspace.snapshot()) // persist the workspace (one JSON column)
3394
- * const snapshot = await store.get(workspace.id)
3395
- * await store.delete(workspace.id) // drop it
3396
- * ```
3397
- */
3398
- var DatabaseWorkspaceStore = class {
3399
- #table;
3400
- /**
3401
- * Wrap a table as a workspace store.
3402
- *
3403
- * @param table - The {@link TableInterface} holding the snapshots — its row is the
3404
- * {@link WorkspaceSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
3405
- */
3406
- constructor(table) {
3407
- this.#table = table;
3408
- }
3409
- /** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkspaceSnapshot`. */
3410
- async get(id) {
3411
- const row = await this.#table.get(id);
3412
- if (row === void 0) return void 0;
3413
- return isWorkspaceSnapshot(row.snapshot) ? row.snapshot : void 0;
3414
- }
3415
- /** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
3416
- async set(snapshot) {
3417
- await this.#table.set({
3418
- id: snapshot.id,
3419
- snapshot
3420
- });
3421
- }
3422
- /** Drop a snapshot by id; an absent id is a no-op (no throw). */
3423
- async delete(id) {
3424
- await this.#table.remove(id);
3425
- }
3426
- };
3427
- //#endregion
3428
- //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
3429
- /**
3430
- * The in-memory {@link WorkspaceStoreInterface} — a process-lifetime `Map` of
3431
- * {@link WorkspaceSnapshot}s keyed by workspace id, the DEFAULT store
3432
- * {@link import('../../factories.js').createMemoryWorkspaceStore} builds.
3433
- *
3434
- * @remarks
3435
- * A plain `Map<string, WorkspaceSnapshot>` (AGENTS §21 — the snapshot is already pure,
3436
- * self-contained JSON, so no encoding is needed for the memory tier). Like the
3437
- * the analogous `MemoryWorkflowStore` in `@orkestrel/workflow` it twins,
3438
- * there is NO idle-TTL and NO eviction: a persisted workspace lives until an explicit `delete`. A
3439
- * durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
3440
- * the {@link import('../WorkspaceManager.js').WorkspaceManager} or the
3441
- * {@link import('../Workspace.js').Workspace} — its driver-pluggable twin is
3442
- * {@link import('./DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} (the snapshot as one opaque
3443
- * JSON column).
3444
- *
3445
- * - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
3446
- * - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
3447
- * - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
3448
- *
3449
- * The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
3450
- * bijection with {@link WorkspaceStoreInterface}). Hydration is a caller concern: a
3451
- * {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
3452
- * live workspace through the constructor `seed` (its `open` / `save`).
3453
- *
3454
- * @example
3455
- * ```ts
3456
- * import { createMemoryWorkspaceStore, createWorkspace } from '@src/core'
3457
- *
3458
- * const store = createMemoryWorkspaceStore()
3459
- * const workspace = createWorkspace()
3460
- * workspace.write('notes.txt', 'hello')
3461
- * await store.set(workspace.snapshot()) // persist the workspace
3462
- * const snapshot = await store.get(workspace.id)
3463
- * await store.delete(workspace.id) // drop it
3464
- * ```
3465
- */
3466
- var MemoryWorkspaceStore = class {
3467
- #snapshots = /* @__PURE__ */ new Map();
3468
- get(id) {
3469
- return Promise.resolve(this.#snapshots.get(id));
3470
- }
3471
- set(snapshot) {
3472
- this.#snapshots.set(snapshot.id, snapshot);
3473
- return Promise.resolve();
3474
- }
3475
- delete(id) {
3476
- this.#snapshots.delete(id);
3477
- return Promise.resolve();
3478
- }
3479
- };
3480
- //#endregion
3481
2226
  //#region src/core/factories.ts
3482
2227
  /**
3483
- * Create a tool — a {@link ToolInterface} binding a {@link ToolDefinition} schema
3484
- * (the `name` / `description` / `parameters` the model sees) to the `execute` handler
3485
- * that runs a call.
3486
- *
3487
- * @remarks
3488
- * Only `name` is required (it keys the tool in a {@link ToolManagerInterface} and is
3489
- * what the model calls); `description` / `parameters` are the optional JSON-Schema the
3490
- * provider advertises (forwarded verbatim). The handler's `args` is the model-supplied
3491
- * `unknown` arguments record — narrow it inside (§14); a `createToolManager` registry
3492
- * isolates a throw into a `ToolResult.error`.
3493
- *
3494
- * @param options - `name` (required), optional `description` / `parameters`, and the
3495
- * `execute` handler (see {@link ToolOptions})
3496
- * @returns A working {@link ToolInterface}
3497
- *
3498
- * @example
3499
- * ```ts
3500
- * import { createTool } from '@src/core'
3501
- *
3502
- * const add = createTool({
3503
- * name: 'add',
3504
- * description: 'Add two numbers',
3505
- * execute: (args) => Number(args.a) + Number(args.b),
3506
- * })
3507
- * ```
3508
- */
3509
- function createTool(options) {
3510
- return new Tool(options);
3511
- }
3512
- /**
3513
- * Create a tool registry — a {@link ToolManagerInterface} that resolves tool names,
3514
- * lists {@link ToolDefinition}s for the provider, and executes calls with per-call
3515
- * error isolation.
3516
- *
3517
- * @remarks
3518
- * Starts empty; `add` registers one tool or a batch (§9.2), `definitions()` yields the
3519
- * schemas to hand a provider, and `execute` runs a {@link ToolCall} (or a batch),
3520
- * ALWAYS resolving a {@link ToolResult} — a handler throw becomes an `error` result and
3521
- * an unknown name a not-found `error`, so a tool throw never escapes and a batch never
3522
- * fails as a whole.
3523
- *
3524
- * @returns An empty {@link ToolManagerInterface}
3525
- *
3526
- * @example
3527
- * ```ts
3528
- * import { createTool, createToolManager } from '@src/core'
3529
- *
3530
- * const tools = createToolManager()
3531
- * tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
3532
- * const result = await tools.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
3533
- * ```
3534
- */
3535
- function createToolManager() {
3536
- return new ToolManager();
3537
- }
3538
- /**
3539
2228
  * Create a conversation — a {@link ConversationInterface} grouping messages above a flat
3540
2229
  * message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
3541
2230
  * rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
@@ -3609,11 +2298,13 @@ function createConversationManager(options) {
3609
2298
  * Create the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
3610
2299
  * process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
3611
2300
  * id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
3612
- * {@link ConversationManagerInterface.save} seam. The EXACT twin of {@link createMemoryWorkspaceStore}.
2301
+ * {@link ConversationManagerInterface.save} seam. The exact twin of
2302
+ * {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
3613
2303
  *
3614
2304
  * @remarks
3615
2305
  * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
3616
- * the structural twin of {@link createMemoryWorkspaceStore}. `get` / `set` / `delete` are async (the
2306
+ * the structural twin of {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
2307
+ * `get` / `set` / `delete` are async (the
3617
2308
  * same shape a durable backend fits); UNLIKE a session store there is NO idle-TTL / eviction — a
3618
2309
  * persisted conversation lives until an explicit `delete`. Its driver-pluggable twin is
3619
2310
  * {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
@@ -3641,12 +2332,14 @@ function createMemoryConversationStore() {
3641
2332
  /**
3642
2333
  * Create a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
3643
2334
  * driver-pluggable backing for the conversation persistence seam, the opt-in twin of
3644
- * {@link createMemoryConversationStore}. The EXACT twin of {@link createDatabaseWorkspaceStore}.
2335
+ * {@link createMemoryConversationStore}. The exact twin of
2336
+ * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
3645
2337
  *
3646
2338
  * @remarks
3647
2339
  * Builds a one-table database (`conversations`, keyed by `id`) over the supplied driver, the snapshot
3648
2340
  * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
3649
- * `rawShape` (a JSON blob), exactly as {@link createDatabaseWorkspaceStore} stores its snapshot. The
2341
+ * `rawShape` (a JSON blob), exactly as
2342
+ * {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
3650
2343
  * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
3651
2344
  * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
3652
2345
  * {@link import('./helpers.js').isConversationSnapshot}). The `driver` DEFAULTS to
@@ -3670,12 +2363,13 @@ function createMemoryConversationStore() {
3670
2363
  * ```
3671
2364
  */
3672
2365
  function createDatabaseConversationStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
2366
+ const columns = {
2367
+ id: (0, _orkestrel_contract.stringShape)(),
2368
+ snapshot: (0, _orkestrel_contract.rawShape)({})
2369
+ };
3673
2370
  return new DatabaseConversationStore((0, _orkestrel_database.createDatabase)({
3674
2371
  driver,
3675
- tables: { conversations: {
3676
- id: (0, _orkestrel_contract.stringShape)(),
3677
- snapshot: (0, _orkestrel_contract.rawShape)({})
3678
- } }
2372
+ tables: { conversations: columns }
3679
2373
  }).table("conversations"));
3680
2374
  }
3681
2375
  /**
@@ -3791,8 +2485,8 @@ function createScopeManager(options) {
3791
2485
  * @remarks
3792
2486
  * `system` is the optional system prompt; `tools` / `instructions` / `workspaces` are pre-built
3793
2487
  * managers to reuse (empty ones are created when omitted, so `context.workspaces` is ALWAYS
3794
- * present); `scope` is the initial active filter (`undefined` ⇒ no filtering, mutable afterwards
3795
- * via `context.scope`). The `messages` store is always fresh. `build()` folds the scoped
2488
+ * present); `scope` is the initial active filter (`undefined` ⇒ no filtering, changeable afterwards
2489
+ * via `context.apply(...)`). The `messages` store is always fresh. `build()` folds the scoped
3796
2490
  * instructions — PLUS the ACTIVE workspace's scope-filtered text files (fenced) — into ONE leading
3797
2491
  * `system` message and appends the scoped conversation (attaching the active workspace's
3798
2492
  * scope-filtered image files' `data` to the last user message), built fresh each call; the active
@@ -3942,8 +2636,9 @@ function createAuthority(options) {
3942
2636
  *
3943
2637
  * @example
3944
2638
  * ```ts
2639
+ * import { createAgentRegistry } from '@src/core'
3945
2640
  * import type { ProviderInterface } from '@src/core'
3946
- * import { createAgentRegistry, createTool } from '@src/core'
2641
+ * import { createTool } from '@orkestrel/tool'
3947
2642
  *
3948
2643
  * declare const provider: ProviderInterface // any concrete implementation supplied by the host app
3949
2644
  * const registry = createAgentRegistry({
@@ -4048,212 +2743,6 @@ function createAgentRunner(options) {
4048
2743
  handler: handleAgentRunnerJob.bind(void 0, registry, allowPartial)
4049
2744
  });
4050
2745
  }
4051
- /**
4052
- * Create a file — an immutable {@link FileInterface} from its `path` + {@link FileContent}
4053
- * and optional {@link import('./types.js').FileState}, with `size` / `lines` DERIVED from the
4054
- * content.
4055
- *
4056
- * @remarks
4057
- * Returns a PLAIN `Object.freeze`d record (NOT a class instance) — the `path` IS its identity
4058
- * (there is no `id`). Only `path` / `content` are required; `state` defaults to `'created'`
4059
- * when omitted. `size` (via {@link computeSize}) and `lines` (via {@link countLines}) are
4060
- * computed from the content here (so they never drift from it). Frozen + plain, so it
4061
- * `structuredClone`s losslessly and is never mutated after creation.
4062
- *
4063
- * @param input - `path` / `content` (required) and an optional `state` (see {@link FileInput})
4064
- * @returns A frozen {@link FileInterface} record
4065
- *
4066
- * @example
4067
- * ```ts
4068
- * import { createFile, createTextContent } from '@src/core'
4069
- *
4070
- * const file = createFile({ path: 'src/main.ts', content: createTextContent('const x = 1', 'typescript') })
4071
- * file.lines // 1
4072
- * ```
4073
- */
4074
- function createFile(input) {
4075
- return Object.freeze({
4076
- path: input.path,
4077
- content: input.content,
4078
- state: input.state ?? "created",
4079
- size: computeSize(input.content),
4080
- lines: countLines(input.content)
4081
- });
4082
- }
4083
- /**
4084
- * Build the TEXT {@link FileContent} arm — the §4.2.3 split constructor for text (a separate
4085
- * function per arm, not one constructor dispatching on a discriminator parameter).
4086
- *
4087
- * @param text - The literal text body
4088
- * @param language - The fenced-code language the text renders as (e.g. `'typescript'`)
4089
- * @returns A `{ text; language }` content arm
4090
- *
4091
- * @example
4092
- * ```ts
4093
- * import { createTextContent } from '@src/core'
4094
- *
4095
- * createTextContent('const x = 1', 'typescript') // { text: 'const x = 1', language: 'typescript' }
4096
- * ```
4097
- */
4098
- function createTextContent(text, language) {
4099
- return {
4100
- text,
4101
- language
4102
- };
4103
- }
4104
- /**
4105
- * Build the BINARY {@link FileContent} arm — the §4.2.3 split constructor for binary (a
4106
- * separate function per arm, not one constructor dispatching on a discriminator parameter).
4107
- * An image is just a binary with an image {@link BinaryMIME}.
4108
- *
4109
- * @param data - The base64-encoded binary payload
4110
- * @param mime - The {@link BinaryMIME} that labels the payload
4111
- * @returns A `{ data; mime }` content arm
4112
- *
4113
- * @example
4114
- * ```ts
4115
- * import { createBinaryContent } from '@src/core'
4116
- *
4117
- * createBinaryContent('<base64>', 'image/png') // { data: '<base64>', mime: 'image/png' }
4118
- * ```
4119
- */
4120
- function createBinaryContent(data, mime) {
4121
- return {
4122
- data,
4123
- mime
4124
- };
4125
- }
4126
- /**
4127
- * Create a workspace — a mutable, `path`-keyed working set of immutable
4128
- * {@link FileInterface}s with the in-memory edit surface (read / write / search / replace /
4129
- * move / remove), observable through its `EmitterInterface` (from `@orkestrel/emitter`).
4130
- *
4131
- * @param options - Optional initial {@link import('./types.js').WorkspaceEventMap} listeners (`on`) and the emitter's `error` handler (see {@link WorkspaceOptions})
4132
- * @returns A working {@link WorkspaceInterface}
4133
- *
4134
- * @example
4135
- * ```ts
4136
- * import { createWorkspace } from '@src/core'
4137
- *
4138
- * const workspace = createWorkspace({ on: { write: (file) => console.log(file.path) } })
4139
- * workspace.write('src/main.ts', 'const x = 1')
4140
- * workspace.file('src/main.ts')?.state // 'created'
4141
- * ```
4142
- */
4143
- function createWorkspace(options) {
4144
- return new Workspace(options);
4145
- }
4146
- /**
4147
- * Create the in-memory workspace store — a {@link WorkspaceStoreInterface} backed by a
4148
- * process-lifetime `Map` of {@link import('./types.js').WorkspaceSnapshot}s keyed by workspace id,
4149
- * the DEFAULT backing for the durable {@link WorkspaceManagerInterface.open} /
4150
- * {@link WorkspaceManagerInterface.save} seam.
4151
- *
4152
- * @remarks
4153
- * A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
4154
- * the structural twin of the analogous `createMemoryWorkflowStore` in `@orkestrel/workflow`.
4155
- * `get` / `set` / `delete` are async (the same shape a durable backend fits); UNLIKE a session
4156
- * store there is NO idle-TTL / eviction — a persisted workspace lives until an explicit `delete`.
4157
- * Its driver-pluggable twin is {@link createDatabaseWorkspaceStore} (the snapshot as one opaque
4158
- * JSON column over a `databases` table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB
4159
- * driver, and it swaps in WITHOUT touching the manager or the workspace. Hydration stays a manager
4160
- * concern: read a snapshot back and rebuild the live workspace through the constructor `seed`.
4161
- *
4162
- * @returns A memory-backed {@link WorkspaceStoreInterface}
4163
- *
4164
- * @example
4165
- * ```ts
4166
- * import { createMemoryWorkspaceStore, createWorkspaceManager } from '@src/core'
4167
- *
4168
- * const store = createMemoryWorkspaceStore()
4169
- * const manager = createWorkspaceManager({ store })
4170
- * const workspace = manager.add()
4171
- * workspace.write('notes.txt', 'hello')
4172
- * await manager.save(workspace.id) // persist the workspace
4173
- * ```
4174
- */
4175
- function createMemoryWorkspaceStore() {
4176
- return new MemoryWorkspaceStore();
4177
- }
4178
- /**
4179
- * Create a {@link DatabaseWorkspaceStore} over any {@link DriverInterface} — the durable,
4180
- * driver-pluggable backing for the workspace persistence seam, the opt-in twin of
4181
- * {@link createMemoryWorkspaceStore}.
4182
- *
4183
- * @remarks
4184
- * Builds a one-table database (`workspaces`, keyed by `id`) over the supplied driver, the snapshot
4185
- * held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
4186
- * `rawShape` (a JSON blob), exactly as
4187
- * the analogous `createDatabaseWorkflowStore` in `@orkestrel/workflow` stores its snapshot. The
4188
- * snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
4189
- * AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
4190
- * {@link import('./helpers.js').isWorkspaceSnapshot}). The `driver` DEFAULTS to
4191
- * {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
4192
- * `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
4193
- * the durability is the driver's job, the store engine is shared. It swaps in behind
4194
- * {@link WorkspaceStoreInterface} WITHOUT touching the manager or the workspace.
4195
- *
4196
- * @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
4197
- * @returns A {@link WorkspaceStoreInterface} over the driver
4198
- *
4199
- * @example
4200
- * ```ts
4201
- * import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspaceManager } from '@src/core'
4202
- *
4203
- * const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
4204
- * const manager = createWorkspaceManager({ store })
4205
- * const workspace = manager.add()
4206
- * workspace.write('notes.txt', 'hello')
4207
- * await manager.save(workspace.id) // persist the workspace (one JSON column)
4208
- * ```
4209
- */
4210
- function createDatabaseWorkspaceStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
4211
- return new DatabaseWorkspaceStore((0, _orkestrel_database.createDatabase)({
4212
- driver,
4213
- tables: { workspaces: {
4214
- id: (0, _orkestrel_contract.stringShape)(),
4215
- snapshot: (0, _orkestrel_contract.rawShape)({})
4216
- } }
4217
- }).table("workspaces"));
4218
- }
4219
- /**
4220
- * Create a workspace registry — a {@link WorkspaceManagerInterface} holding
4221
- * {@link WorkspaceInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
4222
- * the §9 store over the workspace layer plus the `active` / `switch` seam the context renders.
4223
- *
4224
- * @remarks
4225
- * Starts empty; `add(input?)` mints a {@link WorkspaceInterface} (its `id` from the input or a
4226
- * random UUID), flowing the manager's default `on` / `error` in unless the input overrides them,
4227
- * and stores it (an already-present `id` overwrites — last write wins) — and AUTO-ACTIVATES the
4228
- * FIRST one (a registry with workspaces always has one `active`); a later `add` leaves `active`
4229
- * unchanged. `switch(id)` re-points `active` (an unknown `id` returns `undefined`, leaving
4230
- * `active` unchanged — lenient, never throws); `workspace(id)` / `workspaces()` look up;
4231
- * `remove` (one or a batch, §9.2) reports whether any was removed AND clears `active` if it was
4232
- * the removed one; `clear` empties it and clears `active`. Event-free (each workspace owns its
4233
- * own observable `emitter`).
4234
- *
4235
- * With the optional `store` ({@link WorkspaceStoreInterface}, e.g. `createMemoryWorkspaceStore` /
4236
- * `createDatabaseWorkspaceStore`), `open(id)` HYDRATES a workspace on a registry miss (rebuilding it
4237
- * through the constructor `seed` from the snapshot's `files`, then activating it) and `save(id)`
4238
- * PERSISTS a registered workspace's `snapshot()`. Both are LENIENT without a store (open resolves
4239
- * only registered ids, save is a no-op `false`).
4240
- *
4241
- * @param options - Optional default `on` / `error` for created workspaces + the durable `store`
4242
- * (see {@link WorkspaceManagerOptions})
4243
- * @returns An empty {@link WorkspaceManagerInterface}
4244
- *
4245
- * @example
4246
- * ```ts
4247
- * import { createWorkspaceManager } from '@src/core'
4248
- *
4249
- * const workspaces = createWorkspaceManager()
4250
- * const scratch = workspaces.add() // auto-activates — workspaces.active === scratch
4251
- * scratch.write('notes.txt', 'hello')
4252
- * ```
4253
- */
4254
- function createWorkspaceManager(options) {
4255
- return new WorkspaceManager(options);
4256
- }
4257
2746
  //#endregion
4258
2747
  exports.Agent = Agent;
4259
2748
  exports.AgentContext = AgentContext;
@@ -4270,85 +2759,48 @@ exports.DEFAULT_AGENT_LIMIT = DEFAULT_AGENT_LIMIT;
4270
2759
  exports.DEFAULT_AUTHORITY_ZONE = DEFAULT_AUTHORITY_ZONE;
4271
2760
  exports.DEFAULT_CONVERSATION_KEEP = DEFAULT_CONVERSATION_KEEP;
4272
2761
  exports.DatabaseConversationStore = DatabaseConversationStore;
4273
- exports.DatabaseWorkspaceStore = DatabaseWorkspaceStore;
4274
- exports.EXTENSION_TO_LANGUAGE = EXTENSION_TO_LANGUAGE;
4275
2762
  exports.IMAGE_TOKEN_ESTIMATE = IMAGE_TOKEN_ESTIMATE;
4276
2763
  exports.Instruction = Instruction;
4277
2764
  exports.InstructionManager = InstructionManager;
4278
2765
  exports.MESSAGE_TOKEN_OVERHEAD = MESSAGE_TOKEN_OVERHEAD;
4279
2766
  exports.MemoryConversationStore = MemoryConversationStore;
4280
- exports.MemoryWorkspaceStore = MemoryWorkspaceStore;
4281
2767
  exports.ProviderAbortError = ProviderAbortError;
4282
2768
  exports.Scope = Scope;
4283
2769
  exports.ScopeManager = ScopeManager;
4284
2770
  exports.THINK_CLOSE = THINK_CLOSE;
4285
2771
  exports.THINK_OPEN = THINK_OPEN;
4286
2772
  exports.ThinkSplitter = ThinkSplitter;
4287
- exports.Tool = Tool;
4288
- exports.ToolManager = ToolManager;
4289
2773
  exports.WORKSPACE_SECTION_HEADER = WORKSPACE_SECTION_HEADER;
4290
- exports.Workspace = Workspace;
4291
- exports.WorkspaceError = WorkspaceError;
4292
- exports.WorkspaceManager = WorkspaceManager;
4293
- exports.buildToolResult = buildToolResult;
4294
- exports.clampPosition = clampPosition;
4295
- exports.clampRange = clampRange;
4296
- exports.computeSize = computeSize;
4297
- exports.countLines = countLines;
4298
2774
  exports.createAgent = createAgent;
4299
2775
  exports.createAgentContext = createAgentContext;
4300
2776
  exports.createAgentQueue = createAgentQueue;
4301
2777
  exports.createAgentRegistry = createAgentRegistry;
4302
2778
  exports.createAgentRunner = createAgentRunner;
4303
2779
  exports.createAuthority = createAuthority;
4304
- exports.createBinaryContent = createBinaryContent;
4305
2780
  exports.createConversation = createConversation;
4306
2781
  exports.createConversationManager = createConversationManager;
4307
2782
  exports.createDatabaseConversationStore = createDatabaseConversationStore;
4308
- exports.createDatabaseWorkspaceStore = createDatabaseWorkspaceStore;
4309
- exports.createFile = createFile;
4310
2783
  exports.createInstruction = createInstruction;
4311
2784
  exports.createInstructionManager = createInstructionManager;
4312
2785
  exports.createMemoryConversationStore = createMemoryConversationStore;
4313
- exports.createMemoryWorkspaceStore = createMemoryWorkspaceStore;
4314
2786
  exports.createScope = createScope;
4315
2787
  exports.createScopeManager = createScopeManager;
4316
- exports.createTextContent = createTextContent;
4317
2788
  exports.createThinkSplitter = createThinkSplitter;
4318
- exports.createTool = createTool;
4319
- exports.createToolManager = createToolManager;
4320
- exports.createWorkspace = createWorkspace;
4321
- exports.createWorkspaceManager = createWorkspaceManager;
4322
- exports.decodedSize = decodedSize;
4323
- exports.escapeRegExp = escapeRegExp;
4324
2789
  exports.estimateMessages = estimateMessages;
4325
2790
  exports.estimateTokens = estimateTokens;
4326
2791
  exports.fencedFile = fencedFile;
4327
2792
  exports.filterAllowList = filterAllowList;
4328
2793
  exports.handleAgentQueueJob = handleAgentQueueJob;
4329
2794
  exports.handleAgentRunnerJob = handleAgentRunnerJob;
4330
- exports.inferLanguage = inferLanguage;
4331
2795
  exports.isAgentError = isAgentError;
4332
2796
  exports.isAgentJobError = isAgentJobError;
4333
- exports.isBinary = isBinary;
4334
2797
  exports.isConversationError = isConversationError;
4335
2798
  exports.isConversationSnapshot = isConversationSnapshot;
4336
- exports.isFile = isFile;
4337
- exports.isImage = isImage;
4338
2799
  exports.isMessage = isMessage;
4339
2800
  exports.isProviderAbortError = isProviderAbortError;
4340
2801
  exports.isSection = isSection;
4341
- exports.isText = isText;
4342
- exports.isToolCall = isToolCall;
4343
- exports.isValidRange = isValidRange;
4344
- exports.isWorkspaceError = isWorkspaceError;
4345
- exports.isWorkspaceSnapshot = isWorkspaceSnapshot;
4346
- exports.offsetAt = offsetAt;
4347
- exports.rangeOf = rangeOf;
4348
2802
  exports.sanitizeToken = sanitizeToken;
4349
2803
  exports.sanitizeUsage = sanitizeUsage;
4350
2804
  exports.settleAgentJob = settleAgentJob;
4351
- exports.sliceRange = sliceRange;
4352
- exports.spliceRange = spliceRange;
4353
2805
 
4354
2806
  //# sourceMappingURL=index.cjs.map