glove-foundry 0.4.0 → 0.5.0
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 +17 -1
- package/dist/chunk-5NFLGHOC.js +281 -0
- package/dist/chunk-J7ZBJM54.js +6 -0
- package/dist/{chunk-5LDW2N2E.js → chunk-KBMD4RYU.js} +1980 -3464
- package/dist/chunk-LW5PJ2FN.js +546 -0
- package/dist/{chunk-25BGNRJN.js → chunk-SGF3CFUR.js} +248 -120
- package/dist/chunk-TH5ETCG2.js +730 -0
- package/dist/cli.js +7 -3
- package/dist/{client-D0-pKEZM.d.ts → client-DcsnudMI.d.ts} +9 -3
- package/dist/client.d.ts +3 -1
- package/dist/config.d.ts +1 -0
- package/dist/execution-agent.js +9 -4
- package/dist/execution-daemon.d.ts +2 -0
- package/dist/execution-daemon.js +157 -0
- package/dist/execution-loader.d.ts +6 -0
- package/dist/execution-loader.js +20 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +32 -27
- package/dist/station.d.ts +35 -0
- package/dist/station.js +9 -0
- package/docs/architecture.md +1 -1
- package/docs/building-with-foundry.md +19 -2
- package/docs/execution-release.md +57 -0
- package/docs/execution.md +97 -0
- package/docs/release-verification.md +3 -1
- package/package.json +23 -15
- package/templates/minimal/README.md +1 -1
- package/templates/travel-concierge/README.md +1 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveResolvable
|
|
3
|
+
} from "./chunk-5NFLGHOC.js";
|
|
4
|
+
|
|
5
|
+
// src/guidance.ts
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { FactPreparation, FactStore, useFacts } from "glove-facts";
|
|
8
|
+
import { useGoalRunner, useFormRunner } from "glove-memory/tools";
|
|
9
|
+
var defineFacts = (options) => Object.freeze({ ...options });
|
|
10
|
+
var defineGoals = (options) => Object.freeze({ ...options });
|
|
11
|
+
var defineForms = (options) => Object.freeze({ ...options });
|
|
12
|
+
function foundryGuidanceSubject(context, scope = "conversation") {
|
|
13
|
+
return `foundry:${scope}:${createHash("sha256").update(JSON.stringify([
|
|
14
|
+
context.workspaceId,
|
|
15
|
+
context.agentId,
|
|
16
|
+
...scope === "conversation" ? [context.conversationId] : []
|
|
17
|
+
])).digest("hex")}`;
|
|
18
|
+
}
|
|
19
|
+
async function mountFoundryGuidance(glove, context, options) {
|
|
20
|
+
const { facts, goals, forms } = options;
|
|
21
|
+
const subject = (scope) => typeof scope === "object" ? scope.subject : foundryGuidanceSubject(context, scope);
|
|
22
|
+
const handles = {};
|
|
23
|
+
let preparer;
|
|
24
|
+
if (facts) {
|
|
25
|
+
handles.facts = new FactStore(facts.adapter, {
|
|
26
|
+
scope: typeof facts.scope === "object" ? facts.scope : { subject: subject(facts.scope), context: "conversation-evidence" },
|
|
27
|
+
maxRevisions: facts.maxRevisions,
|
|
28
|
+
onUrgent: facts.onUrgent
|
|
29
|
+
});
|
|
30
|
+
if (facts.preparationAgent) {
|
|
31
|
+
if (facts.preparationAgent === glove) throw new Error("Fact preparation requires a dedicated agent.");
|
|
32
|
+
preparer = new FactPreparation(handles.facts, { agent: facts.preparationAgent });
|
|
33
|
+
}
|
|
34
|
+
useFacts(glove, handles.facts, facts.source ?? (() => ({
|
|
35
|
+
source: { kind: "message", id: context.message.id ?? context.runId },
|
|
36
|
+
operationId: context.message.id ?? context.runId
|
|
37
|
+
})));
|
|
38
|
+
}
|
|
39
|
+
if ((goals?.preparation || forms?.preparation) && !preparer) {
|
|
40
|
+
throw new Error("Guidance preparation requires facts with a dedicated preparationAgent.");
|
|
41
|
+
}
|
|
42
|
+
if (goals) {
|
|
43
|
+
const scope = typeof goals.scope === "object" ? goals.scope : {
|
|
44
|
+
subject: subject(goals.scope),
|
|
45
|
+
key: goals.program?.key ?? "conversation"
|
|
46
|
+
};
|
|
47
|
+
if (goals.preparation && preparer.facts.scope().subject !== scope.subject) throw new Error("Goals and facts must share the same subject.");
|
|
48
|
+
handles.goals = useGoalRunner(glove, goals.adapter, {
|
|
49
|
+
...goals,
|
|
50
|
+
scope,
|
|
51
|
+
preparation: goals.preparation ? { ...goals.preparation, preparer } : void 0
|
|
52
|
+
}).runner;
|
|
53
|
+
if (goals.program) await handles.goals.start(goals.program, "Initialize definition-provided goals; preserve existing progress.");
|
|
54
|
+
}
|
|
55
|
+
if (forms) {
|
|
56
|
+
if (forms.preparation && preparer.facts.scope().subject !== subject(forms.scope)) throw new Error("Forms and facts must share the same subject.");
|
|
57
|
+
handles.forms = useFormRunner(glove, forms.adapter, {
|
|
58
|
+
...forms,
|
|
59
|
+
subject: subject(forms.scope),
|
|
60
|
+
preparation: forms.preparation ? { ...forms.preparation, preparer } : void 0
|
|
61
|
+
}).runner;
|
|
62
|
+
}
|
|
63
|
+
let previous = "";
|
|
64
|
+
const snapshot = async () => {
|
|
65
|
+
if (!facts && !goals && !forms && options.contextProviders.length === 0) return;
|
|
66
|
+
const goalState = await handles.goals?.status();
|
|
67
|
+
const factState = await handles.facts?.inspect();
|
|
68
|
+
const formState = forms ? await forms.adapter.findInstances({ subject: subject(forms.scope) }) : void 0;
|
|
69
|
+
const state = {
|
|
70
|
+
goals: goalState ? {
|
|
71
|
+
version: goalState.version,
|
|
72
|
+
status: goalState.status,
|
|
73
|
+
activeGoal: goalState.activeGoal,
|
|
74
|
+
items: goalState.goals.map((g) => ({
|
|
75
|
+
key: g.definition.key,
|
|
76
|
+
title: g.definition.title,
|
|
77
|
+
status: g.status,
|
|
78
|
+
completed: g.items.filter((i) => i.state?.done).length,
|
|
79
|
+
total: g.items.length
|
|
80
|
+
}))
|
|
81
|
+
} : null,
|
|
82
|
+
facts: factState ? {
|
|
83
|
+
version: factState.version,
|
|
84
|
+
revisions: factState.facts.length,
|
|
85
|
+
claims: factState.claims.length,
|
|
86
|
+
urgent: factState.facts.filter((f) => f.urgent).length
|
|
87
|
+
} : null,
|
|
88
|
+
forms: formState?.map((f) => ({
|
|
89
|
+
id: f.id,
|
|
90
|
+
definitionId: f.defId,
|
|
91
|
+
version: f.version,
|
|
92
|
+
status: f.status,
|
|
93
|
+
answered: Object.values(f.entries).filter((e) => e.cursor >= 0).length,
|
|
94
|
+
pendingHooks: Object.keys(f.pendingHooks ?? {}).length,
|
|
95
|
+
blockedOn: f.blockedOn ?? null
|
|
96
|
+
})) ?? null,
|
|
97
|
+
contextProviders: options.contextProviders.length
|
|
98
|
+
};
|
|
99
|
+
const encoded = JSON.stringify(state);
|
|
100
|
+
if (encoded !== previous) {
|
|
101
|
+
context.controls.emit({ type: "foundry.guidance.state", data: state });
|
|
102
|
+
previous = encoded;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const cleanups = [];
|
|
106
|
+
try {
|
|
107
|
+
for (const provider of options.contextProviders) cleanups.push(glove.addContextProvider(provider));
|
|
108
|
+
if (facts || goals || forms) {
|
|
109
|
+
await snapshot();
|
|
110
|
+
cleanups.push(glove.addContextProvider(async () => {
|
|
111
|
+
await snapshot();
|
|
112
|
+
return null;
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
return { handles, snapshot, dispose: () => {
|
|
116
|
+
for (const remove of cleanups.reverse()) remove();
|
|
117
|
+
} };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
for (const remove of cleanups.reverse()) remove();
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/workbench.ts
|
|
125
|
+
import { Effect } from "effect";
|
|
126
|
+
import {
|
|
127
|
+
getToolJsonSchema
|
|
128
|
+
} from "glove-core";
|
|
129
|
+
import {
|
|
130
|
+
JsSession,
|
|
131
|
+
mountJs
|
|
132
|
+
} from "glove-js";
|
|
133
|
+
import {
|
|
134
|
+
LispSession,
|
|
135
|
+
mountLisp
|
|
136
|
+
} from "glove-lisp";
|
|
137
|
+
import {
|
|
138
|
+
PySession,
|
|
139
|
+
mountPy
|
|
140
|
+
} from "glove-python";
|
|
141
|
+
import {
|
|
142
|
+
createWorkingEnvironment,
|
|
143
|
+
fromSnapshot,
|
|
144
|
+
mountWorkingEnvironment
|
|
145
|
+
} from "glove-working-environment";
|
|
146
|
+
var FOUNDRY_WORKING_ENVIRONMENT_BRAND = /* @__PURE__ */ Symbol.for(
|
|
147
|
+
"glove-foundry-working-environment"
|
|
148
|
+
);
|
|
149
|
+
var FOUNDRY_REPL_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-repl");
|
|
150
|
+
function defineWorkingEnvironment(options = {}) {
|
|
151
|
+
if (options.create && options.options) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"defineWorkingEnvironment accepts either create or options, not both."
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return Object.freeze({
|
|
157
|
+
...options,
|
|
158
|
+
[FOUNDRY_WORKING_ENVIRONMENT_BRAND]: true
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function assertSnapshot(value) {
|
|
162
|
+
if (!value || typeof value !== "object") {
|
|
163
|
+
throw new Error("Stored working-environment snapshot must be an object.");
|
|
164
|
+
}
|
|
165
|
+
const snapshot = value;
|
|
166
|
+
if (snapshot.version !== 1 || !Array.isArray(snapshot.dirs) || !snapshot.dirs.every((path) => typeof path === "string") || !Array.isArray(snapshot.files) || !snapshot.files.every(
|
|
167
|
+
(file) => Boolean(
|
|
168
|
+
file && typeof file === "object" && typeof file.path === "string" && typeof file.data === "string" && typeof file.mtime === "number"
|
|
169
|
+
)
|
|
170
|
+
)) {
|
|
171
|
+
throw new Error("Stored working-environment snapshot is invalid or unsupported.");
|
|
172
|
+
}
|
|
173
|
+
return structuredClone(snapshot);
|
|
174
|
+
}
|
|
175
|
+
function environmentSnapshotOwner(scope, context) {
|
|
176
|
+
return {
|
|
177
|
+
scope,
|
|
178
|
+
definitionId: context.definitionId,
|
|
179
|
+
agentId: context.agentId,
|
|
180
|
+
conversationId: context.conversationId,
|
|
181
|
+
workspaceId: context.workspaceId
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function foundryDataEnvironmentPersistence(options = {}) {
|
|
185
|
+
const scope = options.scope ?? "agent";
|
|
186
|
+
const adapter = {
|
|
187
|
+
identifier: `foundry-data:${scope}`,
|
|
188
|
+
load(context) {
|
|
189
|
+
return Effect.map(
|
|
190
|
+
context.data.getWorkingEnvironmentSnapshot(
|
|
191
|
+
environmentSnapshotOwner(scope, context)
|
|
192
|
+
),
|
|
193
|
+
(snapshot) => snapshot ? assertSnapshot(snapshot) : null
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
save(snapshot, context) {
|
|
197
|
+
return context.data.putWorkingEnvironmentSnapshot(
|
|
198
|
+
environmentSnapshotOwner(scope, context),
|
|
199
|
+
structuredClone(snapshot)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
return Object.freeze(adapter);
|
|
204
|
+
}
|
|
205
|
+
function defineRepl(options) {
|
|
206
|
+
const valid = options.language === "javascript" && options.session instanceof JsSession || options.language === "python" && options.session instanceof PySession || options.language === "lisp" && options.session instanceof LispSession;
|
|
207
|
+
if (!valid) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Foundry ${options.language} REPL requires the matching native Glove session.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return Object.freeze({
|
|
213
|
+
...options,
|
|
214
|
+
[FOUNDRY_REPL_BRAND]: true
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
function isProgrammaticSelection(value) {
|
|
218
|
+
return typeof value === "object" && value !== null && "tool" in value;
|
|
219
|
+
}
|
|
220
|
+
function parseProgrammaticToolData(data) {
|
|
221
|
+
if (typeof data !== "string") return data;
|
|
222
|
+
const text = data.trim();
|
|
223
|
+
if (!text.startsWith("{") && !text.startsWith("[")) return data;
|
|
224
|
+
try {
|
|
225
|
+
return JSON.parse(text);
|
|
226
|
+
} catch {
|
|
227
|
+
return data;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
async function runProgrammaticTool(tool, input, signal) {
|
|
231
|
+
if (tool.unAbortable || !signal) return tool.run(input, void 0, signal);
|
|
232
|
+
if (signal.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
233
|
+
return new Promise((resolve, reject) => {
|
|
234
|
+
const abort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
235
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
236
|
+
tool.run(input, void 0, signal).then(
|
|
237
|
+
(value) => {
|
|
238
|
+
signal.removeEventListener("abort", abort);
|
|
239
|
+
resolve(value);
|
|
240
|
+
},
|
|
241
|
+
(cause) => {
|
|
242
|
+
signal.removeEventListener("abort", abort);
|
|
243
|
+
reject(cause);
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function programmaticToolFunction(selection, options) {
|
|
249
|
+
const decorated = isProgrammaticSelection(selection) ? selection : { tool: selection };
|
|
250
|
+
const tool = decorated.tool;
|
|
251
|
+
if (!options.available.has(tool)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Programmatic tool "${tool.name}" is not an exact reference from the assembled agent tool registry.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
const name = decorated.name ?? tool.name;
|
|
257
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Programmatic tool name "${name}" is not a valid JavaScript/Python/Lisp identifier; provide a valid name override.`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
name,
|
|
264
|
+
description: tool.description,
|
|
265
|
+
inputSchema: getToolJsonSchema(tool),
|
|
266
|
+
...decorated.readOnly !== void 0 ? { readOnlyHint: decorated.readOnly } : {},
|
|
267
|
+
...decorated.server !== void 0 ? { server: decorated.server } : {},
|
|
268
|
+
...decorated.serverDescription !== void 0 ? { serverDescription: decorated.serverDescription } : {},
|
|
269
|
+
...decorated.resultShape !== void 0 ? { resultShape: decorated.resultShape } : {},
|
|
270
|
+
async call(input, fnContext = {}) {
|
|
271
|
+
if (options.budget.used >= options.budget.max) {
|
|
272
|
+
options.context.controls.emit({
|
|
273
|
+
type: "foundry.repl.programmatic-tool.limit",
|
|
274
|
+
data: { tool: tool.name, function: name, maxCalls: options.budget.max }
|
|
275
|
+
});
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Programmatic tool-call limit (${options.budget.max}) reached for this run. Return the result already computed or continue in a new run.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
options.budget.used += 1;
|
|
281
|
+
const parsed = tool.input_schema?.safeParse(input);
|
|
282
|
+
if (parsed && !parsed.success) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
parsed.error.issues.slice(0, 3).map((issue) => `${issue.path.join(".") || "input"}: ${issue.message}`).join("; ")
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const validated = parsed?.success ? parsed.data : input;
|
|
288
|
+
const permission = typeof tool.requiresPermission === "function" ? tool.requiresPermission(validated) : Boolean(tool.requiresPermission);
|
|
289
|
+
if (permission) {
|
|
290
|
+
options.context.controls.emit({
|
|
291
|
+
type: "foundry.repl.programmatic-tool.denied",
|
|
292
|
+
data: { tool: tool.name, function: name, reason: "interactive-approval-required" }
|
|
293
|
+
});
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Tool "${tool.name}" requires interactive approval and cannot run inside a programmatic workflow. Call it directly so the approval can be shown.`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const started = Date.now();
|
|
299
|
+
options.context.controls.emit({
|
|
300
|
+
type: "foundry.repl.programmatic-tool.started",
|
|
301
|
+
data: { tool: tool.name, function: name, call: options.budget.used }
|
|
302
|
+
});
|
|
303
|
+
try {
|
|
304
|
+
const result = await runProgrammaticTool(tool, validated, fnContext.signal);
|
|
305
|
+
if (result.status !== "success") {
|
|
306
|
+
throw new Error(result.message ?? `Tool "${tool.name}" failed.`);
|
|
307
|
+
}
|
|
308
|
+
options.context.controls.emit({
|
|
309
|
+
type: "foundry.repl.programmatic-tool.completed",
|
|
310
|
+
data: { tool: tool.name, function: name, durationMs: Date.now() - started }
|
|
311
|
+
});
|
|
312
|
+
return parseProgrammaticToolData(result.data);
|
|
313
|
+
} catch (cause) {
|
|
314
|
+
options.context.controls.emit({
|
|
315
|
+
type: "foundry.repl.programmatic-tool.failed",
|
|
316
|
+
data: {
|
|
317
|
+
tool: tool.name,
|
|
318
|
+
function: name,
|
|
319
|
+
durationMs: Date.now() - started,
|
|
320
|
+
outcome: "error"
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
throw cause;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function registerProgrammaticTools(options) {
|
|
329
|
+
const config = options.repl.programmaticTools;
|
|
330
|
+
if (!config) return 0;
|
|
331
|
+
const max = config.maxCalls ?? 50;
|
|
332
|
+
if (!Number.isInteger(max) || max < 1 || max > 1e3) {
|
|
333
|
+
throw new Error("Foundry programmaticTools.maxCalls must be an integer from 1 to 1000.");
|
|
334
|
+
}
|
|
335
|
+
const tools = Object.freeze([...options.glove.tools]);
|
|
336
|
+
const selected = await resolveResolvable(config.select({
|
|
337
|
+
assembly: options.context,
|
|
338
|
+
glove: options.glove,
|
|
339
|
+
tools,
|
|
340
|
+
...options.workingEnvironment ? { workingEnvironment: options.workingEnvironment } : {},
|
|
341
|
+
...options.vfs ? { vfs: options.vfs } : {}
|
|
342
|
+
}));
|
|
343
|
+
const available = new Set(tools);
|
|
344
|
+
const budget = { used: 0, max };
|
|
345
|
+
const functions = selected.map(
|
|
346
|
+
(selection) => programmaticToolFunction(selection, { available, budget, context: options.context })
|
|
347
|
+
);
|
|
348
|
+
const names = /* @__PURE__ */ new Set();
|
|
349
|
+
for (const fn of functions) {
|
|
350
|
+
if (names.has(fn.name)) throw new Error(`Duplicate programmatic function name "${fn.name}".`);
|
|
351
|
+
names.add(fn.name);
|
|
352
|
+
}
|
|
353
|
+
switch (options.repl.language) {
|
|
354
|
+
case "javascript":
|
|
355
|
+
case "python":
|
|
356
|
+
options.repl.session.registerAll(functions);
|
|
357
|
+
break;
|
|
358
|
+
case "lisp":
|
|
359
|
+
options.repl.session.registerFns(functions);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
return functions.length;
|
|
363
|
+
}
|
|
364
|
+
function persistenceContext(context) {
|
|
365
|
+
return {
|
|
366
|
+
definitionId: context.definitionId,
|
|
367
|
+
agentId: context.agentId,
|
|
368
|
+
conversationId: context.conversationId,
|
|
369
|
+
workspaceId: context.workspaceId,
|
|
370
|
+
runId: context.runId,
|
|
371
|
+
data: context.data,
|
|
372
|
+
signal: context.controls.signal
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function createEnvironment(definition, context) {
|
|
376
|
+
if (definition[FOUNDRY_WORKING_ENVIRONMENT_BRAND] !== true) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
"Agent workingEnvironment must be created with defineWorkingEnvironment(...)."
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const persistence = persistenceContext(context);
|
|
382
|
+
const snapshot = definition.persistence ? await resolveResolvable(definition.persistence.load(persistence)) : null;
|
|
383
|
+
const createContext = {
|
|
384
|
+
assembly: context,
|
|
385
|
+
snapshot
|
|
386
|
+
};
|
|
387
|
+
if (definition.create) {
|
|
388
|
+
return {
|
|
389
|
+
environment: await resolveResolvable(definition.create(createContext)),
|
|
390
|
+
persistence
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const resolvedOptions = typeof definition.options === "function" ? await resolveResolvable(definition.options(createContext)) : definition.options ?? {};
|
|
394
|
+
return {
|
|
395
|
+
environment: await createWorkingEnvironment({
|
|
396
|
+
...resolvedOptions,
|
|
397
|
+
...snapshot && resolvedOptions.filesystem === void 0 ? { filesystem: fromSnapshot(snapshot) } : {}
|
|
398
|
+
}),
|
|
399
|
+
persistence
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function mountFoundryWorkbench(options) {
|
|
403
|
+
let environment;
|
|
404
|
+
let persistence;
|
|
405
|
+
let mountedRepl;
|
|
406
|
+
let replMountPromise;
|
|
407
|
+
const dispose = async () => {
|
|
408
|
+
if (!environment || !options.workingEnvironment) return;
|
|
409
|
+
const failures = [];
|
|
410
|
+
if (options.workingEnvironment.persistence && persistence) {
|
|
411
|
+
try {
|
|
412
|
+
const snapshot = await environment.snapshot();
|
|
413
|
+
await resolveResolvable(
|
|
414
|
+
options.workingEnvironment.persistence.save(snapshot, persistence)
|
|
415
|
+
);
|
|
416
|
+
options.context.controls.emit({
|
|
417
|
+
type: "foundry.working-environment.snapshot.saved",
|
|
418
|
+
data: {
|
|
419
|
+
persistence: options.workingEnvironment.persistence.identifier,
|
|
420
|
+
files: snapshot.files.length
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
} catch (cause) {
|
|
424
|
+
failures.push(cause);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (options.workingEnvironment.close !== false) {
|
|
428
|
+
try {
|
|
429
|
+
await environment.close();
|
|
430
|
+
} catch (cause) {
|
|
431
|
+
failures.push(cause);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (failures.length > 0) {
|
|
435
|
+
throw new AggregateError(
|
|
436
|
+
failures,
|
|
437
|
+
"Foundry working-environment cleanup failed."
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
const mountRepl = () => {
|
|
442
|
+
const repl = options.repl;
|
|
443
|
+
if (!repl) return Promise.resolve();
|
|
444
|
+
if (replMountPromise) return replMountPromise;
|
|
445
|
+
replMountPromise = (async () => {
|
|
446
|
+
if (repl[FOUNDRY_REPL_BRAND] !== true) {
|
|
447
|
+
throw new Error("Agent repl must be created with defineRepl(...).");
|
|
448
|
+
}
|
|
449
|
+
const programmaticTools = await registerProgrammaticTools({
|
|
450
|
+
repl,
|
|
451
|
+
glove: options.glove,
|
|
452
|
+
context: options.context,
|
|
453
|
+
...environment ? { workingEnvironment: environment, vfs: environment.fs } : {}
|
|
454
|
+
});
|
|
455
|
+
switch (repl.language) {
|
|
456
|
+
case "javascript":
|
|
457
|
+
mountJs(options.glove, {
|
|
458
|
+
session: repl.session,
|
|
459
|
+
...repl.mount ?? {},
|
|
460
|
+
exclusive: false
|
|
461
|
+
});
|
|
462
|
+
break;
|
|
463
|
+
case "python":
|
|
464
|
+
mountPy(options.glove, {
|
|
465
|
+
session: repl.session,
|
|
466
|
+
...repl.mount ?? {},
|
|
467
|
+
exclusive: false
|
|
468
|
+
});
|
|
469
|
+
break;
|
|
470
|
+
case "lisp":
|
|
471
|
+
mountLisp(options.glove, {
|
|
472
|
+
session: repl.session,
|
|
473
|
+
...repl.mount ?? {},
|
|
474
|
+
exclusive: false
|
|
475
|
+
});
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
options.context.controls.emit({
|
|
479
|
+
type: "foundry.repl.mounted",
|
|
480
|
+
data: {
|
|
481
|
+
language: repl.language,
|
|
482
|
+
frame: repl.mount?.frame ?? "repl",
|
|
483
|
+
programmaticTools
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
})();
|
|
487
|
+
return replMountPromise;
|
|
488
|
+
};
|
|
489
|
+
try {
|
|
490
|
+
if (options.workingEnvironment) {
|
|
491
|
+
const created = await createEnvironment(
|
|
492
|
+
options.workingEnvironment,
|
|
493
|
+
options.context
|
|
494
|
+
);
|
|
495
|
+
environment = created.environment;
|
|
496
|
+
persistence = created.persistence;
|
|
497
|
+
mountWorkingEnvironment(options.glove, {
|
|
498
|
+
env: environment,
|
|
499
|
+
...options.workingEnvironment.mount ?? {}
|
|
500
|
+
});
|
|
501
|
+
options.context.controls.emit({
|
|
502
|
+
type: "foundry.working-environment.mounted",
|
|
503
|
+
data: {
|
|
504
|
+
tools: environment.tools.map((tool) => tool.name),
|
|
505
|
+
modules: [...environment.moduleDescriptions.keys()],
|
|
506
|
+
warnings: environment.warnings,
|
|
507
|
+
persistence: options.workingEnvironment.persistence?.identifier ?? null
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (options.repl) {
|
|
512
|
+
mountedRepl = options.repl.language === "javascript" ? Object.freeze({ language: "javascript", session: options.repl.session }) : options.repl.language === "python" ? Object.freeze({ language: "python", session: options.repl.session }) : Object.freeze({ language: "lisp", session: options.repl.session });
|
|
513
|
+
if (!options.deferRepl) await mountRepl();
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
...environment ? { workingEnvironment: environment, vfs: environment.fs } : {},
|
|
517
|
+
...mountedRepl ? { repl: mountedRepl } : {},
|
|
518
|
+
mountRepl,
|
|
519
|
+
dispose
|
|
520
|
+
};
|
|
521
|
+
} catch (cause) {
|
|
522
|
+
try {
|
|
523
|
+
await dispose();
|
|
524
|
+
} catch (cleanupCause) {
|
|
525
|
+
throw new AggregateError(
|
|
526
|
+
[cause, cleanupCause],
|
|
527
|
+
"Foundry workbench mounting and cleanup failed."
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
throw cause;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export {
|
|
535
|
+
defineFacts,
|
|
536
|
+
defineGoals,
|
|
537
|
+
defineForms,
|
|
538
|
+
foundryGuidanceSubject,
|
|
539
|
+
mountFoundryGuidance,
|
|
540
|
+
FOUNDRY_WORKING_ENVIRONMENT_BRAND,
|
|
541
|
+
FOUNDRY_REPL_BRAND,
|
|
542
|
+
defineWorkingEnvironment,
|
|
543
|
+
foundryDataEnvironmentPersistence,
|
|
544
|
+
defineRepl,
|
|
545
|
+
mountFoundryWorkbench
|
|
546
|
+
};
|