glove-foundry 0.0.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/LICENSE.md +7 -0
- package/README.md +220 -0
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/dist/chunk-3GCUECPA.js +368 -0
- package/dist/chunk-CRWY7M66.js +3275 -0
- package/dist/chunk-UNHGCCSA.js +4211 -0
- package/dist/chunk-ZFNMFE3T.js +23 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +760 -0
- package/dist/client-CLkZREDr.d.ts +2186 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +10 -0
- package/dist/config.d.ts +49 -0
- package/dist/config.js +8 -0
- package/dist/eslint.d.ts +122 -0
- package/dist/eslint.js +172 -0
- package/dist/execution-agent.d.ts +6 -0
- package/dist/execution-agent.js +23 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +402 -0
- package/docs/README.md +13 -0
- package/docs/architecture.md +175 -0
- package/docs/building-with-foundry.md +403 -0
- package/docs/evaluation-checklist.md +89 -0
- package/docs/implementation-backlog.md +67 -0
- package/docs/inspector.md +56 -0
- package/package.json +92 -0
|
@@ -0,0 +1,3275 @@
|
|
|
1
|
+
// src/application.ts
|
|
2
|
+
var FOUNDRY_APPLICATION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
3
|
+
"glove-foundry-application"
|
|
4
|
+
);
|
|
5
|
+
function isFoundryApplication(value) {
|
|
6
|
+
return Boolean(
|
|
7
|
+
value && typeof value === "object" && value[FOUNDRY_APPLICATION_BRAND] === true
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
function assertUnique(label, values) {
|
|
11
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12
|
+
for (const value of values) {
|
|
13
|
+
if (seen.has(value.id)) {
|
|
14
|
+
throw new Error(`Duplicate Foundry ${label} id "${value.id}".`);
|
|
15
|
+
}
|
|
16
|
+
seen.add(value.id);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function defineApplication(options) {
|
|
20
|
+
if (!options.name.trim()) throw new Error("Foundry application name is required.");
|
|
21
|
+
const accounts = Object.freeze([...options.accounts ?? []]);
|
|
22
|
+
const routes = Object.freeze([...options.routes ?? []]);
|
|
23
|
+
const bindings = Object.freeze([...options.bindings ?? []]);
|
|
24
|
+
assertUnique("account", accounts);
|
|
25
|
+
assertUnique("route", routes);
|
|
26
|
+
assertUnique("binding", bindings);
|
|
27
|
+
return Object.freeze({
|
|
28
|
+
...options,
|
|
29
|
+
accounts,
|
|
30
|
+
routes,
|
|
31
|
+
bindings,
|
|
32
|
+
[FOUNDRY_APPLICATION_BRAND]: true
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
var EMPTY_FOUNDRY_APPLICATION = defineApplication({
|
|
36
|
+
name: "Glove Foundry"
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// src/definition.ts
|
|
40
|
+
import { Displaymanager, Glove } from "glove-core";
|
|
41
|
+
import { Effect } from "effect";
|
|
42
|
+
|
|
43
|
+
// src/identity.ts
|
|
44
|
+
var identities = /* @__PURE__ */ new WeakMap();
|
|
45
|
+
function fileIdentified(value, kind, explicitId) {
|
|
46
|
+
const cell = {
|
|
47
|
+
kind,
|
|
48
|
+
...explicitId ? { id: explicitId } : {},
|
|
49
|
+
explicit: explicitId !== void 0
|
|
50
|
+
};
|
|
51
|
+
const identified = { ...value };
|
|
52
|
+
Object.defineProperty(identified, "id", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: false,
|
|
55
|
+
get() {
|
|
56
|
+
if (cell.id) return cell.id;
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Foundry ${kind} identity has not been bound yet. Default-export it from its convention file and let Foundry discovery derive the id.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
identities.set(identified, cell);
|
|
63
|
+
return identified;
|
|
64
|
+
}
|
|
65
|
+
function bindFileIdentity(value, id2, expectedKind) {
|
|
66
|
+
const cell = identities.get(value);
|
|
67
|
+
if (!cell) {
|
|
68
|
+
const current = value.id;
|
|
69
|
+
if (typeof current === "string" && current !== id2) {
|
|
70
|
+
throw new Error(`Foundry file route resolves to "${id2}" but the definition declares "${current}".`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (expectedKind && cell.kind !== expectedKind) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`Foundry ${expectedKind} route "${id2}" received a ${cell.kind} definition.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (cell.id && cell.id !== id2) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Foundry ${cell.kind} route resolves to "${id2}" but the definition declares "${cell.id}". Remove the explicit id and let the file own identity.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
cell.id = id2;
|
|
85
|
+
}
|
|
86
|
+
function fileDefinitionKey(value) {
|
|
87
|
+
const cell = identities.get(value);
|
|
88
|
+
return cell ?? value;
|
|
89
|
+
}
|
|
90
|
+
function fileDefinitionLabel(value) {
|
|
91
|
+
const cell = identities.get(value);
|
|
92
|
+
return cell?.id ?? `<${cell?.kind ?? "definition"} file route>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/definition.ts
|
|
96
|
+
var FOUNDRY_AGENT_DEFINITION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
97
|
+
"glove-foundry-agent-definition"
|
|
98
|
+
);
|
|
99
|
+
var FOUNDRY_AGENT_BRAND = FOUNDRY_AGENT_DEFINITION_BRAND;
|
|
100
|
+
var FOUNDRY_EVENT_PREFIX = "__GLOVE_FOUNDRY_EVENT__";
|
|
101
|
+
var FOUNDRY_APPLICATION_ENV = "GLOVE_FOUNDRY_APPLICATION_FILE";
|
|
102
|
+
var FOUNDRY_AGENT_ROUTE_ENV = "GLOVE_FOUNDRY_AGENT_ROUTE";
|
|
103
|
+
var FOUNDRY_AGENT_FILE_ENV = "GLOVE_FOUNDRY_AGENT_FILE";
|
|
104
|
+
var FOUNDRY_EXECUTION_MARKER = "__glove_foundry_execution_v1";
|
|
105
|
+
function defineCall(options) {
|
|
106
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(options.name)) {
|
|
107
|
+
throw new Error(`Invalid Foundry call name "${options.name}".`);
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({ ...options });
|
|
110
|
+
}
|
|
111
|
+
var ROUTE_PATTERN = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
112
|
+
function assertAgentRoute(route) {
|
|
113
|
+
if (!ROUTE_PATTERN.test(route)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Invalid Foundry agent id "${route}". Use lowercase path segments containing letters, digits, and hyphens.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (route.includes("__")) {
|
|
119
|
+
throw new Error(`Invalid Foundry agent id "${route}": "__" is reserved.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function internalAgentName(route) {
|
|
123
|
+
assertAgentRoute(route);
|
|
124
|
+
return `foundry_${route.replaceAll("/", "__").replaceAll("-", "_")}`;
|
|
125
|
+
}
|
|
126
|
+
function routeFromInternalAgentName(name) {
|
|
127
|
+
if (!name.startsWith("foundry_")) return name;
|
|
128
|
+
return name.slice("foundry_".length).replaceAll("__", "/").replaceAll("_", "-");
|
|
129
|
+
}
|
|
130
|
+
function isFoundryAgentDefinition(value) {
|
|
131
|
+
return Boolean(
|
|
132
|
+
value && typeof value === "object" && value[FOUNDRY_AGENT_DEFINITION_BRAND] === true
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
var isFoundryAgent = isFoundryAgentDefinition;
|
|
136
|
+
function validateDefinition(options, route) {
|
|
137
|
+
const id2 = route ?? options.id;
|
|
138
|
+
if (id2) assertAgentRoute(id2);
|
|
139
|
+
if (!options.model && !options.run && !options.handler && !options.spawn) {
|
|
140
|
+
throw new Error(`Foundry agent "${id2 ?? "<file route>"}" must define model, run, handler, or spawn.`);
|
|
141
|
+
}
|
|
142
|
+
if (options.model && options.systemPrompt === void 0) {
|
|
143
|
+
throw new Error(`Foundry agent "${id2 ?? "<file route>"}" must define systemPrompt when it defines a model.`);
|
|
144
|
+
}
|
|
145
|
+
if (options.inboxes !== void 0 && typeof options.inboxes !== "function") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Foundry agent "${id2 ?? "<file route>"}" inboxes must be a lazy resolver function.`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
const forbidden = ["input", "output"];
|
|
151
|
+
for (const key of forbidden) {
|
|
152
|
+
if (key in options) {
|
|
153
|
+
throw new Error(`Foundry agent "${id2 ?? "<file route>"}" cannot define ${key}; invocation contracts are framework-owned.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function defineAgent(options) {
|
|
158
|
+
validateDefinition(options);
|
|
159
|
+
const { id: id2, ...definition } = options;
|
|
160
|
+
return Object.freeze(fileIdentified({
|
|
161
|
+
...definition,
|
|
162
|
+
tags: Object.freeze([...options.tags ?? []]),
|
|
163
|
+
[FOUNDRY_AGENT_DEFINITION_BRAND]: true
|
|
164
|
+
}, "agent", id2));
|
|
165
|
+
}
|
|
166
|
+
var CONVENTION_EXPORTS = [
|
|
167
|
+
"id",
|
|
168
|
+
"description",
|
|
169
|
+
"tags",
|
|
170
|
+
"components",
|
|
171
|
+
"mcpAdapter",
|
|
172
|
+
"accountSessions",
|
|
173
|
+
"store",
|
|
174
|
+
"model",
|
|
175
|
+
"systemPrompt",
|
|
176
|
+
"displayManager",
|
|
177
|
+
"serverMode",
|
|
178
|
+
"maxRetries",
|
|
179
|
+
"maxConsecutiveErrors",
|
|
180
|
+
"compactionLimit",
|
|
181
|
+
"compactionInstructions",
|
|
182
|
+
"maxTurns",
|
|
183
|
+
"enableToolResultSummary",
|
|
184
|
+
"tools",
|
|
185
|
+
"hooks",
|
|
186
|
+
"skills",
|
|
187
|
+
"subagents",
|
|
188
|
+
"memory",
|
|
189
|
+
"inboxes",
|
|
190
|
+
"subscribers",
|
|
191
|
+
"layers",
|
|
192
|
+
"calls",
|
|
193
|
+
"schedules",
|
|
194
|
+
"playbooks",
|
|
195
|
+
"mesh",
|
|
196
|
+
"workingEnvironment",
|
|
197
|
+
"repl",
|
|
198
|
+
"configure",
|
|
199
|
+
"build",
|
|
200
|
+
"spawn",
|
|
201
|
+
"run",
|
|
202
|
+
"handler"
|
|
203
|
+
];
|
|
204
|
+
function defineAgentFromModule(route, module) {
|
|
205
|
+
assertAgentRoute(route);
|
|
206
|
+
if (isFoundryAgentDefinition(module.default)) {
|
|
207
|
+
const definition2 = module.default;
|
|
208
|
+
bindFileIdentity(definition2, route, "agent");
|
|
209
|
+
validateDefinition(definition2, route);
|
|
210
|
+
return definition2;
|
|
211
|
+
}
|
|
212
|
+
if (!module.description) {
|
|
213
|
+
throw new Error(`Foundry route "${route}" must default-export defineAgent(...) or export description.`);
|
|
214
|
+
}
|
|
215
|
+
const options = {};
|
|
216
|
+
for (const key of CONVENTION_EXPORTS) {
|
|
217
|
+
if (module[key] !== void 0) options[key] = module[key];
|
|
218
|
+
}
|
|
219
|
+
const definition = defineAgent(options);
|
|
220
|
+
bindFileIdentity(definition, route, "agent");
|
|
221
|
+
return definition;
|
|
222
|
+
}
|
|
223
|
+
function defineSubagent(options) {
|
|
224
|
+
if (!/^[A-Za-z][\w-]*$/.test(options.name)) throw new Error(`Invalid Foundry subagent name "${options.name}".`);
|
|
225
|
+
return Object.freeze({
|
|
226
|
+
name: options.name,
|
|
227
|
+
description: options.description,
|
|
228
|
+
factory: async ({ parentStore, parentControls, prompt }) => {
|
|
229
|
+
const store = await parentStore.createSubAgentStore?.(options.name, options.durable ?? false) ?? void 0;
|
|
230
|
+
const glove = new Glove({
|
|
231
|
+
...store ? { store } : {},
|
|
232
|
+
model: options.model ?? parentControls.glove.model,
|
|
233
|
+
displayManager: parentControls.displayManager ?? new Displaymanager(),
|
|
234
|
+
systemPrompt: options.systemPrompt,
|
|
235
|
+
serverMode: options.serverMode ?? parentControls.glove.serverMode,
|
|
236
|
+
...options.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
|
|
237
|
+
...options.maxConsecutiveErrors !== void 0 ? { maxConsecutiveErrors: options.maxConsecutiveErrors } : {},
|
|
238
|
+
compaction_config: {
|
|
239
|
+
compaction_instructions: options.compactionInstructions ?? `Preserve the ${options.name} subagent's findings and unresolved work.`,
|
|
240
|
+
...options.maxTurns !== void 0 ? { max_turns: options.maxTurns } : {},
|
|
241
|
+
...options.compactionLimit !== void 0 ? { compaction_context_limit: options.compactionLimit } : {}
|
|
242
|
+
},
|
|
243
|
+
...options.enableToolResultSummary !== void 0 ? { enableToolResultSummary: options.enableToolResultSummary } : {}
|
|
244
|
+
}).build();
|
|
245
|
+
for (const tool of options.tools ?? []) glove.fold(tool);
|
|
246
|
+
for (const hook of options.hooks ?? []) glove.defineHook(hook.name, hook.handler);
|
|
247
|
+
for (const skill of options.skills ?? []) glove.defineSkill(skill);
|
|
248
|
+
for (const subagent of options.subagents ?? []) glove.defineSubAgent(subagent);
|
|
249
|
+
if (options.configure) {
|
|
250
|
+
const message = { sender: "user", text: prompt };
|
|
251
|
+
const request = {
|
|
252
|
+
agentId: options.name,
|
|
253
|
+
conversationId: `subagent:${options.name}`,
|
|
254
|
+
workspaceId: "subagent",
|
|
255
|
+
message: prompt,
|
|
256
|
+
source: { kind: "spawn" }
|
|
257
|
+
};
|
|
258
|
+
await resolveResolvable(options.configure({
|
|
259
|
+
definitionId: options.name,
|
|
260
|
+
agentId: options.name,
|
|
261
|
+
conversationId: request.conversationId,
|
|
262
|
+
workspaceId: request.workspaceId,
|
|
263
|
+
runId: `subagent:${options.name}`,
|
|
264
|
+
input: prompt,
|
|
265
|
+
request,
|
|
266
|
+
message,
|
|
267
|
+
messageInput: prompt,
|
|
268
|
+
messageText: prompt,
|
|
269
|
+
history: [],
|
|
270
|
+
messages: [message],
|
|
271
|
+
glove,
|
|
272
|
+
signal: new AbortController().signal,
|
|
273
|
+
emit: () => void 0
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
return glove;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
async function resolveResolvable(value) {
|
|
281
|
+
if (Effect.isEffect(value)) return Effect.runPromise(value);
|
|
282
|
+
return Promise.resolve(value);
|
|
283
|
+
}
|
|
284
|
+
function defineRoutes(routes) {
|
|
285
|
+
return Object.freeze({ ...routes });
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/capabilities.ts
|
|
289
|
+
import { Effect as Effect2 } from "effect";
|
|
290
|
+
import { mountMcp } from "glove-mcp";
|
|
291
|
+
import {
|
|
292
|
+
useContext,
|
|
293
|
+
useEpisodicCurator,
|
|
294
|
+
useEpisodicReader,
|
|
295
|
+
useMemoryCurator,
|
|
296
|
+
useMemoryReader,
|
|
297
|
+
useResourcesCurator,
|
|
298
|
+
useResourcesReader
|
|
299
|
+
} from "glove-memory/tools";
|
|
300
|
+
var FOUNDRY_SHARED_TOOL_BRAND = /* @__PURE__ */ Symbol.for(
|
|
301
|
+
"glove-foundry-shared-tool"
|
|
302
|
+
);
|
|
303
|
+
var FOUNDRY_AGENT_APPLICATION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
304
|
+
"glove-foundry-agent-application"
|
|
305
|
+
);
|
|
306
|
+
var FOUNDRY_MCP_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-mcp");
|
|
307
|
+
var FOUNDRY_MEMORY_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-memory");
|
|
308
|
+
function install(capability, ...configuration) {
|
|
309
|
+
const config = configuration[0];
|
|
310
|
+
const selection = configuration[1];
|
|
311
|
+
const kind = FOUNDRY_SHARED_TOOL_BRAND in capability ? "tool" : FOUNDRY_AGENT_APPLICATION_BRAND in capability ? "application" : "mcp";
|
|
312
|
+
const installation = {
|
|
313
|
+
kind,
|
|
314
|
+
...config !== void 0 ? { config } : {}
|
|
315
|
+
};
|
|
316
|
+
Object.defineProperty(installation, "id", {
|
|
317
|
+
enumerable: true,
|
|
318
|
+
get: () => capability.id
|
|
319
|
+
});
|
|
320
|
+
if (selection?.account) {
|
|
321
|
+
Object.defineProperty(installation, "accountId", {
|
|
322
|
+
enumerable: true,
|
|
323
|
+
get: () => selection.account.id
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return Object.freeze(installation);
|
|
327
|
+
}
|
|
328
|
+
function installationKey(installation) {
|
|
329
|
+
return `${installation.kind}:${installation.id}`;
|
|
330
|
+
}
|
|
331
|
+
function assertCapabilityId(id2, label) {
|
|
332
|
+
if (!/^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/.test(id2)) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Invalid Foundry ${label} id "${id2}". Use lowercase path segments containing letters, digits, and hyphens.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function decodeCapabilityConfig(definition, installation) {
|
|
339
|
+
if (!definition.config) return installation.config;
|
|
340
|
+
const parsed = definition.config.safeParse(installation.config ?? {});
|
|
341
|
+
if (!parsed.success) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Invalid config for ${installation.kind} "${definition.id}": ${parsed.error.message}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return parsed.data;
|
|
347
|
+
}
|
|
348
|
+
function defineSharedTool(options) {
|
|
349
|
+
if (options.id) assertCapabilityId(options.id, "shared tool");
|
|
350
|
+
const { id: id2, ...definition } = options;
|
|
351
|
+
return Object.freeze(fileIdentified({
|
|
352
|
+
...definition,
|
|
353
|
+
[FOUNDRY_SHARED_TOOL_BRAND]: true
|
|
354
|
+
}, "tool", id2));
|
|
355
|
+
}
|
|
356
|
+
function defineAgentApplication(options) {
|
|
357
|
+
if (options.id) assertCapabilityId(options.id, "agent application");
|
|
358
|
+
const label = options.id ?? "<application file route>";
|
|
359
|
+
const inbound = Object.freeze([...options.inbound ?? []]);
|
|
360
|
+
const outbound = Object.freeze([...options.outbound ?? []]);
|
|
361
|
+
for (const transmission of inbound) {
|
|
362
|
+
if (!transmission.inbound) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`Application "${label}" declares an outbound-only transmission as inbound.`
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const transmission of outbound) {
|
|
369
|
+
if (!transmission.outbound) {
|
|
370
|
+
throw new Error(
|
|
371
|
+
`Application "${label}" declares an inbound-only transmission as outbound.`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const transmissionDefinitions = /* @__PURE__ */ new Map();
|
|
376
|
+
for (const transmission of [
|
|
377
|
+
...options.transmissions ?? [],
|
|
378
|
+
...inbound,
|
|
379
|
+
...outbound
|
|
380
|
+
]) {
|
|
381
|
+
const key = fileDefinitionKey(transmission);
|
|
382
|
+
const existing = transmissionDefinitions.get(key);
|
|
383
|
+
if (!existing) transmissionDefinitions.set(key, transmission);
|
|
384
|
+
}
|
|
385
|
+
const transmissions = Object.freeze([...transmissionDefinitions.values()]);
|
|
386
|
+
const connections = Object.freeze([...options.connections ?? []]);
|
|
387
|
+
const connectionIds = /* @__PURE__ */ new Set();
|
|
388
|
+
for (const connection of connections) {
|
|
389
|
+
const connectionKey = fileDefinitionKey(connection);
|
|
390
|
+
if (connectionIds.has(connectionKey)) {
|
|
391
|
+
throw new Error(`Application "${label}" contains the same connection more than once.`);
|
|
392
|
+
}
|
|
393
|
+
connectionIds.add(connectionKey);
|
|
394
|
+
for (const transmission of connection.transmissions) {
|
|
395
|
+
if (!transmissionDefinitions.has(fileDefinitionKey(transmission))) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Application "${label}" contains a connection for a transmission it does not own.`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const { id: id2, ...definition } = options;
|
|
403
|
+
return Object.freeze(fileIdentified({
|
|
404
|
+
...definition,
|
|
405
|
+
inbound,
|
|
406
|
+
outbound,
|
|
407
|
+
transmissions,
|
|
408
|
+
connections,
|
|
409
|
+
[FOUNDRY_AGENT_APPLICATION_BRAND]: true
|
|
410
|
+
}, "application", id2));
|
|
411
|
+
}
|
|
412
|
+
var defineApp = defineAgentApplication;
|
|
413
|
+
function defineMcp(options) {
|
|
414
|
+
if (options.id) assertCapabilityId(options.id, "MCP");
|
|
415
|
+
const { id: id2, ...definition } = options;
|
|
416
|
+
return Object.freeze(fileIdentified({
|
|
417
|
+
...definition,
|
|
418
|
+
[FOUNDRY_MCP_BRAND]: true
|
|
419
|
+
}, "mcp", id2));
|
|
420
|
+
}
|
|
421
|
+
function defineMemory(options) {
|
|
422
|
+
if (options.id) assertCapabilityId(options.id, "memory profile");
|
|
423
|
+
if (!options.entity && !options.episodic && !options.resources && !options.context && !options.mount) {
|
|
424
|
+
throw new Error(`Foundry memory profile "${options.id ?? "<file route>"}" is empty.`);
|
|
425
|
+
}
|
|
426
|
+
const { id: id2, ...definition } = options;
|
|
427
|
+
return Object.freeze(fileIdentified({
|
|
428
|
+
...definition,
|
|
429
|
+
[FOUNDRY_MEMORY_BRAND]: true
|
|
430
|
+
}, "memory", id2));
|
|
431
|
+
}
|
|
432
|
+
var EMPTY_CAPABILITY_REGISTRY = Object.freeze({
|
|
433
|
+
tools: Object.freeze([]),
|
|
434
|
+
applications: Object.freeze([]),
|
|
435
|
+
mcp: Object.freeze([]),
|
|
436
|
+
memory: Object.freeze([])
|
|
437
|
+
});
|
|
438
|
+
function inboxMethods(store) {
|
|
439
|
+
return typeof store.getInboxItems === "function" && typeof store.addInboxItem === "function" && typeof store.updateInboxItem === "function" && typeof store.getResolvedInboxItems === "function";
|
|
440
|
+
}
|
|
441
|
+
function isInboxCapableStore(store) {
|
|
442
|
+
return inboxMethods(store);
|
|
443
|
+
}
|
|
444
|
+
function indexRegistry(values, kind) {
|
|
445
|
+
const result = /* @__PURE__ */ new Map();
|
|
446
|
+
for (const value of values) {
|
|
447
|
+
if (result.has(value.id)) {
|
|
448
|
+
throw new Error(`Duplicate Foundry ${kind} id "${value.id}".`);
|
|
449
|
+
}
|
|
450
|
+
result.set(value.id, value);
|
|
451
|
+
}
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
function mountMemory(profile, context) {
|
|
455
|
+
return Effect2.gen(function* () {
|
|
456
|
+
if (profile.entity) {
|
|
457
|
+
const adapter = yield* profile.entity.adapter(context);
|
|
458
|
+
const options = profile.entity.tools ? { tools: profile.entity.tools } : void 0;
|
|
459
|
+
if ((profile.entity.access ?? "reader") === "curator") {
|
|
460
|
+
useMemoryCurator(context.glove, adapter, options);
|
|
461
|
+
} else {
|
|
462
|
+
useMemoryReader(context.glove, adapter, options);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (profile.episodic) {
|
|
466
|
+
const adapter = yield* profile.episodic.adapter(context);
|
|
467
|
+
const options = profile.episodic.tools ? { tools: profile.episodic.tools } : void 0;
|
|
468
|
+
if ((profile.episodic.access ?? "reader") === "curator") {
|
|
469
|
+
useEpisodicCurator(context.glove, adapter, options);
|
|
470
|
+
} else {
|
|
471
|
+
useEpisodicReader(context.glove, adapter, options);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (profile.resources) {
|
|
475
|
+
const adapter = yield* profile.resources.adapter(context);
|
|
476
|
+
const options = profile.resources.tools ? { tools: profile.resources.tools } : void 0;
|
|
477
|
+
if ((profile.resources.access ?? "reader") === "curator") {
|
|
478
|
+
useResourcesCurator(context.glove, adapter, options);
|
|
479
|
+
} else {
|
|
480
|
+
useResourcesReader(context.glove, adapter, options);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (profile.context) {
|
|
484
|
+
const adapter = yield* profile.context.adapter(context);
|
|
485
|
+
useContext(
|
|
486
|
+
context.glove,
|
|
487
|
+
adapter,
|
|
488
|
+
profile.context.tools ? { tools: profile.context.tools } : void 0
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (profile.mount) yield* profile.mount(context);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function configureMemory(profile, config) {
|
|
495
|
+
return Object.freeze({ profile, config });
|
|
496
|
+
}
|
|
497
|
+
function mountAgentDefinitionMemory(options) {
|
|
498
|
+
return Effect2.gen(function* () {
|
|
499
|
+
const memory = indexRegistry(options.registry.memory, "memory profile");
|
|
500
|
+
const seen = /* @__PURE__ */ new Set();
|
|
501
|
+
for (const selection of options.memory) {
|
|
502
|
+
const reference = "profile" in selection ? selection.profile : selection;
|
|
503
|
+
const profile = memory.get(reference.id);
|
|
504
|
+
if (!profile) {
|
|
505
|
+
throw new Error(`Unknown agent memory profile "${reference.id}".`);
|
|
506
|
+
}
|
|
507
|
+
if (seen.has(`memory:${profile.id}`)) {
|
|
508
|
+
throw new Error(`Duplicate agent memory profile "${profile.id}".`);
|
|
509
|
+
}
|
|
510
|
+
seen.add(`memory:${profile.id}`);
|
|
511
|
+
const config = typeof selection === "object" && "profile" in selection ? selection.config : void 0;
|
|
512
|
+
const capability = {
|
|
513
|
+
kind: "memory",
|
|
514
|
+
id: profile.id,
|
|
515
|
+
...config !== void 0 ? { config } : {}
|
|
516
|
+
};
|
|
517
|
+
const context = {
|
|
518
|
+
...options.context,
|
|
519
|
+
surface: { kind: "memory", id: profile.id },
|
|
520
|
+
config: decodeCapabilityConfig(profile, capability)
|
|
521
|
+
};
|
|
522
|
+
yield* mountMemory(profile, context);
|
|
523
|
+
options.context.emit({
|
|
524
|
+
type: "foundry.definition.memory.mounted",
|
|
525
|
+
data: { id: profile.id }
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
function installRegistry(options) {
|
|
531
|
+
return Effect2.gen(function* () {
|
|
532
|
+
const tools = indexRegistry(options.registry.tools, "shared tool");
|
|
533
|
+
const applications = indexRegistry(
|
|
534
|
+
options.registry.applications,
|
|
535
|
+
"agent application"
|
|
536
|
+
);
|
|
537
|
+
const mcp = indexRegistry(options.registry.mcp, "MCP");
|
|
538
|
+
const seen = /* @__PURE__ */ new Set();
|
|
539
|
+
const installedMcp = [];
|
|
540
|
+
const installed = [];
|
|
541
|
+
for (const installation of options.installations) {
|
|
542
|
+
assertCapabilityId(installation.id, installation.kind);
|
|
543
|
+
if (installation.config && typeof installation.config === "object" && Object.prototype.hasOwnProperty.call(installation.config, "accountId")) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Installation config for ${installation.kind} "${installation.id}" cannot contain accountId. Select a persisted account on the installation, or pass { account } as install()'s third argument in code.`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const key = installationKey(installation);
|
|
549
|
+
if (seen.has(key)) continue;
|
|
550
|
+
seen.add(key);
|
|
551
|
+
const definition = installation.kind === "tool" ? tools.get(installation.id) : installation.kind === "application" ? applications.get(installation.id) : mcp.get(installation.id);
|
|
552
|
+
if (!definition) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Agent "${options.context.agentId}" requests unknown ${installation.kind} "${installation.id}".`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const config = decodeCapabilityConfig(
|
|
558
|
+
definition,
|
|
559
|
+
installation
|
|
560
|
+
);
|
|
561
|
+
const context = {
|
|
562
|
+
...options.context,
|
|
563
|
+
installation,
|
|
564
|
+
config,
|
|
565
|
+
...installation.accountId ? {
|
|
566
|
+
accountId: installation.accountId,
|
|
567
|
+
...options.accountSessions ? {
|
|
568
|
+
withAccountSession: (operation, use) => options.accountSessions.withSession({
|
|
569
|
+
accountId: installation.accountId,
|
|
570
|
+
operation,
|
|
571
|
+
agentId: options.context.agentId,
|
|
572
|
+
conversationId: options.context.conversationId,
|
|
573
|
+
workspaceId: options.context.workspaceId
|
|
574
|
+
}, use)
|
|
575
|
+
} : {}
|
|
576
|
+
} : {}
|
|
577
|
+
};
|
|
578
|
+
if (installation.kind === "tool") {
|
|
579
|
+
const shared = definition;
|
|
580
|
+
const tool = shared.tool ?? (yield* shared.create(context));
|
|
581
|
+
options.context.glove.fold(tool);
|
|
582
|
+
} else if (installation.kind === "application") {
|
|
583
|
+
const { glove: _glove, store: _store, ...headlessContext } = context;
|
|
584
|
+
const application = definition;
|
|
585
|
+
const contribution = application.install ? yield* application.install(headlessContext) : void 0;
|
|
586
|
+
for (const tool of contribution?.tools ?? []) options.context.glove.fold(tool);
|
|
587
|
+
} else {
|
|
588
|
+
installedMcp.push(definition);
|
|
589
|
+
}
|
|
590
|
+
installed.push(installation);
|
|
591
|
+
options.context.emit({
|
|
592
|
+
type: "foundry.installation.completed",
|
|
593
|
+
data: { kind: installation.kind, id: installation.id }
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (installedMcp.length > 0) {
|
|
597
|
+
if (!options.mcpAdapter) {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`Agent "${options.context.agentId}" installs MCP capabilities but the application does not define mcpAdapter.`
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
const adapter = yield* options.mcpAdapter({
|
|
603
|
+
...options.context,
|
|
604
|
+
installed: installedMcp
|
|
605
|
+
});
|
|
606
|
+
const selectedIds = new Set(installedMcp.map((entry) => entry.id));
|
|
607
|
+
const scopedAdapter = {
|
|
608
|
+
identifier: adapter.identifier,
|
|
609
|
+
getActive: async () => [...selectedIds],
|
|
610
|
+
activate: (id2) => adapter.activate(id2),
|
|
611
|
+
deactivate: (id2) => adapter.deactivate(id2),
|
|
612
|
+
...adapter.getAccessToken ? { getAccessToken: (id2) => adapter.getAccessToken(id2) } : {},
|
|
613
|
+
...adapter.getAuthHeaders ? { getAuthHeaders: (id2) => adapter.getAuthHeaders(id2) } : {}
|
|
614
|
+
};
|
|
615
|
+
yield* Effect2.tryPromise({
|
|
616
|
+
try: () => mountMcp(options.context.glove, {
|
|
617
|
+
adapter: scopedAdapter,
|
|
618
|
+
entries: installedMcp.map((definition) => ({
|
|
619
|
+
...definition.entry,
|
|
620
|
+
id: definition.id
|
|
621
|
+
})),
|
|
622
|
+
ambiguityPolicy: { type: "auto-pick-best" }
|
|
623
|
+
}),
|
|
624
|
+
catch: (cause) => cause
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return Object.freeze(installed);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function isFoundryCapability(value) {
|
|
631
|
+
if (!value || typeof value !== "object") return false;
|
|
632
|
+
const object = value;
|
|
633
|
+
return object[FOUNDRY_SHARED_TOOL_BRAND] === true || object[FOUNDRY_AGENT_APPLICATION_BRAND] === true || object[FOUNDRY_MCP_BRAND] === true || object[FOUNDRY_MEMORY_BRAND] === true;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/surfaces.ts
|
|
637
|
+
import { Effect as Effect3 } from "effect";
|
|
638
|
+
var FOUNDRY_LAYER_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-layer");
|
|
639
|
+
var FOUNDRY_SUBSCRIBER_BRAND = /* @__PURE__ */ Symbol.for(
|
|
640
|
+
"glove-foundry-subscriber"
|
|
641
|
+
);
|
|
642
|
+
var SURFACE_ID = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
643
|
+
function assertSurfaceId(id2, label) {
|
|
644
|
+
if (!SURFACE_ID.test(id2)) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Invalid Foundry ${label} id "${id2}". Use lowercase path segments containing letters, digits, and hyphens.`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function defineLayer(options) {
|
|
651
|
+
if (options.id) assertSurfaceId(options.id, "layer");
|
|
652
|
+
const { id: id2, ...definition } = options;
|
|
653
|
+
return Object.freeze(fileIdentified({
|
|
654
|
+
...definition,
|
|
655
|
+
[FOUNDRY_LAYER_BRAND]: true
|
|
656
|
+
}, "layer", id2));
|
|
657
|
+
}
|
|
658
|
+
function configureLayer(layer, config) {
|
|
659
|
+
return Object.freeze({ layer, config });
|
|
660
|
+
}
|
|
661
|
+
function defineSubscriber(options) {
|
|
662
|
+
if (options.id) assertSurfaceId(options.id, "subscriber");
|
|
663
|
+
const { id: id2, ...definition } = options;
|
|
664
|
+
return Object.freeze(fileIdentified({
|
|
665
|
+
...definition,
|
|
666
|
+
[FOUNDRY_SUBSCRIBER_BRAND]: true
|
|
667
|
+
}, "subscriber", id2));
|
|
668
|
+
}
|
|
669
|
+
var EMPTY_NATIVE_REGISTRY = Object.freeze({
|
|
670
|
+
layers: Object.freeze([]),
|
|
671
|
+
subscribers: Object.freeze([])
|
|
672
|
+
});
|
|
673
|
+
function indexById(values, label) {
|
|
674
|
+
const indexed = /* @__PURE__ */ new Map();
|
|
675
|
+
for (const value of values) {
|
|
676
|
+
if (indexed.has(value.id)) {
|
|
677
|
+
throw new Error(`Duplicate Foundry ${label} id "${value.id}".`);
|
|
678
|
+
}
|
|
679
|
+
indexed.set(value.id, value);
|
|
680
|
+
}
|
|
681
|
+
return indexed;
|
|
682
|
+
}
|
|
683
|
+
async function mountFoundrySurfaces(options) {
|
|
684
|
+
const layers = indexById(options.registry.layers, "layer");
|
|
685
|
+
const subscribers = indexById(options.registry.subscribers, "subscriber");
|
|
686
|
+
const cleanup = [];
|
|
687
|
+
const disposeAll = async () => {
|
|
688
|
+
const failures = [];
|
|
689
|
+
for (const dispose of cleanup.reverse()) {
|
|
690
|
+
try {
|
|
691
|
+
const result = dispose();
|
|
692
|
+
if (Effect3.isEffect(result)) await Effect3.runPromise(result);
|
|
693
|
+
else await Promise.resolve(result);
|
|
694
|
+
} catch (cause) {
|
|
695
|
+
failures.push(cause);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (failures.length > 0) {
|
|
699
|
+
throw new AggregateError(failures, "Foundry surface cleanup failed.");
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
try {
|
|
703
|
+
for (const selected of options.subscribers ?? []) {
|
|
704
|
+
let definition = selected;
|
|
705
|
+
if (typeof definition === "object" && definition !== null && FOUNDRY_SUBSCRIBER_BRAND in definition) {
|
|
706
|
+
const registered = subscribers.get(definition.id);
|
|
707
|
+
if (!registered) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`Agent "${options.context.agentId}" references unknown subscriber "${definition.id}".`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
definition = registered;
|
|
713
|
+
}
|
|
714
|
+
let subscriber;
|
|
715
|
+
let subscriberId = "inline";
|
|
716
|
+
if (typeof definition === "object" && definition !== null && definition[FOUNDRY_SUBSCRIBER_BRAND] === true) {
|
|
717
|
+
const foundrySubscriber = definition;
|
|
718
|
+
const created = typeof foundrySubscriber.create === "function" ? foundrySubscriber.create(options.context) : foundrySubscriber.create;
|
|
719
|
+
subscriber = Effect3.isEffect(created) ? await Effect3.runPromise(created) : await Promise.resolve(created);
|
|
720
|
+
subscriberId = foundrySubscriber.id;
|
|
721
|
+
} else {
|
|
722
|
+
subscriber = definition;
|
|
723
|
+
}
|
|
724
|
+
options.context.glove.addSubscriber(subscriber);
|
|
725
|
+
cleanup.push(() => options.context.glove.removeSubscriber(subscriber));
|
|
726
|
+
options.context.emit({
|
|
727
|
+
type: "foundry.subscriber.mounted",
|
|
728
|
+
data: { id: subscriberId }
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
for (const selected of options.layers ?? []) {
|
|
732
|
+
const reference = FOUNDRY_LAYER_BRAND in selected ? { layer: selected } : selected;
|
|
733
|
+
const definition = layers.get(reference.layer.id);
|
|
734
|
+
if (!definition) {
|
|
735
|
+
throw new Error(
|
|
736
|
+
`Agent "${options.context.agentId}" references unknown layer "${reference.layer.id}".`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
const referenceConfig = "config" in reference ? reference.config : void 0;
|
|
740
|
+
const parsed = definition.config ? definition.config.safeParse(referenceConfig ?? {}) : { success: true, data: referenceConfig };
|
|
741
|
+
if (!parsed.success) {
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Invalid config for layer "${definition.id}": ${parsed.error.message}`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const dispose = await Effect3.runPromise(
|
|
747
|
+
definition.setup({ ...options.context, config: parsed.data })
|
|
748
|
+
);
|
|
749
|
+
if (dispose) cleanup.push(dispose);
|
|
750
|
+
options.context.emit({
|
|
751
|
+
type: "foundry.layer.mounted",
|
|
752
|
+
data: { id: definition.id }
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
} catch (cause) {
|
|
756
|
+
try {
|
|
757
|
+
await disposeAll();
|
|
758
|
+
} catch (cleanupCause) {
|
|
759
|
+
throw new AggregateError(
|
|
760
|
+
[cause, cleanupCause],
|
|
761
|
+
"Foundry surface mount and cleanup failed."
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
throw cause;
|
|
765
|
+
}
|
|
766
|
+
return disposeAll;
|
|
767
|
+
}
|
|
768
|
+
function isFoundryLayer(value) {
|
|
769
|
+
return Boolean(
|
|
770
|
+
value && typeof value === "object" && value[FOUNDRY_LAYER_BRAND] === true
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
function isFoundrySubscriber(value) {
|
|
774
|
+
return Boolean(
|
|
775
|
+
value && typeof value === "object" && value[FOUNDRY_SUBSCRIBER_BRAND] === true
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// src/playbook.ts
|
|
780
|
+
import { createHash } from "node:crypto";
|
|
781
|
+
var ID = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
782
|
+
function assertData(value, path, seen = /* @__PURE__ */ new Set()) {
|
|
783
|
+
if (value === void 0) throw new Error(`Playbook ${path} cannot contain undefined.`);
|
|
784
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
785
|
+
throw new Error(`Playbook ${path} must contain a finite number.`);
|
|
786
|
+
}
|
|
787
|
+
if (typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") {
|
|
788
|
+
throw new Error(`Playbook ${path} must be serializable data, not ${typeof value}.`);
|
|
789
|
+
}
|
|
790
|
+
if (!value || typeof value !== "object") return;
|
|
791
|
+
const prototype = Object.getPrototypeOf(value);
|
|
792
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
793
|
+
throw new Error(`Playbook ${path} must contain plain objects and arrays only.`);
|
|
794
|
+
}
|
|
795
|
+
if (seen.has(value)) throw new Error(`Playbook ${path} cannot contain circular data.`);
|
|
796
|
+
seen.add(value);
|
|
797
|
+
if (Array.isArray(value)) {
|
|
798
|
+
value.forEach((item, index) => assertData(item, `${path}[${index}]`, seen));
|
|
799
|
+
} else {
|
|
800
|
+
for (const [key, item] of Object.entries(value)) assertData(item, `${path}.${key}`, seen);
|
|
801
|
+
}
|
|
802
|
+
seen.delete(value);
|
|
803
|
+
}
|
|
804
|
+
function cloneData(value) {
|
|
805
|
+
if (Array.isArray(value)) return value.map(cloneData);
|
|
806
|
+
if (value && typeof value === "object") {
|
|
807
|
+
return Object.fromEntries(
|
|
808
|
+
Object.entries(value).map(([key, item]) => [key, cloneData(item)])
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
return value;
|
|
812
|
+
}
|
|
813
|
+
function freezeData(value) {
|
|
814
|
+
if (!value || typeof value !== "object") return value;
|
|
815
|
+
for (const item of Object.values(value)) freezeData(item);
|
|
816
|
+
return Object.freeze(value);
|
|
817
|
+
}
|
|
818
|
+
function normalizePlaybook(playbook) {
|
|
819
|
+
const authored = "transmission" in playbook;
|
|
820
|
+
const id2 = playbook.id;
|
|
821
|
+
if (!id2) throw new Error("A persisted playbook must have an id.");
|
|
822
|
+
const transmissionId = authored ? playbook.transmission.id : playbook.transmissionId;
|
|
823
|
+
const normalized = {
|
|
824
|
+
id: id2,
|
|
825
|
+
transmissionId,
|
|
826
|
+
...playbook.enabled !== void 0 ? { enabled: playbook.enabled } : {},
|
|
827
|
+
...playbook.match ? {
|
|
828
|
+
match: {
|
|
829
|
+
...playbook.match.event ? { event: authored ? playbook.match.event.id : playbook.match.event } : {},
|
|
830
|
+
...(authored ? playbook.match.routes?.length : playbook.match.routeIds?.length) ? {
|
|
831
|
+
routeIds: authored ? playbook.match.routes?.map((route) => route.id) : playbook.match.routeIds
|
|
832
|
+
} : {},
|
|
833
|
+
...playbook.match.predicate ? {
|
|
834
|
+
predicate: {
|
|
835
|
+
name: authored ? playbook.match.predicate.definition.id : playbook.match.predicate.name,
|
|
836
|
+
...playbook.match.predicate.parameters ? { parameters: playbook.match.predicate.parameters } : {}
|
|
837
|
+
}
|
|
838
|
+
} : {}
|
|
839
|
+
}
|
|
840
|
+
} : {},
|
|
841
|
+
directives: authored ? playbook.directives.map((directive) => ({
|
|
842
|
+
action: directive.action.id,
|
|
843
|
+
instruction: directive.instruction,
|
|
844
|
+
...directive.parameters ? { parameters: directive.parameters } : {}
|
|
845
|
+
})) : playbook.directives,
|
|
846
|
+
...playbook.applications ? {
|
|
847
|
+
applications: authored ? playbook.applications.map((application) => application.id) : playbook.applications
|
|
848
|
+
} : {},
|
|
849
|
+
...playbook.outbound ? {
|
|
850
|
+
outbound: authored ? playbook.outbound.map(
|
|
851
|
+
(outbound) => ({
|
|
852
|
+
routeId: outbound.route.id,
|
|
853
|
+
...outbound.application ? { applicationId: outbound.application.id } : {},
|
|
854
|
+
...outbound.event ? { event: outbound.event.id } : {},
|
|
855
|
+
...outbound.account ? { accountId: outbound.account.id } : {},
|
|
856
|
+
...outbound.applicationAccount ? { applicationAccountId: outbound.applicationAccount.id } : {},
|
|
857
|
+
...outbound.instruction ? { instruction: outbound.instruction } : {}
|
|
858
|
+
})
|
|
859
|
+
) : playbook.outbound.map(
|
|
860
|
+
(outbound) => ({ ...outbound })
|
|
861
|
+
)
|
|
862
|
+
} : {},
|
|
863
|
+
...playbook.serialization ? { serialization: playbook.serialization } : {},
|
|
864
|
+
..."origin" in playbook && playbook.origin ? { origin: playbook.origin } : {},
|
|
865
|
+
..."playbookName" in playbook && playbook.playbookName ? { playbookName: playbook.playbookName } : {},
|
|
866
|
+
..."definitionRevision" in playbook && playbook.definitionRevision ? { definitionRevision: playbook.definitionRevision } : {}
|
|
867
|
+
};
|
|
868
|
+
if (!ID.test(id2)) throw new Error(`Invalid playbook id "${id2}".`);
|
|
869
|
+
if (!ID.test(transmissionId)) {
|
|
870
|
+
throw new Error(`Invalid transmission id "${transmissionId}" in playbook "${playbook.id}".`);
|
|
871
|
+
}
|
|
872
|
+
if (normalized.directives.length === 0) {
|
|
873
|
+
throw new Error(`Playbook "${playbook.id}" must define at least one action directive.`);
|
|
874
|
+
}
|
|
875
|
+
if (normalized.match?.predicate && !normalized.match.predicate.name) {
|
|
876
|
+
throw new Error(`Playbook "${playbook.id}" predicate must reference a definition.`);
|
|
877
|
+
}
|
|
878
|
+
for (const outbound of normalized.outbound ?? []) {
|
|
879
|
+
if (!outbound.routeId) throw new Error(`Playbook "${playbook.id}" outbound must reference a route.`);
|
|
880
|
+
}
|
|
881
|
+
assertData(normalized, normalized.id);
|
|
882
|
+
return freezeData(cloneData(normalized));
|
|
883
|
+
}
|
|
884
|
+
function composePlaybook(playbook) {
|
|
885
|
+
if (!playbook.name.trim()) throw new Error("A composed playbook name is required.");
|
|
886
|
+
if (playbook.directives.length === 0) throw new Error("A playbook must define an action directive.");
|
|
887
|
+
for (const directive of playbook.directives) {
|
|
888
|
+
if (!directive.instruction) throw new Error("A playbook directive must have an instruction.");
|
|
889
|
+
if (directive.parameters) assertData(directive.parameters, "directive.parameters");
|
|
890
|
+
}
|
|
891
|
+
const transmissionEvents = new Set(
|
|
892
|
+
(playbook.transmission.events ?? []).map(fileDefinitionKey)
|
|
893
|
+
);
|
|
894
|
+
for (const event of [
|
|
895
|
+
playbook.match?.event,
|
|
896
|
+
...(playbook.outbound ?? []).map((entry) => entry.event)
|
|
897
|
+
]) {
|
|
898
|
+
if (event && !transmissionEvents.has(fileDefinitionKey(event))) {
|
|
899
|
+
throw new Error("A playbook event must be declared by its transmission definition.");
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
if (playbook.serialization) assertData(playbook.serialization, "serialization");
|
|
903
|
+
return Object.freeze({
|
|
904
|
+
...playbook,
|
|
905
|
+
directives: Object.freeze([...playbook.directives]),
|
|
906
|
+
[FOUNDRY_COMPOSED_PLAYBOOK_BRAND]: true
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function materializeComposedPlaybook(playbook, id2, revision) {
|
|
910
|
+
return normalizePlaybook({
|
|
911
|
+
...playbook,
|
|
912
|
+
id: id2,
|
|
913
|
+
origin: "agent-definition",
|
|
914
|
+
playbookName: playbook.name,
|
|
915
|
+
definitionRevision: revision
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
function reconstructPlaybook(playbook) {
|
|
919
|
+
return normalizePlaybook(playbook);
|
|
920
|
+
}
|
|
921
|
+
var FOUNDRY_PLAYBOOK_ACTION_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-playbook-action");
|
|
922
|
+
var FOUNDRY_COMPOSED_PLAYBOOK_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-composed-playbook");
|
|
923
|
+
function agentPlaybookId(definitionId, agentId, name) {
|
|
924
|
+
return `playbook-${createHash("sha256").update(`${definitionId}\0${agentId}\0${name}`).digest("hex").slice(0, 24)}`;
|
|
925
|
+
}
|
|
926
|
+
function composedPlaybookRevision(playbook) {
|
|
927
|
+
const materialized = materializeComposedPlaybook(playbook, "playbook-revision", "pending");
|
|
928
|
+
const { id: _id, definitionRevision: _revision, ...data } = materialized;
|
|
929
|
+
return createHash("sha256").update(JSON.stringify(data)).digest("hex");
|
|
930
|
+
}
|
|
931
|
+
function definePlaybookAction(options = {}) {
|
|
932
|
+
if (options.id && !ID.test(options.id)) {
|
|
933
|
+
throw new Error(`Invalid playbook action id "${options.id}".`);
|
|
934
|
+
}
|
|
935
|
+
const { id: id2, ...definition } = options;
|
|
936
|
+
return Object.freeze(fileIdentified({
|
|
937
|
+
...definition,
|
|
938
|
+
[FOUNDRY_PLAYBOOK_ACTION_BRAND]: true
|
|
939
|
+
}, "action", id2));
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/subscription.ts
|
|
943
|
+
var FOUNDRY_PLAYBOOK_SUBSCRIPTION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
944
|
+
"glove-foundry-playbook-subscription"
|
|
945
|
+
);
|
|
946
|
+
var ID2 = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
947
|
+
function freezeData2(value) {
|
|
948
|
+
if (!value || typeof value !== "object") return value;
|
|
949
|
+
for (const child of Object.values(value)) freezeData2(child);
|
|
950
|
+
return Object.freeze(value);
|
|
951
|
+
}
|
|
952
|
+
function definePlaybookSubscription(options) {
|
|
953
|
+
if (options.id && !ID2.test(options.id)) {
|
|
954
|
+
throw new Error(`Invalid playbook subscription id "${options.id}".`);
|
|
955
|
+
}
|
|
956
|
+
if (options.targets.length === 0) {
|
|
957
|
+
throw new Error("A playbook subscription must target at least one agent.");
|
|
958
|
+
}
|
|
959
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
960
|
+
const targets = options.targets.map((target) => {
|
|
961
|
+
const authored2 = {
|
|
962
|
+
provisioning: target.provisioning ?? { mode: "singleton" },
|
|
963
|
+
context: structuredClone(target.context ?? {}),
|
|
964
|
+
installations: target.installations ?? []
|
|
965
|
+
};
|
|
966
|
+
Object.defineProperty(authored2, "definitionId", {
|
|
967
|
+
enumerable: true,
|
|
968
|
+
get: () => target.agent.id
|
|
969
|
+
});
|
|
970
|
+
return Object.freeze(authored2);
|
|
971
|
+
});
|
|
972
|
+
const authored = fileIdentified({
|
|
973
|
+
workspaceId: options.workspaceId ?? "default",
|
|
974
|
+
enabled: options.enabled ?? true,
|
|
975
|
+
playbook: options.playbook,
|
|
976
|
+
targets,
|
|
977
|
+
createdAt: options.createdAt ?? now,
|
|
978
|
+
updatedAt: options.updatedAt ?? now,
|
|
979
|
+
[FOUNDRY_PLAYBOOK_SUBSCRIPTION_BRAND]: true
|
|
980
|
+
}, "subscription", options.id);
|
|
981
|
+
return Object.freeze(authored);
|
|
982
|
+
}
|
|
983
|
+
function reconstructPlaybookSubscription(subscription) {
|
|
984
|
+
return freezeData2({
|
|
985
|
+
...structuredClone(subscription),
|
|
986
|
+
playbook: reconstructPlaybook(subscription.playbook)
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/primitives.ts
|
|
991
|
+
import { randomUUID } from "node:crypto";
|
|
992
|
+
import { Effect as Effect4 } from "effect";
|
|
993
|
+
function toGloveMessage(input) {
|
|
994
|
+
if (typeof input === "string") {
|
|
995
|
+
return freezeGloveMessage({ sender: "user", text: input });
|
|
996
|
+
}
|
|
997
|
+
const text = input.filter((part) => part.type === "text" && part.text).map((part) => part.text).join("\n");
|
|
998
|
+
const media = input.filter((part) => part.type !== "text").map(cloneContentPart);
|
|
999
|
+
if (media.length === 0) {
|
|
1000
|
+
return freezeGloveMessage({ sender: "user", text });
|
|
1001
|
+
}
|
|
1002
|
+
return freezeGloveMessage({
|
|
1003
|
+
sender: "user",
|
|
1004
|
+
text: text.length > 0 ? text : "[multimodal message]",
|
|
1005
|
+
content: [
|
|
1006
|
+
...text.length > 0 ? [{ type: "text", text }] : [],
|
|
1007
|
+
...media
|
|
1008
|
+
]
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
function freezeGloveMessage(message) {
|
|
1012
|
+
return freezeInstanceData(structuredClone(message));
|
|
1013
|
+
}
|
|
1014
|
+
function toGloveRequestInput(input) {
|
|
1015
|
+
return typeof input === "string" ? input : input.map(cloneContentPart);
|
|
1016
|
+
}
|
|
1017
|
+
function cloneContentPart(part) {
|
|
1018
|
+
return {
|
|
1019
|
+
...part,
|
|
1020
|
+
...part.source ? { source: { ...part.source } } : {}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
var MemoryFoundryDataAdapter = class {
|
|
1024
|
+
identifier;
|
|
1025
|
+
agents = /* @__PURE__ */ new Map();
|
|
1026
|
+
agentsByProvisioningKey = /* @__PURE__ */ new Map();
|
|
1027
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
1028
|
+
inboundDeliveries = /* @__PURE__ */ new Map();
|
|
1029
|
+
activations = /* @__PURE__ */ new Map();
|
|
1030
|
+
conversations = /* @__PURE__ */ new Map();
|
|
1031
|
+
workspace = /* @__PURE__ */ new Map();
|
|
1032
|
+
workingEnvironments = /* @__PURE__ */ new Map();
|
|
1033
|
+
inbox = /* @__PURE__ */ new Map();
|
|
1034
|
+
tasks = /* @__PURE__ */ new Map();
|
|
1035
|
+
environment;
|
|
1036
|
+
pendingAgents;
|
|
1037
|
+
pendingSubscriptions;
|
|
1038
|
+
constructor(options) {
|
|
1039
|
+
this.identifier = options?.identifier ?? "foundry-memory-data";
|
|
1040
|
+
this.environment = [...options?.environment ?? []];
|
|
1041
|
+
this.pendingAgents = [...options?.agents ?? []];
|
|
1042
|
+
this.pendingSubscriptions = [...options?.subscriptions ?? []];
|
|
1043
|
+
for (const activation of options?.activations ?? []) {
|
|
1044
|
+
this.activations.set(activation.id, reconstructActivation(activation));
|
|
1045
|
+
}
|
|
1046
|
+
for (const conversation of options?.conversations ?? []) this.conversations.set(conversation.id, conversation);
|
|
1047
|
+
}
|
|
1048
|
+
/** Resolve file-owned identities only after Foundry discovery has bound them. */
|
|
1049
|
+
materializeSeeds() {
|
|
1050
|
+
for (const agent of this.pendingAgents) {
|
|
1051
|
+
const reconstructed = reconstructAgentInstance(agent);
|
|
1052
|
+
this.agents.set(agent.id, reconstructed);
|
|
1053
|
+
if (reconstructed.provisioningKey) {
|
|
1054
|
+
this.agentsByProvisioningKey.set(reconstructed.provisioningKey, reconstructed.id);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
this.pendingAgents = [];
|
|
1058
|
+
for (const subscription of this.pendingSubscriptions) {
|
|
1059
|
+
this.subscriptions.set(subscription.id, reconstructPlaybookSubscription(subscription));
|
|
1060
|
+
}
|
|
1061
|
+
this.pendingSubscriptions = [];
|
|
1062
|
+
}
|
|
1063
|
+
getAgent(id2) {
|
|
1064
|
+
return Effect4.sync(() => {
|
|
1065
|
+
this.materializeSeeds();
|
|
1066
|
+
return this.agents.get(id2) ?? null;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
putAgent(agent) {
|
|
1070
|
+
return Effect4.sync(() => {
|
|
1071
|
+
this.materializeSeeds();
|
|
1072
|
+
const reconstructed = reconstructAgentInstance(agent);
|
|
1073
|
+
const prior = this.agents.get(agent.id);
|
|
1074
|
+
if (prior?.provisioningKey && prior.provisioningKey !== reconstructed.provisioningKey) {
|
|
1075
|
+
this.agentsByProvisioningKey.delete(prior.provisioningKey);
|
|
1076
|
+
}
|
|
1077
|
+
this.agents.set(agent.id, reconstructed);
|
|
1078
|
+
if (reconstructed.provisioningKey) {
|
|
1079
|
+
this.agentsByProvisioningKey.set(reconstructed.provisioningKey, reconstructed.id);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
listAgents(definitionId) {
|
|
1084
|
+
return Effect4.sync(() => {
|
|
1085
|
+
this.materializeSeeds();
|
|
1086
|
+
return [...this.agents.values()].filter((item) => !definitionId || item.definitionId === definitionId);
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
provisionAgent(input) {
|
|
1090
|
+
return Effect4.sync(() => {
|
|
1091
|
+
this.materializeSeeds();
|
|
1092
|
+
const existingId = this.agentsByProvisioningKey.get(input.provisioningKey);
|
|
1093
|
+
if (existingId) return this.agents.get(existingId);
|
|
1094
|
+
const created = createAgentInstance(input.definitionId, {
|
|
1095
|
+
...input,
|
|
1096
|
+
id: input.id ?? `agent_${randomUUID()}`
|
|
1097
|
+
}, input.provisioningKey);
|
|
1098
|
+
this.agents.set(created.id, created);
|
|
1099
|
+
this.agentsByProvisioningKey.set(input.provisioningKey, created.id);
|
|
1100
|
+
return created;
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
getPlaybookSubscription(id2) {
|
|
1104
|
+
return Effect4.sync(() => {
|
|
1105
|
+
this.materializeSeeds();
|
|
1106
|
+
return this.subscriptions.get(id2) ?? null;
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
putPlaybookSubscription(subscription) {
|
|
1110
|
+
return Effect4.sync(() => {
|
|
1111
|
+
this.materializeSeeds();
|
|
1112
|
+
this.subscriptions.set(subscription.id, reconstructPlaybookSubscription(subscription));
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
deletePlaybookSubscription(id2) {
|
|
1116
|
+
return Effect4.sync(() => {
|
|
1117
|
+
this.materializeSeeds();
|
|
1118
|
+
return this.subscriptions.delete(id2);
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
listPlaybookSubscriptions(workspaceId) {
|
|
1122
|
+
return Effect4.sync(() => {
|
|
1123
|
+
this.materializeSeeds();
|
|
1124
|
+
return [...this.subscriptions.values()].filter((item) => !workspaceId || item.workspaceId === workspaceId);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
getInboundDelivery(key) {
|
|
1128
|
+
return Effect4.succeed(this.inboundDeliveries.get(key) ?? null);
|
|
1129
|
+
}
|
|
1130
|
+
claimInboundDelivery(key) {
|
|
1131
|
+
return Effect4.sync(() => {
|
|
1132
|
+
if (this.inboundDeliveries.has(key)) return false;
|
|
1133
|
+
this.inboundDeliveries.set(key, Object.freeze({
|
|
1134
|
+
key,
|
|
1135
|
+
status: "pending",
|
|
1136
|
+
runIds: Object.freeze([]),
|
|
1137
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1138
|
+
}));
|
|
1139
|
+
return true;
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
completeInboundDelivery(key, runIds) {
|
|
1143
|
+
return Effect4.sync(() => {
|
|
1144
|
+
const prior = this.inboundDeliveries.get(key);
|
|
1145
|
+
if (!prior) throw new Error(`Inbound delivery claim "${key}" does not exist.`);
|
|
1146
|
+
this.inboundDeliveries.set(key, Object.freeze({
|
|
1147
|
+
...prior,
|
|
1148
|
+
status: "completed",
|
|
1149
|
+
runIds: Object.freeze([...runIds]),
|
|
1150
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1151
|
+
}));
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
releaseInboundDelivery(key) {
|
|
1155
|
+
return Effect4.sync(() => {
|
|
1156
|
+
if (this.inboundDeliveries.get(key)?.status === "pending") {
|
|
1157
|
+
this.inboundDeliveries.delete(key);
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
getActivation(id2) {
|
|
1162
|
+
return Effect4.succeed(this.activations.get(id2) ?? null);
|
|
1163
|
+
}
|
|
1164
|
+
putActivation(activation) {
|
|
1165
|
+
return Effect4.sync(() => {
|
|
1166
|
+
this.activations.set(activation.id, reconstructActivation(activation));
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
listActivations(workspaceId) {
|
|
1170
|
+
return Effect4.succeed(
|
|
1171
|
+
[...this.activations.values()].filter((item) => !workspaceId || item.workspaceId === workspaceId)
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
getConversation(id2) {
|
|
1175
|
+
return Effect4.succeed(this.conversations.get(id2) ?? null);
|
|
1176
|
+
}
|
|
1177
|
+
putConversation(conversation) {
|
|
1178
|
+
return Effect4.sync(() => {
|
|
1179
|
+
this.conversations.set(conversation.id, Object.freeze({ ...conversation }));
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
listConversations(agentId) {
|
|
1183
|
+
return Effect4.succeed([...this.conversations.values()].filter((item) => item.agentId === agentId));
|
|
1184
|
+
}
|
|
1185
|
+
getWorkspaceEntry(workspaceId, key) {
|
|
1186
|
+
return Effect4.succeed(this.workspace.get(`${workspaceId}:${key}`) ?? null);
|
|
1187
|
+
}
|
|
1188
|
+
putWorkspaceEntry(entry) {
|
|
1189
|
+
return Effect4.sync(() => {
|
|
1190
|
+
this.workspace.set(`${entry.workspaceId}:${entry.key}`, Object.freeze({ ...entry }));
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
listWorkspaceEntries(workspaceId) {
|
|
1194
|
+
return Effect4.succeed([...this.workspace.values()].filter((item) => item.workspaceId === workspaceId));
|
|
1195
|
+
}
|
|
1196
|
+
getWorkingEnvironmentSnapshot(owner) {
|
|
1197
|
+
return Effect4.sync(() => {
|
|
1198
|
+
const snapshot = this.workingEnvironments.get(workingEnvironmentOwnerKey(owner));
|
|
1199
|
+
return snapshot ? structuredClone(snapshot) : null;
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
putWorkingEnvironmentSnapshot(owner, snapshot) {
|
|
1203
|
+
return Effect4.sync(() => {
|
|
1204
|
+
this.workingEnvironments.set(
|
|
1205
|
+
workingEnvironmentOwnerKey(owner),
|
|
1206
|
+
structuredClone(snapshot)
|
|
1207
|
+
);
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
putInboxItem(item) {
|
|
1211
|
+
return Effect4.sync(() => {
|
|
1212
|
+
this.inbox.set(item.id, Object.freeze({ ...item }));
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
listInboxItems(workspaceId) {
|
|
1216
|
+
return Effect4.succeed([...this.inbox.values()].filter((item) => item.workspaceId === workspaceId));
|
|
1217
|
+
}
|
|
1218
|
+
putTask(task) {
|
|
1219
|
+
return Effect4.sync(() => {
|
|
1220
|
+
this.tasks.set(task.id, Object.freeze({ ...task }));
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
listTasks(workspaceId) {
|
|
1224
|
+
return Effect4.succeed([...this.tasks.values()].filter((item) => item.workspaceId === workspaceId));
|
|
1225
|
+
}
|
|
1226
|
+
listEnvironment(scope) {
|
|
1227
|
+
return Effect4.succeed(this.environment.filter(
|
|
1228
|
+
(item) => item.workspaceId === scope.workspaceId && (item.scope === "workspace" || item.scope === "agent" && item.agentId === scope.agentId || item.scope === "conversation" && item.conversationId === scope.conversationId)
|
|
1229
|
+
));
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
function workingEnvironmentOwnerKey(owner) {
|
|
1233
|
+
return [
|
|
1234
|
+
owner.scope,
|
|
1235
|
+
owner.workspaceId,
|
|
1236
|
+
owner.definitionId,
|
|
1237
|
+
owner.agentId,
|
|
1238
|
+
owner.scope === "conversation" ? owner.conversationId : ""
|
|
1239
|
+
].join("\0");
|
|
1240
|
+
}
|
|
1241
|
+
function reconstructActivation(activation) {
|
|
1242
|
+
return Object.freeze({
|
|
1243
|
+
...activation,
|
|
1244
|
+
...activation.payload !== void 0 ? { payload: freezeInstanceData(structuredClone(activation.payload)) } : {},
|
|
1245
|
+
timing: Object.freeze({ ...activation.timing })
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
function createAgentInstance(definitionId, options = {}, provisioningKey) {
|
|
1249
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1250
|
+
return reconstructAgentInstance({
|
|
1251
|
+
id: options.id ?? `agent_${randomUUID()}`,
|
|
1252
|
+
definitionId,
|
|
1253
|
+
workspaceId: options.workspaceId ?? "default",
|
|
1254
|
+
...provisioningKey ? { provisioningKey } : {},
|
|
1255
|
+
context: options.context ?? {},
|
|
1256
|
+
installations: options.installations ?? [],
|
|
1257
|
+
playbooks: options.playbooks ?? [],
|
|
1258
|
+
createdAt: now,
|
|
1259
|
+
updatedAt: now
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
function defineAgentInstance(definition, options = {}) {
|
|
1263
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1264
|
+
const seed = {
|
|
1265
|
+
id: options.id ?? `agent_${randomUUID()}`,
|
|
1266
|
+
workspaceId: options.workspaceId ?? "default",
|
|
1267
|
+
context: options.context ?? {},
|
|
1268
|
+
installations: options.installations ?? [],
|
|
1269
|
+
playbooks: options.playbooks ?? [],
|
|
1270
|
+
createdAt: now,
|
|
1271
|
+
updatedAt: now
|
|
1272
|
+
};
|
|
1273
|
+
Object.defineProperty(seed, "definitionId", {
|
|
1274
|
+
enumerable: true,
|
|
1275
|
+
get: () => definition.id
|
|
1276
|
+
});
|
|
1277
|
+
return Object.freeze(seed);
|
|
1278
|
+
}
|
|
1279
|
+
function reconstructAgentInstance(agent) {
|
|
1280
|
+
return Object.freeze({
|
|
1281
|
+
...agent,
|
|
1282
|
+
context: freezeInstanceData(structuredClone(agent.context)),
|
|
1283
|
+
installations: normalizeAgentInstallations(agent.installations),
|
|
1284
|
+
playbooks: Object.freeze(agent.playbooks.map(reconstructPlaybook))
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function normalizeAgentInstallations(installations) {
|
|
1288
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const installation of installations) {
|
|
1290
|
+
const copy = Object.freeze({
|
|
1291
|
+
kind: installation.kind,
|
|
1292
|
+
id: installation.id,
|
|
1293
|
+
...installation.accountId !== void 0 ? { accountId: installation.accountId } : {},
|
|
1294
|
+
...installation.config !== void 0 ? { config: freezeInstanceData(structuredClone(installation.config)) } : {}
|
|
1295
|
+
});
|
|
1296
|
+
byKey.set(installationKey(copy), copy);
|
|
1297
|
+
}
|
|
1298
|
+
return Object.freeze([...byKey.values()]);
|
|
1299
|
+
}
|
|
1300
|
+
function freezeInstanceData(value) {
|
|
1301
|
+
if (!value || typeof value !== "object") return value;
|
|
1302
|
+
for (const child of Object.values(value)) {
|
|
1303
|
+
freezeInstanceData(child);
|
|
1304
|
+
}
|
|
1305
|
+
return Object.freeze(value);
|
|
1306
|
+
}
|
|
1307
|
+
function createConversation(agent, options = {}) {
|
|
1308
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1309
|
+
return Object.freeze({
|
|
1310
|
+
id: options.id ?? `conversation_${randomUUID()}`,
|
|
1311
|
+
agentId: agent.id,
|
|
1312
|
+
workspaceId: options.workspaceId ?? agent.workspaceId,
|
|
1313
|
+
...options.title ? { title: options.title } : {},
|
|
1314
|
+
context: Object.freeze({ ...options.context ?? {} }),
|
|
1315
|
+
createdAt: now,
|
|
1316
|
+
updatedAt: now
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
function id(prefix) {
|
|
1320
|
+
return `${prefix}_${randomUUID()}`;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/schedule.ts
|
|
1324
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1325
|
+
import { Duration } from "effect";
|
|
1326
|
+
var FOUNDRY_SCHEDULE_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-schedule");
|
|
1327
|
+
var COMPACT_DURATION = /^(\d+)\s*(ms|s|m|h|d|w)$/i;
|
|
1328
|
+
function durationMillis(value) {
|
|
1329
|
+
const compact = value.match(COMPACT_DURATION);
|
|
1330
|
+
const input = compact ? `${compact[1]} ${{ ms: "millis", s: "seconds", m: "minutes", h: "hours", d: "days", w: "weeks" }[compact[2].toLowerCase()]}` : value;
|
|
1331
|
+
const milliseconds = Duration.toMillis(Duration.decode(input));
|
|
1332
|
+
if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
1333
|
+
throw new Error("Duration must be finite and greater than zero.");
|
|
1334
|
+
}
|
|
1335
|
+
return milliseconds;
|
|
1336
|
+
}
|
|
1337
|
+
function normalizeScheduleTiming(timing, now = Date.now()) {
|
|
1338
|
+
if (timing.kind === "after") {
|
|
1339
|
+
return Object.freeze({ kind: "at", at: new Date(now + durationMillis(timing.duration)).toISOString() });
|
|
1340
|
+
}
|
|
1341
|
+
if (timing.kind === "every") {
|
|
1342
|
+
return Object.freeze({ kind: "every", intervalMs: durationMillis(timing.interval) });
|
|
1343
|
+
}
|
|
1344
|
+
if (timing.kind === "cron") {
|
|
1345
|
+
if (!timing.expression.trim()) throw new Error("A cron expression is required.");
|
|
1346
|
+
return Object.freeze({ kind: "cron", expression: timing.expression, timezone: timing.timezone ?? "UTC" });
|
|
1347
|
+
}
|
|
1348
|
+
const at = new Date(timing.at);
|
|
1349
|
+
if (Number.isNaN(at.getTime())) throw new Error(`Invalid schedule date "${timing.at}".`);
|
|
1350
|
+
return Object.freeze({ kind: "at", at: at.toISOString() });
|
|
1351
|
+
}
|
|
1352
|
+
function defineSchedule(options) {
|
|
1353
|
+
if (!options.name.trim()) throw new Error("A Foundry schedule name is required.");
|
|
1354
|
+
if (!options.message.trim()) throw new Error("A Foundry schedule message is required.");
|
|
1355
|
+
normalizeScheduleTiming(options.timing);
|
|
1356
|
+
return Object.freeze({
|
|
1357
|
+
...options,
|
|
1358
|
+
...options.payload !== void 0 ? { payload: structuredClone(options.payload) } : {},
|
|
1359
|
+
timing: Object.freeze({ ...options.timing }),
|
|
1360
|
+
enabled: options.enabled ?? true,
|
|
1361
|
+
[FOUNDRY_SCHEDULE_BRAND]: true
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
function isFoundrySchedule(value) {
|
|
1365
|
+
return Boolean(value && typeof value === "object" && value[FOUNDRY_SCHEDULE_BRAND] === true);
|
|
1366
|
+
}
|
|
1367
|
+
function agentScheduleActivationId(definitionId, agentId, name) {
|
|
1368
|
+
const digest = createHash2("sha256").update(`${definitionId}\0${agentId}\0${name}`).digest("hex").slice(0, 24);
|
|
1369
|
+
return `activation_${digest}`;
|
|
1370
|
+
}
|
|
1371
|
+
function agentScheduleRevision(schedule) {
|
|
1372
|
+
return createHash2("sha256").update(JSON.stringify({
|
|
1373
|
+
message: schedule.message,
|
|
1374
|
+
payload: schedule.payload,
|
|
1375
|
+
timing: schedule.timing,
|
|
1376
|
+
enabled: schedule.enabled !== false
|
|
1377
|
+
})).digest("hex");
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/core-tools.ts
|
|
1381
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1382
|
+
import { Effect as Effect5, JSONSchema, Schema } from "effect";
|
|
1383
|
+
import { z } from "zod";
|
|
1384
|
+
var FOUNDRY_CORE_COMMAND_EVENT = "foundry.core.command";
|
|
1385
|
+
function commandId() {
|
|
1386
|
+
return `command_${randomUUID2()}`;
|
|
1387
|
+
}
|
|
1388
|
+
function durationSchema(description) {
|
|
1389
|
+
return z.string().describe(description).refine((value) => {
|
|
1390
|
+
try {
|
|
1391
|
+
durationMillis(value);
|
|
1392
|
+
return true;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return false;
|
|
1395
|
+
}
|
|
1396
|
+
}, "Use a positive duration such as 30s, 5m, 2h, or '5 minutes'.");
|
|
1397
|
+
}
|
|
1398
|
+
function afterDuration(value) {
|
|
1399
|
+
return normalizeScheduleTiming({ kind: "after", duration: value }).at;
|
|
1400
|
+
}
|
|
1401
|
+
var timingInputSchema = z.discriminatedUnion("kind", [
|
|
1402
|
+
z.object({ kind: z.literal("at"), at: z.string().datetime() }),
|
|
1403
|
+
z.object({ kind: z.literal("after"), duration: durationSchema("Delay before the one-time activation") }),
|
|
1404
|
+
z.object({ kind: z.literal("every"), interval: durationSchema("Interval between recurring activations") }),
|
|
1405
|
+
z.object({ kind: z.literal("cron"), expression: z.string().min(1), timezone: z.string().min(1).default("UTC") })
|
|
1406
|
+
]);
|
|
1407
|
+
function toolSegment(value) {
|
|
1408
|
+
return value.replaceAll("/", "__").replaceAll("-", "_");
|
|
1409
|
+
}
|
|
1410
|
+
function outboundToolSchema(transmission, routeIds) {
|
|
1411
|
+
const document = JSONSchema.make(transmission.outbound.input);
|
|
1412
|
+
const { $schema: _schema, $defs, ...payload } = document;
|
|
1413
|
+
return {
|
|
1414
|
+
type: "object",
|
|
1415
|
+
additionalProperties: false,
|
|
1416
|
+
properties: {
|
|
1417
|
+
routeId: {
|
|
1418
|
+
type: "string",
|
|
1419
|
+
description: "Authorized Foundry outbound route id",
|
|
1420
|
+
...routeIds.length > 0 ? { enum: routeIds } : {}
|
|
1421
|
+
},
|
|
1422
|
+
payload
|
|
1423
|
+
},
|
|
1424
|
+
required: ["routeId", "payload"],
|
|
1425
|
+
...$defs ? { $defs } : {}
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function createInstalledApplicationTransmissionTools(context, applications, installations, playbooks = context.agentInstance.playbooks) {
|
|
1429
|
+
const installed = new Set(
|
|
1430
|
+
installations.filter((item) => item.kind === "application").map((item) => item.id)
|
|
1431
|
+
);
|
|
1432
|
+
const emit = (command) => {
|
|
1433
|
+
context.controls.commands.push(command);
|
|
1434
|
+
context.controls.emit({ type: FOUNDRY_CORE_COMMAND_EVENT, data: command });
|
|
1435
|
+
return success(command);
|
|
1436
|
+
};
|
|
1437
|
+
const tools = [];
|
|
1438
|
+
for (const application of applications) {
|
|
1439
|
+
if (!installed.has(application.id)) continue;
|
|
1440
|
+
for (const transmission of application.transmissions ?? []) {
|
|
1441
|
+
if (!transmission.outbound) continue;
|
|
1442
|
+
const routeIds = [...new Set(
|
|
1443
|
+
playbooks.filter(
|
|
1444
|
+
(playbook) => playbook.enabled !== false && playbook.transmissionId === transmission.id
|
|
1445
|
+
).flatMap(
|
|
1446
|
+
(playbook) => (playbook.outbound ?? []).filter(
|
|
1447
|
+
(outbound) => outbound.applicationId ? outbound.applicationId === application.id : (playbook.applications ?? []).includes(application.id)
|
|
1448
|
+
).map((outbound) => outbound.routeId)
|
|
1449
|
+
)
|
|
1450
|
+
)].sort();
|
|
1451
|
+
tools.push({
|
|
1452
|
+
name: `glove_app_${toolSegment(application.id)}__${toolSegment(transmission.id)}_send`,
|
|
1453
|
+
description: [
|
|
1454
|
+
`Send through the ${transmission.name} outbound transmission installed by the ${application.id} application.`,
|
|
1455
|
+
routeIds.length > 0 ? `Allowed playbook routes: ${routeIds.join(", ")}.` : "Supply a route authorized for this agent run."
|
|
1456
|
+
].join(" "),
|
|
1457
|
+
jsonSchema: outboundToolSchema(transmission, routeIds),
|
|
1458
|
+
async do(input) {
|
|
1459
|
+
if (routeIds.length > 0 && !routeIds.includes(input.routeId)) {
|
|
1460
|
+
return {
|
|
1461
|
+
status: "error",
|
|
1462
|
+
data: null,
|
|
1463
|
+
message: `Route "${input.routeId}" is not selected for application "${application.id}".`
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
try {
|
|
1467
|
+
const payload = await Schema.decodeUnknownPromise(
|
|
1468
|
+
transmission.outbound.input
|
|
1469
|
+
)(input.payload);
|
|
1470
|
+
return emit({
|
|
1471
|
+
id: commandId(),
|
|
1472
|
+
type: "transmit",
|
|
1473
|
+
definitionId: context.definitionId,
|
|
1474
|
+
agentId: context.agentId,
|
|
1475
|
+
conversationId: context.conversationId,
|
|
1476
|
+
workspaceId: context.workspaceId,
|
|
1477
|
+
routeId: input.routeId,
|
|
1478
|
+
payload,
|
|
1479
|
+
applicationId: application.id,
|
|
1480
|
+
transmissionId: transmission.id
|
|
1481
|
+
});
|
|
1482
|
+
} catch (cause) {
|
|
1483
|
+
return {
|
|
1484
|
+
status: "error",
|
|
1485
|
+
data: null,
|
|
1486
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return Object.freeze(tools);
|
|
1494
|
+
}
|
|
1495
|
+
function success(command) {
|
|
1496
|
+
return {
|
|
1497
|
+
status: "success",
|
|
1498
|
+
data: {
|
|
1499
|
+
commandId: command.id,
|
|
1500
|
+
accepted: true,
|
|
1501
|
+
type: command.type,
|
|
1502
|
+
...command.type === "sleep" ? { wakeAt: command.wakeAt } : {},
|
|
1503
|
+
...command.type === "schedule" ? { timing: command.timing } : {},
|
|
1504
|
+
..."activationId" in command ? { activationId: command.activationId } : {}
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function createFoundryCoreTools(context, desiredSchedules = []) {
|
|
1509
|
+
const emit = (command) => {
|
|
1510
|
+
context.controls.commands.push(command);
|
|
1511
|
+
context.controls.emit({ type: FOUNDRY_CORE_COMMAND_EVENT, data: command });
|
|
1512
|
+
return success(command);
|
|
1513
|
+
};
|
|
1514
|
+
const scheduleView = () => {
|
|
1515
|
+
const records = new Map(
|
|
1516
|
+
context.activations.filter((item) => item.kind === "scheduled").map((item) => [item.id, item])
|
|
1517
|
+
);
|
|
1518
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1519
|
+
for (const desired of desiredSchedules) {
|
|
1520
|
+
if (records.has(desired.id) || !desired.enabled) continue;
|
|
1521
|
+
records.set(desired.id, {
|
|
1522
|
+
id: desired.id,
|
|
1523
|
+
kind: "scheduled",
|
|
1524
|
+
definitionId: context.definitionId,
|
|
1525
|
+
agentId: context.agentId,
|
|
1526
|
+
conversationId: context.conversationId,
|
|
1527
|
+
workspaceId: context.workspaceId,
|
|
1528
|
+
message: desired.message,
|
|
1529
|
+
...desired.payload !== void 0 ? { payload: desired.payload } : {},
|
|
1530
|
+
timing: desired.timing,
|
|
1531
|
+
origin: "agent-definition",
|
|
1532
|
+
scheduleName: desired.name,
|
|
1533
|
+
definitionRevision: desired.revision,
|
|
1534
|
+
status: "pending",
|
|
1535
|
+
createdByRunId: context.runId,
|
|
1536
|
+
createdAt: now,
|
|
1537
|
+
updatedAt: now
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
for (const command of context.controls.commands) {
|
|
1541
|
+
if (command.type === "schedule" && command.agentId === context.agentId) {
|
|
1542
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1543
|
+
records.set(command.id, {
|
|
1544
|
+
id: command.id,
|
|
1545
|
+
kind: "scheduled",
|
|
1546
|
+
definitionId: command.definitionId,
|
|
1547
|
+
agentId: context.agentId,
|
|
1548
|
+
conversationId: command.conversationId ?? context.conversationId,
|
|
1549
|
+
workspaceId: command.workspaceId,
|
|
1550
|
+
message: command.message,
|
|
1551
|
+
...command.payload !== void 0 ? { payload: command.payload } : {},
|
|
1552
|
+
timing: command.timing,
|
|
1553
|
+
origin: "agent-tool",
|
|
1554
|
+
status: "pending",
|
|
1555
|
+
createdByRunId: context.runId,
|
|
1556
|
+
createdAt,
|
|
1557
|
+
updatedAt: createdAt
|
|
1558
|
+
});
|
|
1559
|
+
} else if (command.type === "schedule.update") {
|
|
1560
|
+
const current = records.get(command.activationId);
|
|
1561
|
+
if (current) records.set(command.activationId, {
|
|
1562
|
+
...current,
|
|
1563
|
+
...command.patch,
|
|
1564
|
+
status: "pending",
|
|
1565
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1566
|
+
});
|
|
1567
|
+
} else if (command.type === "schedule.cancel") {
|
|
1568
|
+
const current = records.get(command.activationId);
|
|
1569
|
+
if (current) records.set(command.activationId, {
|
|
1570
|
+
...current,
|
|
1571
|
+
status: "cancelled",
|
|
1572
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return [...records.values()];
|
|
1577
|
+
};
|
|
1578
|
+
return [
|
|
1579
|
+
{
|
|
1580
|
+
name: "glove_foundry_spawn",
|
|
1581
|
+
description: "Immediately invoke an agent. Use glove_foundry_schedule for future or recurring work.",
|
|
1582
|
+
inputSchema: z.object({
|
|
1583
|
+
definitionId: z.string().optional(),
|
|
1584
|
+
agentId: z.string().optional(),
|
|
1585
|
+
conversationId: z.string().optional(),
|
|
1586
|
+
message: z.string(),
|
|
1587
|
+
payload: z.unknown().optional()
|
|
1588
|
+
}),
|
|
1589
|
+
async do(input) {
|
|
1590
|
+
return emit({
|
|
1591
|
+
id: commandId(),
|
|
1592
|
+
type: "spawn",
|
|
1593
|
+
definitionId: input.definitionId ?? context.definitionId,
|
|
1594
|
+
...input.agentId ? { agentId: input.agentId } : {},
|
|
1595
|
+
...input.conversationId ? { conversationId: input.conversationId } : {},
|
|
1596
|
+
workspaceId: context.workspaceId,
|
|
1597
|
+
message: input.message,
|
|
1598
|
+
...input.payload !== void 0 ? { payload: input.payload } : {}
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
},
|
|
1602
|
+
{
|
|
1603
|
+
name: "glove_foundry_schedule",
|
|
1604
|
+
description: "Create an ad hoc future or recurring activation. Agent-definition schedules are loaded separately through the lazy schedules resolver.",
|
|
1605
|
+
inputSchema: z.object({
|
|
1606
|
+
definitionId: z.string().optional(),
|
|
1607
|
+
agentId: z.string().optional(),
|
|
1608
|
+
conversationId: z.string().optional(),
|
|
1609
|
+
message: z.string(),
|
|
1610
|
+
payload: z.unknown().optional(),
|
|
1611
|
+
timing: timingInputSchema
|
|
1612
|
+
}),
|
|
1613
|
+
async do(input) {
|
|
1614
|
+
return emit({
|
|
1615
|
+
id: commandId(),
|
|
1616
|
+
type: "schedule",
|
|
1617
|
+
definitionId: input.definitionId ?? context.definitionId,
|
|
1618
|
+
...input.agentId ?? (!input.definitionId ? context.agentId : void 0) ? { agentId: input.agentId ?? context.agentId } : {},
|
|
1619
|
+
...input.conversationId ?? (!input.definitionId && !input.agentId ? context.conversationId : void 0) ? { conversationId: input.conversationId ?? context.conversationId } : {},
|
|
1620
|
+
workspaceId: context.workspaceId,
|
|
1621
|
+
message: input.message,
|
|
1622
|
+
...input.payload !== void 0 ? { payload: input.payload } : {},
|
|
1623
|
+
timing: normalizeScheduleTiming(input.timing)
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
},
|
|
1627
|
+
{
|
|
1628
|
+
name: "glove_foundry_schedules",
|
|
1629
|
+
description: "List, update, or cancel scheduled triggers owned by this agent instance. Use the activation id returned by list.",
|
|
1630
|
+
inputSchema: z.discriminatedUnion("action", [
|
|
1631
|
+
z.object({
|
|
1632
|
+
action: z.literal("list"),
|
|
1633
|
+
status: z.enum(["active", "completed", "cancelled", "all"]).default("active")
|
|
1634
|
+
}),
|
|
1635
|
+
z.object({
|
|
1636
|
+
action: z.literal("update"),
|
|
1637
|
+
activationId: z.string().min(1),
|
|
1638
|
+
message: z.string().min(1).optional(),
|
|
1639
|
+
payload: z.unknown().optional(),
|
|
1640
|
+
timing: timingInputSchema.optional()
|
|
1641
|
+
}),
|
|
1642
|
+
z.object({ action: z.literal("cancel"), activationId: z.string().min(1) })
|
|
1643
|
+
]),
|
|
1644
|
+
async do(input) {
|
|
1645
|
+
if (input.action === "list") {
|
|
1646
|
+
const scheduled = scheduleView();
|
|
1647
|
+
const filter = input.status ?? "active";
|
|
1648
|
+
const filtered = filter === "all" ? scheduled : filter === "active" ? scheduled.filter((item) => item.status === "pending" || item.status === "active") : scheduled.filter((item) => item.status === filter);
|
|
1649
|
+
return { status: "success", data: filtered };
|
|
1650
|
+
}
|
|
1651
|
+
if (!scheduleView().some((item) => item.id === input.activationId && item.agentId === context.agentId && item.kind === "scheduled")) {
|
|
1652
|
+
return { status: "error", data: null, message: `Schedule "${input.activationId}" is not owned by this agent instance.` };
|
|
1653
|
+
}
|
|
1654
|
+
if (input.action === "cancel") {
|
|
1655
|
+
return emit({
|
|
1656
|
+
id: commandId(),
|
|
1657
|
+
type: "schedule.cancel",
|
|
1658
|
+
activationId: input.activationId,
|
|
1659
|
+
definitionId: context.definitionId,
|
|
1660
|
+
agentId: context.agentId,
|
|
1661
|
+
conversationId: context.conversationId,
|
|
1662
|
+
workspaceId: context.workspaceId
|
|
1663
|
+
});
|
|
1664
|
+
}
|
|
1665
|
+
const patch = {};
|
|
1666
|
+
if (input.message !== void 0) patch.message = input.message;
|
|
1667
|
+
if (input.payload !== void 0) patch.payload = input.payload;
|
|
1668
|
+
if (input.timing !== void 0) patch.timing = normalizeScheduleTiming(input.timing);
|
|
1669
|
+
if (Object.keys(patch).length === 0) {
|
|
1670
|
+
return { status: "error", data: null, message: "Provide message, payload, or timing to update." };
|
|
1671
|
+
}
|
|
1672
|
+
return emit({
|
|
1673
|
+
id: commandId(),
|
|
1674
|
+
type: "schedule.update",
|
|
1675
|
+
activationId: input.activationId,
|
|
1676
|
+
patch,
|
|
1677
|
+
definitionId: context.definitionId,
|
|
1678
|
+
agentId: context.agentId,
|
|
1679
|
+
conversationId: context.conversationId,
|
|
1680
|
+
workspaceId: context.workspaceId
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
},
|
|
1684
|
+
{
|
|
1685
|
+
name: "glove_foundry_sleep",
|
|
1686
|
+
description: "Suspend this logical run, then wake the same agent instance and conversation at a date or after a duration.",
|
|
1687
|
+
inputSchema: z.discriminatedUnion("kind", [
|
|
1688
|
+
z.object({
|
|
1689
|
+
kind: z.literal("until"),
|
|
1690
|
+
at: z.string().datetime(),
|
|
1691
|
+
message: z.string().default("Continue the suspended work.")
|
|
1692
|
+
}),
|
|
1693
|
+
z.object({
|
|
1694
|
+
kind: z.literal("for"),
|
|
1695
|
+
duration: durationSchema("How long this agent should sleep"),
|
|
1696
|
+
message: z.string().default("Continue the suspended work.")
|
|
1697
|
+
})
|
|
1698
|
+
]),
|
|
1699
|
+
async do(input) {
|
|
1700
|
+
return emit({
|
|
1701
|
+
id: commandId(),
|
|
1702
|
+
type: "sleep",
|
|
1703
|
+
definitionId: context.definitionId,
|
|
1704
|
+
agentId: context.agentId,
|
|
1705
|
+
conversationId: context.conversationId,
|
|
1706
|
+
workspaceId: context.workspaceId,
|
|
1707
|
+
wakeAt: input.kind === "until" ? input.at : afterDuration(input.duration),
|
|
1708
|
+
message: input.message
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
name: "glove_foundry_background",
|
|
1714
|
+
description: "Start work in the background and optionally reconvene its result into this conversation's shared inbox.",
|
|
1715
|
+
inputSchema: z.object({
|
|
1716
|
+
definitionId: z.string().optional(),
|
|
1717
|
+
message: z.string(),
|
|
1718
|
+
payload: z.unknown().optional(),
|
|
1719
|
+
reconvene: z.boolean().default(true)
|
|
1720
|
+
}),
|
|
1721
|
+
async do(input) {
|
|
1722
|
+
return emit({
|
|
1723
|
+
id: commandId(),
|
|
1724
|
+
type: "background",
|
|
1725
|
+
definitionId: input.definitionId ?? context.definitionId,
|
|
1726
|
+
agentId: context.agentId,
|
|
1727
|
+
conversationId: context.conversationId,
|
|
1728
|
+
workspaceId: context.workspaceId,
|
|
1729
|
+
message: input.message,
|
|
1730
|
+
...input.payload !== void 0 ? { payload: input.payload } : {},
|
|
1731
|
+
reconvene: input.reconvene
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
{
|
|
1736
|
+
name: "glove_foundry_transmit",
|
|
1737
|
+
description: "Deliver an outbound event through a route declared by this instance's playbook and authorized for this run.",
|
|
1738
|
+
inputSchema: z.object({
|
|
1739
|
+
routeId: z.string(),
|
|
1740
|
+
payload: z.unknown()
|
|
1741
|
+
}),
|
|
1742
|
+
async do(input) {
|
|
1743
|
+
return emit({
|
|
1744
|
+
id: commandId(),
|
|
1745
|
+
type: "transmit",
|
|
1746
|
+
definitionId: context.definitionId,
|
|
1747
|
+
agentId: context.agentId,
|
|
1748
|
+
conversationId: context.conversationId,
|
|
1749
|
+
workspaceId: context.workspaceId,
|
|
1750
|
+
routeId: input.routeId,
|
|
1751
|
+
payload: input.payload
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: "glove_foundry_conversations",
|
|
1757
|
+
description: "List the conversations owned by this runtime agent instance.",
|
|
1758
|
+
inputSchema: z.object({}),
|
|
1759
|
+
async do() {
|
|
1760
|
+
return { status: "success", data: await Effect5.runPromise(context.data.listConversations(context.agentId)) };
|
|
1761
|
+
}
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
name: "glove_foundry_workspace",
|
|
1765
|
+
description: "Read or write a value in the agent's shared workspace.",
|
|
1766
|
+
inputSchema: z.discriminatedUnion("action", [
|
|
1767
|
+
z.object({ action: z.literal("get"), key: z.string() }),
|
|
1768
|
+
z.object({ action: z.literal("set"), key: z.string(), value: z.unknown() }),
|
|
1769
|
+
z.object({ action: z.literal("list") })
|
|
1770
|
+
]),
|
|
1771
|
+
async do(input) {
|
|
1772
|
+
if (input.action === "get") {
|
|
1773
|
+
return { status: "success", data: await Effect5.runPromise(context.data.getWorkspaceEntry(context.workspaceId, input.key)) };
|
|
1774
|
+
}
|
|
1775
|
+
if (input.action === "list") {
|
|
1776
|
+
return { status: "success", data: await Effect5.runPromise(context.data.listWorkspaceEntries(context.workspaceId)) };
|
|
1777
|
+
}
|
|
1778
|
+
await Effect5.runPromise(context.data.putWorkspaceEntry({
|
|
1779
|
+
workspaceId: context.workspaceId,
|
|
1780
|
+
key: input.key,
|
|
1781
|
+
value: input.value,
|
|
1782
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1783
|
+
}));
|
|
1784
|
+
return { status: "success", data: { key: input.key, saved: true } };
|
|
1785
|
+
}
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
name: "glove_foundry_shared_inbox",
|
|
1789
|
+
description: "List shared workspace inbox items or post a new item for another agent/conversation.",
|
|
1790
|
+
inputSchema: z.discriminatedUnion("action", [
|
|
1791
|
+
z.object({ action: z.literal("list") }),
|
|
1792
|
+
z.object({
|
|
1793
|
+
action: z.literal("post"),
|
|
1794
|
+
topic: z.string(),
|
|
1795
|
+
payload: z.unknown(),
|
|
1796
|
+
agentId: z.string().optional(),
|
|
1797
|
+
conversationId: z.string().optional()
|
|
1798
|
+
}),
|
|
1799
|
+
z.object({
|
|
1800
|
+
action: z.literal("update"),
|
|
1801
|
+
itemId: z.string(),
|
|
1802
|
+
status: z.enum(["pending", "resolved", "dismissed"])
|
|
1803
|
+
})
|
|
1804
|
+
]),
|
|
1805
|
+
async do(input) {
|
|
1806
|
+
if (input.action === "list") {
|
|
1807
|
+
return { status: "success", data: await Effect5.runPromise(context.data.listInboxItems(context.workspaceId)) };
|
|
1808
|
+
}
|
|
1809
|
+
if (input.action === "update") {
|
|
1810
|
+
const current = (await Effect5.runPromise(context.data.listInboxItems(context.workspaceId))).find((item3) => item3.id === input.itemId);
|
|
1811
|
+
if (!current) return { status: "error", data: null, error: `Shared inbox item "${input.itemId}" was not found.` };
|
|
1812
|
+
const item2 = { ...current, status: input.status, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1813
|
+
await Effect5.runPromise(context.data.putInboxItem(item2));
|
|
1814
|
+
return { status: "success", data: item2 };
|
|
1815
|
+
}
|
|
1816
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1817
|
+
const item = {
|
|
1818
|
+
id: id("inbox"),
|
|
1819
|
+
workspaceId: context.workspaceId,
|
|
1820
|
+
agentId: input.agentId ?? context.agentId,
|
|
1821
|
+
conversationId: input.conversationId ?? context.conversationId,
|
|
1822
|
+
topic: input.topic,
|
|
1823
|
+
payload: input.payload,
|
|
1824
|
+
status: "pending",
|
|
1825
|
+
createdAt: now,
|
|
1826
|
+
updatedAt: now
|
|
1827
|
+
};
|
|
1828
|
+
await Effect5.runPromise(context.data.putInboxItem(item));
|
|
1829
|
+
return { status: "success", data: item };
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
name: "glove_foundry_tasks",
|
|
1834
|
+
description: "List shared workspace tasks or create a task scoped to this agent conversation.",
|
|
1835
|
+
inputSchema: z.discriminatedUnion("action", [
|
|
1836
|
+
z.object({ action: z.literal("list") }),
|
|
1837
|
+
z.object({ action: z.literal("create"), title: z.string(), detail: z.string().optional() }),
|
|
1838
|
+
z.object({
|
|
1839
|
+
action: z.literal("update"),
|
|
1840
|
+
taskId: z.string(),
|
|
1841
|
+
status: z.enum(["open", "in-progress", "completed", "cancelled"])
|
|
1842
|
+
})
|
|
1843
|
+
]),
|
|
1844
|
+
async do(input) {
|
|
1845
|
+
if (input.action === "list") {
|
|
1846
|
+
return { status: "success", data: await Effect5.runPromise(context.data.listTasks(context.workspaceId)) };
|
|
1847
|
+
}
|
|
1848
|
+
if (input.action === "update") {
|
|
1849
|
+
const current = (await Effect5.runPromise(context.data.listTasks(context.workspaceId))).find((task3) => task3.id === input.taskId);
|
|
1850
|
+
if (!current) return { status: "error", data: null, error: `Task "${input.taskId}" was not found.` };
|
|
1851
|
+
const task2 = { ...current, status: input.status, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1852
|
+
await Effect5.runPromise(context.data.putTask(task2));
|
|
1853
|
+
return { status: "success", data: task2 };
|
|
1854
|
+
}
|
|
1855
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1856
|
+
const task = {
|
|
1857
|
+
id: id("task"),
|
|
1858
|
+
workspaceId: context.workspaceId,
|
|
1859
|
+
agentId: context.agentId,
|
|
1860
|
+
conversationId: context.conversationId,
|
|
1861
|
+
title: input.title,
|
|
1862
|
+
...input.detail ? { detail: input.detail } : {},
|
|
1863
|
+
status: "open",
|
|
1864
|
+
createdAt: now,
|
|
1865
|
+
updatedAt: now
|
|
1866
|
+
};
|
|
1867
|
+
await Effect5.runPromise(context.data.putTask(task));
|
|
1868
|
+
return { status: "success", data: task };
|
|
1869
|
+
}
|
|
1870
|
+
},
|
|
1871
|
+
{
|
|
1872
|
+
name: "glove_foundry_environment",
|
|
1873
|
+
description: "List non-secret environment metadata visible to this workspace, agent, and conversation.",
|
|
1874
|
+
inputSchema: z.object({}),
|
|
1875
|
+
async do() {
|
|
1876
|
+
return {
|
|
1877
|
+
status: "success",
|
|
1878
|
+
data: await Effect5.runPromise(context.data.listEnvironment({
|
|
1879
|
+
workspaceId: context.workspaceId,
|
|
1880
|
+
agentId: context.agentId,
|
|
1881
|
+
conversationId: context.conversationId
|
|
1882
|
+
}))
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
];
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/workbench.ts
|
|
1890
|
+
import { Effect as Effect6 } from "effect";
|
|
1891
|
+
import {
|
|
1892
|
+
JsSession,
|
|
1893
|
+
mountJs
|
|
1894
|
+
} from "glove-js";
|
|
1895
|
+
import {
|
|
1896
|
+
LispSession,
|
|
1897
|
+
mountLisp
|
|
1898
|
+
} from "glove-lisp";
|
|
1899
|
+
import {
|
|
1900
|
+
PySession,
|
|
1901
|
+
mountPy
|
|
1902
|
+
} from "glove-python";
|
|
1903
|
+
import {
|
|
1904
|
+
createWorkingEnvironment,
|
|
1905
|
+
fromSnapshot,
|
|
1906
|
+
mountWorkingEnvironment
|
|
1907
|
+
} from "glove-working-environment";
|
|
1908
|
+
var FOUNDRY_WORKING_ENVIRONMENT_BRAND = /* @__PURE__ */ Symbol.for(
|
|
1909
|
+
"glove-foundry-working-environment"
|
|
1910
|
+
);
|
|
1911
|
+
var FOUNDRY_REPL_BRAND = /* @__PURE__ */ Symbol.for("glove-foundry-repl");
|
|
1912
|
+
function defineWorkingEnvironment(options = {}) {
|
|
1913
|
+
if (options.create && options.options) {
|
|
1914
|
+
throw new Error(
|
|
1915
|
+
"defineWorkingEnvironment accepts either create or options, not both."
|
|
1916
|
+
);
|
|
1917
|
+
}
|
|
1918
|
+
return Object.freeze({
|
|
1919
|
+
...options,
|
|
1920
|
+
[FOUNDRY_WORKING_ENVIRONMENT_BRAND]: true
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
function assertSnapshot(value) {
|
|
1924
|
+
if (!value || typeof value !== "object") {
|
|
1925
|
+
throw new Error("Stored working-environment snapshot must be an object.");
|
|
1926
|
+
}
|
|
1927
|
+
const snapshot = value;
|
|
1928
|
+
if (snapshot.version !== 1 || !Array.isArray(snapshot.dirs) || !snapshot.dirs.every((path) => typeof path === "string") || !Array.isArray(snapshot.files) || !snapshot.files.every(
|
|
1929
|
+
(file) => Boolean(
|
|
1930
|
+
file && typeof file === "object" && typeof file.path === "string" && typeof file.data === "string" && typeof file.mtime === "number"
|
|
1931
|
+
)
|
|
1932
|
+
)) {
|
|
1933
|
+
throw new Error("Stored working-environment snapshot is invalid or unsupported.");
|
|
1934
|
+
}
|
|
1935
|
+
return structuredClone(snapshot);
|
|
1936
|
+
}
|
|
1937
|
+
function environmentSnapshotOwner(scope, context) {
|
|
1938
|
+
return {
|
|
1939
|
+
scope,
|
|
1940
|
+
definitionId: context.definitionId,
|
|
1941
|
+
agentId: context.agentId,
|
|
1942
|
+
conversationId: context.conversationId,
|
|
1943
|
+
workspaceId: context.workspaceId
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
function foundryDataEnvironmentPersistence(options = {}) {
|
|
1947
|
+
const scope = options.scope ?? "agent";
|
|
1948
|
+
const adapter = {
|
|
1949
|
+
identifier: `foundry-data:${scope}`,
|
|
1950
|
+
load(context) {
|
|
1951
|
+
return Effect6.map(
|
|
1952
|
+
context.data.getWorkingEnvironmentSnapshot(
|
|
1953
|
+
environmentSnapshotOwner(scope, context)
|
|
1954
|
+
),
|
|
1955
|
+
(snapshot) => snapshot ? assertSnapshot(snapshot) : null
|
|
1956
|
+
);
|
|
1957
|
+
},
|
|
1958
|
+
save(snapshot, context) {
|
|
1959
|
+
return context.data.putWorkingEnvironmentSnapshot(
|
|
1960
|
+
environmentSnapshotOwner(scope, context),
|
|
1961
|
+
structuredClone(snapshot)
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
return Object.freeze(adapter);
|
|
1966
|
+
}
|
|
1967
|
+
function defineRepl(options) {
|
|
1968
|
+
const valid = options.language === "javascript" && options.session instanceof JsSession || options.language === "python" && options.session instanceof PySession || options.language === "lisp" && options.session instanceof LispSession;
|
|
1969
|
+
if (!valid) {
|
|
1970
|
+
throw new Error(
|
|
1971
|
+
`Foundry ${options.language} REPL requires the matching native Glove session.`
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
return Object.freeze({
|
|
1975
|
+
...options,
|
|
1976
|
+
[FOUNDRY_REPL_BRAND]: true
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
function persistenceContext(context) {
|
|
1980
|
+
return {
|
|
1981
|
+
definitionId: context.definitionId,
|
|
1982
|
+
agentId: context.agentId,
|
|
1983
|
+
conversationId: context.conversationId,
|
|
1984
|
+
workspaceId: context.workspaceId,
|
|
1985
|
+
runId: context.runId,
|
|
1986
|
+
data: context.data,
|
|
1987
|
+
signal: context.controls.signal
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
async function createEnvironment(definition, context) {
|
|
1991
|
+
if (definition[FOUNDRY_WORKING_ENVIRONMENT_BRAND] !== true) {
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
"Agent workingEnvironment must be created with defineWorkingEnvironment(...)."
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
const persistence = persistenceContext(context);
|
|
1997
|
+
const snapshot = definition.persistence ? await resolveResolvable(definition.persistence.load(persistence)) : null;
|
|
1998
|
+
const createContext = {
|
|
1999
|
+
assembly: context,
|
|
2000
|
+
snapshot
|
|
2001
|
+
};
|
|
2002
|
+
if (definition.create) {
|
|
2003
|
+
return {
|
|
2004
|
+
environment: await resolveResolvable(definition.create(createContext)),
|
|
2005
|
+
persistence
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
const resolvedOptions = typeof definition.options === "function" ? await resolveResolvable(definition.options(createContext)) : definition.options ?? {};
|
|
2009
|
+
return {
|
|
2010
|
+
environment: await createWorkingEnvironment({
|
|
2011
|
+
...resolvedOptions,
|
|
2012
|
+
...snapshot && resolvedOptions.filesystem === void 0 ? { filesystem: fromSnapshot(snapshot) } : {}
|
|
2013
|
+
}),
|
|
2014
|
+
persistence
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
async function mountFoundryWorkbench(options) {
|
|
2018
|
+
let environment;
|
|
2019
|
+
let persistence;
|
|
2020
|
+
let mountedRepl;
|
|
2021
|
+
const dispose = async () => {
|
|
2022
|
+
if (!environment || !options.workingEnvironment) return;
|
|
2023
|
+
const failures = [];
|
|
2024
|
+
if (options.workingEnvironment.persistence && persistence) {
|
|
2025
|
+
try {
|
|
2026
|
+
const snapshot = await environment.snapshot();
|
|
2027
|
+
await resolveResolvable(
|
|
2028
|
+
options.workingEnvironment.persistence.save(snapshot, persistence)
|
|
2029
|
+
);
|
|
2030
|
+
options.context.controls.emit({
|
|
2031
|
+
type: "foundry.working-environment.snapshot.saved",
|
|
2032
|
+
data: {
|
|
2033
|
+
persistence: options.workingEnvironment.persistence.identifier,
|
|
2034
|
+
files: snapshot.files.length
|
|
2035
|
+
}
|
|
2036
|
+
});
|
|
2037
|
+
} catch (cause) {
|
|
2038
|
+
failures.push(cause);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (options.workingEnvironment.close !== false) {
|
|
2042
|
+
try {
|
|
2043
|
+
await environment.close();
|
|
2044
|
+
} catch (cause) {
|
|
2045
|
+
failures.push(cause);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (failures.length > 0) {
|
|
2049
|
+
throw new AggregateError(
|
|
2050
|
+
failures,
|
|
2051
|
+
"Foundry working-environment cleanup failed."
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
};
|
|
2055
|
+
try {
|
|
2056
|
+
if (options.workingEnvironment) {
|
|
2057
|
+
const created = await createEnvironment(
|
|
2058
|
+
options.workingEnvironment,
|
|
2059
|
+
options.context
|
|
2060
|
+
);
|
|
2061
|
+
environment = created.environment;
|
|
2062
|
+
persistence = created.persistence;
|
|
2063
|
+
mountWorkingEnvironment(options.glove, {
|
|
2064
|
+
env: environment,
|
|
2065
|
+
...options.workingEnvironment.mount ?? {}
|
|
2066
|
+
});
|
|
2067
|
+
options.context.controls.emit({
|
|
2068
|
+
type: "foundry.working-environment.mounted",
|
|
2069
|
+
data: {
|
|
2070
|
+
tools: environment.tools.map((tool) => tool.name),
|
|
2071
|
+
modules: [...environment.moduleDescriptions.keys()],
|
|
2072
|
+
warnings: environment.warnings,
|
|
2073
|
+
persistence: options.workingEnvironment.persistence?.identifier ?? null
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
if (options.repl) {
|
|
2078
|
+
if (options.repl[FOUNDRY_REPL_BRAND] !== true) {
|
|
2079
|
+
throw new Error("Agent repl must be created with defineRepl(...).");
|
|
2080
|
+
}
|
|
2081
|
+
switch (options.repl.language) {
|
|
2082
|
+
case "javascript":
|
|
2083
|
+
mountJs(options.glove, {
|
|
2084
|
+
session: options.repl.session,
|
|
2085
|
+
...options.repl.mount ?? {}
|
|
2086
|
+
});
|
|
2087
|
+
mountedRepl = Object.freeze({
|
|
2088
|
+
language: "javascript",
|
|
2089
|
+
session: options.repl.session
|
|
2090
|
+
});
|
|
2091
|
+
break;
|
|
2092
|
+
case "python":
|
|
2093
|
+
mountPy(options.glove, {
|
|
2094
|
+
session: options.repl.session,
|
|
2095
|
+
...options.repl.mount ?? {}
|
|
2096
|
+
});
|
|
2097
|
+
mountedRepl = Object.freeze({
|
|
2098
|
+
language: "python",
|
|
2099
|
+
session: options.repl.session
|
|
2100
|
+
});
|
|
2101
|
+
break;
|
|
2102
|
+
case "lisp":
|
|
2103
|
+
mountLisp(options.glove, {
|
|
2104
|
+
session: options.repl.session,
|
|
2105
|
+
...options.repl.mount ?? {}
|
|
2106
|
+
});
|
|
2107
|
+
mountedRepl = Object.freeze({
|
|
2108
|
+
language: "lisp",
|
|
2109
|
+
session: options.repl.session
|
|
2110
|
+
});
|
|
2111
|
+
break;
|
|
2112
|
+
}
|
|
2113
|
+
options.context.controls.emit({
|
|
2114
|
+
type: "foundry.repl.mounted",
|
|
2115
|
+
data: {
|
|
2116
|
+
language: options.repl.language,
|
|
2117
|
+
frame: options.repl.mount?.frame ?? "repl"
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
...environment ? { workingEnvironment: environment, vfs: environment.fs } : {},
|
|
2123
|
+
...mountedRepl ? { repl: mountedRepl } : {},
|
|
2124
|
+
dispose
|
|
2125
|
+
};
|
|
2126
|
+
} catch (cause) {
|
|
2127
|
+
try {
|
|
2128
|
+
await dispose();
|
|
2129
|
+
} catch (cleanupCause) {
|
|
2130
|
+
throw new AggregateError(
|
|
2131
|
+
[cause, cleanupCause],
|
|
2132
|
+
"Foundry workbench mounting and cleanup failed."
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
throw cause;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
// src/agent-runtime.ts
|
|
2140
|
+
import { pathToFileURL } from "node:url";
|
|
2141
|
+
import { Displaymanager as Displaymanager2, Glove as Glove2 } from "glove-core";
|
|
2142
|
+
import { mountMesh } from "glove-mesh";
|
|
2143
|
+
import { Effect as Effect7 } from "effect";
|
|
2144
|
+
import { signal } from "station-signal";
|
|
2145
|
+
import { z as z2 } from "zod";
|
|
2146
|
+
var INBOX_ITEMS_SCHEMA = z2.array(z2.object({
|
|
2147
|
+
id: z2.string().min(1),
|
|
2148
|
+
tag: z2.string(),
|
|
2149
|
+
request: z2.string(),
|
|
2150
|
+
response: z2.string().nullable(),
|
|
2151
|
+
status: z2.enum(["pending", "resolved", "consumed"]),
|
|
2152
|
+
blocking: z2.boolean(),
|
|
2153
|
+
created_at: z2.string(),
|
|
2154
|
+
resolved_at: z2.string().nullable()
|
|
2155
|
+
}));
|
|
2156
|
+
async function hydrateInbox(store, items, emit) {
|
|
2157
|
+
if (!isInboxCapableStore(store)) {
|
|
2158
|
+
throw new Error("Agent inbox loading requires an inbox-capable Glove StoreAdapter.");
|
|
2159
|
+
}
|
|
2160
|
+
const loaded = INBOX_ITEMS_SCHEMA.parse(items);
|
|
2161
|
+
const existing = new Map((await store.getInboxItems()).map((item) => [item.id, item]));
|
|
2162
|
+
let added = 0;
|
|
2163
|
+
let updated = 0;
|
|
2164
|
+
for (const item of loaded) {
|
|
2165
|
+
const current = existing.get(item.id);
|
|
2166
|
+
if (!current) {
|
|
2167
|
+
await store.addInboxItem({ ...item });
|
|
2168
|
+
added++;
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
if (current.status !== item.status || current.response !== item.response || current.resolved_at !== item.resolved_at) {
|
|
2172
|
+
await store.updateInboxItem(item.id, {
|
|
2173
|
+
status: item.status,
|
|
2174
|
+
response: item.response,
|
|
2175
|
+
resolved_at: item.resolved_at
|
|
2176
|
+
});
|
|
2177
|
+
updated++;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
emit({
|
|
2181
|
+
type: "foundry.definition.inboxes.loaded",
|
|
2182
|
+
data: { count: loaded.length, added, updated }
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2185
|
+
function extractOutput(result) {
|
|
2186
|
+
if (result == null || typeof result !== "object") return result;
|
|
2187
|
+
const object = result;
|
|
2188
|
+
if (Array.isArray(object.messages)) {
|
|
2189
|
+
const last = object.messages.at(-1);
|
|
2190
|
+
return last?.text ?? "";
|
|
2191
|
+
}
|
|
2192
|
+
return "text" in object ? object.text : result;
|
|
2193
|
+
}
|
|
2194
|
+
function serializable(value) {
|
|
2195
|
+
try {
|
|
2196
|
+
return JSON.parse(JSON.stringify(value));
|
|
2197
|
+
} catch {
|
|
2198
|
+
return String(value);
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function writeAgentEvent(type, data) {
|
|
2202
|
+
const line = `${FOUNDRY_EVENT_PREFIX}${JSON.stringify({
|
|
2203
|
+
type,
|
|
2204
|
+
data: serializable(data),
|
|
2205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2206
|
+
})}
|
|
2207
|
+
`;
|
|
2208
|
+
return new Promise((resolveWrite, rejectWrite) => {
|
|
2209
|
+
process.stdout.write(line, (error) => {
|
|
2210
|
+
if (error) rejectWrite(error);
|
|
2211
|
+
else resolveWrite();
|
|
2212
|
+
});
|
|
2213
|
+
});
|
|
2214
|
+
}
|
|
2215
|
+
function executionSubscriber() {
|
|
2216
|
+
return {
|
|
2217
|
+
async record(eventType, data) {
|
|
2218
|
+
await writeAgentEvent(eventType, data);
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
2222
|
+
async function resolveAgentValue(definition, value, context, field) {
|
|
2223
|
+
context.controls.emit({
|
|
2224
|
+
type: "foundry.assembly.resolve.start",
|
|
2225
|
+
data: { field }
|
|
2226
|
+
});
|
|
2227
|
+
const resolved = await resolveResolvable(
|
|
2228
|
+
typeof value === "function" ? value(definition, context) : value
|
|
2229
|
+
);
|
|
2230
|
+
context.controls.emit({
|
|
2231
|
+
type: "foundry.assembly.resolve.complete",
|
|
2232
|
+
data: {
|
|
2233
|
+
field,
|
|
2234
|
+
count: Array.isArray(resolved) ? resolved.length : void 0
|
|
2235
|
+
}
|
|
2236
|
+
});
|
|
2237
|
+
return resolved;
|
|
2238
|
+
}
|
|
2239
|
+
var HANDLER_ONLY_MODEL = {
|
|
2240
|
+
name: "foundry-handler",
|
|
2241
|
+
setSystemPrompt: () => void 0,
|
|
2242
|
+
prompt: async () => {
|
|
2243
|
+
throw new Error(
|
|
2244
|
+
"This is a model-free Foundry agent. Define run/spawn or configure a model."
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
async function loadRuntimeApplication() {
|
|
2249
|
+
const path = process.env[FOUNDRY_APPLICATION_ENV];
|
|
2250
|
+
if (!path) return EMPTY_FOUNDRY_APPLICATION;
|
|
2251
|
+
const imported = await import(pathToFileURL(path).href);
|
|
2252
|
+
if (!isFoundryApplication(imported.default)) {
|
|
2253
|
+
throw new Error(
|
|
2254
|
+
`${path} must default-export defineApplication(...) for installation resolution.`
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
return imported.default;
|
|
2258
|
+
}
|
|
2259
|
+
async function executeSpawned(spawned, message, signal2) {
|
|
2260
|
+
if (spawned && typeof spawned === "object" && "processRequest" in spawned && typeof spawned.processRequest === "function") {
|
|
2261
|
+
return extractOutput(
|
|
2262
|
+
await spawned.processRequest(toGloveRequestInput(message), signal2)
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
if (spawned && typeof spawned === "object" && "run" in spawned && typeof spawned.run === "function") {
|
|
2266
|
+
return spawned.run(
|
|
2267
|
+
message,
|
|
2268
|
+
signal2
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
return spawned;
|
|
2272
|
+
}
|
|
2273
|
+
async function runDefinition(definition, id2, runtimeValue) {
|
|
2274
|
+
const envelope = runtimeValue;
|
|
2275
|
+
const request = envelope.request;
|
|
2276
|
+
const input = request;
|
|
2277
|
+
const runId = process.env.STATION_SIGNAL_RUN_ID ?? "unknown";
|
|
2278
|
+
const subscriber = executionSubscriber();
|
|
2279
|
+
const abortController = new AbortController();
|
|
2280
|
+
const onTerminate = () => abortController.abort();
|
|
2281
|
+
process.once("SIGTERM", onTerminate);
|
|
2282
|
+
let disposeSurfaces;
|
|
2283
|
+
const cleanups = [];
|
|
2284
|
+
const application = await loadRuntimeApplication();
|
|
2285
|
+
const storeFactory = definition.store ?? application.conversationStore;
|
|
2286
|
+
const store = storeFactory ? await storeFactory({
|
|
2287
|
+
definitionId: id2,
|
|
2288
|
+
agentId: request.agentId,
|
|
2289
|
+
conversationId: request.conversationId,
|
|
2290
|
+
workspaceId: request.workspaceId
|
|
2291
|
+
}) : null;
|
|
2292
|
+
const history = Object.freeze(
|
|
2293
|
+
(store ? await store.getMessages() : []).map(freezeGloveMessage)
|
|
2294
|
+
);
|
|
2295
|
+
const message = toGloveMessage(request.message);
|
|
2296
|
+
const messages = Object.freeze([...history, message]);
|
|
2297
|
+
const controls = {
|
|
2298
|
+
signal: abortController.signal,
|
|
2299
|
+
commands: [],
|
|
2300
|
+
emit: (event) => {
|
|
2301
|
+
void writeAgentEvent(event.type, event.data).catch((cause) => {
|
|
2302
|
+
process.stderr.write(
|
|
2303
|
+
`Foundry failed to forward agent event: ${cause instanceof Error ? cause.message : String(cause)}
|
|
2304
|
+
`
|
|
2305
|
+
);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
};
|
|
2309
|
+
try {
|
|
2310
|
+
const registry = definition.components ?? {
|
|
2311
|
+
capabilities: EMPTY_CAPABILITY_REGISTRY,
|
|
2312
|
+
native: { layers: Object.freeze([]), subscribers: Object.freeze([]) }
|
|
2313
|
+
};
|
|
2314
|
+
const installations = envelope.agent.installations;
|
|
2315
|
+
const data = application.data ?? new MemoryFoundryDataAdapter();
|
|
2316
|
+
const initialAssemblyContext = {
|
|
2317
|
+
definitionId: id2,
|
|
2318
|
+
agentId: request.agentId,
|
|
2319
|
+
conversationId: request.conversationId,
|
|
2320
|
+
workspaceId: request.workspaceId,
|
|
2321
|
+
name: internalAgentName(id2),
|
|
2322
|
+
runId,
|
|
2323
|
+
mode: "agent",
|
|
2324
|
+
request,
|
|
2325
|
+
agentInstance: envelope.agent,
|
|
2326
|
+
conversation: envelope.conversation,
|
|
2327
|
+
activations: Object.freeze([...envelope.activations ?? []]),
|
|
2328
|
+
data,
|
|
2329
|
+
input,
|
|
2330
|
+
message,
|
|
2331
|
+
messageInput: request.message,
|
|
2332
|
+
messageText: message.text,
|
|
2333
|
+
history,
|
|
2334
|
+
messages,
|
|
2335
|
+
installations,
|
|
2336
|
+
store,
|
|
2337
|
+
subscriber,
|
|
2338
|
+
controls
|
|
2339
|
+
};
|
|
2340
|
+
const assemblyContext = initialAssemblyContext;
|
|
2341
|
+
const resolveOptional = async (field, value, fallback) => value === void 0 ? fallback : resolveAgentValue(
|
|
2342
|
+
definition,
|
|
2343
|
+
value,
|
|
2344
|
+
assemblyContext,
|
|
2345
|
+
field
|
|
2346
|
+
);
|
|
2347
|
+
const [model, systemPrompt, displayManager, compactionLimit, compactionInstructions, maxTurns] = await Promise.all([
|
|
2348
|
+
resolveOptional("model", definition.model, HANDLER_ONLY_MODEL),
|
|
2349
|
+
resolveOptional("systemPrompt", definition.systemPrompt, ""),
|
|
2350
|
+
resolveOptional("displayManager", definition.displayManager, new Displaymanager2()),
|
|
2351
|
+
resolveOptional("compactionLimit", definition.compactionLimit, void 0),
|
|
2352
|
+
resolveOptional("compactionInstructions", definition.compactionInstructions, "Preserve goals, decisions, unresolved work, tool results, and pending inbox items."),
|
|
2353
|
+
resolveOptional("maxTurns", definition.maxTurns, void 0)
|
|
2354
|
+
]);
|
|
2355
|
+
const base = new Glove2({
|
|
2356
|
+
...store ? { store } : {},
|
|
2357
|
+
model,
|
|
2358
|
+
displayManager,
|
|
2359
|
+
systemPrompt,
|
|
2360
|
+
serverMode: definition.serverMode ?? true,
|
|
2361
|
+
...definition.maxRetries !== void 0 ? { maxRetries: definition.maxRetries } : {},
|
|
2362
|
+
...definition.maxConsecutiveErrors !== void 0 ? { maxConsecutiveErrors: definition.maxConsecutiveErrors } : {},
|
|
2363
|
+
compaction_config: {
|
|
2364
|
+
compaction_instructions: compactionInstructions,
|
|
2365
|
+
...maxTurns !== void 0 ? { max_turns: maxTurns } : {},
|
|
2366
|
+
...compactionLimit !== void 0 ? { compaction_context_limit: compactionLimit } : {}
|
|
2367
|
+
},
|
|
2368
|
+
...definition.enableToolResultSummary !== void 0 ? { enableToolResultSummary: definition.enableToolResultSummary } : {}
|
|
2369
|
+
}).build();
|
|
2370
|
+
base.addSubscriber(subscriber);
|
|
2371
|
+
const [tools, hooks, skills, subagents, memory, inboxItems, layers, subscribers, calls, playbooks, schedules, mesh, workingEnvironment, repl] = await Promise.all([
|
|
2372
|
+
resolveOptional("tools", definition.tools, []),
|
|
2373
|
+
resolveOptional("hooks", definition.hooks, []),
|
|
2374
|
+
resolveOptional("skills", definition.skills, []),
|
|
2375
|
+
resolveOptional("subagents", definition.subagents, []),
|
|
2376
|
+
resolveOptional("memory", definition.memory, []),
|
|
2377
|
+
definition.inboxes ? resolveAgentValue(
|
|
2378
|
+
definition,
|
|
2379
|
+
definition.inboxes,
|
|
2380
|
+
assemblyContext,
|
|
2381
|
+
"inboxes"
|
|
2382
|
+
) : Promise.resolve([]),
|
|
2383
|
+
resolveOptional("layers", definition.layers, []),
|
|
2384
|
+
resolveOptional("subscribers", definition.subscribers, []),
|
|
2385
|
+
resolveOptional("calls", definition.calls, []),
|
|
2386
|
+
resolveOptional("playbooks", definition.playbooks, []),
|
|
2387
|
+
resolveOptional("schedules", definition.schedules, []),
|
|
2388
|
+
resolveOptional("mesh", definition.mesh, void 0),
|
|
2389
|
+
resolveOptional("workingEnvironment", definition.workingEnvironment, void 0),
|
|
2390
|
+
resolveOptional("repl", definition.repl, void 0)
|
|
2391
|
+
]);
|
|
2392
|
+
const playbookNames = /* @__PURE__ */ new Set();
|
|
2393
|
+
const desiredPlaybooks = definition.playbooks === void 0 ? envelope.agent.playbooks : playbooks.map((playbook) => {
|
|
2394
|
+
if (playbook[FOUNDRY_COMPOSED_PLAYBOOK_BRAND] !== true) {
|
|
2395
|
+
throw new Error("Agent playbooks must be created at runtime with composePlaybook(...).");
|
|
2396
|
+
}
|
|
2397
|
+
if (playbookNames.has(playbook.name)) {
|
|
2398
|
+
throw new Error(`Duplicate composed playbook name "${playbook.name}".`);
|
|
2399
|
+
}
|
|
2400
|
+
playbookNames.add(playbook.name);
|
|
2401
|
+
const revision = composedPlaybookRevision(playbook);
|
|
2402
|
+
return materializeComposedPlaybook(
|
|
2403
|
+
playbook,
|
|
2404
|
+
agentPlaybookId(id2, request.agentId, playbook.name),
|
|
2405
|
+
revision
|
|
2406
|
+
);
|
|
2407
|
+
});
|
|
2408
|
+
if (definition.playbooks !== void 0) {
|
|
2409
|
+
const playbookSync = {
|
|
2410
|
+
id: `playbook_sync_${runId}`,
|
|
2411
|
+
type: "playbook.sync",
|
|
2412
|
+
definitionId: id2,
|
|
2413
|
+
agentId: request.agentId,
|
|
2414
|
+
conversationId: request.conversationId,
|
|
2415
|
+
workspaceId: request.workspaceId,
|
|
2416
|
+
playbooks: desiredPlaybooks
|
|
2417
|
+
};
|
|
2418
|
+
controls.commands.push(playbookSync);
|
|
2419
|
+
controls.emit({ type: FOUNDRY_CORE_COMMAND_EVENT, data: playbookSync });
|
|
2420
|
+
controls.emit({
|
|
2421
|
+
type: "foundry.definition.playbooks.composed",
|
|
2422
|
+
data: { count: desiredPlaybooks.length, names: desiredPlaybooks.map((item) => item.playbookName) }
|
|
2423
|
+
});
|
|
2424
|
+
}
|
|
2425
|
+
const scheduleNames = /* @__PURE__ */ new Set();
|
|
2426
|
+
const desiredSchedules = schedules.map((schedule) => {
|
|
2427
|
+
if (!isFoundrySchedule(schedule)) {
|
|
2428
|
+
throw new Error("Agent schedules must be created with defineSchedule(...).");
|
|
2429
|
+
}
|
|
2430
|
+
if (scheduleNames.has(schedule.name)) {
|
|
2431
|
+
throw new Error(`Duplicate agent schedule name "${schedule.name}".`);
|
|
2432
|
+
}
|
|
2433
|
+
scheduleNames.add(schedule.name);
|
|
2434
|
+
return {
|
|
2435
|
+
id: agentScheduleActivationId(id2, request.agentId, schedule.name),
|
|
2436
|
+
name: schedule.name,
|
|
2437
|
+
revision: agentScheduleRevision(schedule),
|
|
2438
|
+
message: schedule.message,
|
|
2439
|
+
...schedule.payload !== void 0 ? { payload: schedule.payload } : {},
|
|
2440
|
+
timing: normalizeScheduleTiming(schedule.timing),
|
|
2441
|
+
enabled: schedule.enabled !== false
|
|
2442
|
+
};
|
|
2443
|
+
});
|
|
2444
|
+
const scheduleSync = {
|
|
2445
|
+
id: `schedule_sync_${runId}`,
|
|
2446
|
+
type: "schedule.sync",
|
|
2447
|
+
definitionId: id2,
|
|
2448
|
+
agentId: request.agentId,
|
|
2449
|
+
conversationId: request.conversationId,
|
|
2450
|
+
workspaceId: request.workspaceId,
|
|
2451
|
+
schedules: desiredSchedules
|
|
2452
|
+
};
|
|
2453
|
+
controls.commands.push(scheduleSync);
|
|
2454
|
+
controls.emit({ type: FOUNDRY_CORE_COMMAND_EVENT, data: scheduleSync });
|
|
2455
|
+
controls.emit({
|
|
2456
|
+
type: "foundry.definition.schedules.loaded",
|
|
2457
|
+
data: { count: desiredSchedules.length, names: desiredSchedules.map((item) => item.name) }
|
|
2458
|
+
});
|
|
2459
|
+
const effectiveInstallations = [...installations];
|
|
2460
|
+
for (const tool of createFoundryCoreTools(assemblyContext, desiredSchedules)) base.fold(tool);
|
|
2461
|
+
const applicationTransmissionTools = createInstalledApplicationTransmissionTools(
|
|
2462
|
+
assemblyContext,
|
|
2463
|
+
registry.capabilities.applications,
|
|
2464
|
+
effectiveInstallations,
|
|
2465
|
+
desiredPlaybooks
|
|
2466
|
+
);
|
|
2467
|
+
for (const tool of applicationTransmissionTools) base.fold(tool);
|
|
2468
|
+
if (applicationTransmissionTools.length > 0) {
|
|
2469
|
+
controls.emit({
|
|
2470
|
+
type: "foundry.application.transmission-tools.mounted",
|
|
2471
|
+
data: {
|
|
2472
|
+
tools: applicationTransmissionTools.map((tool) => tool.name)
|
|
2473
|
+
}
|
|
2474
|
+
});
|
|
2475
|
+
}
|
|
2476
|
+
for (const tool of tools) base.fold(tool);
|
|
2477
|
+
for (const hook of hooks) base.defineHook(hook.name, hook.handler);
|
|
2478
|
+
for (const skill of skills) base.defineSkill(skill);
|
|
2479
|
+
for (const subagent of subagents) base.defineSubAgent(subagent);
|
|
2480
|
+
const workbench = await mountFoundryWorkbench({
|
|
2481
|
+
glove: base,
|
|
2482
|
+
context: assemblyContext,
|
|
2483
|
+
...workingEnvironment ? { workingEnvironment } : {},
|
|
2484
|
+
...repl ? { repl } : {}
|
|
2485
|
+
});
|
|
2486
|
+
cleanups.push(workbench.dispose);
|
|
2487
|
+
const callByName = /* @__PURE__ */ new Map();
|
|
2488
|
+
const invoke = async (name, callInput) => {
|
|
2489
|
+
const call = callByName.get(name);
|
|
2490
|
+
if (!call) throw new Error(`Foundry call "${name}" is not available.`);
|
|
2491
|
+
const parsed = call.input.parse(callInput);
|
|
2492
|
+
const value = await resolveResolvable(call.handler(parsed, callContext));
|
|
2493
|
+
return call.output.parse(value);
|
|
2494
|
+
};
|
|
2495
|
+
const surfaceContext = {
|
|
2496
|
+
definitionId: id2,
|
|
2497
|
+
agentId: request.agentId,
|
|
2498
|
+
conversationId: request.conversationId,
|
|
2499
|
+
workspaceId: request.workspaceId,
|
|
2500
|
+
runId,
|
|
2501
|
+
input,
|
|
2502
|
+
request,
|
|
2503
|
+
message,
|
|
2504
|
+
messageInput: request.message,
|
|
2505
|
+
messageText: message.text,
|
|
2506
|
+
history,
|
|
2507
|
+
messages,
|
|
2508
|
+
glove: base,
|
|
2509
|
+
...workbench.workingEnvironment ? { workingEnvironment: workbench.workingEnvironment, vfs: workbench.vfs } : {},
|
|
2510
|
+
...workbench.repl ? { repl: workbench.repl } : {},
|
|
2511
|
+
signal: abortController.signal,
|
|
2512
|
+
emit: controls.emit
|
|
2513
|
+
};
|
|
2514
|
+
const callContext = {
|
|
2515
|
+
...surfaceContext,
|
|
2516
|
+
installations: effectiveInstallations,
|
|
2517
|
+
invoke
|
|
2518
|
+
};
|
|
2519
|
+
for (const call of calls) {
|
|
2520
|
+
if (callByName.has(call.name)) throw new Error(`Duplicate Foundry call "${call.name}".`);
|
|
2521
|
+
callByName.set(call.name, call);
|
|
2522
|
+
if (call.exposeToAgent ?? true) {
|
|
2523
|
+
base.fold({
|
|
2524
|
+
name: call.name,
|
|
2525
|
+
description: call.description,
|
|
2526
|
+
inputSchema: call.input,
|
|
2527
|
+
async do(callInput) {
|
|
2528
|
+
try {
|
|
2529
|
+
return { status: "success", data: await invoke(call.name, callInput) };
|
|
2530
|
+
} catch (cause) {
|
|
2531
|
+
return {
|
|
2532
|
+
status: "error",
|
|
2533
|
+
data: null,
|
|
2534
|
+
message: cause instanceof Error ? cause.message : String(cause)
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
disposeSurfaces = await mountFoundrySurfaces({
|
|
2542
|
+
registry: registry.native,
|
|
2543
|
+
layers,
|
|
2544
|
+
subscribers,
|
|
2545
|
+
context: surfaceContext
|
|
2546
|
+
});
|
|
2547
|
+
await Effect7.runPromise(
|
|
2548
|
+
installRegistry({
|
|
2549
|
+
registry: registry.capabilities,
|
|
2550
|
+
installations: effectiveInstallations,
|
|
2551
|
+
context: {
|
|
2552
|
+
definitionId: id2,
|
|
2553
|
+
agentId: request.agentId,
|
|
2554
|
+
conversationId: request.conversationId,
|
|
2555
|
+
workspaceId: request.workspaceId,
|
|
2556
|
+
runId,
|
|
2557
|
+
input,
|
|
2558
|
+
request,
|
|
2559
|
+
message,
|
|
2560
|
+
messageInput: request.message,
|
|
2561
|
+
messageText: message.text,
|
|
2562
|
+
history,
|
|
2563
|
+
messages,
|
|
2564
|
+
glove: base,
|
|
2565
|
+
store: base.store,
|
|
2566
|
+
emit: controls.emit
|
|
2567
|
+
},
|
|
2568
|
+
...definition.mcpAdapter ? { mcpAdapter: definition.mcpAdapter } : {},
|
|
2569
|
+
...definition.accountSessions ? { accountSessions: definition.accountSessions } : {}
|
|
2570
|
+
})
|
|
2571
|
+
);
|
|
2572
|
+
await Effect7.runPromise(
|
|
2573
|
+
mountAgentDefinitionMemory({
|
|
2574
|
+
registry: registry.capabilities,
|
|
2575
|
+
memory,
|
|
2576
|
+
context: {
|
|
2577
|
+
definitionId: id2,
|
|
2578
|
+
agentId: request.agentId,
|
|
2579
|
+
conversationId: request.conversationId,
|
|
2580
|
+
workspaceId: request.workspaceId,
|
|
2581
|
+
runId,
|
|
2582
|
+
input,
|
|
2583
|
+
request,
|
|
2584
|
+
message,
|
|
2585
|
+
messageInput: request.message,
|
|
2586
|
+
messageText: message.text,
|
|
2587
|
+
history,
|
|
2588
|
+
messages,
|
|
2589
|
+
glove: base,
|
|
2590
|
+
store: base.store,
|
|
2591
|
+
emit: controls.emit
|
|
2592
|
+
}
|
|
2593
|
+
})
|
|
2594
|
+
);
|
|
2595
|
+
if (definition.inboxes) {
|
|
2596
|
+
await hydrateInbox(base.store, inboxItems, controls.emit);
|
|
2597
|
+
}
|
|
2598
|
+
if (mesh) {
|
|
2599
|
+
await mountMesh(base, {
|
|
2600
|
+
adapter: mesh.adapter,
|
|
2601
|
+
identity: {
|
|
2602
|
+
id: mesh.identity?.id ?? request.agentId,
|
|
2603
|
+
name: mesh.identity?.name ?? request.agentId,
|
|
2604
|
+
description: mesh.identity?.description ?? definition.description,
|
|
2605
|
+
...mesh.identity?.capabilities ? { capabilities: mesh.identity.capabilities } : {},
|
|
2606
|
+
metadata: {
|
|
2607
|
+
definitionId: id2,
|
|
2608
|
+
conversationId: request.conversationId,
|
|
2609
|
+
workspaceId: request.workspaceId,
|
|
2610
|
+
...mesh.identity?.metadata ?? {}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
});
|
|
2614
|
+
cleanups.push(() => mesh.adapter.unregister());
|
|
2615
|
+
}
|
|
2616
|
+
if (definition.configure) {
|
|
2617
|
+
await resolveResolvable(definition.configure(base, callContext));
|
|
2618
|
+
}
|
|
2619
|
+
const glove = definition.build ? await resolveResolvable(definition.build(base, assemblyContext)) ?? base : base;
|
|
2620
|
+
const runtimeContext = { ...callContext, glove };
|
|
2621
|
+
const defaultRun = async () => extractOutput(
|
|
2622
|
+
await glove.processRequest(
|
|
2623
|
+
toGloveRequestInput(assemblyContext.messageInput),
|
|
2624
|
+
abortController.signal
|
|
2625
|
+
)
|
|
2626
|
+
);
|
|
2627
|
+
const spawn = async (messageInput = assemblyContext.messageInput) => definition.spawn ? executeSpawned(
|
|
2628
|
+
await resolveResolvable(definition.spawn(glove, runtimeContext, messageInput)),
|
|
2629
|
+
messageInput,
|
|
2630
|
+
abortController.signal
|
|
2631
|
+
) : defaultRun();
|
|
2632
|
+
const handlerContext = {
|
|
2633
|
+
...runtimeContext,
|
|
2634
|
+
defaultRun,
|
|
2635
|
+
defaultHandler: defaultRun,
|
|
2636
|
+
spawn
|
|
2637
|
+
};
|
|
2638
|
+
const result = definition.run ? await resolveResolvable(definition.run(glove, handlerContext)) : definition.handler ? await resolveResolvable(definition.handler(handlerContext)) : definition.spawn ? await spawn() : await defaultRun();
|
|
2639
|
+
const sleep = [...controls.commands].reverse().find(
|
|
2640
|
+
(command) => command.type === "sleep"
|
|
2641
|
+
);
|
|
2642
|
+
return {
|
|
2643
|
+
status: sleep ? "suspended" : "completed",
|
|
2644
|
+
value: serializable(result),
|
|
2645
|
+
agentId: request.agentId,
|
|
2646
|
+
conversationId: request.conversationId,
|
|
2647
|
+
workspaceId: request.workspaceId,
|
|
2648
|
+
...sleep && sleep.type === "sleep" ? { suspension: { commandId: sleep.id, wakeAt: sleep.wakeAt } } : {}
|
|
2649
|
+
};
|
|
2650
|
+
} finally {
|
|
2651
|
+
const failures = [];
|
|
2652
|
+
for (const cleanup of cleanups.reverse()) {
|
|
2653
|
+
try {
|
|
2654
|
+
await cleanup();
|
|
2655
|
+
} catch (cause) {
|
|
2656
|
+
failures.push(cause);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
try {
|
|
2660
|
+
if (disposeSurfaces) await disposeSurfaces();
|
|
2661
|
+
} catch (cause) {
|
|
2662
|
+
failures.push(cause);
|
|
2663
|
+
}
|
|
2664
|
+
process.removeListener("SIGTERM", onTerminate);
|
|
2665
|
+
if (failures.length > 0) throw new AggregateError(failures, "Foundry agent cleanup failed.");
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function compileAgentDefinition(definition, route) {
|
|
2669
|
+
const contentPartSchema = z2.object({
|
|
2670
|
+
type: z2.enum(["text", "image", "video", "document"]),
|
|
2671
|
+
text: z2.string().optional(),
|
|
2672
|
+
source: z2.object({
|
|
2673
|
+
type: z2.enum(["base64", "url"]),
|
|
2674
|
+
media_type: z2.string(),
|
|
2675
|
+
data: z2.string().optional(),
|
|
2676
|
+
url: z2.string().optional()
|
|
2677
|
+
}).optional()
|
|
2678
|
+
});
|
|
2679
|
+
const requestSchema = z2.object({
|
|
2680
|
+
agentId: z2.string().min(1),
|
|
2681
|
+
conversationId: z2.string().min(1),
|
|
2682
|
+
workspaceId: z2.string().min(1),
|
|
2683
|
+
message: z2.union([z2.string(), z2.array(contentPartSchema)]),
|
|
2684
|
+
payload: z2.unknown().optional(),
|
|
2685
|
+
context: z2.record(z2.string(), z2.unknown()).optional(),
|
|
2686
|
+
source: z2.object({
|
|
2687
|
+
kind: z2.enum(["direct", "transmission", "activation", "spawn", "background"]),
|
|
2688
|
+
id: z2.string().optional(),
|
|
2689
|
+
provider: z2.string().optional(),
|
|
2690
|
+
eventId: z2.string().optional(),
|
|
2691
|
+
threadKey: z2.string().optional()
|
|
2692
|
+
}).optional()
|
|
2693
|
+
});
|
|
2694
|
+
const executionEnvelope = z2.object({
|
|
2695
|
+
[FOUNDRY_EXECUTION_MARKER]: z2.literal(true),
|
|
2696
|
+
request: requestSchema,
|
|
2697
|
+
agent: z2.object({
|
|
2698
|
+
id: z2.string(),
|
|
2699
|
+
definitionId: z2.string(),
|
|
2700
|
+
workspaceId: z2.string(),
|
|
2701
|
+
context: z2.record(z2.string(), z2.unknown()),
|
|
2702
|
+
installations: z2.array(z2.object({
|
|
2703
|
+
kind: z2.enum(["tool", "application", "mcp"]),
|
|
2704
|
+
id: z2.string(),
|
|
2705
|
+
config: z2.unknown().optional()
|
|
2706
|
+
})),
|
|
2707
|
+
playbooks: z2.array(z2.object({
|
|
2708
|
+
id: z2.string(),
|
|
2709
|
+
transmissionId: z2.string(),
|
|
2710
|
+
enabled: z2.boolean().optional(),
|
|
2711
|
+
match: z2.object({
|
|
2712
|
+
event: z2.string().optional(),
|
|
2713
|
+
routeIds: z2.array(z2.string()).optional(),
|
|
2714
|
+
predicate: z2.object({
|
|
2715
|
+
name: z2.string(),
|
|
2716
|
+
parameters: z2.record(z2.string(), z2.unknown()).optional()
|
|
2717
|
+
}).optional()
|
|
2718
|
+
}).optional(),
|
|
2719
|
+
directives: z2.array(z2.object({
|
|
2720
|
+
action: z2.string(),
|
|
2721
|
+
instruction: z2.string(),
|
|
2722
|
+
parameters: z2.record(z2.string(), z2.unknown()).optional()
|
|
2723
|
+
})),
|
|
2724
|
+
applications: z2.array(z2.string()).optional(),
|
|
2725
|
+
outbound: z2.array(z2.object({
|
|
2726
|
+
routeId: z2.string(),
|
|
2727
|
+
applicationId: z2.string().optional(),
|
|
2728
|
+
event: z2.string().optional(),
|
|
2729
|
+
accountId: z2.string().optional(),
|
|
2730
|
+
applicationAccountId: z2.string().optional(),
|
|
2731
|
+
instruction: z2.string().optional()
|
|
2732
|
+
})).optional(),
|
|
2733
|
+
serialization: z2.record(z2.string(), z2.unknown()).optional(),
|
|
2734
|
+
origin: z2.enum(["agent-definition", "instance"]).optional(),
|
|
2735
|
+
playbookName: z2.string().optional(),
|
|
2736
|
+
definitionRevision: z2.string().optional()
|
|
2737
|
+
})),
|
|
2738
|
+
createdAt: z2.string(),
|
|
2739
|
+
updatedAt: z2.string()
|
|
2740
|
+
}),
|
|
2741
|
+
conversation: z2.object({
|
|
2742
|
+
id: z2.string(),
|
|
2743
|
+
agentId: z2.string(),
|
|
2744
|
+
workspaceId: z2.string(),
|
|
2745
|
+
title: z2.string().optional(),
|
|
2746
|
+
context: z2.record(z2.string(), z2.unknown()),
|
|
2747
|
+
createdAt: z2.string(),
|
|
2748
|
+
updatedAt: z2.string()
|
|
2749
|
+
}),
|
|
2750
|
+
activations: z2.array(z2.object({
|
|
2751
|
+
id: z2.string(),
|
|
2752
|
+
kind: z2.enum(["scheduled", "sleep"]),
|
|
2753
|
+
definitionId: z2.string(),
|
|
2754
|
+
agentId: z2.string(),
|
|
2755
|
+
conversationId: z2.string(),
|
|
2756
|
+
workspaceId: z2.string(),
|
|
2757
|
+
message: z2.string(),
|
|
2758
|
+
payload: z2.unknown().optional(),
|
|
2759
|
+
timing: z2.union([
|
|
2760
|
+
z2.object({ kind: z2.literal("at"), at: z2.string() }),
|
|
2761
|
+
z2.object({ kind: z2.literal("every"), intervalMs: z2.number() }),
|
|
2762
|
+
z2.object({ kind: z2.literal("cron"), expression: z2.string(), timezone: z2.string() })
|
|
2763
|
+
]),
|
|
2764
|
+
origin: z2.enum(["agent-definition", "agent-tool"]),
|
|
2765
|
+
scheduleName: z2.string().optional(),
|
|
2766
|
+
definitionRevision: z2.string().optional(),
|
|
2767
|
+
status: z2.enum(["pending", "active", "completed", "cancelled"]),
|
|
2768
|
+
createdByRunId: z2.string(),
|
|
2769
|
+
lastRunId: z2.string().optional(),
|
|
2770
|
+
createdAt: z2.string(),
|
|
2771
|
+
updatedAt: z2.string()
|
|
2772
|
+
})).default([])
|
|
2773
|
+
});
|
|
2774
|
+
let builder = signal(internalAgentName(route)).input(executionEnvelope).output(z2.object({
|
|
2775
|
+
status: z2.enum(["completed", "suspended"]),
|
|
2776
|
+
value: z2.unknown(),
|
|
2777
|
+
agentId: z2.string(),
|
|
2778
|
+
conversationId: z2.string(),
|
|
2779
|
+
workspaceId: z2.string(),
|
|
2780
|
+
suspension: z2.object({ commandId: z2.string(), wakeAt: z2.string() }).optional()
|
|
2781
|
+
}));
|
|
2782
|
+
const built = builder.run(
|
|
2783
|
+
(value) => runDefinition(definition, route, value)
|
|
2784
|
+
);
|
|
2785
|
+
return built;
|
|
2786
|
+
}
|
|
2787
|
+
function compileAgentModule(route, module) {
|
|
2788
|
+
return compileAgentDefinition(defineAgentFromModule(route, module), route);
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// src/connection.ts
|
|
2792
|
+
var FOUNDRY_CONNECTION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
2793
|
+
"glove-foundry-application-connection"
|
|
2794
|
+
);
|
|
2795
|
+
var ID3 = /^[a-z][a-z0-9-]*$/;
|
|
2796
|
+
function defineConnection(options) {
|
|
2797
|
+
if (options.id && !ID3.test(options.id)) {
|
|
2798
|
+
throw new Error(`Invalid application connection id "${options.id}".`);
|
|
2799
|
+
}
|
|
2800
|
+
if (options.transmissions.length === 0) {
|
|
2801
|
+
throw new Error(`Application connection "${options.id}" must receive at least one transmission.`);
|
|
2802
|
+
}
|
|
2803
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2804
|
+
for (const transmission of options.transmissions) {
|
|
2805
|
+
if (!transmission.inbound) {
|
|
2806
|
+
throw new Error(
|
|
2807
|
+
`Application connection "${options.id ?? "<file route>"}" references an outbound-only transmission.`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
const key = fileDefinitionKey(transmission);
|
|
2811
|
+
if (seen.has(key)) {
|
|
2812
|
+
throw new Error(
|
|
2813
|
+
`Application connection "${options.id ?? "<file route>"}" repeats a transmission.`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
seen.add(key);
|
|
2817
|
+
}
|
|
2818
|
+
const { id: id2, ...definition } = options;
|
|
2819
|
+
return Object.freeze(fileIdentified({
|
|
2820
|
+
...definition,
|
|
2821
|
+
transmissions: Object.freeze([...options.transmissions]),
|
|
2822
|
+
[FOUNDRY_CONNECTION_BRAND]: true
|
|
2823
|
+
}, "connection", id2));
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// src/integration.ts
|
|
2827
|
+
import { JSONSchema as JSONSchema2 } from "effect";
|
|
2828
|
+
var FOUNDRY_TRANSMISSION_BRAND = /* @__PURE__ */ Symbol.for(
|
|
2829
|
+
"glove-foundry-transmission"
|
|
2830
|
+
);
|
|
2831
|
+
var FOUNDRY_TRANSMISSION_PREDICATE_BRAND = /* @__PURE__ */ Symbol.for(
|
|
2832
|
+
"glove-foundry-transmission-predicate"
|
|
2833
|
+
);
|
|
2834
|
+
var FOUNDRY_TRANSMISSION_EVENT_BRAND = /* @__PURE__ */ Symbol.for(
|
|
2835
|
+
"glove-foundry-transmission-event"
|
|
2836
|
+
);
|
|
2837
|
+
var TRANSMISSION_ID = /^[a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)*$/;
|
|
2838
|
+
var CAPABILITY_ID = /^[a-z][a-z0-9-]*(?::[a-z][a-z0-9-]*)?$/;
|
|
2839
|
+
var CREDENTIAL_FIELD_NAMES = /* @__PURE__ */ new Set([
|
|
2840
|
+
"token",
|
|
2841
|
+
"accesstoken",
|
|
2842
|
+
"refreshtoken",
|
|
2843
|
+
"secret",
|
|
2844
|
+
"clientsecret",
|
|
2845
|
+
"password",
|
|
2846
|
+
"apikey",
|
|
2847
|
+
"credential",
|
|
2848
|
+
"credentials"
|
|
2849
|
+
]);
|
|
2850
|
+
function assertSchemaContainsNoCredentialFields(transmissionId, area, schema) {
|
|
2851
|
+
const json = JSONSchema2.make(schema);
|
|
2852
|
+
const visit = (value, path) => {
|
|
2853
|
+
if (!value || typeof value !== "object") return;
|
|
2854
|
+
if (Array.isArray(value)) {
|
|
2855
|
+
value.forEach((entry, index) => visit(entry, `${path}[${index}]`));
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const object = value;
|
|
2859
|
+
const properties = object.properties;
|
|
2860
|
+
if (properties && typeof properties === "object") {
|
|
2861
|
+
for (const [name, child] of Object.entries(
|
|
2862
|
+
properties
|
|
2863
|
+
)) {
|
|
2864
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2865
|
+
if (CREDENTIAL_FIELD_NAMES.has(normalized)) {
|
|
2866
|
+
throw new Error(
|
|
2867
|
+
`Foundry transmission "${transmissionId}" declares credential field "${path}.${name}" in ${area}. Store an opaque accessRef on the account instead.`
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
visit(child, `${path}.${name}`);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
for (const [name, child] of Object.entries(object)) {
|
|
2874
|
+
if (name !== "properties") visit(child, path);
|
|
2875
|
+
}
|
|
2876
|
+
};
|
|
2877
|
+
visit(json, area);
|
|
2878
|
+
}
|
|
2879
|
+
function isFoundryTransmission(value) {
|
|
2880
|
+
return Boolean(
|
|
2881
|
+
value && typeof value === "object" && value[FOUNDRY_TRANSMISSION_BRAND] === true
|
|
2882
|
+
);
|
|
2883
|
+
}
|
|
2884
|
+
function defineTransmissionPredicate(options) {
|
|
2885
|
+
if (options.id && !TRANSMISSION_ID.test(options.id)) {
|
|
2886
|
+
throw new Error(`Invalid transmission predicate id "${options.id}".`);
|
|
2887
|
+
}
|
|
2888
|
+
const { id: id2, ...definition } = options;
|
|
2889
|
+
return Object.freeze(fileIdentified({
|
|
2890
|
+
...definition,
|
|
2891
|
+
[FOUNDRY_TRANSMISSION_PREDICATE_BRAND]: true
|
|
2892
|
+
}, "predicate", id2));
|
|
2893
|
+
}
|
|
2894
|
+
function defineTransmissionEvent(options) {
|
|
2895
|
+
if (options.id && !TRANSMISSION_ID.test(options.id)) {
|
|
2896
|
+
throw new Error(`Invalid transmission event id "${options.id}".`);
|
|
2897
|
+
}
|
|
2898
|
+
const { id: id2, ...definition } = options;
|
|
2899
|
+
return Object.freeze(fileIdentified({
|
|
2900
|
+
...definition,
|
|
2901
|
+
[FOUNDRY_TRANSMISSION_EVENT_BRAND]: true
|
|
2902
|
+
}, "event", id2));
|
|
2903
|
+
}
|
|
2904
|
+
function transmissionPredicate(transmission, id2) {
|
|
2905
|
+
return transmission.inbound?.predicates?.find((candidate) => candidate.id === id2);
|
|
2906
|
+
}
|
|
2907
|
+
function defineTransmission(options) {
|
|
2908
|
+
if (options.id && !TRANSMISSION_ID.test(options.id)) {
|
|
2909
|
+
throw new Error(
|
|
2910
|
+
`Invalid Foundry transmission id "${options.id}". Use lowercase letters, digits, and hyphens.`
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2913
|
+
const label = options.id ?? "<transmission file route>";
|
|
2914
|
+
const capabilities = options.capabilities ?? [];
|
|
2915
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2916
|
+
for (const capability of capabilities) {
|
|
2917
|
+
if (!CAPABILITY_ID.test(capability.id)) {
|
|
2918
|
+
throw new Error(
|
|
2919
|
+
`Invalid capability id "${capability.id}" in transmission "${label}".`
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
if (seen.has(capability.id)) {
|
|
2923
|
+
throw new Error(
|
|
2924
|
+
`Duplicate capability "${capability.id}" in transmission "${label}".`
|
|
2925
|
+
);
|
|
2926
|
+
}
|
|
2927
|
+
seen.add(capability.id);
|
|
2928
|
+
}
|
|
2929
|
+
const eventKeys = /* @__PURE__ */ new Set();
|
|
2930
|
+
for (const event of options.events ?? []) {
|
|
2931
|
+
const key = fileDefinitionKey(event);
|
|
2932
|
+
if (eventKeys.has(key)) {
|
|
2933
|
+
throw new Error(`Duplicate event in transmission "${label}".`);
|
|
2934
|
+
}
|
|
2935
|
+
eventKeys.add(key);
|
|
2936
|
+
}
|
|
2937
|
+
if (options.account) {
|
|
2938
|
+
assertSchemaContainsNoCredentialFields(
|
|
2939
|
+
label,
|
|
2940
|
+
"account.metadata",
|
|
2941
|
+
options.account.metadata
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
if (options.inbound) {
|
|
2945
|
+
assertSchemaContainsNoCredentialFields(
|
|
2946
|
+
label,
|
|
2947
|
+
"inbound.config",
|
|
2948
|
+
options.inbound.config
|
|
2949
|
+
);
|
|
2950
|
+
const predicateIds = /* @__PURE__ */ new Set();
|
|
2951
|
+
for (const predicate of options.inbound.predicates ?? []) {
|
|
2952
|
+
const key = fileDefinitionKey(predicate);
|
|
2953
|
+
if (predicateIds.has(key)) {
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
`Duplicate predicate in transmission "${label}".`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
predicateIds.add(key);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
if (options.outbound) {
|
|
2962
|
+
assertSchemaContainsNoCredentialFields(
|
|
2963
|
+
label,
|
|
2964
|
+
"outbound.config",
|
|
2965
|
+
options.outbound.config
|
|
2966
|
+
);
|
|
2967
|
+
}
|
|
2968
|
+
const { id: id2, ...definition } = options;
|
|
2969
|
+
return Object.freeze(fileIdentified({
|
|
2970
|
+
...definition,
|
|
2971
|
+
capabilities: Object.freeze([...capabilities]),
|
|
2972
|
+
events: Object.freeze([...options.events ?? []]),
|
|
2973
|
+
...options.inbound ? {
|
|
2974
|
+
inbound: Object.freeze({
|
|
2975
|
+
...options.inbound,
|
|
2976
|
+
predicates: Object.freeze([...options.inbound.predicates ?? []])
|
|
2977
|
+
})
|
|
2978
|
+
} : {},
|
|
2979
|
+
[FOUNDRY_TRANSMISSION_BRAND]: true
|
|
2980
|
+
}, "transmission", id2));
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/discovery.ts
|
|
2984
|
+
import { readdir } from "node:fs/promises";
|
|
2985
|
+
import { dirname, extname, relative, resolve, sep } from "node:path";
|
|
2986
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2987
|
+
var AGENT_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".mts", ".js", ".mjs"]);
|
|
2988
|
+
var LOCAL_DEFINITION_FILES = [
|
|
2989
|
+
{ suffix: ".action", kind: "action", brand: FOUNDRY_PLAYBOOK_ACTION_BRAND },
|
|
2990
|
+
{ suffix: ".app", kind: "application", brand: FOUNDRY_AGENT_APPLICATION_BRAND },
|
|
2991
|
+
{ suffix: ".connection", kind: "connection", brand: FOUNDRY_CONNECTION_BRAND },
|
|
2992
|
+
{ suffix: ".event", kind: "event", brand: FOUNDRY_TRANSMISSION_EVENT_BRAND },
|
|
2993
|
+
{ suffix: ".layer", kind: "layer", brand: FOUNDRY_LAYER_BRAND },
|
|
2994
|
+
{ suffix: ".mcp", kind: "mcp", brand: FOUNDRY_MCP_BRAND },
|
|
2995
|
+
{ suffix: ".memory", kind: "memory", brand: FOUNDRY_MEMORY_BRAND },
|
|
2996
|
+
{ suffix: ".predicate", kind: "predicate", brand: FOUNDRY_TRANSMISSION_PREDICATE_BRAND },
|
|
2997
|
+
{ suffix: ".subscriber", kind: "subscriber", brand: FOUNDRY_SUBSCRIBER_BRAND },
|
|
2998
|
+
{ suffix: ".subscription", kind: "subscription", brand: FOUNDRY_PLAYBOOK_SUBSCRIPTION_BRAND },
|
|
2999
|
+
{ suffix: ".tool", kind: "tool", brand: FOUNDRY_SHARED_TOOL_BRAND },
|
|
3000
|
+
{ suffix: ".transmission", kind: "transmission", brand: FOUNDRY_TRANSMISSION_BRAND }
|
|
3001
|
+
];
|
|
3002
|
+
function hasAgentExtension(file) {
|
|
3003
|
+
if (file.endsWith(".d.ts")) return false;
|
|
3004
|
+
return AGENT_EXTENSIONS.has(extname(file));
|
|
3005
|
+
}
|
|
3006
|
+
function normalizePath(path) {
|
|
3007
|
+
return path.split(sep).join("/");
|
|
3008
|
+
}
|
|
3009
|
+
function localDefinitionRoute(agentDirectory, filePath, suffix) {
|
|
3010
|
+
const relativePath = normalizePath(relative(agentDirectory, filePath));
|
|
3011
|
+
const extension = extname(relativePath);
|
|
3012
|
+
const withoutSuffix = relativePath.slice(0, -extension.length - suffix.length);
|
|
3013
|
+
const segments = withoutSuffix.split("/");
|
|
3014
|
+
if (["actions", "apps", "connections", "events", "layers", "mcp", "memory", "playbooks", "predicates", "subscribers", "subscriptions", "tools", "transmissions"].includes(segments[0] ?? "")) {
|
|
3015
|
+
segments.shift();
|
|
3016
|
+
}
|
|
3017
|
+
return segments.join("/");
|
|
3018
|
+
}
|
|
3019
|
+
async function bindAgentLocalDefinitions(agentDirectory) {
|
|
3020
|
+
let entries;
|
|
3021
|
+
try {
|
|
3022
|
+
entries = await readdir(agentDirectory, { recursive: true });
|
|
3023
|
+
} catch {
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
for (const entry of entries.sort()) {
|
|
3027
|
+
if (entry.endsWith(".d.ts")) continue;
|
|
3028
|
+
const extension = extname(entry);
|
|
3029
|
+
if (!AGENT_EXTENSIONS.has(extension)) continue;
|
|
3030
|
+
const matched = LOCAL_DEFINITION_FILES.find(
|
|
3031
|
+
({ suffix }) => entry.slice(0, -extension.length).endsWith(suffix)
|
|
3032
|
+
);
|
|
3033
|
+
if (!matched) continue;
|
|
3034
|
+
const filePath = resolve(agentDirectory, entry);
|
|
3035
|
+
const url = pathToFileURL2(filePath);
|
|
3036
|
+
const imported = await import(url.href);
|
|
3037
|
+
const value = imported.default;
|
|
3038
|
+
if (!value || typeof value !== "object" || value[matched.brand] !== true) {
|
|
3039
|
+
throw new Error(
|
|
3040
|
+
`${normalizePath(relative(agentDirectory, filePath))} must default-export its Foundry ${matched.kind} definition so the file can own its identity.`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
const route = localDefinitionRoute(agentDirectory, filePath, matched.suffix);
|
|
3044
|
+
bindFileIdentity(value, route, matched.kind);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function routeFromAgentFile(agentsDir, filePath) {
|
|
3048
|
+
const rel = normalizePath(relative(agentsDir, filePath));
|
|
3049
|
+
if (rel.startsWith("../") || rel === "..") return null;
|
|
3050
|
+
const extension = extname(rel);
|
|
3051
|
+
if (!AGENT_EXTENSIONS.has(extension)) return null;
|
|
3052
|
+
const withoutExtension = rel.slice(0, -extension.length);
|
|
3053
|
+
if (withoutExtension === "agent") return "default";
|
|
3054
|
+
if (withoutExtension.endsWith("/agent")) {
|
|
3055
|
+
return withoutExtension.slice(0, -"/agent".length);
|
|
3056
|
+
}
|
|
3057
|
+
if (withoutExtension.endsWith(".agent")) {
|
|
3058
|
+
return withoutExtension.slice(0, -".agent".length);
|
|
3059
|
+
}
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
async function findAgentFiles(agentsDir) {
|
|
3063
|
+
const absolute = resolve(agentsDir);
|
|
3064
|
+
let entries;
|
|
3065
|
+
try {
|
|
3066
|
+
entries = await readdir(absolute, { recursive: true });
|
|
3067
|
+
} catch (error) {
|
|
3068
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3069
|
+
throw new Error(`Cannot read Foundry agents directory ${absolute}: ${message}`);
|
|
3070
|
+
}
|
|
3071
|
+
return entries.filter((entry) => hasAgentExtension(entry)).map((entry) => resolve(absolute, entry)).filter((file) => routeFromAgentFile(absolute, file) !== null).sort();
|
|
3072
|
+
}
|
|
3073
|
+
async function discoverAgents(options) {
|
|
3074
|
+
const agentsDir = resolve(options.agentsDir);
|
|
3075
|
+
const files = await findAgentFiles(agentsDir);
|
|
3076
|
+
const discovered = [];
|
|
3077
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3078
|
+
for (const filePath of files) {
|
|
3079
|
+
const route = routeFromAgentFile(agentsDir, filePath);
|
|
3080
|
+
if (!route) continue;
|
|
3081
|
+
const url = pathToFileURL2(filePath);
|
|
3082
|
+
if (options.cacheBust) url.searchParams.set("t", String(Date.now()));
|
|
3083
|
+
const imported = await import(url.href);
|
|
3084
|
+
if (imported.id && (options.strictFileRoutes ?? true) && imported.id !== route) {
|
|
3085
|
+
throw new Error(
|
|
3086
|
+
`Foundry route mismatch in ${filePath}: file resolves to "${route}" but the agent declares "${imported.id}".`
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
const definition = defineAgentFromModule(route, imported);
|
|
3090
|
+
await bindAgentLocalDefinitions(
|
|
3091
|
+
dirname(filePath)
|
|
3092
|
+
);
|
|
3093
|
+
const prior = seen.get(route);
|
|
3094
|
+
if (prior) {
|
|
3095
|
+
throw new Error(
|
|
3096
|
+
`Duplicate Foundry agent id "${route}" in ${prior} and ${filePath}.`
|
|
3097
|
+
);
|
|
3098
|
+
}
|
|
3099
|
+
seen.set(route, filePath);
|
|
3100
|
+
discovered.push({
|
|
3101
|
+
route,
|
|
3102
|
+
filePath,
|
|
3103
|
+
relativePath: normalizePath(relative(agentsDir, filePath)),
|
|
3104
|
+
definition,
|
|
3105
|
+
executionName: internalAgentName(route)
|
|
3106
|
+
});
|
|
3107
|
+
}
|
|
3108
|
+
return discovered;
|
|
3109
|
+
}
|
|
3110
|
+
function createManifest(agents) {
|
|
3111
|
+
return {
|
|
3112
|
+
version: 1,
|
|
3113
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3114
|
+
agents: agents.map(({ route, definition, relativePath }) => ({
|
|
3115
|
+
id: route,
|
|
3116
|
+
description: definition.description,
|
|
3117
|
+
mode: "agent",
|
|
3118
|
+
file: relativePath,
|
|
3119
|
+
tags: definition.tags ?? [],
|
|
3120
|
+
invocationContract: "foundry/request-v1",
|
|
3121
|
+
resultContract: "foundry/result-v1",
|
|
3122
|
+
assembly: "foundry",
|
|
3123
|
+
handler: definition.run || definition.handler || definition.spawn ? "custom" : "glove",
|
|
3124
|
+
layers: (typeof definition.layers === "function" ? [] : definition.layers ?? []).map(
|
|
3125
|
+
(selection) => FOUNDRY_LAYER_BRAND in selection ? selection.id : selection.layer.id
|
|
3126
|
+
),
|
|
3127
|
+
subscribers: (typeof definition.subscribers === "function" ? [] : definition.subscribers ?? []).flatMap(
|
|
3128
|
+
(subscriber) => "id" in subscriber ? [String(subscriber.id)] : []
|
|
3129
|
+
),
|
|
3130
|
+
tools: (typeof definition.tools === "function" ? [] : definition.tools ?? []).map((tool) => tool.name),
|
|
3131
|
+
hooks: (typeof definition.hooks === "function" ? [] : definition.hooks ?? []).map((hook) => hook.name),
|
|
3132
|
+
skills: (typeof definition.skills === "function" ? [] : definition.skills ?? []).map((skill) => skill.name),
|
|
3133
|
+
subagents: (typeof definition.subagents === "function" ? [] : definition.subagents ?? []).map(
|
|
3134
|
+
(subagent) => subagent.name
|
|
3135
|
+
),
|
|
3136
|
+
memory: (typeof definition.memory === "function" ? [] : definition.memory ?? []).map(
|
|
3137
|
+
(selection) => {
|
|
3138
|
+
const profile = "profile" in selection ? selection.profile : selection;
|
|
3139
|
+
return profile.id;
|
|
3140
|
+
}
|
|
3141
|
+
),
|
|
3142
|
+
inboxLoader: definition.inboxes !== void 0,
|
|
3143
|
+
calls: (typeof definition.calls === "function" ? [] : definition.calls ?? []).map(
|
|
3144
|
+
(call) => call.name
|
|
3145
|
+
),
|
|
3146
|
+
schedules: (typeof definition.schedules === "function" ? [] : definition.schedules ?? []).map(
|
|
3147
|
+
(schedule) => schedule.name
|
|
3148
|
+
),
|
|
3149
|
+
playbooks: (typeof definition.playbooks === "function" ? [] : definition.playbooks ?? []).map(
|
|
3150
|
+
(playbook) => playbook.name
|
|
3151
|
+
),
|
|
3152
|
+
mesh: definition.mesh !== void 0,
|
|
3153
|
+
workingEnvironment: definition.workingEnvironment !== void 0,
|
|
3154
|
+
repl: definition.repl === void 0 ? null : typeof definition.repl === "function" ? "dynamic" : definition.repl.language,
|
|
3155
|
+
lazy: [
|
|
3156
|
+
"model",
|
|
3157
|
+
"systemPrompt",
|
|
3158
|
+
"displayManager",
|
|
3159
|
+
"compactionLimit",
|
|
3160
|
+
"compactionInstructions",
|
|
3161
|
+
"maxTurns",
|
|
3162
|
+
"tools",
|
|
3163
|
+
"hooks",
|
|
3164
|
+
"skills",
|
|
3165
|
+
"subagents",
|
|
3166
|
+
"memory",
|
|
3167
|
+
"inboxes",
|
|
3168
|
+
"subscribers",
|
|
3169
|
+
"layers",
|
|
3170
|
+
"calls",
|
|
3171
|
+
"schedules",
|
|
3172
|
+
"playbooks",
|
|
3173
|
+
"mesh",
|
|
3174
|
+
"workingEnvironment",
|
|
3175
|
+
"repl"
|
|
3176
|
+
].filter((field) => typeof definition[field] === "function")
|
|
3177
|
+
}))
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
export {
|
|
3182
|
+
FOUNDRY_APPLICATION_BRAND,
|
|
3183
|
+
isFoundryApplication,
|
|
3184
|
+
defineApplication,
|
|
3185
|
+
EMPTY_FOUNDRY_APPLICATION,
|
|
3186
|
+
bindFileIdentity,
|
|
3187
|
+
fileDefinitionKey,
|
|
3188
|
+
fileDefinitionLabel,
|
|
3189
|
+
FOUNDRY_AGENT_DEFINITION_BRAND,
|
|
3190
|
+
FOUNDRY_AGENT_BRAND,
|
|
3191
|
+
FOUNDRY_EVENT_PREFIX,
|
|
3192
|
+
FOUNDRY_APPLICATION_ENV,
|
|
3193
|
+
FOUNDRY_AGENT_ROUTE_ENV,
|
|
3194
|
+
FOUNDRY_AGENT_FILE_ENV,
|
|
3195
|
+
FOUNDRY_EXECUTION_MARKER,
|
|
3196
|
+
defineCall,
|
|
3197
|
+
internalAgentName,
|
|
3198
|
+
routeFromInternalAgentName,
|
|
3199
|
+
isFoundryAgentDefinition,
|
|
3200
|
+
isFoundryAgent,
|
|
3201
|
+
defineAgent,
|
|
3202
|
+
defineAgentFromModule,
|
|
3203
|
+
defineSubagent,
|
|
3204
|
+
defineRoutes,
|
|
3205
|
+
FOUNDRY_SHARED_TOOL_BRAND,
|
|
3206
|
+
FOUNDRY_AGENT_APPLICATION_BRAND,
|
|
3207
|
+
FOUNDRY_MCP_BRAND,
|
|
3208
|
+
FOUNDRY_MEMORY_BRAND,
|
|
3209
|
+
install,
|
|
3210
|
+
installationKey,
|
|
3211
|
+
defineSharedTool,
|
|
3212
|
+
defineAgentApplication,
|
|
3213
|
+
defineApp,
|
|
3214
|
+
defineMcp,
|
|
3215
|
+
defineMemory,
|
|
3216
|
+
EMPTY_CAPABILITY_REGISTRY,
|
|
3217
|
+
isInboxCapableStore,
|
|
3218
|
+
configureMemory,
|
|
3219
|
+
mountAgentDefinitionMemory,
|
|
3220
|
+
installRegistry,
|
|
3221
|
+
isFoundryCapability,
|
|
3222
|
+
FOUNDRY_LAYER_BRAND,
|
|
3223
|
+
FOUNDRY_SUBSCRIBER_BRAND,
|
|
3224
|
+
defineLayer,
|
|
3225
|
+
configureLayer,
|
|
3226
|
+
defineSubscriber,
|
|
3227
|
+
EMPTY_NATIVE_REGISTRY,
|
|
3228
|
+
mountFoundrySurfaces,
|
|
3229
|
+
isFoundryLayer,
|
|
3230
|
+
isFoundrySubscriber,
|
|
3231
|
+
composePlaybook,
|
|
3232
|
+
reconstructPlaybook,
|
|
3233
|
+
FOUNDRY_PLAYBOOK_ACTION_BRAND,
|
|
3234
|
+
FOUNDRY_COMPOSED_PLAYBOOK_BRAND,
|
|
3235
|
+
definePlaybookAction,
|
|
3236
|
+
definePlaybookSubscription,
|
|
3237
|
+
reconstructPlaybookSubscription,
|
|
3238
|
+
toGloveMessage,
|
|
3239
|
+
toGloveRequestInput,
|
|
3240
|
+
MemoryFoundryDataAdapter,
|
|
3241
|
+
createAgentInstance,
|
|
3242
|
+
defineAgentInstance,
|
|
3243
|
+
reconstructAgentInstance,
|
|
3244
|
+
normalizeAgentInstallations,
|
|
3245
|
+
createConversation,
|
|
3246
|
+
id,
|
|
3247
|
+
FOUNDRY_SCHEDULE_BRAND,
|
|
3248
|
+
defineSchedule,
|
|
3249
|
+
isFoundrySchedule,
|
|
3250
|
+
FOUNDRY_CORE_COMMAND_EVENT,
|
|
3251
|
+
createInstalledApplicationTransmissionTools,
|
|
3252
|
+
createFoundryCoreTools,
|
|
3253
|
+
FOUNDRY_WORKING_ENVIRONMENT_BRAND,
|
|
3254
|
+
FOUNDRY_REPL_BRAND,
|
|
3255
|
+
defineWorkingEnvironment,
|
|
3256
|
+
foundryDataEnvironmentPersistence,
|
|
3257
|
+
defineRepl,
|
|
3258
|
+
compileAgentDefinition,
|
|
3259
|
+
compileAgentModule,
|
|
3260
|
+
FOUNDRY_CONNECTION_BRAND,
|
|
3261
|
+
defineConnection,
|
|
3262
|
+
FOUNDRY_TRANSMISSION_BRAND,
|
|
3263
|
+
FOUNDRY_TRANSMISSION_PREDICATE_BRAND,
|
|
3264
|
+
FOUNDRY_TRANSMISSION_EVENT_BRAND,
|
|
3265
|
+
isFoundryTransmission,
|
|
3266
|
+
defineTransmissionPredicate,
|
|
3267
|
+
defineTransmissionEvent,
|
|
3268
|
+
transmissionPredicate,
|
|
3269
|
+
defineTransmission,
|
|
3270
|
+
bindAgentLocalDefinitions,
|
|
3271
|
+
routeFromAgentFile,
|
|
3272
|
+
findAgentFiles,
|
|
3273
|
+
discoverAgents,
|
|
3274
|
+
createManifest
|
|
3275
|
+
};
|