glove-foundry 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -4
- package/dist/{chunk-P65RT7H5.js → chunk-25BGNRJN.js} +1452 -178
- package/dist/{chunk-CRWY7M66.js → chunk-5LDW2N2E.js} +1086 -91
- package/dist/{chunk-3GCUECPA.js → chunk-GKZLWD5M.js} +66 -2
- package/dist/cli.js +186 -40
- package/dist/{client-CLkZREDr.d.ts → client-D0-pKEZM.d.ts} +365 -12
- package/dist/client.d.ts +4 -1
- package/dist/client.js +1 -1
- package/dist/config.d.ts +15 -1
- package/dist/execution-agent.js +1 -1
- package/dist/index.d.ts +91 -5
- package/dist/index.js +428 -6
- package/docs/architecture.md +51 -0
- package/docs/building-with-foundry.md +336 -4
- package/docs/evaluation-checklist.md +10 -1
- package/docs/guidance.md +146 -0
- package/docs/inspector.md +39 -1
- package/docs/release-verification.md +74 -0
- package/package.json +15 -13
- package/templates/minimal/README.md +30 -2
- package/templates/travel-concierge/README.md +33 -5
package/dist/index.js
CHANGED
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
BindingId,
|
|
12
12
|
BindingNotFound,
|
|
13
13
|
CapabilityId,
|
|
14
|
-
EMPTY_AGENT_COMPOSITION,
|
|
15
14
|
EventId,
|
|
16
15
|
EventNotFound,
|
|
17
16
|
EventReference,
|
|
@@ -38,23 +37,23 @@ import {
|
|
|
38
37
|
TopologyStore,
|
|
39
38
|
TransmissionId,
|
|
40
39
|
compileApplicationManifest,
|
|
41
|
-
composeAgent,
|
|
42
40
|
grantResolverLive,
|
|
43
41
|
memoryAccountDirectory,
|
|
44
42
|
memoryEventStore,
|
|
45
43
|
memoryTopologyStore,
|
|
46
44
|
serializeInboundTransmissionXml,
|
|
47
45
|
writeGeneratedTypes
|
|
48
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-25BGNRJN.js";
|
|
49
47
|
import {
|
|
50
48
|
FoundryClient,
|
|
51
49
|
FoundryRunHandle,
|
|
52
50
|
createFoundryClient
|
|
53
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-GKZLWD5M.js";
|
|
54
52
|
import {
|
|
55
53
|
defineConfig
|
|
56
54
|
} from "./chunk-ZFNMFE3T.js";
|
|
57
55
|
import {
|
|
56
|
+
EMPTY_AGENT_COMPOSITION,
|
|
58
57
|
EMPTY_CAPABILITY_REGISTRY,
|
|
59
58
|
EMPTY_FOUNDRY_APPLICATION,
|
|
60
59
|
EMPTY_NATIVE_REGISTRY,
|
|
@@ -65,6 +64,7 @@ import {
|
|
|
65
64
|
FOUNDRY_AGENT_ROUTE_ENV,
|
|
66
65
|
FOUNDRY_APPLICATION_BRAND,
|
|
67
66
|
FOUNDRY_APPLICATION_ENV,
|
|
67
|
+
FOUNDRY_APPROVAL_DIRECTORY_ENV,
|
|
68
68
|
FOUNDRY_COMPOSED_PLAYBOOK_BRAND,
|
|
69
69
|
FOUNDRY_CONNECTION_BRAND,
|
|
70
70
|
FOUNDRY_CORE_COMMAND_EVENT,
|
|
@@ -82,12 +82,15 @@ import {
|
|
|
82
82
|
FOUNDRY_TRANSMISSION_EVENT_BRAND,
|
|
83
83
|
FOUNDRY_TRANSMISSION_PREDICATE_BRAND,
|
|
84
84
|
FOUNDRY_WORKING_ENVIRONMENT_BRAND,
|
|
85
|
+
FoundryApprovalDisplayManager,
|
|
85
86
|
MemoryFoundryDataAdapter,
|
|
87
|
+
composeAgent,
|
|
86
88
|
composePlaybook,
|
|
87
89
|
configureLayer,
|
|
88
90
|
configureMemory,
|
|
89
91
|
createAgentInstance,
|
|
90
92
|
createConversation,
|
|
93
|
+
createFoundryApproval,
|
|
91
94
|
createFoundryCoreTools,
|
|
92
95
|
createInstalledApplicationTransmissionTools,
|
|
93
96
|
createManifest,
|
|
@@ -99,6 +102,9 @@ import {
|
|
|
99
102
|
defineApplication,
|
|
100
103
|
defineCall,
|
|
101
104
|
defineConnection,
|
|
105
|
+
defineFacts,
|
|
106
|
+
defineForms,
|
|
107
|
+
defineGoals,
|
|
102
108
|
defineLayer,
|
|
103
109
|
defineMcp,
|
|
104
110
|
defineMemory,
|
|
@@ -117,9 +123,11 @@ import {
|
|
|
117
123
|
discoverAgents,
|
|
118
124
|
findAgentFiles,
|
|
119
125
|
foundryDataEnvironmentPersistence,
|
|
126
|
+
foundryGuidanceSubject,
|
|
120
127
|
install,
|
|
121
128
|
installRegistry,
|
|
122
129
|
installationKey,
|
|
130
|
+
installedApplicationTransmissionToolName,
|
|
123
131
|
internalAgentName,
|
|
124
132
|
isFoundryAgent,
|
|
125
133
|
isFoundryAgentDefinition,
|
|
@@ -130,17 +138,419 @@ import {
|
|
|
130
138
|
isFoundrySubscriber,
|
|
131
139
|
isFoundryTransmission,
|
|
132
140
|
isInboxCapableStore,
|
|
141
|
+
listFoundryApprovals,
|
|
133
142
|
mountAgentDefinitionMemory,
|
|
134
143
|
mountFoundrySurfaces,
|
|
135
144
|
reconstructAgentInstance,
|
|
136
145
|
reconstructPlaybook,
|
|
137
146
|
reconstructPlaybookSubscription,
|
|
147
|
+
resolveFoundryApproval,
|
|
138
148
|
routeFromAgentFile,
|
|
139
149
|
routeFromInternalAgentName,
|
|
140
150
|
toGloveMessage,
|
|
141
151
|
toGloveRequestInput,
|
|
142
|
-
transmissionPredicate
|
|
143
|
-
|
|
152
|
+
transmissionPredicate,
|
|
153
|
+
withFoundryPermissions
|
|
154
|
+
} from "./chunk-5LDW2N2E.js";
|
|
155
|
+
|
|
156
|
+
// src/file-data.ts
|
|
157
|
+
import { randomUUID } from "node:crypto";
|
|
158
|
+
import { mkdir, open, readFile, rename, stat, unlink } from "node:fs/promises";
|
|
159
|
+
import { dirname } from "node:path";
|
|
160
|
+
import { Effect } from "effect";
|
|
161
|
+
function emptyState(environment) {
|
|
162
|
+
return {
|
|
163
|
+
version: 1,
|
|
164
|
+
agents: [],
|
|
165
|
+
subscriptions: [],
|
|
166
|
+
inboundDeliveries: [],
|
|
167
|
+
activations: [],
|
|
168
|
+
conversations: [],
|
|
169
|
+
workspaceEntries: [],
|
|
170
|
+
workingEnvironments: [],
|
|
171
|
+
inbox: [],
|
|
172
|
+
tasks: [],
|
|
173
|
+
environment: structuredClone([...environment])
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function replaceById(values, value) {
|
|
177
|
+
const index = values.findIndex((candidate) => candidate.id === value.id);
|
|
178
|
+
if (index === -1) values.push(structuredClone(value));
|
|
179
|
+
else values[index] = structuredClone(value);
|
|
180
|
+
}
|
|
181
|
+
function environmentOwnerKey(owner) {
|
|
182
|
+
return [
|
|
183
|
+
owner.scope,
|
|
184
|
+
owner.workspaceId,
|
|
185
|
+
owner.definitionId,
|
|
186
|
+
owner.agentId,
|
|
187
|
+
owner.scope === "conversation" ? owner.conversationId : ""
|
|
188
|
+
].join("\0");
|
|
189
|
+
}
|
|
190
|
+
function effect(operation) {
|
|
191
|
+
return Effect.tryPromise({ try: operation, catch: (cause) => cause });
|
|
192
|
+
}
|
|
193
|
+
function sleep(ms) {
|
|
194
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
+
}
|
|
196
|
+
function errorCode(cause) {
|
|
197
|
+
return cause && typeof cause === "object" && "code" in cause ? String(cause.code) : void 0;
|
|
198
|
+
}
|
|
199
|
+
var FileFoundryDataAdapter = class {
|
|
200
|
+
identifier;
|
|
201
|
+
file;
|
|
202
|
+
lockFile;
|
|
203
|
+
lockTimeoutMs;
|
|
204
|
+
staleLockMs;
|
|
205
|
+
seeds;
|
|
206
|
+
initialized;
|
|
207
|
+
queue = Promise.resolve();
|
|
208
|
+
constructor(options) {
|
|
209
|
+
if (!options.file.trim()) throw new Error("FileFoundryDataAdapter requires a file path.");
|
|
210
|
+
this.file = options.file;
|
|
211
|
+
this.lockFile = `${options.file}.lock`;
|
|
212
|
+
this.identifier = options.identifier ?? `foundry-file:${options.file}`;
|
|
213
|
+
this.lockTimeoutMs = options.lockTimeoutMs ?? 1e4;
|
|
214
|
+
this.staleLockMs = options.staleLockMs ?? 3e4;
|
|
215
|
+
this.seeds = {
|
|
216
|
+
agents: [...options.agents ?? []],
|
|
217
|
+
subscriptions: [...options.subscriptions ?? []],
|
|
218
|
+
activations: [...options.activations ?? []],
|
|
219
|
+
conversations: [...options.conversations ?? []],
|
|
220
|
+
environment: [...options.environment ?? []]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
serialize(operation) {
|
|
224
|
+
const current = this.queue.then(operation, operation);
|
|
225
|
+
this.queue = current.then(() => void 0, () => void 0);
|
|
226
|
+
return current;
|
|
227
|
+
}
|
|
228
|
+
async acquireLock() {
|
|
229
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
230
|
+
const deadline = Date.now() + this.lockTimeoutMs;
|
|
231
|
+
while (true) {
|
|
232
|
+
try {
|
|
233
|
+
const handle = await open(this.lockFile, "wx");
|
|
234
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, acquiredAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
235
|
+
await handle.close();
|
|
236
|
+
return async () => {
|
|
237
|
+
try {
|
|
238
|
+
await unlink(this.lockFile);
|
|
239
|
+
} catch (cause) {
|
|
240
|
+
if (errorCode(cause) !== "ENOENT") throw cause;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
} catch (cause) {
|
|
244
|
+
if (errorCode(cause) !== "EEXIST") throw cause;
|
|
245
|
+
try {
|
|
246
|
+
const lock = await stat(this.lockFile);
|
|
247
|
+
if (Date.now() - lock.mtimeMs > this.staleLockMs) {
|
|
248
|
+
await unlink(this.lockFile);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
} catch (staleCause) {
|
|
252
|
+
if (errorCode(staleCause) === "ENOENT") continue;
|
|
253
|
+
throw staleCause;
|
|
254
|
+
}
|
|
255
|
+
if (Date.now() >= deadline) {
|
|
256
|
+
throw new Error(`Timed out waiting for Foundry data lock "${this.lockFile}".`);
|
|
257
|
+
}
|
|
258
|
+
await sleep(20 + Math.floor(Math.random() * 30));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async read() {
|
|
263
|
+
try {
|
|
264
|
+
const parsed = JSON.parse(await readFile(this.file, "utf8"));
|
|
265
|
+
if (parsed.version !== 1) {
|
|
266
|
+
throw new Error(`Unsupported Foundry data version ${String(parsed.version)} in "${this.file}".`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
version: 1,
|
|
270
|
+
agents: parsed.agents ?? [],
|
|
271
|
+
subscriptions: parsed.subscriptions ?? [],
|
|
272
|
+
inboundDeliveries: parsed.inboundDeliveries ?? [],
|
|
273
|
+
activations: parsed.activations ?? [],
|
|
274
|
+
conversations: parsed.conversations ?? [],
|
|
275
|
+
workspaceEntries: parsed.workspaceEntries ?? [],
|
|
276
|
+
workingEnvironments: parsed.workingEnvironments ?? [],
|
|
277
|
+
inbox: parsed.inbox ?? [],
|
|
278
|
+
tasks: parsed.tasks ?? [],
|
|
279
|
+
environment: parsed.environment ?? []
|
|
280
|
+
};
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
if (errorCode(cause) === "ENOENT") return emptyState(this.seeds.environment);
|
|
283
|
+
throw cause;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async write(state) {
|
|
287
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
288
|
+
const temporary = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
|
|
289
|
+
const handle = await open(temporary, "wx", 384);
|
|
290
|
+
try {
|
|
291
|
+
await handle.writeFile(`${JSON.stringify(state, null, 2)}
|
|
292
|
+
`, "utf8");
|
|
293
|
+
await handle.sync();
|
|
294
|
+
} finally {
|
|
295
|
+
await handle.close();
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await rename(temporary, this.file);
|
|
299
|
+
} catch (cause) {
|
|
300
|
+
try {
|
|
301
|
+
await unlink(temporary);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
throw cause;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async mutate(operation) {
|
|
308
|
+
return this.serialize(async () => {
|
|
309
|
+
const release = await this.acquireLock();
|
|
310
|
+
try {
|
|
311
|
+
const state = await this.read();
|
|
312
|
+
const result = await operation(state);
|
|
313
|
+
await this.write(state);
|
|
314
|
+
return result;
|
|
315
|
+
} finally {
|
|
316
|
+
await release();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
async ensureInitialized() {
|
|
321
|
+
this.initialized ??= this.mutate((state) => {
|
|
322
|
+
for (const seed of this.seeds.agents) {
|
|
323
|
+
if (!state.agents.some((agent) => agent.id === seed.id)) {
|
|
324
|
+
state.agents.push(structuredClone(reconstructAgentInstance(seed)));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const seed of this.seeds.subscriptions) {
|
|
328
|
+
if (!state.subscriptions.some((item) => item.id === seed.id)) {
|
|
329
|
+
state.subscriptions.push(structuredClone(reconstructPlaybookSubscription(seed)));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
for (const seed of this.seeds.activations) {
|
|
333
|
+
if (!state.activations.some((item) => item.id === seed.id)) {
|
|
334
|
+
state.activations.push(structuredClone(seed));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (const seed of this.seeds.conversations) {
|
|
338
|
+
if (!state.conversations.some((item) => item.id === seed.id)) {
|
|
339
|
+
state.conversations.push(structuredClone(seed));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
for (const seed of this.seeds.environment) {
|
|
343
|
+
const key = `${seed.scope}:${seed.workspaceId}:${seed.agentId ?? ""}:${seed.conversationId ?? ""}:${seed.key}`;
|
|
344
|
+
if (!state.environment.some(
|
|
345
|
+
(item) => `${item.scope}:${item.workspaceId}:${item.agentId ?? ""}:${item.conversationId ?? ""}:${item.key}` === key
|
|
346
|
+
)) state.environment.push(structuredClone(seed));
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
await this.initialized;
|
|
350
|
+
}
|
|
351
|
+
async snapshot() {
|
|
352
|
+
await this.ensureInitialized();
|
|
353
|
+
await this.queue;
|
|
354
|
+
return this.read();
|
|
355
|
+
}
|
|
356
|
+
getAgent(id) {
|
|
357
|
+
return effect(async () => {
|
|
358
|
+
const value = (await this.snapshot()).agents.find((agent) => agent.id === id);
|
|
359
|
+
return value ? reconstructAgentInstance(value) : null;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
putAgent(agent) {
|
|
363
|
+
return effect(async () => {
|
|
364
|
+
await this.ensureInitialized();
|
|
365
|
+
await this.mutate((state) => replaceById(state.agents, reconstructAgentInstance(agent)));
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
listAgents(definitionId) {
|
|
369
|
+
return effect(async () => (await this.snapshot()).agents.filter((agent) => !definitionId || agent.definitionId === definitionId).map(reconstructAgentInstance));
|
|
370
|
+
}
|
|
371
|
+
provisionAgent(input) {
|
|
372
|
+
return effect(async () => {
|
|
373
|
+
await this.ensureInitialized();
|
|
374
|
+
return this.mutate((state) => {
|
|
375
|
+
const existing = state.agents.find((agent) => agent.provisioningKey === input.provisioningKey);
|
|
376
|
+
if (existing) return reconstructAgentInstance(existing);
|
|
377
|
+
const created = createAgentInstance(input.definitionId, {
|
|
378
|
+
...input,
|
|
379
|
+
id: input.id ?? `agent_${randomUUID()}`
|
|
380
|
+
}, input.provisioningKey);
|
|
381
|
+
state.agents.push(structuredClone(created));
|
|
382
|
+
return created;
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
getPlaybookSubscription(id) {
|
|
387
|
+
return effect(async () => {
|
|
388
|
+
const value = (await this.snapshot()).subscriptions.find((item) => item.id === id);
|
|
389
|
+
return value ? reconstructPlaybookSubscription(value) : null;
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
putPlaybookSubscription(subscription) {
|
|
393
|
+
return effect(async () => {
|
|
394
|
+
await this.ensureInitialized();
|
|
395
|
+
await this.mutate((state) => replaceById(state.subscriptions, reconstructPlaybookSubscription(subscription)));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
deletePlaybookSubscription(id) {
|
|
399
|
+
return effect(async () => {
|
|
400
|
+
await this.ensureInitialized();
|
|
401
|
+
return this.mutate((state) => {
|
|
402
|
+
const length = state.subscriptions.length;
|
|
403
|
+
state.subscriptions = state.subscriptions.filter((item) => item.id !== id);
|
|
404
|
+
return state.subscriptions.length !== length;
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
listPlaybookSubscriptions(workspaceId) {
|
|
409
|
+
return effect(async () => (await this.snapshot()).subscriptions.filter((item) => !workspaceId || item.workspaceId === workspaceId).map(reconstructPlaybookSubscription));
|
|
410
|
+
}
|
|
411
|
+
getInboundDelivery(key) {
|
|
412
|
+
return effect(async () => (await this.snapshot()).inboundDeliveries.find((item) => item.key === key) ?? null);
|
|
413
|
+
}
|
|
414
|
+
claimInboundDelivery(key) {
|
|
415
|
+
return effect(async () => {
|
|
416
|
+
await this.ensureInitialized();
|
|
417
|
+
return this.mutate((state) => {
|
|
418
|
+
if (state.inboundDeliveries.some((item) => item.key === key)) return false;
|
|
419
|
+
state.inboundDeliveries.push({
|
|
420
|
+
key,
|
|
421
|
+
status: "pending",
|
|
422
|
+
runIds: [],
|
|
423
|
+
claimedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
424
|
+
});
|
|
425
|
+
return true;
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
completeInboundDelivery(key, runIds) {
|
|
430
|
+
return effect(async () => {
|
|
431
|
+
await this.ensureInitialized();
|
|
432
|
+
await this.mutate((state) => {
|
|
433
|
+
const prior = state.inboundDeliveries.find((item) => item.key === key);
|
|
434
|
+
if (!prior) throw new Error(`Inbound delivery claim "${key}" does not exist.`);
|
|
435
|
+
const index = state.inboundDeliveries.findIndex((item) => item.key === key);
|
|
436
|
+
state.inboundDeliveries[index] = structuredClone({
|
|
437
|
+
...prior,
|
|
438
|
+
status: "completed",
|
|
439
|
+
runIds: [...runIds],
|
|
440
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
releaseInboundDelivery(key) {
|
|
446
|
+
return effect(async () => {
|
|
447
|
+
await this.ensureInitialized();
|
|
448
|
+
await this.mutate((state) => {
|
|
449
|
+
state.inboundDeliveries = state.inboundDeliveries.filter(
|
|
450
|
+
(item) => item.key !== key || item.status !== "pending"
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
getActivation(id) {
|
|
456
|
+
return effect(async () => structuredClone((await this.snapshot()).activations.find((item) => item.id === id) ?? null));
|
|
457
|
+
}
|
|
458
|
+
putActivation(activation) {
|
|
459
|
+
return effect(async () => {
|
|
460
|
+
await this.ensureInitialized();
|
|
461
|
+
await this.mutate((state) => replaceById(state.activations, activation));
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
listActivations(workspaceId) {
|
|
465
|
+
return effect(async () => structuredClone((await this.snapshot()).activations.filter((item) => !workspaceId || item.workspaceId === workspaceId)));
|
|
466
|
+
}
|
|
467
|
+
getConversation(id) {
|
|
468
|
+
return effect(async () => structuredClone((await this.snapshot()).conversations.find((item) => item.id === id) ?? null));
|
|
469
|
+
}
|
|
470
|
+
putConversation(conversation) {
|
|
471
|
+
return effect(async () => {
|
|
472
|
+
await this.ensureInitialized();
|
|
473
|
+
await this.mutate((state) => replaceById(state.conversations, conversation));
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
listConversations(agentId) {
|
|
477
|
+
return effect(async () => structuredClone((await this.snapshot()).conversations.filter((item) => item.agentId === agentId)));
|
|
478
|
+
}
|
|
479
|
+
getWorkspaceEntry(workspaceId, key) {
|
|
480
|
+
return effect(async () => structuredClone((await this.snapshot()).workspaceEntries.find((item) => item.workspaceId === workspaceId && item.key === key) ?? null));
|
|
481
|
+
}
|
|
482
|
+
putWorkspaceEntry(entry) {
|
|
483
|
+
return effect(async () => {
|
|
484
|
+
await this.ensureInitialized();
|
|
485
|
+
await this.mutate((state) => {
|
|
486
|
+
const index = state.workspaceEntries.findIndex((item) => item.workspaceId === entry.workspaceId && item.key === entry.key);
|
|
487
|
+
if (index === -1) state.workspaceEntries.push(structuredClone(entry));
|
|
488
|
+
else state.workspaceEntries[index] = structuredClone(entry);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
listWorkspaceEntries(workspaceId) {
|
|
493
|
+
return effect(async () => structuredClone((await this.snapshot()).workspaceEntries.filter((item) => item.workspaceId === workspaceId)));
|
|
494
|
+
}
|
|
495
|
+
compareAndSetWorkspaceEntry(entry, expectedUpdatedAt) {
|
|
496
|
+
return effect(async () => {
|
|
497
|
+
await this.ensureInitialized();
|
|
498
|
+
return this.mutate((state) => {
|
|
499
|
+
const index = state.workspaceEntries.findIndex((item) => item.workspaceId === entry.workspaceId && item.key === entry.key);
|
|
500
|
+
const current = state.workspaceEntries[index];
|
|
501
|
+
if ((current?.updatedAt ?? null) !== expectedUpdatedAt) return false;
|
|
502
|
+
if (!Number.isFinite(Date.parse(entry.updatedAt)) || current && Date.parse(entry.updatedAt) <= Date.parse(current.updatedAt)) {
|
|
503
|
+
throw new Error("An atomic workspace update must advance updatedAt.");
|
|
504
|
+
}
|
|
505
|
+
if (index === -1) state.workspaceEntries.push(structuredClone(entry));
|
|
506
|
+
else state.workspaceEntries[index] = structuredClone(entry);
|
|
507
|
+
return true;
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
getWorkingEnvironmentSnapshot(owner) {
|
|
512
|
+
return effect(async () => {
|
|
513
|
+
const key = environmentOwnerKey(owner);
|
|
514
|
+
const value = (await this.snapshot()).workingEnvironments.find((item) => environmentOwnerKey(item.owner) === key);
|
|
515
|
+
return value ? structuredClone(value.snapshot) : null;
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
putWorkingEnvironmentSnapshot(owner, snapshot) {
|
|
519
|
+
return effect(async () => {
|
|
520
|
+
await this.ensureInitialized();
|
|
521
|
+
await this.mutate((state) => {
|
|
522
|
+
const key = environmentOwnerKey(owner);
|
|
523
|
+
const index = state.workingEnvironments.findIndex((item) => environmentOwnerKey(item.owner) === key);
|
|
524
|
+
const value = structuredClone({ owner, snapshot });
|
|
525
|
+
if (index === -1) state.workingEnvironments.push(value);
|
|
526
|
+
else state.workingEnvironments[index] = value;
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
putInboxItem(item) {
|
|
531
|
+
return effect(async () => {
|
|
532
|
+
await this.ensureInitialized();
|
|
533
|
+
await this.mutate((state) => replaceById(state.inbox, item));
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
listInboxItems(workspaceId) {
|
|
537
|
+
return effect(async () => structuredClone((await this.snapshot()).inbox.filter((item) => item.workspaceId === workspaceId)));
|
|
538
|
+
}
|
|
539
|
+
putTask(task) {
|
|
540
|
+
return effect(async () => {
|
|
541
|
+
await this.ensureInitialized();
|
|
542
|
+
await this.mutate((state) => replaceById(state.tasks, task));
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
listTasks(workspaceId) {
|
|
546
|
+
return effect(async () => structuredClone((await this.snapshot()).tasks.filter((item) => item.workspaceId === workspaceId)));
|
|
547
|
+
}
|
|
548
|
+
listEnvironment(scope) {
|
|
549
|
+
return effect(async () => structuredClone((await this.snapshot()).environment.filter(
|
|
550
|
+
(item) => item.workspaceId === scope.workspaceId && (item.scope === "workspace" || item.scope === "agent" && item.agentId === scope.agentId || item.scope === "conversation" && item.conversationId === scope.conversationId)
|
|
551
|
+
)));
|
|
552
|
+
}
|
|
553
|
+
};
|
|
144
554
|
|
|
145
555
|
// src/authoring.ts
|
|
146
556
|
import { Schema } from "effect";
|
|
@@ -287,6 +697,7 @@ export {
|
|
|
287
697
|
FOUNDRY_AGENT_ROUTE_ENV,
|
|
288
698
|
FOUNDRY_APPLICATION_BRAND,
|
|
289
699
|
FOUNDRY_APPLICATION_ENV,
|
|
700
|
+
FOUNDRY_APPROVAL_DIRECTORY_ENV,
|
|
290
701
|
FOUNDRY_COMPOSED_PLAYBOOK_BRAND,
|
|
291
702
|
FOUNDRY_CONNECTION_BRAND,
|
|
292
703
|
FOUNDRY_CORE_COMMAND_EVENT,
|
|
@@ -304,7 +715,9 @@ export {
|
|
|
304
715
|
FOUNDRY_TRANSMISSION_EVENT_BRAND,
|
|
305
716
|
FOUNDRY_TRANSMISSION_PREDICATE_BRAND,
|
|
306
717
|
FOUNDRY_WORKING_ENVIRONMENT_BRAND,
|
|
718
|
+
FileFoundryDataAdapter,
|
|
307
719
|
FoundryApplicationManifest,
|
|
720
|
+
FoundryApprovalDisplayManager,
|
|
308
721
|
FoundryClient,
|
|
309
722
|
FoundryManifestCapability,
|
|
310
723
|
FoundryManifestTransmission,
|
|
@@ -335,6 +748,7 @@ export {
|
|
|
335
748
|
configureMemory,
|
|
336
749
|
createAgentInstance,
|
|
337
750
|
createConversation,
|
|
751
|
+
createFoundryApproval,
|
|
338
752
|
createFoundryClient,
|
|
339
753
|
createFoundryCoreTools,
|
|
340
754
|
createInstalledApplicationTransmissionTools,
|
|
@@ -350,6 +764,9 @@ export {
|
|
|
350
764
|
defineCall,
|
|
351
765
|
defineConfig,
|
|
352
766
|
defineConnection,
|
|
767
|
+
defineFacts,
|
|
768
|
+
defineForms,
|
|
769
|
+
defineGoals,
|
|
353
770
|
defineInboundRoute,
|
|
354
771
|
defineLayer,
|
|
355
772
|
defineMcp,
|
|
@@ -370,10 +787,12 @@ export {
|
|
|
370
787
|
discoverAgents,
|
|
371
788
|
findAgentFiles,
|
|
372
789
|
foundryDataEnvironmentPersistence,
|
|
790
|
+
foundryGuidanceSubject,
|
|
373
791
|
grantResolverLive,
|
|
374
792
|
install,
|
|
375
793
|
installRegistry,
|
|
376
794
|
installationKey,
|
|
795
|
+
installedApplicationTransmissionToolName,
|
|
377
796
|
internalAgentName,
|
|
378
797
|
isFoundryAgent,
|
|
379
798
|
isFoundryAgentDefinition,
|
|
@@ -384,6 +803,7 @@ export {
|
|
|
384
803
|
isFoundrySubscriber,
|
|
385
804
|
isFoundryTransmission,
|
|
386
805
|
isInboxCapableStore,
|
|
806
|
+
listFoundryApprovals,
|
|
387
807
|
memoryAccountDirectory,
|
|
388
808
|
memoryEventStore,
|
|
389
809
|
memoryTopologyStore,
|
|
@@ -392,11 +812,13 @@ export {
|
|
|
392
812
|
reconstructAgentInstance,
|
|
393
813
|
reconstructPlaybook,
|
|
394
814
|
reconstructPlaybookSubscription,
|
|
815
|
+
resolveFoundryApproval,
|
|
395
816
|
routeFromAgentFile,
|
|
396
817
|
routeFromInternalAgentName,
|
|
397
818
|
serializeInboundTransmissionXml,
|
|
398
819
|
toGloveMessage,
|
|
399
820
|
toGloveRequestInput,
|
|
400
821
|
transmissionPredicate,
|
|
822
|
+
withFoundryPermissions,
|
|
401
823
|
writeGeneratedTypes
|
|
402
824
|
};
|
package/docs/architecture.md
CHANGED
|
@@ -40,6 +40,8 @@ interface AgentInstance {
|
|
|
40
40
|
|
|
41
41
|
`FoundryDataAdapter` is the source of truth. Every run reloads the instance and conversation, then assembles a fresh Glove from the referenced definition and current message. Lazy `playbooks` and `schedules` resolvers reconcile desired runtime data before Foundry executes subsequent activations. `configureAgent` atomically replaces frontend-editable context, installations, and playbooks.
|
|
42
42
|
|
|
43
|
+
Foundry ships an in-memory adapter for tests and a dependency-free `FileFoundryDataAdapter` for durable single-host deployments. The file adapter coordinates local worker processes with an advisory lock and commits state through atomic rename. Multi-host deployments provide a database-backed implementation of the same interface so the application model does not change.
|
|
44
|
+
|
|
43
45
|
Definitions never accept an invocation input or output schema. Foundry owns those contracts.
|
|
44
46
|
|
|
45
47
|
## Reference normalization
|
|
@@ -86,6 +88,12 @@ An application owns one or many transmission definitions. A transmission may def
|
|
|
86
88
|
- inbound config/event schemas, authentication, normalization, classification, predicates, and serialization; and
|
|
87
89
|
- outbound config/input/output schemas and a delivery adapter.
|
|
88
90
|
|
|
91
|
+
An outbound contract may define `observe(input)` as a secret-safe projection for
|
|
92
|
+
retained telemetry. The validated delivery input itself crosses the worker boundary
|
|
93
|
+
through a private mode-0600 command record, not the observable event stream, and is
|
|
94
|
+
removed after the command settles. Without an explicit projection, Foundry records
|
|
95
|
+
only that the payload was redacted.
|
|
96
|
+
|
|
89
97
|
Applications are not installed by default. Their optional install hook is a headless factory that may contribute tools without receiving the Glove runtime or store. Foundry passes an operation-scoped account-session function when the user provided one; it never passes raw credentials.
|
|
90
98
|
|
|
91
99
|
## Inbound activation
|
|
@@ -132,12 +140,31 @@ A connection is colocated with its application and names the inbound transmissio
|
|
|
132
140
|
|
|
133
141
|
The supervisor derives desired connections from installed application + active playbook combinations. It starts, stops, retries, and observes them. Its implementation can use any backend; retry/worker topology is not exposed as an authoring primitive.
|
|
134
142
|
|
|
143
|
+
`receive()` is dispatch-oriented by default. A connection may set
|
|
144
|
+
`awaitCompletion: true` on an event when its transport requires an ordered session:
|
|
145
|
+
the Effect then remains open until all runs dispatched for that event are terminal,
|
|
146
|
+
or until the connection is aborted. This is an adapter-level ordering choice, not a
|
|
147
|
+
global runtime lock.
|
|
148
|
+
|
|
135
149
|
## Conversations and workspaces
|
|
136
150
|
|
|
137
151
|
Conversations are independent data records owned by an instance. Transmission conversations derive stable ids from route, instance, and external thread. Direct conversations can be created freely.
|
|
138
152
|
|
|
139
153
|
Workspace entries, inbox items, tasks, scoped environment values, and optional working-environment snapshots share the data adapter. They are suitable for document handles and coordination state across agents.
|
|
140
154
|
|
|
155
|
+
Adapters may implement `compareAndSetWorkspaceEntry(entry, expectedUpdatedAt)` for
|
|
156
|
+
optimistic atomic coordination. `null` requires absence; a successful replacement
|
|
157
|
+
must advance `updatedAt`. The memory and file adapters implement the same contract,
|
|
158
|
+
with file writes protected by the existing advisory lock. Consumers validate their
|
|
159
|
+
own value schema and retry a conflict against newly read state. Use this contract
|
|
160
|
+
consistently for every writer to a coordinated key; an unconditional
|
|
161
|
+
`putWorkspaceEntry` is not a compare-and-set. Workspace entries are shared
|
|
162
|
+
coordination data, not private memory or credentials.
|
|
163
|
+
|
|
164
|
+
Session loops and goal continuation are composable
|
|
165
|
+
agent policy over this data and normal scheduled activations. They do not introduce
|
|
166
|
+
a second scheduler, memory store, filesystem, or agent loop.
|
|
167
|
+
|
|
141
168
|
## Working environment and REPL boundary
|
|
142
169
|
|
|
143
170
|
Foundry owns contextual selection, mounting, lifecycle, and observability. The native packages retain their own semantics:
|
|
@@ -148,10 +175,27 @@ Foundry owns contextual selection, mounting, lifecycle, and observability. The n
|
|
|
148
175
|
|
|
149
176
|
An agent can resolve either surface from its current instance, conversation, workspace, or message. Foundry exposes the mounted environment and guarded VFS to execution contexts, snapshots before cleanup when a persistence adapter is present, and emits safe mount/save telemetry. It permits one REPL per assembled agent to prevent colliding execution and discovery tool names.
|
|
150
177
|
|
|
178
|
+
A REPL may declare `programmaticTools` as an explicit selector over the complete
|
|
179
|
+
live Glove tool registry. Foundry delays that REPL's public mount until calls,
|
|
180
|
+
instance installations, memory, mesh, and `configure` have finished, then
|
|
181
|
+
registers only the exact tool references returned by the selector. This makes
|
|
182
|
+
the primed function catalogue match the current message without turning every
|
|
183
|
+
agent tool into ambient authority. The projection owns a bounded call budget,
|
|
184
|
+
revalidates Zod inputs, propagates cancellation, and fails closed when a tool
|
|
185
|
+
input requires an interactive approval. Programmatic calls emit metadata-only
|
|
186
|
+
events; inputs, results, credentials, and interpreter state are not copied into
|
|
187
|
+
the trace.
|
|
188
|
+
|
|
151
189
|
The durable boundary is the VFS. Native REPL interpreter bindings are run-scoped and are not serialized by Foundry.
|
|
152
190
|
|
|
153
191
|
## Lazy assembly
|
|
154
192
|
|
|
193
|
+
Goals, facts, forms, and custom context providers are first-class lazy fields too.
|
|
194
|
+
Foundry mounts native runners with conversation-local scopes by default. Adapters
|
|
195
|
+
retain authoritative workflow state. A definition-provided goal program initializes
|
|
196
|
+
only absent state; later changes require an explicit native versioned revision.
|
|
197
|
+
See [guidance](./guidance.md) for preparation and persistence boundaries.
|
|
198
|
+
|
|
155
199
|
Every run resolves model, system prompt, tools, memory, inboxes, subscribers, layers, calls, playbooks, schedules, mesh, working environment, REPL, and custom build/run functions against `AgentAssemblyContext`. Resolvers can use both the instance and the current message, which makes message-dependent provisioning a first-class behavior rather than an environment switch.
|
|
156
200
|
|
|
157
201
|
Installable applications, shared tools, and MCPs are filtered by the current instance desired state. Memory belongs to the definition but can be selected lazily. Inboxes must always be lazy functions.
|
|
@@ -173,3 +217,10 @@ Foundry stores only account metadata and opaque access references. It does not:
|
|
|
173
217
|
- expose credential material to prompts or the inspector.
|
|
174
218
|
|
|
175
219
|
Applications define account-session and provider adapters. Playbooks and instance installation data select account references.
|
|
220
|
+
|
|
221
|
+
The HTTP control plane follows the same rule. A non-loopback listener is rejected
|
|
222
|
+
unless the application supplies a `requestAuthorization` adapter. The server passes
|
|
223
|
+
only method, path, query, selected credential headers, and peer address directly to
|
|
224
|
+
that Effect; it retains neither the credential nor the adapter's internal identity
|
|
225
|
+
state. Browser Basic auth, bearer tokens, session cookies, OIDC proxy assertions,
|
|
226
|
+
rotation, rate limits, and revocation therefore remain deployment-owned policies.
|