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/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-P65RT7H5.js";
46
+ } from "./chunk-25BGNRJN.js";
49
47
  import {
50
48
  FoundryClient,
51
49
  FoundryRunHandle,
52
50
  createFoundryClient
53
- } from "./chunk-3GCUECPA.js";
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
- } from "./chunk-CRWY7M66.js";
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
  };
@@ -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.