@orkestrel/agent 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -9
- package/dist/src/core/index.cjs +127 -1614
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +108 -1716
- package/dist/src/core/index.d.ts +108 -1716
- package/dist/src/core/index.js +122 -1573
- package/dist/src/core/index.js.map +1 -1
- package/package.json +11 -9
package/dist/src/core/index.cjs
CHANGED
|
@@ -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('
|
|
471
|
+
* {@link import('@orkestrel/workspace').WorkspaceManager}); the observability lives
|
|
556
472
|
* on each {@link Conversation}.
|
|
557
473
|
*
|
|
558
474
|
* @remarks
|
|
@@ -667,28 +583,59 @@ var ConversationManager = class {
|
|
|
667
583
|
//#endregion
|
|
668
584
|
//#region src/core/helpers.ts
|
|
669
585
|
/**
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
* @remarks
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* ```
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
586
|
+
* Project an unknown value onto the canonical JSON representation of an
|
|
587
|
+
* {@link AgentResult}.
|
|
588
|
+
*
|
|
589
|
+
* @remarks
|
|
590
|
+
* This is a total hostile-boundary projection. Each structural field is captured once
|
|
591
|
+
* through Contract's sanctioned exception boundary, so conforming accessors and inherited
|
|
592
|
+
* properties are supported while a throwing getter or revoked proxy returns `undefined`.
|
|
593
|
+
* Present usage counts must be finite numbers; negative and fractional values are preserved,
|
|
594
|
+
* not normalized. Extra input properties are dropped while a fresh exact plain object is rebuilt
|
|
595
|
+
* and deep-gated through
|
|
596
|
+
* {@link import('@orkestrel/contract').parseJSONValue}.
|
|
597
|
+
*
|
|
598
|
+
* @param value - The unknown value to project
|
|
599
|
+
* @returns A fresh JSON value containing only AgentResult fields, or `undefined` when invalid
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```ts
|
|
603
|
+
* import { agentResultToJSON } from '@orkestrel/agent'
|
|
604
|
+
*
|
|
605
|
+
* agentResultToJSON({ content: 'done', usage: { prompt: 2, completion: 1, total: 3 }, partial: false })
|
|
606
|
+
* // { content: 'done', usage: { prompt: 2, completion: 1, total: 3 }, partial: false }
|
|
607
|
+
* ```
|
|
608
|
+
*/
|
|
609
|
+
function agentResultToJSON(value) {
|
|
610
|
+
const captured = (0, _orkestrel_contract.attempt)(() => {
|
|
611
|
+
if (!(0, _orkestrel_contract.isObject)(value)) return void 0;
|
|
612
|
+
const content = Reflect.get(value, "content");
|
|
613
|
+
const thinking = Reflect.get(value, "thinking");
|
|
614
|
+
const usage = Reflect.get(value, "usage");
|
|
615
|
+
const partial = Reflect.get(value, "partial");
|
|
616
|
+
if (!(0, _orkestrel_contract.isString)(content) || !(0, _orkestrel_contract.isBoolean)(partial)) return void 0;
|
|
617
|
+
if (thinking !== void 0 && !(0, _orkestrel_contract.isString)(thinking)) return void 0;
|
|
618
|
+
let projectedUsage;
|
|
619
|
+
if (usage !== void 0) {
|
|
620
|
+
if (!(0, _orkestrel_contract.isObject)(usage)) return void 0;
|
|
621
|
+
const prompt = Reflect.get(usage, "prompt");
|
|
622
|
+
const completion = Reflect.get(usage, "completion");
|
|
623
|
+
const total = Reflect.get(usage, "total");
|
|
624
|
+
if (!(0, _orkestrel_contract.isFiniteNumber)(prompt) || !(0, _orkestrel_contract.isFiniteNumber)(completion) || !(0, _orkestrel_contract.isFiniteNumber)(total)) return;
|
|
625
|
+
projectedUsage = {
|
|
626
|
+
prompt,
|
|
627
|
+
completion,
|
|
628
|
+
total
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
content,
|
|
633
|
+
...thinking === void 0 ? {} : { thinking },
|
|
634
|
+
...projectedUsage === void 0 ? {} : { usage: projectedUsage },
|
|
635
|
+
partial
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
return captured.success ? (0, _orkestrel_contract.parseJSONValue)(captured.value) : void 0;
|
|
692
639
|
}
|
|
693
640
|
/**
|
|
694
641
|
* Filter a list of items by a {@link import('./types.js').ScopeInterface} allow-list of
|
|
@@ -851,201 +798,22 @@ function handleAgentRunnerJob(registry, allowPartial, controller) {
|
|
|
851
798
|
return settleAgentJob(registry.build(controller.input, controller.signal), allowPartial);
|
|
852
799
|
}
|
|
853
800
|
/**
|
|
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
801
|
* Whether an `unknown` is structurally a {@link MessageInterface} record — the per-message step of
|
|
1036
802
|
* the {@link isConversationSnapshot} read-boundary narrow (AGENTS §14: narrow an untrusted storage
|
|
1037
|
-
* read via a guard, never an `as`). The conversation analogue of
|
|
803
|
+
* read via a guard, never an `as`). The conversation analogue of
|
|
804
|
+
* {@link import('@orkestrel/workspace').isFile}.
|
|
1038
805
|
*
|
|
1039
806
|
* @remarks
|
|
1040
807
|
* A total guard (it NEVER throws — adversarial input returns `false`). It checks the message's
|
|
1041
808
|
* 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
|
|
809
|
+
* — a `calls` array EVERY element of which is a valid
|
|
810
|
+
* {@link import('@orkestrel/tool').ToolCall} ({@link isToolCall} — the
|
|
1043
811
|
* ASI06 fail-closed deepening: a tampered `calls` element rejects the message, so the snapshot
|
|
1044
812
|
* reads back as absent rather than replaying a malformed call) and an `images` that is an array
|
|
1045
813
|
* (an absent optional passes). The `role` is left as a broad `string` here (an open
|
|
1046
814
|
* {@link import('./types.js').MessageRole}, so any storage-read role string is accepted
|
|
1047
|
-
* defensively rather than rejected against the current literal set)
|
|
1048
|
-
*
|
|
815
|
+
* defensively rather than rejected against the current literal set). Enough to safely impose the
|
|
816
|
+
* {@link MessageInterface} type at
|
|
1049
817
|
* a storage boundary WITHOUT a cast.
|
|
1050
818
|
*
|
|
1051
819
|
* @param value - The value to test (one element of a snapshot's `messages` / a section's `messages`)
|
|
@@ -1062,7 +830,7 @@ function buildToolResult(result) {
|
|
|
1062
830
|
function isMessage(value) {
|
|
1063
831
|
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
1064
832
|
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;
|
|
833
|
+
if (value.calls !== void 0 && !((0, _orkestrel_contract.isArray)(value.calls) && value.calls.every(_orkestrel_tool.isToolCall))) return false;
|
|
1066
834
|
return value.images === void 0 || (0, _orkestrel_contract.isArray)(value.images);
|
|
1067
835
|
}
|
|
1068
836
|
/**
|
|
@@ -1096,7 +864,7 @@ function isSection(value) {
|
|
|
1096
864
|
* UNTRUSTED snapshot read (a storage row a
|
|
1097
865
|
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
1098
866
|
* reads back from its opaque JSON column, a snapshot loaded from disk). The EXACT analogue of
|
|
1099
|
-
* {@link isWorkspaceSnapshot}.
|
|
867
|
+
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}.
|
|
1100
868
|
*
|
|
1101
869
|
* @remarks
|
|
1102
870
|
* A total guard (it NEVER throws — adversarial input returns `false`). It checks the snapshot's
|
|
@@ -1105,7 +873,7 @@ function isSection(value) {
|
|
|
1105
873
|
* {@link SectionInterface} ({@link isSection}), and a `messages` array EVERY element of which is a
|
|
1106
874
|
* valid {@link MessageInterface} ({@link isMessage}) — enough to safely impose the
|
|
1107
875
|
* {@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
|
|
876
|
+
* {@link import('@orkestrel/workspace').isWorkspaceSnapshot}. A malformed blob (a non-record, a missing / non-string `id`, a
|
|
1109
877
|
* non-string `summary` when present, a non-array `sections` / `messages`, or any malformed
|
|
1110
878
|
* element) resolves `false`, so a
|
|
1111
879
|
* {@link import('./conversations/stores/DatabaseConversationStore.js').DatabaseConversationStore}
|
|
@@ -1130,273 +898,6 @@ function isConversationSnapshot(value) {
|
|
|
1130
898
|
return (0, _orkestrel_contract.isArray)(value.messages) && value.messages.every(isMessage);
|
|
1131
899
|
}
|
|
1132
900
|
/**
|
|
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
901
|
* Render a path-addressed text body as a fenced reference block — the framing an
|
|
1401
902
|
* {@link import('./AgentContext.js').AgentContext}'s ACTIVE-workspace text-file render emits (the
|
|
1402
903
|
* active workspace is the SOLE document/image context).
|
|
@@ -1406,7 +907,7 @@ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
|
|
|
1406
907
|
* fenced code block tagged with `language`, the `content` verbatim inside. Pure string assembly,
|
|
1407
908
|
* total — never throws. The one fenced-file format string for the whole module — `AgentContext.build()`
|
|
1408
909
|
* frames an active workspace's text files with it (each carries its own `language` on its
|
|
1409
|
-
* {@link FileContent} text arm).
|
|
910
|
+
* {@link import('@orkestrel/workspace').FileContent} text arm).
|
|
1410
911
|
*
|
|
1411
912
|
* @param path - The file path shown on the `File:` label line
|
|
1412
913
|
* @param language - The fenced-code language tag (e.g. `'typescript'`)
|
|
@@ -1425,29 +926,6 @@ function fencedFile(path, language, content) {
|
|
|
1425
926
|
return `File: ${path}\n\`\`\`${language}\n${content}\n\`\`\``;
|
|
1426
927
|
}
|
|
1427
928
|
/**
|
|
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
929
|
* Sanitize one reported token count into a safe non-negative integer.
|
|
1452
930
|
*
|
|
1453
931
|
* @param value - The token count to sanitize
|
|
@@ -1489,7 +967,7 @@ function sanitizeUsage(usage) {
|
|
|
1489
967
|
* at construction.
|
|
1490
968
|
*
|
|
1491
969
|
* @remarks
|
|
1492
|
-
* A thin immutable value object (mirroring {@link import('
|
|
970
|
+
* A thin immutable value object (mirroring {@link import('@orkestrel/tool').Tool}): the
|
|
1493
971
|
* constructor mints a fresh `id` (`crypto.randomUUID()`), copies the input's `name` /
|
|
1494
972
|
* `content`, resolves `priority` to the input's value or `0`, and carries the input's
|
|
1495
973
|
* per-item `format` override ONLY when supplied (assigned just when present, mirroring a
|
|
@@ -1567,552 +1045,52 @@ var InstructionManager = class {
|
|
|
1567
1045
|
});
|
|
1568
1046
|
this.#format = options?.format;
|
|
1569
1047
|
}
|
|
1570
|
-
get emitter() {
|
|
1571
|
-
return this.#emitter;
|
|
1572
|
-
}
|
|
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;
|
|
1048
|
+
get emitter() {
|
|
1049
|
+
return this.#emitter;
|
|
2048
1050
|
}
|
|
2049
1051
|
get count() {
|
|
2050
|
-
return this.#
|
|
2051
|
-
}
|
|
2052
|
-
get active() {
|
|
2053
|
-
return this.#active === void 0 ? void 0 : this.#workspaces.get(this.#active);
|
|
1052
|
+
return this.#instructions.size;
|
|
2054
1053
|
}
|
|
2055
|
-
|
|
2056
|
-
return this.#
|
|
1054
|
+
get description() {
|
|
1055
|
+
return this.#format?.open ?? "## Instructions";
|
|
2057
1056
|
}
|
|
2058
|
-
|
|
2059
|
-
return
|
|
1057
|
+
get framing() {
|
|
1058
|
+
return this.#format;
|
|
2060
1059
|
}
|
|
2061
1060
|
add(input) {
|
|
2062
|
-
|
|
2063
|
-
|
|
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;
|
|
1061
|
+
if ((0, _orkestrel_contract.isArray)(input)) return input.map((one) => this.#create(one));
|
|
1062
|
+
return this.#create(input);
|
|
2072
1063
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
if (workspace === void 0) return void 0;
|
|
2076
|
-
this.#active = id;
|
|
2077
|
-
return workspace;
|
|
1064
|
+
instruction(name) {
|
|
1065
|
+
return this.#instructions.get(name);
|
|
2078
1066
|
}
|
|
2079
|
-
|
|
2080
|
-
|
|
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;
|
|
1067
|
+
instructions() {
|
|
1068
|
+
return [...this.#instructions.values()].sort((a, b) => b.priority - a.priority);
|
|
2094
1069
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
if (this.#store === void 0 || workspace === void 0) return false;
|
|
2098
|
-
await this.#store.set(workspace.snapshot());
|
|
2099
|
-
return true;
|
|
1070
|
+
format(instruction) {
|
|
1071
|
+
return this.#format?.render?.(instruction) ?? instruction.content;
|
|
2100
1072
|
}
|
|
2101
|
-
remove(
|
|
2102
|
-
if ((0, _orkestrel_contract.isArray)(
|
|
1073
|
+
remove(names) {
|
|
1074
|
+
if ((0, _orkestrel_contract.isArray)(names)) {
|
|
2103
1075
|
let removed = false;
|
|
2104
|
-
for (const
|
|
1076
|
+
for (const name of names) if (this.#delete(name)) removed = true;
|
|
2105
1077
|
return removed;
|
|
2106
1078
|
}
|
|
2107
|
-
return this.#
|
|
1079
|
+
return this.#delete(names);
|
|
2108
1080
|
}
|
|
2109
1081
|
clear() {
|
|
2110
|
-
this.#
|
|
2111
|
-
this.#
|
|
1082
|
+
this.#instructions.clear();
|
|
1083
|
+
this.#emitter.emit("clear");
|
|
2112
1084
|
}
|
|
2113
|
-
#
|
|
2114
|
-
const
|
|
2115
|
-
|
|
1085
|
+
#create(input) {
|
|
1086
|
+
const instruction = new Instruction(input);
|
|
1087
|
+
this.#instructions.set(instruction.name, instruction);
|
|
1088
|
+
this.#emitter.emit("add", instruction);
|
|
1089
|
+
return instruction;
|
|
1090
|
+
}
|
|
1091
|
+
#delete(name) {
|
|
1092
|
+
const removed = this.#instructions.delete(name);
|
|
1093
|
+
if (removed) this.#emitter.emit("remove", name);
|
|
2116
1094
|
return removed;
|
|
2117
1095
|
}
|
|
2118
1096
|
};
|
|
@@ -2195,10 +1173,10 @@ var AgentContext = class {
|
|
|
2195
1173
|
constructor(options) {
|
|
2196
1174
|
this.#system = options?.system;
|
|
2197
1175
|
this.#instructions = options?.instructions ?? new InstructionManager();
|
|
2198
|
-
this.#workspaces = options?.workspaces ?? new WorkspaceManager();
|
|
1176
|
+
this.#workspaces = options?.workspaces ?? new _orkestrel_workspace.WorkspaceManager();
|
|
2199
1177
|
this.#conversations = options?.conversations ?? new ConversationManager();
|
|
2200
1178
|
if (this.#conversations.active === void 0) this.#conversations.add();
|
|
2201
|
-
this.#tools = options?.tools ?? new ToolManager();
|
|
1179
|
+
this.#tools = options?.tools ?? new _orkestrel_tool.ToolManager();
|
|
2202
1180
|
this.#scope = options?.scope;
|
|
2203
1181
|
}
|
|
2204
1182
|
get system() {
|
|
@@ -2232,7 +1210,7 @@ var AgentContext = class {
|
|
|
2232
1210
|
const instructions = filterAllowList(scope?.instructions, this.#instructions.instructions(), (one) => one.name);
|
|
2233
1211
|
this.#section(parts, this.#header(this.#instructions, format?.instructions), instructions, (one) => this.#render(this.#instructions, format?.instructions, one), this.#footer(this.#instructions, format?.instructions));
|
|
2234
1212
|
const files = filterAllowList(scope?.files, this.#workspaces.active?.files() ?? [], (one) => one.path);
|
|
2235
|
-
const workspaceTexts = files.filter((file) => isText(file.content));
|
|
1213
|
+
const workspaceTexts = files.filter((file) => (0, _orkestrel_workspace.isText)(file.content));
|
|
2236
1214
|
this.#section(parts, WORKSPACE_SECTION_HEADER, workspaceTexts, (file) => this.#fenced(file), void 0);
|
|
2237
1215
|
const conversation = (this.#conversations.active ?? this.#ensure()).view();
|
|
2238
1216
|
const tail = this.#attach(conversation, this.#workspaceImages(files));
|
|
@@ -2284,12 +1262,12 @@ var AgentContext = class {
|
|
|
2284
1262
|
};
|
|
2285
1263
|
}
|
|
2286
1264
|
#fenced(file) {
|
|
2287
|
-
if (isText(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
|
|
1265
|
+
if ((0, _orkestrel_workspace.isText)(file.content)) return fencedFile(file.path, file.content.language, file.content.text);
|
|
2288
1266
|
return fencedFile(file.path, "text", "");
|
|
2289
1267
|
}
|
|
2290
1268
|
#workspaceImages(files) {
|
|
2291
1269
|
const data = [];
|
|
2292
|
-
for (const file of files) if (isBinary(file.content) &&
|
|
1270
|
+
for (const file of files) if ((0, _orkestrel_workspace.isBinary)(file.content) && file.content.mime.startsWith("image/")) data.push(file.content.data);
|
|
2293
1271
|
return data;
|
|
2294
1272
|
}
|
|
2295
1273
|
#ensure() {
|
|
@@ -2623,7 +1601,7 @@ var Agent = class {
|
|
|
2623
1601
|
};
|
|
2624
1602
|
const toolMessage = this.#context.messages.add({
|
|
2625
1603
|
role: "tool",
|
|
2626
|
-
content: JSON.stringify(outcomeResult.value
|
|
1604
|
+
content: outcomeResult.success ? JSON.stringify(outcomeResult.value) : outcomeResult.error
|
|
2627
1605
|
});
|
|
2628
1606
|
messages.push(toolMessage);
|
|
2629
1607
|
}
|
|
@@ -2700,6 +1678,7 @@ var Agent = class {
|
|
|
2700
1678
|
}
|
|
2701
1679
|
#denial(call, reason) {
|
|
2702
1680
|
return {
|
|
1681
|
+
success: false,
|
|
2703
1682
|
id: call.id,
|
|
2704
1683
|
name: call.name,
|
|
2705
1684
|
error: reason !== void 0 ? `denied: ${reason}` : "denied by authority"
|
|
@@ -2843,7 +1822,7 @@ var AgentRegistry = class {
|
|
|
2843
1822
|
};
|
|
2844
1823
|
}
|
|
2845
1824
|
#manager(names) {
|
|
2846
|
-
const manager = new ToolManager();
|
|
1825
|
+
const manager = new _orkestrel_tool.ToolManager();
|
|
2847
1826
|
if (names !== void 0) for (const name of names) manager.add(this.tool(name));
|
|
2848
1827
|
return manager;
|
|
2849
1828
|
}
|
|
@@ -2873,8 +1852,8 @@ var AgentRegistry = class {
|
|
|
2873
1852
|
* (an ALLOWLIST — only matched rules that allow get through), pass an `allowed: false`
|
|
2874
1853
|
* `fallback`.
|
|
2875
1854
|
* - **Consulted before each tool call.** The agent loop calls `evaluate({ call })` for
|
|
2876
|
-
* every {@link import('
|
|
2877
|
-
* back to the model as a denial {@link import('
|
|
1855
|
+
* every {@link import('@orkestrel/tool').ToolCall} the model emits; a denied call is fed
|
|
1856
|
+
* back to the model as a denial {@link import('@orkestrel/tool').ToolResult} (a `tool`
|
|
2878
1857
|
* chunk + a tool message) instead of being executed — no tool run, no budget cost —
|
|
2879
1858
|
* so the model sees the denial and can react.
|
|
2880
1859
|
* - **Synchronous now.** The async human-approval handshake (request / grant / deny) is
|
|
@@ -2917,7 +1896,7 @@ var Authority = class {
|
|
|
2917
1896
|
* conversation's durable state IS a row, so persistence reduces to keyed point-access (`get` / `set`
|
|
2918
1897
|
* / `delete`) over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
2919
1898
|
* {@link import('./MemoryConversationStore.js').MemoryConversationStore}. The EXACT twin of
|
|
2920
|
-
* {@link import('
|
|
1899
|
+
* {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}.
|
|
2921
1900
|
*
|
|
2922
1901
|
* @remarks
|
|
2923
1902
|
* The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
|
|
@@ -2925,7 +1904,7 @@ var Authority = class {
|
|
|
2925
1904
|
* IndexedDB backend swaps in WITHOUT touching the
|
|
2926
1905
|
* {@link import('../ConversationManager.js').ConversationManager} or the
|
|
2927
1906
|
* {@link import('../Conversation.js').Conversation} — the same seam as
|
|
2928
|
-
* {@link import('
|
|
1907
|
+
* {@link import('@orkestrel/workspace').DatabaseWorkspaceStore}. The
|
|
2929
1908
|
* driver defaults to memory ({@link import('../../factories.js').createDatabaseConversationStore}
|
|
2930
1909
|
* passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
|
|
2931
1910
|
* durable plumbing by passing a JSON / SQLite / IndexedDB driver.
|
|
@@ -2996,12 +1975,12 @@ var DatabaseConversationStore = class {
|
|
|
2996
1975
|
* The in-memory {@link ConversationStoreInterface} — a process-lifetime `Map` of
|
|
2997
1976
|
* {@link ConversationSnapshot}s keyed by conversation id, the DEFAULT store
|
|
2998
1977
|
* {@link import('../../factories.js').createMemoryConversationStore} builds. The EXACT twin of
|
|
2999
|
-
* {@link import('
|
|
1978
|
+
* {@link import('@orkestrel/workspace').MemoryWorkspaceStore}.
|
|
3000
1979
|
*
|
|
3001
1980
|
* @remarks
|
|
3002
1981
|
* A plain `Map<string, ConversationSnapshot>` (AGENTS §21 — the snapshot is already pure,
|
|
3003
1982
|
* self-contained JSON, so no encoding is needed for the memory tier). Like the
|
|
3004
|
-
* {@link import('
|
|
1983
|
+
* {@link import('@orkestrel/workspace').MemoryWorkspaceStore} it twins,
|
|
3005
1984
|
* there is NO idle-TTL and NO eviction: a persisted conversation lives until an explicit `delete`. A
|
|
3006
1985
|
* durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
|
|
3007
1986
|
* the {@link import('../ConversationManager.js').ConversationManager} or the
|
|
@@ -3299,238 +2278,8 @@ var ThinkSplitter = class {
|
|
|
3299
2278
|
}
|
|
3300
2279
|
};
|
|
3301
2280
|
//#endregion
|
|
3302
|
-
//#region src/core/tools/Tool.ts
|
|
3303
|
-
/**
|
|
3304
|
-
* A registered tool — its {@link import('../types.js').ToolDefinition} schema (the
|
|
3305
|
-
* `name` / `description` / `parameters` the model sees) bound to the `execute`
|
|
3306
|
-
* handler that runs a call.
|
|
3307
|
-
*
|
|
3308
|
-
* @remarks
|
|
3309
|
-
* A thin value object: the constructor stores the schema fields and the handler, and
|
|
3310
|
-
* `execute` delegates to the handler verbatim. `parameters` is kept by reference (the
|
|
3311
|
-
* open JSON-Schema the provider forwards as-is) — not cloned. The handler's `args` is
|
|
3312
|
-
* the model-supplied `unknown` arguments record, narrowed inside the handler (§14);
|
|
3313
|
-
* isolating a throw is the {@link ToolManager}'s job, not this class's. Event-free —
|
|
3314
|
-
* no Emitter, no events.
|
|
3315
|
-
*
|
|
3316
|
-
* @example
|
|
3317
|
-
* ```ts
|
|
3318
|
-
* const tool = new Tool({
|
|
3319
|
-
* name: 'add',
|
|
3320
|
-
* description: 'Add two numbers',
|
|
3321
|
-
* parameters: { type: 'object', properties: { a: { type: 'number' }, b: { type: 'number' } } },
|
|
3322
|
-
* execute: (args) => Number(args.a) + Number(args.b),
|
|
3323
|
-
* })
|
|
3324
|
-
* ```
|
|
3325
|
-
*/
|
|
3326
|
-
var Tool = class {
|
|
3327
|
-
name;
|
|
3328
|
-
description;
|
|
3329
|
-
summary;
|
|
3330
|
-
parameters;
|
|
3331
|
-
#execute;
|
|
3332
|
-
constructor(options) {
|
|
3333
|
-
this.name = options.name;
|
|
3334
|
-
if (options.description !== void 0) this.description = options.description;
|
|
3335
|
-
if (options.summary !== void 0) this.summary = options.summary;
|
|
3336
|
-
if (options.parameters !== void 0) this.parameters = options.parameters;
|
|
3337
|
-
this.#execute = options.execute;
|
|
3338
|
-
}
|
|
3339
|
-
execute(args) {
|
|
3340
|
-
return this.#execute(args);
|
|
3341
|
-
}
|
|
3342
|
-
};
|
|
3343
|
-
//#endregion
|
|
3344
|
-
//#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
|
|
3345
|
-
/**
|
|
3346
|
-
* A {@link WorkspaceStoreInterface} backed by one table of the `databases` layer — a workspace's
|
|
3347
|
-
* durable state IS a row, so persistence reduces to keyed point-access (`get` / `set` / `delete`)
|
|
3348
|
-
* over a {@link TableInterface}, the driver-pluggable twin of the plain-`Map`
|
|
3349
|
-
* {@link import('./MemoryWorkspaceStore.js').MemoryWorkspaceStore}.
|
|
3350
|
-
*
|
|
3351
|
-
* @remarks
|
|
3352
|
-
* The store is driver-agnostic: it holds a single {@link TableInterface} whose backend (memory,
|
|
3353
|
-
* JSON, SQLite, IndexedDB) is chosen by whoever builds it (the factories), so a JSON / SQLite /
|
|
3354
|
-
* IndexedDB backend swaps in WITHOUT touching the
|
|
3355
|
-
* {@link import('../WorkspaceManager.js').WorkspaceManager} or the
|
|
3356
|
-
* {@link import('../Workspace.js').Workspace} — the same seam as
|
|
3357
|
-
* the analogous `DatabaseWorkflowStore` in `@orkestrel/workflow`. The
|
|
3358
|
-
* driver defaults to memory ({@link import('../../factories.js').createDatabaseWorkspaceStore}
|
|
3359
|
-
* passes `createMemoryDriver()`), so it ALSO works in memory out of the box; you opt into the
|
|
3360
|
-
* durable plumbing by passing a JSON / SQLite / IndexedDB driver.
|
|
3361
|
-
*
|
|
3362
|
-
* The {@link WorkspaceSnapshot} is stored as ONE OPAQUE JSON COLUMN — the table is a row of
|
|
3363
|
-
* `{ id; snapshot }` ({@link WorkspaceSnapshotRow}), the snapshot the whole JSON blob (a `rawShape`
|
|
3364
|
-
* column the factory builds) — exactly as `DatabaseWorkflowStore` stores its snapshot. The snapshot
|
|
3365
|
-
* is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless AND
|
|
3366
|
-
* keeps the row type flat (`snapshot` reads back as `unknown`).
|
|
3367
|
-
*
|
|
3368
|
-
* - **`set(snapshot)` upserts under the snapshot's OWN `id`** (no separate id param) — it writes
|
|
3369
|
-
* the row `{ id: snapshot.id, snapshot }`.
|
|
3370
|
-
* - **`get(id)` resolves the stored snapshot for an id**, narrowing the opaque JSON column back to
|
|
3371
|
-
* a {@link WorkspaceSnapshot} ({@link import('../../helpers.js').isWorkspaceSnapshot} — the AGENTS
|
|
3372
|
-
* §14 boundary narrow for an untrusted storage read), or `undefined` if none is stored.
|
|
3373
|
-
* - **`delete(id)` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
3374
|
-
*
|
|
3375
|
-
* UNLIKE a session store there is NO idle-TTL / eviction — a persisted workspace lives until an
|
|
3376
|
-
* explicit `delete`. The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the
|
|
3377
|
-
* §22 method bijection with {@link WorkspaceStoreInterface}). Hydration stays a caller concern: a
|
|
3378
|
-
* {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
|
|
3379
|
-
* live workspace through the constructor `seed` (its `open` / `save`).
|
|
3380
|
-
*
|
|
3381
|
-
* @example
|
|
3382
|
-
* ```ts
|
|
3383
|
-
* import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspace } from '@src/core'
|
|
3384
|
-
*
|
|
3385
|
-
* const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
|
|
3386
|
-
* const workspace = createWorkspace()
|
|
3387
|
-
* workspace.write('notes.txt', 'hello')
|
|
3388
|
-
* await store.set(workspace.snapshot()) // persist the workspace (one JSON column)
|
|
3389
|
-
* const snapshot = await store.get(workspace.id)
|
|
3390
|
-
* await store.delete(workspace.id) // drop it
|
|
3391
|
-
* ```
|
|
3392
|
-
*/
|
|
3393
|
-
var DatabaseWorkspaceStore = class {
|
|
3394
|
-
#table;
|
|
3395
|
-
/**
|
|
3396
|
-
* Wrap a table as a workspace store.
|
|
3397
|
-
*
|
|
3398
|
-
* @param table - The {@link TableInterface} holding the snapshots — its row is the
|
|
3399
|
-
* {@link WorkspaceSnapshotRow} `{ id; snapshot }` shape (the snapshot one opaque JSON column)
|
|
3400
|
-
*/
|
|
3401
|
-
constructor(table) {
|
|
3402
|
-
this.#table = table;
|
|
3403
|
-
}
|
|
3404
|
-
/** Resolve the persisted snapshot for `id`, narrowing the opaque JSON column back to a `WorkspaceSnapshot`. */
|
|
3405
|
-
async get(id) {
|
|
3406
|
-
const row = await this.#table.get(id);
|
|
3407
|
-
if (row === void 0) return void 0;
|
|
3408
|
-
return isWorkspaceSnapshot(row.snapshot) ? row.snapshot : void 0;
|
|
3409
|
-
}
|
|
3410
|
-
/** Insert or replace under the snapshot's OWN `id` (no separate id param) — the row is `{ id, snapshot }`. */
|
|
3411
|
-
async set(snapshot) {
|
|
3412
|
-
await this.#table.set({
|
|
3413
|
-
id: snapshot.id,
|
|
3414
|
-
snapshot
|
|
3415
|
-
});
|
|
3416
|
-
}
|
|
3417
|
-
/** Drop a snapshot by id; an absent id is a no-op (no throw). */
|
|
3418
|
-
async delete(id) {
|
|
3419
|
-
await this.#table.remove(id);
|
|
3420
|
-
}
|
|
3421
|
-
};
|
|
3422
|
-
//#endregion
|
|
3423
|
-
//#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
|
|
3424
|
-
/**
|
|
3425
|
-
* The in-memory {@link WorkspaceStoreInterface} — a process-lifetime `Map` of
|
|
3426
|
-
* {@link WorkspaceSnapshot}s keyed by workspace id, the DEFAULT store
|
|
3427
|
-
* {@link import('../../factories.js').createMemoryWorkspaceStore} builds.
|
|
3428
|
-
*
|
|
3429
|
-
* @remarks
|
|
3430
|
-
* A plain `Map<string, WorkspaceSnapshot>` (AGENTS §21 — the snapshot is already pure,
|
|
3431
|
-
* self-contained JSON, so no encoding is needed for the memory tier). Like the
|
|
3432
|
-
* the analogous `MemoryWorkflowStore` in `@orkestrel/workflow` it twins,
|
|
3433
|
-
* there is NO idle-TTL and NO eviction: a persisted workspace lives until an explicit `delete`. A
|
|
3434
|
-
* durable backend (JSON / SQLite / IndexedDB) swaps in through the SAME interface without touching
|
|
3435
|
-
* the {@link import('../WorkspaceManager.js').WorkspaceManager} or the
|
|
3436
|
-
* {@link import('../Workspace.js').Workspace} — its driver-pluggable twin is
|
|
3437
|
-
* {@link import('./DatabaseWorkspaceStore.js').DatabaseWorkspaceStore} (the snapshot as one opaque
|
|
3438
|
-
* JSON column).
|
|
3439
|
-
*
|
|
3440
|
-
* - **`get` resolves the persisted snapshot for an id**, or `undefined` if none is stored.
|
|
3441
|
-
* - **`set` inserts / replaces under the snapshot's OWN `id`** (no separate id param).
|
|
3442
|
-
* - **`delete` drops a snapshot by id**; an absent id is a no-op (no throw).
|
|
3443
|
-
*
|
|
3444
|
-
* The public surface is EXACTLY `get` / `set` / `delete` — no extra members (the §22 method
|
|
3445
|
-
* bijection with {@link WorkspaceStoreInterface}). Hydration is a caller concern: a
|
|
3446
|
-
* {@link import('../WorkspaceManager.js').WorkspaceManager} reads a snapshot back and rebuilds the
|
|
3447
|
-
* live workspace through the constructor `seed` (its `open` / `save`).
|
|
3448
|
-
*
|
|
3449
|
-
* @example
|
|
3450
|
-
* ```ts
|
|
3451
|
-
* import { createMemoryWorkspaceStore, createWorkspace } from '@src/core'
|
|
3452
|
-
*
|
|
3453
|
-
* const store = createMemoryWorkspaceStore()
|
|
3454
|
-
* const workspace = createWorkspace()
|
|
3455
|
-
* workspace.write('notes.txt', 'hello')
|
|
3456
|
-
* await store.set(workspace.snapshot()) // persist the workspace
|
|
3457
|
-
* const snapshot = await store.get(workspace.id)
|
|
3458
|
-
* await store.delete(workspace.id) // drop it
|
|
3459
|
-
* ```
|
|
3460
|
-
*/
|
|
3461
|
-
var MemoryWorkspaceStore = class {
|
|
3462
|
-
#snapshots = /* @__PURE__ */ new Map();
|
|
3463
|
-
get(id) {
|
|
3464
|
-
return Promise.resolve(this.#snapshots.get(id));
|
|
3465
|
-
}
|
|
3466
|
-
set(snapshot) {
|
|
3467
|
-
this.#snapshots.set(snapshot.id, snapshot);
|
|
3468
|
-
return Promise.resolve();
|
|
3469
|
-
}
|
|
3470
|
-
delete(id) {
|
|
3471
|
-
this.#snapshots.delete(id);
|
|
3472
|
-
return Promise.resolve();
|
|
3473
|
-
}
|
|
3474
|
-
};
|
|
3475
|
-
//#endregion
|
|
3476
2281
|
//#region src/core/factories.ts
|
|
3477
2282
|
/**
|
|
3478
|
-
* Create a tool — a {@link ToolInterface} binding a {@link ToolDefinition} schema
|
|
3479
|
-
* (the `name` / `description` / `parameters` the model sees) to the `execute` handler
|
|
3480
|
-
* that runs a call.
|
|
3481
|
-
*
|
|
3482
|
-
* @remarks
|
|
3483
|
-
* Only `name` is required (it keys the tool in a {@link ToolManagerInterface} and is
|
|
3484
|
-
* what the model calls); `description` / `parameters` are the optional JSON-Schema the
|
|
3485
|
-
* provider advertises (forwarded verbatim). The handler's `args` is the model-supplied
|
|
3486
|
-
* `unknown` arguments record — narrow it inside (§14); a `createToolManager` registry
|
|
3487
|
-
* isolates a throw into a `ToolResult.error`.
|
|
3488
|
-
*
|
|
3489
|
-
* @param options - `name` (required), optional `description` / `parameters`, and the
|
|
3490
|
-
* `execute` handler (see {@link ToolOptions})
|
|
3491
|
-
* @returns A working {@link ToolInterface}
|
|
3492
|
-
*
|
|
3493
|
-
* @example
|
|
3494
|
-
* ```ts
|
|
3495
|
-
* import { createTool } from '@src/core'
|
|
3496
|
-
*
|
|
3497
|
-
* const add = createTool({
|
|
3498
|
-
* name: 'add',
|
|
3499
|
-
* description: 'Add two numbers',
|
|
3500
|
-
* execute: (args) => Number(args.a) + Number(args.b),
|
|
3501
|
-
* })
|
|
3502
|
-
* ```
|
|
3503
|
-
*/
|
|
3504
|
-
function createTool(options) {
|
|
3505
|
-
return new Tool(options);
|
|
3506
|
-
}
|
|
3507
|
-
/**
|
|
3508
|
-
* Create a tool registry — a {@link ToolManagerInterface} that resolves tool names,
|
|
3509
|
-
* lists {@link ToolDefinition}s for the provider, and executes calls with per-call
|
|
3510
|
-
* error isolation.
|
|
3511
|
-
*
|
|
3512
|
-
* @remarks
|
|
3513
|
-
* Starts empty; `add` registers one tool or a batch (§9.2), `definitions()` yields the
|
|
3514
|
-
* schemas to hand a provider, and `execute` runs a {@link ToolCall} (or a batch),
|
|
3515
|
-
* ALWAYS resolving a {@link ToolResult} — a handler throw becomes an `error` result and
|
|
3516
|
-
* an unknown name a not-found `error`, so a tool throw never escapes and a batch never
|
|
3517
|
-
* fails as a whole.
|
|
3518
|
-
*
|
|
3519
|
-
* @returns An empty {@link ToolManagerInterface}
|
|
3520
|
-
*
|
|
3521
|
-
* @example
|
|
3522
|
-
* ```ts
|
|
3523
|
-
* import { createTool, createToolManager } from '@src/core'
|
|
3524
|
-
*
|
|
3525
|
-
* const tools = createToolManager()
|
|
3526
|
-
* tools.add(createTool({ name: 'add', execute: (a) => Number(a.x) + Number(a.y) }))
|
|
3527
|
-
* const result = await tools.execute({ id: '1', name: 'add', arguments: { x: 1, y: 2 } })
|
|
3528
|
-
* ```
|
|
3529
|
-
*/
|
|
3530
|
-
function createToolManager() {
|
|
3531
|
-
return new ToolManager();
|
|
3532
|
-
}
|
|
3533
|
-
/**
|
|
3534
2283
|
* Create a conversation — a {@link ConversationInterface} grouping messages above a flat
|
|
3535
2284
|
* message store it OWNS DIRECTLY, with compaction into summarized sections, a regenerated
|
|
3536
2285
|
* rollup `summary`, on-demand `rehydrate`, and substring `search`, driven by a
|
|
@@ -3604,11 +2353,13 @@ function createConversationManager(options) {
|
|
|
3604
2353
|
* Create the in-memory conversation store — a {@link ConversationStoreInterface} backed by a
|
|
3605
2354
|
* process-lifetime `Map` of {@link import('./types.js').ConversationSnapshot}s keyed by conversation
|
|
3606
2355
|
* id, the DEFAULT backing for the durable {@link ConversationManagerInterface.open} /
|
|
3607
|
-
* {@link ConversationManagerInterface.save} seam. The
|
|
2356
|
+
* {@link ConversationManagerInterface.save} seam. The exact twin of
|
|
2357
|
+
* {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
|
|
3608
2358
|
*
|
|
3609
2359
|
* @remarks
|
|
3610
2360
|
* A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
|
|
3611
|
-
* the structural twin of {@link createMemoryWorkspaceStore}.
|
|
2361
|
+
* the structural twin of {@link import('@orkestrel/workspace').createMemoryWorkspaceStore}.
|
|
2362
|
+
* `get` / `set` / `delete` are async (the
|
|
3612
2363
|
* same shape a durable backend fits); UNLIKE a session store there is NO idle-TTL / eviction — a
|
|
3613
2364
|
* persisted conversation lives until an explicit `delete`. Its driver-pluggable twin is
|
|
3614
2365
|
* {@link createDatabaseConversationStore} (the snapshot as one opaque JSON column over a `databases`
|
|
@@ -3636,12 +2387,14 @@ function createMemoryConversationStore() {
|
|
|
3636
2387
|
/**
|
|
3637
2388
|
* Create a {@link DatabaseConversationStore} over any {@link DriverInterface} — the durable,
|
|
3638
2389
|
* driver-pluggable backing for the conversation persistence seam, the opt-in twin of
|
|
3639
|
-
* {@link createMemoryConversationStore}. The
|
|
2390
|
+
* {@link createMemoryConversationStore}. The exact twin of
|
|
2391
|
+
* {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore}.
|
|
3640
2392
|
*
|
|
3641
2393
|
* @remarks
|
|
3642
2394
|
* Builds a one-table database (`conversations`, keyed by `id`) over the supplied driver, the snapshot
|
|
3643
2395
|
* held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
|
|
3644
|
-
* `rawShape` (a JSON blob), exactly as
|
|
2396
|
+
* `rawShape` (a JSON blob), exactly as
|
|
2397
|
+
* {@link import('@orkestrel/workspace').createDatabaseWorkspaceStore} stores its snapshot. The
|
|
3645
2398
|
* snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
3646
2399
|
* AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
|
|
3647
2400
|
* {@link import('./helpers.js').isConversationSnapshot}). The `driver` DEFAULTS to
|
|
@@ -3665,12 +2418,13 @@ function createMemoryConversationStore() {
|
|
|
3665
2418
|
* ```
|
|
3666
2419
|
*/
|
|
3667
2420
|
function createDatabaseConversationStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
|
|
2421
|
+
const columns = {
|
|
2422
|
+
id: (0, _orkestrel_contract.stringShape)(),
|
|
2423
|
+
snapshot: (0, _orkestrel_contract.rawShape)({})
|
|
2424
|
+
};
|
|
3668
2425
|
return new DatabaseConversationStore((0, _orkestrel_database.createDatabase)({
|
|
3669
2426
|
driver,
|
|
3670
|
-
tables: { conversations:
|
|
3671
|
-
id: (0, _orkestrel_contract.stringShape)(),
|
|
3672
|
-
snapshot: (0, _orkestrel_contract.rawShape)({})
|
|
3673
|
-
} }
|
|
2427
|
+
tables: { conversations: columns }
|
|
3674
2428
|
}).table("conversations"));
|
|
3675
2429
|
}
|
|
3676
2430
|
/**
|
|
@@ -3937,8 +2691,9 @@ function createAuthority(options) {
|
|
|
3937
2691
|
*
|
|
3938
2692
|
* @example
|
|
3939
2693
|
* ```ts
|
|
2694
|
+
* import { createAgentRegistry } from '@src/core'
|
|
3940
2695
|
* import type { ProviderInterface } from '@src/core'
|
|
3941
|
-
* import {
|
|
2696
|
+
* import { createTool } from '@orkestrel/tool'
|
|
3942
2697
|
*
|
|
3943
2698
|
* declare const provider: ProviderInterface // any concrete implementation supplied by the host app
|
|
3944
2699
|
* const registry = createAgentRegistry({
|
|
@@ -4043,212 +2798,6 @@ function createAgentRunner(options) {
|
|
|
4043
2798
|
handler: handleAgentRunnerJob.bind(void 0, registry, allowPartial)
|
|
4044
2799
|
});
|
|
4045
2800
|
}
|
|
4046
|
-
/**
|
|
4047
|
-
* Create a file — an immutable {@link FileInterface} from its `path` + {@link FileContent}
|
|
4048
|
-
* and optional {@link import('./types.js').FileState}, with `size` / `lines` DERIVED from the
|
|
4049
|
-
* content.
|
|
4050
|
-
*
|
|
4051
|
-
* @remarks
|
|
4052
|
-
* Returns a PLAIN `Object.freeze`d record (NOT a class instance) — the `path` IS its identity
|
|
4053
|
-
* (there is no `id`). Only `path` / `content` are required; `state` defaults to `'created'`
|
|
4054
|
-
* when omitted. `size` (via {@link computeSize}) and `lines` (via {@link countLines}) are
|
|
4055
|
-
* computed from the content here (so they never drift from it). Frozen + plain, so it
|
|
4056
|
-
* `structuredClone`s losslessly and is never mutated after creation.
|
|
4057
|
-
*
|
|
4058
|
-
* @param input - `path` / `content` (required) and an optional `state` (see {@link FileInput})
|
|
4059
|
-
* @returns A frozen {@link FileInterface} record
|
|
4060
|
-
*
|
|
4061
|
-
* @example
|
|
4062
|
-
* ```ts
|
|
4063
|
-
* import { createFile, createTextContent } from '@src/core'
|
|
4064
|
-
*
|
|
4065
|
-
* const file = createFile({ path: 'src/main.ts', content: createTextContent('const x = 1', 'typescript') })
|
|
4066
|
-
* file.lines // 1
|
|
4067
|
-
* ```
|
|
4068
|
-
*/
|
|
4069
|
-
function createFile(input) {
|
|
4070
|
-
return Object.freeze({
|
|
4071
|
-
path: input.path,
|
|
4072
|
-
content: input.content,
|
|
4073
|
-
state: input.state ?? "created",
|
|
4074
|
-
size: computeSize(input.content),
|
|
4075
|
-
lines: countLines(input.content)
|
|
4076
|
-
});
|
|
4077
|
-
}
|
|
4078
|
-
/**
|
|
4079
|
-
* Build the TEXT {@link FileContent} arm — the §4.2.3 split constructor for text (a separate
|
|
4080
|
-
* function per arm, not one constructor dispatching on a discriminator parameter).
|
|
4081
|
-
*
|
|
4082
|
-
* @param text - The literal text body
|
|
4083
|
-
* @param language - The fenced-code language the text renders as (e.g. `'typescript'`)
|
|
4084
|
-
* @returns A `{ text; language }` content arm
|
|
4085
|
-
*
|
|
4086
|
-
* @example
|
|
4087
|
-
* ```ts
|
|
4088
|
-
* import { createTextContent } from '@src/core'
|
|
4089
|
-
*
|
|
4090
|
-
* createTextContent('const x = 1', 'typescript') // { text: 'const x = 1', language: 'typescript' }
|
|
4091
|
-
* ```
|
|
4092
|
-
*/
|
|
4093
|
-
function createTextContent(text, language) {
|
|
4094
|
-
return {
|
|
4095
|
-
text,
|
|
4096
|
-
language
|
|
4097
|
-
};
|
|
4098
|
-
}
|
|
4099
|
-
/**
|
|
4100
|
-
* Build the BINARY {@link FileContent} arm — the §4.2.3 split constructor for binary (a
|
|
4101
|
-
* separate function per arm, not one constructor dispatching on a discriminator parameter).
|
|
4102
|
-
* An image is just a binary with an image {@link BinaryMIME}.
|
|
4103
|
-
*
|
|
4104
|
-
* @param data - The base64-encoded binary payload
|
|
4105
|
-
* @param mime - The {@link BinaryMIME} that labels the payload
|
|
4106
|
-
* @returns A `{ data; mime }` content arm
|
|
4107
|
-
*
|
|
4108
|
-
* @example
|
|
4109
|
-
* ```ts
|
|
4110
|
-
* import { createBinaryContent } from '@src/core'
|
|
4111
|
-
*
|
|
4112
|
-
* createBinaryContent('<base64>', 'image/png') // { data: '<base64>', mime: 'image/png' }
|
|
4113
|
-
* ```
|
|
4114
|
-
*/
|
|
4115
|
-
function createBinaryContent(data, mime) {
|
|
4116
|
-
return {
|
|
4117
|
-
data,
|
|
4118
|
-
mime
|
|
4119
|
-
};
|
|
4120
|
-
}
|
|
4121
|
-
/**
|
|
4122
|
-
* Create a workspace — a mutable, `path`-keyed working set of immutable
|
|
4123
|
-
* {@link FileInterface}s with the in-memory edit surface (read / write / search / replace /
|
|
4124
|
-
* move / remove), observable through its `EmitterInterface` (from `@orkestrel/emitter`).
|
|
4125
|
-
*
|
|
4126
|
-
* @param options - Optional initial {@link import('./types.js').WorkspaceEventMap} listeners (`on`) and the emitter's `error` handler (see {@link WorkspaceOptions})
|
|
4127
|
-
* @returns A working {@link WorkspaceInterface}
|
|
4128
|
-
*
|
|
4129
|
-
* @example
|
|
4130
|
-
* ```ts
|
|
4131
|
-
* import { createWorkspace } from '@src/core'
|
|
4132
|
-
*
|
|
4133
|
-
* const workspace = createWorkspace({ on: { write: (file) => console.log(file.path) } })
|
|
4134
|
-
* workspace.write('src/main.ts', 'const x = 1')
|
|
4135
|
-
* workspace.file('src/main.ts')?.state // 'created'
|
|
4136
|
-
* ```
|
|
4137
|
-
*/
|
|
4138
|
-
function createWorkspace(options) {
|
|
4139
|
-
return new Workspace(options);
|
|
4140
|
-
}
|
|
4141
|
-
/**
|
|
4142
|
-
* Create the in-memory workspace store — a {@link WorkspaceStoreInterface} backed by a
|
|
4143
|
-
* process-lifetime `Map` of {@link import('./types.js').WorkspaceSnapshot}s keyed by workspace id,
|
|
4144
|
-
* the DEFAULT backing for the durable {@link WorkspaceManagerInterface.open} /
|
|
4145
|
-
* {@link WorkspaceManagerInterface.save} seam.
|
|
4146
|
-
*
|
|
4147
|
-
* @remarks
|
|
4148
|
-
* A plain `Map` (the snapshot is already pure JSON, so no encoding is needed for the memory tier),
|
|
4149
|
-
* the structural twin of the analogous `createMemoryWorkflowStore` in `@orkestrel/workflow`.
|
|
4150
|
-
* `get` / `set` / `delete` are async (the same shape a durable backend fits); UNLIKE a session
|
|
4151
|
-
* store there is NO idle-TTL / eviction — a persisted workspace lives until an explicit `delete`.
|
|
4152
|
-
* Its driver-pluggable twin is {@link createDatabaseWorkspaceStore} (the snapshot as one opaque
|
|
4153
|
-
* JSON column over a `databases` table) — for a DURABLE store pass it a JSON / SQLite / IndexedDB
|
|
4154
|
-
* driver, and it swaps in WITHOUT touching the manager or the workspace. Hydration stays a manager
|
|
4155
|
-
* concern: read a snapshot back and rebuild the live workspace through the constructor `seed`.
|
|
4156
|
-
*
|
|
4157
|
-
* @returns A memory-backed {@link WorkspaceStoreInterface}
|
|
4158
|
-
*
|
|
4159
|
-
* @example
|
|
4160
|
-
* ```ts
|
|
4161
|
-
* import { createMemoryWorkspaceStore, createWorkspaceManager } from '@src/core'
|
|
4162
|
-
*
|
|
4163
|
-
* const store = createMemoryWorkspaceStore()
|
|
4164
|
-
* const manager = createWorkspaceManager({ store })
|
|
4165
|
-
* const workspace = manager.add()
|
|
4166
|
-
* workspace.write('notes.txt', 'hello')
|
|
4167
|
-
* await manager.save(workspace.id) // persist the workspace
|
|
4168
|
-
* ```
|
|
4169
|
-
*/
|
|
4170
|
-
function createMemoryWorkspaceStore() {
|
|
4171
|
-
return new MemoryWorkspaceStore();
|
|
4172
|
-
}
|
|
4173
|
-
/**
|
|
4174
|
-
* Create a {@link DatabaseWorkspaceStore} over any {@link DriverInterface} — the durable,
|
|
4175
|
-
* driver-pluggable backing for the workspace persistence seam, the opt-in twin of
|
|
4176
|
-
* {@link createMemoryWorkspaceStore}.
|
|
4177
|
-
*
|
|
4178
|
-
* @remarks
|
|
4179
|
-
* Builds a one-table database (`workspaces`, keyed by `id`) over the supplied driver, the snapshot
|
|
4180
|
-
* held as ONE OPAQUE JSON COLUMN — the column map is `{ id; snapshot }` where `snapshot` is a
|
|
4181
|
-
* `rawShape` (a JSON blob), exactly as
|
|
4182
|
-
* the analogous `createDatabaseWorkflowStore` in `@orkestrel/workflow` stores its snapshot. The
|
|
4183
|
-
* snapshot is already a COMPLETE, self-contained, pure-JSON payload, so storing it whole is lossless
|
|
4184
|
-
* AND keeps the row type FLAT (the column reads back as `unknown`, narrowed on `get` by
|
|
4185
|
-
* {@link import('./helpers.js').isWorkspaceSnapshot}). The `driver` DEFAULTS to
|
|
4186
|
-
* {@link createMemoryDriver}, so the store ALSO works in memory out of the box; pass a server
|
|
4187
|
-
* `createJSONDriver` / `createSQLiteDriver` (or a browser IndexedDB driver) for a persistent one —
|
|
4188
|
-
* the durability is the driver's job, the store engine is shared. It swaps in behind
|
|
4189
|
-
* {@link WorkspaceStoreInterface} WITHOUT touching the manager or the workspace.
|
|
4190
|
-
*
|
|
4191
|
-
* @param driver - The storage backend the snapshots persist to (defaults to {@link createMemoryDriver})
|
|
4192
|
-
* @returns A {@link WorkspaceStoreInterface} over the driver
|
|
4193
|
-
*
|
|
4194
|
-
* @example
|
|
4195
|
-
* ```ts
|
|
4196
|
-
* import { createDatabaseWorkspaceStore, createMemoryDriver, createWorkspaceManager } from '@src/core'
|
|
4197
|
-
*
|
|
4198
|
-
* const store = createDatabaseWorkspaceStore(createMemoryDriver()) // a durable driver swaps in here
|
|
4199
|
-
* const manager = createWorkspaceManager({ store })
|
|
4200
|
-
* const workspace = manager.add()
|
|
4201
|
-
* workspace.write('notes.txt', 'hello')
|
|
4202
|
-
* await manager.save(workspace.id) // persist the workspace (one JSON column)
|
|
4203
|
-
* ```
|
|
4204
|
-
*/
|
|
4205
|
-
function createDatabaseWorkspaceStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
|
|
4206
|
-
return new DatabaseWorkspaceStore((0, _orkestrel_database.createDatabase)({
|
|
4207
|
-
driver,
|
|
4208
|
-
tables: { workspaces: {
|
|
4209
|
-
id: (0, _orkestrel_contract.stringShape)(),
|
|
4210
|
-
snapshot: (0, _orkestrel_contract.rawShape)({})
|
|
4211
|
-
} }
|
|
4212
|
-
}).table("workspaces"));
|
|
4213
|
-
}
|
|
4214
|
-
/**
|
|
4215
|
-
* Create a workspace registry — a {@link WorkspaceManagerInterface} holding
|
|
4216
|
-
* {@link WorkspaceInterface}s keyed by their `id` (in insertion order) WITH an active pointer:
|
|
4217
|
-
* the §9 store over the workspace layer plus the `active` / `switch` seam the context renders.
|
|
4218
|
-
*
|
|
4219
|
-
* @remarks
|
|
4220
|
-
* Starts empty; `add(input?)` mints a {@link WorkspaceInterface} (its `id` from the input or a
|
|
4221
|
-
* random UUID), flowing the manager's default `on` / `error` in unless the input overrides them,
|
|
4222
|
-
* and stores it (an already-present `id` overwrites — last write wins) — and AUTO-ACTIVATES the
|
|
4223
|
-
* FIRST one (a registry with workspaces always has one `active`); a later `add` leaves `active`
|
|
4224
|
-
* unchanged. `switch(id)` re-points `active` (an unknown `id` returns `undefined`, leaving
|
|
4225
|
-
* `active` unchanged — lenient, never throws); `workspace(id)` / `workspaces()` look up;
|
|
4226
|
-
* `remove` (one or a batch, §9.2) reports whether any was removed AND clears `active` if it was
|
|
4227
|
-
* the removed one; `clear` empties it and clears `active`. Event-free (each workspace owns its
|
|
4228
|
-
* own observable `emitter`).
|
|
4229
|
-
*
|
|
4230
|
-
* With the optional `store` ({@link WorkspaceStoreInterface}, e.g. `createMemoryWorkspaceStore` /
|
|
4231
|
-
* `createDatabaseWorkspaceStore`), `open(id)` HYDRATES a workspace on a registry miss (rebuilding it
|
|
4232
|
-
* through the constructor `seed` from the snapshot's `files`, then activating it) and `save(id)`
|
|
4233
|
-
* PERSISTS a registered workspace's `snapshot()`. Both are LENIENT without a store (open resolves
|
|
4234
|
-
* only registered ids, save is a no-op `false`).
|
|
4235
|
-
*
|
|
4236
|
-
* @param options - Optional default `on` / `error` for created workspaces + the durable `store`
|
|
4237
|
-
* (see {@link WorkspaceManagerOptions})
|
|
4238
|
-
* @returns An empty {@link WorkspaceManagerInterface}
|
|
4239
|
-
*
|
|
4240
|
-
* @example
|
|
4241
|
-
* ```ts
|
|
4242
|
-
* import { createWorkspaceManager } from '@src/core'
|
|
4243
|
-
*
|
|
4244
|
-
* const workspaces = createWorkspaceManager()
|
|
4245
|
-
* const scratch = workspaces.add() // auto-activates — workspaces.active === scratch
|
|
4246
|
-
* scratch.write('notes.txt', 'hello')
|
|
4247
|
-
* ```
|
|
4248
|
-
*/
|
|
4249
|
-
function createWorkspaceManager(options) {
|
|
4250
|
-
return new WorkspaceManager(options);
|
|
4251
|
-
}
|
|
4252
2801
|
//#endregion
|
|
4253
2802
|
exports.Agent = Agent;
|
|
4254
2803
|
exports.AgentContext = AgentContext;
|
|
@@ -4265,85 +2814,49 @@ exports.DEFAULT_AGENT_LIMIT = DEFAULT_AGENT_LIMIT;
|
|
|
4265
2814
|
exports.DEFAULT_AUTHORITY_ZONE = DEFAULT_AUTHORITY_ZONE;
|
|
4266
2815
|
exports.DEFAULT_CONVERSATION_KEEP = DEFAULT_CONVERSATION_KEEP;
|
|
4267
2816
|
exports.DatabaseConversationStore = DatabaseConversationStore;
|
|
4268
|
-
exports.DatabaseWorkspaceStore = DatabaseWorkspaceStore;
|
|
4269
|
-
exports.EXTENSION_TO_LANGUAGE = EXTENSION_TO_LANGUAGE;
|
|
4270
2817
|
exports.IMAGE_TOKEN_ESTIMATE = IMAGE_TOKEN_ESTIMATE;
|
|
4271
2818
|
exports.Instruction = Instruction;
|
|
4272
2819
|
exports.InstructionManager = InstructionManager;
|
|
4273
2820
|
exports.MESSAGE_TOKEN_OVERHEAD = MESSAGE_TOKEN_OVERHEAD;
|
|
4274
2821
|
exports.MemoryConversationStore = MemoryConversationStore;
|
|
4275
|
-
exports.MemoryWorkspaceStore = MemoryWorkspaceStore;
|
|
4276
2822
|
exports.ProviderAbortError = ProviderAbortError;
|
|
4277
2823
|
exports.Scope = Scope;
|
|
4278
2824
|
exports.ScopeManager = ScopeManager;
|
|
4279
2825
|
exports.THINK_CLOSE = THINK_CLOSE;
|
|
4280
2826
|
exports.THINK_OPEN = THINK_OPEN;
|
|
4281
2827
|
exports.ThinkSplitter = ThinkSplitter;
|
|
4282
|
-
exports.Tool = Tool;
|
|
4283
|
-
exports.ToolManager = ToolManager;
|
|
4284
2828
|
exports.WORKSPACE_SECTION_HEADER = WORKSPACE_SECTION_HEADER;
|
|
4285
|
-
exports.
|
|
4286
|
-
exports.WorkspaceError = WorkspaceError;
|
|
4287
|
-
exports.WorkspaceManager = WorkspaceManager;
|
|
4288
|
-
exports.buildToolResult = buildToolResult;
|
|
4289
|
-
exports.clampPosition = clampPosition;
|
|
4290
|
-
exports.clampRange = clampRange;
|
|
4291
|
-
exports.computeSize = computeSize;
|
|
4292
|
-
exports.countLines = countLines;
|
|
2829
|
+
exports.agentResultToJSON = agentResultToJSON;
|
|
4293
2830
|
exports.createAgent = createAgent;
|
|
4294
2831
|
exports.createAgentContext = createAgentContext;
|
|
4295
2832
|
exports.createAgentQueue = createAgentQueue;
|
|
4296
2833
|
exports.createAgentRegistry = createAgentRegistry;
|
|
4297
2834
|
exports.createAgentRunner = createAgentRunner;
|
|
4298
2835
|
exports.createAuthority = createAuthority;
|
|
4299
|
-
exports.createBinaryContent = createBinaryContent;
|
|
4300
2836
|
exports.createConversation = createConversation;
|
|
4301
2837
|
exports.createConversationManager = createConversationManager;
|
|
4302
2838
|
exports.createDatabaseConversationStore = createDatabaseConversationStore;
|
|
4303
|
-
exports.createDatabaseWorkspaceStore = createDatabaseWorkspaceStore;
|
|
4304
|
-
exports.createFile = createFile;
|
|
4305
2839
|
exports.createInstruction = createInstruction;
|
|
4306
2840
|
exports.createInstructionManager = createInstructionManager;
|
|
4307
2841
|
exports.createMemoryConversationStore = createMemoryConversationStore;
|
|
4308
|
-
exports.createMemoryWorkspaceStore = createMemoryWorkspaceStore;
|
|
4309
2842
|
exports.createScope = createScope;
|
|
4310
2843
|
exports.createScopeManager = createScopeManager;
|
|
4311
|
-
exports.createTextContent = createTextContent;
|
|
4312
2844
|
exports.createThinkSplitter = createThinkSplitter;
|
|
4313
|
-
exports.createTool = createTool;
|
|
4314
|
-
exports.createToolManager = createToolManager;
|
|
4315
|
-
exports.createWorkspace = createWorkspace;
|
|
4316
|
-
exports.createWorkspaceManager = createWorkspaceManager;
|
|
4317
|
-
exports.decodedSize = decodedSize;
|
|
4318
|
-
exports.escapeRegExp = escapeRegExp;
|
|
4319
2845
|
exports.estimateMessages = estimateMessages;
|
|
4320
2846
|
exports.estimateTokens = estimateTokens;
|
|
4321
2847
|
exports.fencedFile = fencedFile;
|
|
4322
2848
|
exports.filterAllowList = filterAllowList;
|
|
4323
2849
|
exports.handleAgentQueueJob = handleAgentQueueJob;
|
|
4324
2850
|
exports.handleAgentRunnerJob = handleAgentRunnerJob;
|
|
4325
|
-
exports.inferLanguage = inferLanguage;
|
|
4326
2851
|
exports.isAgentError = isAgentError;
|
|
4327
2852
|
exports.isAgentJobError = isAgentJobError;
|
|
4328
|
-
exports.isBinary = isBinary;
|
|
4329
2853
|
exports.isConversationError = isConversationError;
|
|
4330
2854
|
exports.isConversationSnapshot = isConversationSnapshot;
|
|
4331
|
-
exports.isFile = isFile;
|
|
4332
|
-
exports.isImage = isImage;
|
|
4333
2855
|
exports.isMessage = isMessage;
|
|
4334
2856
|
exports.isProviderAbortError = isProviderAbortError;
|
|
4335
2857
|
exports.isSection = isSection;
|
|
4336
|
-
exports.isText = isText;
|
|
4337
|
-
exports.isToolCall = isToolCall;
|
|
4338
|
-
exports.isValidRange = isValidRange;
|
|
4339
|
-
exports.isWorkspaceError = isWorkspaceError;
|
|
4340
|
-
exports.isWorkspaceSnapshot = isWorkspaceSnapshot;
|
|
4341
|
-
exports.offsetAt = offsetAt;
|
|
4342
|
-
exports.rangeOf = rangeOf;
|
|
4343
2858
|
exports.sanitizeToken = sanitizeToken;
|
|
4344
2859
|
exports.sanitizeUsage = sanitizeUsage;
|
|
4345
2860
|
exports.settleAgentJob = settleAgentJob;
|
|
4346
|
-
exports.sliceRange = sliceRange;
|
|
4347
|
-
exports.spliceRange = spliceRange;
|
|
4348
2861
|
|
|
4349
2862
|
//# sourceMappingURL=index.cjs.map
|