glove-foundry 0.3.2 → 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.
@@ -2,6 +2,7 @@ import {
2
2
  DEFAULT_FOUNDRY_CONFIG
3
3
  } from "./chunk-ZFNMFE3T.js";
4
4
  import {
5
+ EMPTY_AGENT_COMPOSITION,
5
6
  EMPTY_CAPABILITY_REGISTRY,
6
7
  EMPTY_FOUNDRY_APPLICATION,
7
8
  EMPTY_NATIVE_REGISTRY,
@@ -9,6 +10,8 @@ import {
9
10
  FOUNDRY_AGENT_FILE_ENV,
10
11
  FOUNDRY_AGENT_ROUTE_ENV,
11
12
  FOUNDRY_APPLICATION_ENV,
13
+ FOUNDRY_APPROVAL_DIRECTORY_ENV,
14
+ FOUNDRY_CORE_COMMAND_DIRECTORY_ENV,
12
15
  FOUNDRY_CORE_COMMAND_EVENT,
13
16
  FOUNDRY_EVENT_PREFIX,
14
17
  FOUNDRY_EXECUTION_MARKER,
@@ -19,6 +22,7 @@ import {
19
22
  FOUNDRY_SUBSCRIBER_BRAND,
20
23
  MemoryFoundryDataAdapter,
21
24
  bindFileIdentity,
25
+ claimFoundryCoreCommandRequest,
22
26
  compileAgentDefinition,
23
27
  createAgentInstance,
24
28
  createConversation,
@@ -26,16 +30,20 @@ import {
26
30
  definePlaybookSubscription,
27
31
  defineSharedTool,
28
32
  discoverAgents,
29
- fileDefinitionKey,
30
- fileDefinitionLabel,
33
+ freezeGloveMessage,
34
+ getFoundryCoreCommandRequest,
31
35
  id,
32
36
  installationKey,
37
+ listFoundryApprovals,
38
+ monitorFoundryCoreCommandRequest,
33
39
  normalizeAgentInstallations,
34
40
  reconstructPlaybook,
35
41
  reconstructPlaybookSubscription,
42
+ resolveFoundryApproval,
36
43
  routeFromInternalAgentName,
44
+ settleFoundryCoreCommandRequest,
37
45
  transmissionPredicate
38
- } from "./chunk-CRWY7M66.js";
46
+ } from "./chunk-5LDW2N2E.js";
39
47
 
40
48
  // src/codegen.ts
41
49
  import { mkdir, writeFile } from "node:fs/promises";
@@ -64,87 +72,6 @@ ${routes}
64
72
  return { routesFile, manifestFile };
65
73
  }
66
74
 
67
- // src/composition.ts
68
- function addUnique(values, value, kind) {
69
- const key = fileDefinitionKey(value);
70
- if (values.some((candidate) => fileDefinitionKey(candidate) === key)) {
71
- throw new Error(`Duplicate agent-local ${kind} "${fileDefinitionLabel(value)}".`);
72
- }
73
- values.push(value);
74
- }
75
- function composeAgent(...sources) {
76
- const capabilities = {
77
- tools: [...EMPTY_CAPABILITY_REGISTRY.tools],
78
- applications: [...EMPTY_CAPABILITY_REGISTRY.applications],
79
- mcp: [...EMPTY_CAPABILITY_REGISTRY.mcp],
80
- memory: [...EMPTY_CAPABILITY_REGISTRY.memory]
81
- };
82
- const native = {
83
- layers: [...EMPTY_NATIVE_REGISTRY.layers],
84
- subscribers: [...EMPTY_NATIVE_REGISTRY.subscribers]
85
- };
86
- const visit = (source) => {
87
- if (!source) return;
88
- if (typeof source === "function") {
89
- visit(source());
90
- return;
91
- }
92
- if (Array.isArray(source)) {
93
- for (const child of source) visit(child);
94
- return;
95
- }
96
- if ("capabilities" in source && "native" in source) {
97
- visit([
98
- ...source.capabilities.tools,
99
- ...source.capabilities.applications,
100
- ...source.capabilities.mcp,
101
- ...source.capabilities.memory,
102
- ...source.native.layers,
103
- ...source.native.subscribers
104
- ]);
105
- return;
106
- }
107
- const branded = source;
108
- if (branded[FOUNDRY_SHARED_TOOL_BRAND] === true) {
109
- addUnique(capabilities.tools, branded, "tool");
110
- } else if (branded[FOUNDRY_AGENT_APPLICATION_BRAND] === true) {
111
- addUnique(
112
- capabilities.applications,
113
- branded,
114
- "application"
115
- );
116
- } else if (branded[FOUNDRY_MCP_BRAND] === true) {
117
- addUnique(capabilities.mcp, branded, "MCP");
118
- } else if (branded[FOUNDRY_MEMORY_BRAND] === true) {
119
- addUnique(
120
- capabilities.memory,
121
- branded,
122
- "memory"
123
- );
124
- } else if (branded[FOUNDRY_LAYER_BRAND] === true) {
125
- addUnique(native.layers, branded, "layer");
126
- } else if (branded[FOUNDRY_SUBSCRIBER_BRAND] === true) {
127
- addUnique(native.subscribers, branded, "subscriber");
128
- } else {
129
- throw new Error("composeAgent received an unrecognized Foundry definition.");
130
- }
131
- };
132
- for (const source of sources) visit(source);
133
- return Object.freeze({
134
- capabilities: Object.freeze({
135
- tools: Object.freeze(capabilities.tools),
136
- applications: Object.freeze(capabilities.applications),
137
- mcp: Object.freeze(capabilities.mcp),
138
- memory: Object.freeze(capabilities.memory)
139
- }),
140
- native: Object.freeze({
141
- layers: Object.freeze(native.layers),
142
- subscribers: Object.freeze(native.subscribers)
143
- })
144
- });
145
- }
146
- var EMPTY_AGENT_COMPOSITION = composeAgent();
147
-
148
75
  // src/domain.ts
149
76
  import { Schema } from "effect";
150
77
  var Slug = Schema.String.pipe(
@@ -753,6 +680,7 @@ var MemoryObservabilityAdapter = class {
753
680
  }
754
681
  };
755
682
  function categoryForAgentEvent(type) {
683
+ if (type === "foundry.guidance.state") return "memory";
756
684
  if (type.startsWith("tool_")) return "tool";
757
685
  if (type.startsWith("foundry.installation")) return "application";
758
686
  if (type.startsWith("foundry.layer") || type.startsWith("foundry.subscriber")) {
@@ -771,6 +699,17 @@ function categoryForAgentEvent(type) {
771
699
  }
772
700
  return "system";
773
701
  }
702
+ function observableAgentEventData(type, data2) {
703
+ if (type !== "foundry.core.command" || !data2 || typeof data2 !== "object") return data2;
704
+ const command = data2;
705
+ if (command.type !== "transmit" || !("payload" in command)) return data2;
706
+ const { payload: _payload, observability, ...metadata } = command;
707
+ void _payload;
708
+ return {
709
+ ...metadata,
710
+ payload: observability ?? { redacted: true }
711
+ };
712
+ }
774
713
  function encodedAgentEvent(message) {
775
714
  const start = message.indexOf(FOUNDRY_EVENT_PREFIX);
776
715
  if (start < 0) return null;
@@ -874,7 +813,7 @@ var FoundryObserver = class {
874
813
  agent: this.route(run.signalName),
875
814
  runId: run.id,
876
815
  ...encoded.timestamp ? { timestamp: encoded.timestamp } : {},
877
- data: encoded.data
816
+ data: observableAgentEventData(encoded.type, encoded.data)
878
817
  });
879
818
  return;
880
819
  }
@@ -899,6 +838,10 @@ var FoundryObserver = class {
899
838
 
900
839
  // src/runtime.ts
901
840
  import { existsSync } from "node:fs";
841
+ import { mkdir as mkdir2, rm } from "node:fs/promises";
842
+ import { randomUUID as randomUUID2 } from "node:crypto";
843
+ import { tmpdir } from "node:os";
844
+ import { join } from "node:path";
902
845
  import { fileURLToPath } from "node:url";
903
846
  import {
904
847
  Effect as Effect5,
@@ -1023,6 +966,9 @@ var ApplicationConnectionSupervisor = class {
1023
966
  routeId: input.route.id,
1024
967
  eventId: input.eventId,
1025
968
  threadKey: input.threadKey,
969
+ ...input.conversationKey ? { conversationKey: input.conversationKey } : {},
970
+ ...input.conversationScope ? { conversationScope: input.conversationScope } : {},
971
+ ...input.awaitCompletion ? { awaitCompletion: true, signal } : {},
1026
972
  raw: input.raw
1027
973
  });
1028
974
  },
@@ -1174,7 +1120,7 @@ function assembleToolModule(route, module) {
1174
1120
  tool: { ...body, name }
1175
1121
  });
1176
1122
  }
1177
- function addUnique2(target, values, label) {
1123
+ function addUnique(target, values, label) {
1178
1124
  const ids = new Set(target.map((value) => value.id));
1179
1125
  for (const value of values) {
1180
1126
  if (ids.has(value.id)) {
@@ -1220,17 +1166,17 @@ async function discoverFoundryRegistry(options) {
1220
1166
  );
1221
1167
  }
1222
1168
  if (config.kind === "tool") {
1223
- addUnique2(registry.tools, [value], "shared tool");
1169
+ addUnique(registry.tools, [value], "shared tool");
1224
1170
  } else if (config.kind === "application") {
1225
- addUnique2(
1171
+ addUnique(
1226
1172
  registry.applications,
1227
1173
  [value],
1228
1174
  "agent application"
1229
1175
  );
1230
1176
  } else if (config.kind === "mcp") {
1231
- addUnique2(registry.mcp, [value], "MCP");
1177
+ addUnique(registry.mcp, [value], "MCP");
1232
1178
  } else if (config.kind === "memory") {
1233
- addUnique2(
1179
+ addUnique(
1234
1180
  registry.memory,
1235
1181
  [value],
1236
1182
  "memory profile"
@@ -1275,9 +1221,9 @@ async function discoverFoundryRegistry(options) {
1275
1221
  );
1276
1222
  }
1277
1223
  if (config.kind === "layer") {
1278
- addUnique2(native.layers, [value], "layer");
1224
+ addUnique(native.layers, [value], "layer");
1279
1225
  } else {
1280
- addUnique2(
1226
+ addUnique(
1281
1227
  native.subscribers,
1282
1228
  [value],
1283
1229
  "subscriber"
@@ -1366,6 +1312,17 @@ var FoundryRuntime = class _FoundryRuntime {
1366
1312
  services;
1367
1313
  runnerLoops = [];
1368
1314
  materializedActivations = /* @__PURE__ */ new Set();
1315
+ activationTimers = /* @__PURE__ */ new Map();
1316
+ approvalDirectory = join(
1317
+ tmpdir(),
1318
+ "glove-foundry-approvals",
1319
+ randomUUID2()
1320
+ );
1321
+ coreCommandDirectory = join(
1322
+ tmpdir(),
1323
+ "glove-foundry-core-commands",
1324
+ randomUUID2()
1325
+ );
1369
1326
  started = false;
1370
1327
  disposed = false;
1371
1328
  constructor(options) {
@@ -1444,7 +1401,27 @@ var FoundryRuntime = class _FoundryRuntime {
1444
1401
  );
1445
1402
  this.connectionSupervisor = new ApplicationConnectionSupervisor({
1446
1403
  receive: async (input) => {
1447
- await this.dispatchInbound(input);
1404
+ const runs = await this.dispatchInbound(input);
1405
+ if (!input.awaitCompletion || runs.length === 0) return;
1406
+ if (input.signal?.aborted) return;
1407
+ const completion = Promise.all(runs.map(
1408
+ (run) => this.waitForRun(run.id, { timeoutMs: Math.max(1e3, run.timeoutMs + 1e3) })
1409
+ ));
1410
+ await new Promise((resolveWait) => {
1411
+ let settled = false;
1412
+ const finish = () => {
1413
+ if (settled) return;
1414
+ settled = true;
1415
+ input.signal?.removeEventListener("abort", finish);
1416
+ resolveWait();
1417
+ };
1418
+ if (input.signal?.aborted) {
1419
+ finish();
1420
+ return;
1421
+ }
1422
+ input.signal?.addEventListener("abort", finish, { once: true });
1423
+ void completion.then(finish, finish);
1424
+ });
1448
1425
  },
1449
1426
  emit: (event) => {
1450
1427
  this.observability.append({
@@ -1474,6 +1451,8 @@ var FoundryRuntime = class _FoundryRuntime {
1474
1451
  if (discovered) {
1475
1452
  resolvedEnvironment[FOUNDRY_AGENT_FILE_ENV] = discovered.filePath;
1476
1453
  }
1454
+ resolvedEnvironment[FOUNDRY_APPROVAL_DIRECTORY_ENV] = this.approvalDirectory;
1455
+ resolvedEnvironment[FOUNDRY_CORE_COMMAND_DIRECTORY_ENV] = this.coreCommandDirectory;
1477
1456
  }
1478
1457
  if (this.applicationFilePath) {
1479
1458
  resolvedEnvironment[FOUNDRY_APPLICATION_ENV] = this.applicationFilePath;
@@ -1562,6 +1541,10 @@ var FoundryRuntime = class _FoundryRuntime {
1562
1541
  }
1563
1542
  this.started = true;
1564
1543
  try {
1544
+ await Promise.all([
1545
+ mkdir2(this.approvalDirectory, { recursive: true, mode: 448 }),
1546
+ mkdir2(this.coreCommandDirectory, { recursive: true, mode: 448 })
1547
+ ]);
1565
1548
  await this.seedTopology();
1566
1549
  for (const instance of await this.listAgentInstances()) {
1567
1550
  if (!this.byRoute.has(instance.definitionId)) {
@@ -1602,9 +1585,15 @@ var FoundryRuntime = class _FoundryRuntime {
1602
1585
  if (this.disposed) return;
1603
1586
  if (!this.started) {
1604
1587
  await this.services.dispose();
1588
+ await Promise.all([
1589
+ rm(this.approvalDirectory, { recursive: true, force: true }),
1590
+ rm(this.coreCommandDirectory, { recursive: true, force: true })
1591
+ ]);
1605
1592
  this.disposed = true;
1606
1593
  return;
1607
1594
  }
1595
+ for (const timer of this.activationTimers.values()) clearTimeout(timer);
1596
+ this.activationTimers.clear();
1608
1597
  await this.connectionSupervisor.stopAll();
1609
1598
  this.observability.append({ type: "runtime.stop.signals", category: "system", data: {} });
1610
1599
  await this.signalRunner.stop({ graceful: true, timeoutMs: 1e4 });
@@ -1614,6 +1603,10 @@ var FoundryRuntime = class _FoundryRuntime {
1614
1603
  await this.envStore.close();
1615
1604
  await this.scheduleAdapter.close?.();
1616
1605
  await this.services.dispose();
1606
+ await Promise.all([
1607
+ rm(this.approvalDirectory, { recursive: true, force: true }),
1608
+ rm(this.coreCommandDirectory, { recursive: true, force: true })
1609
+ ]);
1617
1610
  this.started = false;
1618
1611
  this.disposed = true;
1619
1612
  this.observability.append({ type: "runtime.stopped", category: "system", data: {} });
@@ -1639,6 +1632,10 @@ var FoundryRuntime = class _FoundryRuntime {
1639
1632
  if (request.workspaceId !== agent.workspaceId || request.workspaceId !== conversation.workspaceId) {
1640
1633
  throw new Error("The request, agent instance, and conversation must share a workspace.");
1641
1634
  }
1635
+ await Effect5.runPromise(this.data.putConversation(Object.freeze({
1636
+ ...conversation,
1637
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1638
+ })));
1642
1639
  const runId = await this.signalRunner.triggerSignal(
1643
1640
  discovered.executionName,
1644
1641
  await this.executionEnvelope(request)
@@ -1772,6 +1769,97 @@ var FoundryRuntime = class _FoundryRuntime {
1772
1769
  listConversations(agentId) {
1773
1770
  return Effect5.runPromise(this.data.listConversations(agentId));
1774
1771
  }
1772
+ async updateConversation(agentId, conversationId, options) {
1773
+ const agent = await Effect5.runPromise(this.data.getAgent(agentId));
1774
+ if (!agent) throw new Error(`Foundry agent instance "${agentId}" was not found.`);
1775
+ const current = await Effect5.runPromise(this.data.getConversation(conversationId));
1776
+ if (!current || current.agentId !== agent.id) {
1777
+ throw new Error(`Conversation "${conversationId}" does not belong to agent instance "${agent.id}".`);
1778
+ }
1779
+ const title = options.title?.trim();
1780
+ if (options.title !== void 0 && (!title || title.length > 120)) {
1781
+ throw new Error("A conversation title must contain 1 to 120 characters.");
1782
+ }
1783
+ const conversation = Object.freeze({
1784
+ ...current,
1785
+ ...title ? { title } : {},
1786
+ ...options.context ? { context: Object.freeze(structuredClone(options.context)) } : {},
1787
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1788
+ });
1789
+ await Effect5.runPromise(this.data.putConversation(conversation));
1790
+ this.observability.append({
1791
+ type: "conversation.updated",
1792
+ category: "agent",
1793
+ agent: agent.definitionId,
1794
+ data: { agentId, conversationId, titleChanged: options.title !== void 0 }
1795
+ });
1796
+ return conversation;
1797
+ }
1798
+ /**
1799
+ * Read the same StoreAdapter the child agent process uses. Foundry never
1800
+ * reconstructs chat history from runs or observability events: adapters stay
1801
+ * the single source of truth for durable conversations.
1802
+ */
1803
+ async conversationTranscript(agentId, conversationId, options = {}) {
1804
+ const agent = await Effect5.runPromise(this.data.getAgent(agentId));
1805
+ if (!agent) throw new Error(`Foundry agent instance "${agentId}" was not found.`);
1806
+ const conversation = await Effect5.runPromise(this.data.getConversation(conversationId));
1807
+ if (!conversation || conversation.agentId !== agent.id) {
1808
+ throw new Error(`Conversation "${conversationId}" does not belong to agent instance "${agent.id}".`);
1809
+ }
1810
+ if (conversation.workspaceId !== agent.workspaceId) {
1811
+ throw new Error("The agent instance and conversation must share a workspace.");
1812
+ }
1813
+ const discovered = this.byRoute.get(agent.definitionId);
1814
+ if (!discovered) {
1815
+ throw new Error(`Foundry agent definition "${agent.definitionId}" was not found.`);
1816
+ }
1817
+ const storeFactory = discovered.definition.store ?? this.application.conversationStore;
1818
+ const limit = Math.max(1, Math.min(500, Math.floor(options.limit ?? 100)));
1819
+ if (!storeFactory) {
1820
+ return Object.freeze({
1821
+ conversation,
1822
+ messages: Object.freeze([]),
1823
+ turnCount: 0,
1824
+ tokenCount: 0,
1825
+ page: Object.freeze({ offset: 0, limit, total: 0, hasEarlier: false, hasLater: false }),
1826
+ persisted: false
1827
+ });
1828
+ }
1829
+ const store = await storeFactory({
1830
+ definitionId: agent.definitionId,
1831
+ agentId: agent.id,
1832
+ conversationId: conversation.id,
1833
+ workspaceId: conversation.workspaceId
1834
+ });
1835
+ const [messages, turnCount, tokenCount, tokenConsumption] = await Promise.all([
1836
+ store.getMessages(),
1837
+ store.getTurnCount(),
1838
+ store.getTokenCount(),
1839
+ store.getTokenConsumption?.()
1840
+ ]);
1841
+ const total = messages.length;
1842
+ const offset = Math.max(0, Math.min(
1843
+ total,
1844
+ Math.floor(options.offset ?? Math.max(0, total - limit))
1845
+ ));
1846
+ const visibleMessages = messages.slice(offset, offset + limit);
1847
+ return Object.freeze({
1848
+ conversation,
1849
+ messages: Object.freeze(visibleMessages.map(freezeGloveMessage)),
1850
+ turnCount,
1851
+ tokenCount,
1852
+ ...tokenConsumption ? { tokenConsumption: Object.freeze({ ...tokenConsumption }) } : {},
1853
+ page: Object.freeze({
1854
+ offset,
1855
+ limit,
1856
+ total,
1857
+ hasEarlier: offset > 0,
1858
+ hasLater: offset + visibleMessages.length < total
1859
+ }),
1860
+ persisted: true
1861
+ });
1862
+ }
1775
1863
  listWorkspaceEntries(workspaceId) {
1776
1864
  return Effect5.runPromise(this.data.listWorkspaceEntries(workspaceId));
1777
1865
  }
@@ -1845,6 +1933,77 @@ var FoundryRuntime = class _FoundryRuntime {
1845
1933
  cancel(runId) {
1846
1934
  return this.signalRunner.cancel(runId);
1847
1935
  }
1936
+ /**
1937
+ * Change course at a run boundary.
1938
+ *
1939
+ * Foundry does not inject text into an arbitrary in-flight tool. If the
1940
+ * source is active, steering cooperatively cancels it first, waits for that
1941
+ * terminal boundary, then starts a replacement turn in the same durable
1942
+ * conversation with explicit lineage in the request context.
1943
+ */
1944
+ async steer(runId, message) {
1945
+ const source = await this.getRun(runId);
1946
+ if (!source) throw new Error(`Foundry run "${runId}" was not found.`);
1947
+ if (!source.agentId || !source.conversationId) {
1948
+ throw new Error(`Foundry run "${runId}" does not expose an agent conversation to steer.`);
1949
+ }
1950
+ const active = source.status === "pending" || source.status === "running";
1951
+ const interrupted = active ? await this.cancel(runId) : false;
1952
+ if (interrupted) {
1953
+ const terminal = await this.waitForRun(runId, {
1954
+ pollMs: 25,
1955
+ timeoutMs: Math.min(Math.max(source.timeoutMs, 1e3), 3e4)
1956
+ });
1957
+ if (!terminal || (terminal.status === "pending" || terminal.status === "running")) {
1958
+ throw new Error(`Foundry could not reach a safe steering boundary for run "${runId}".`);
1959
+ }
1960
+ }
1961
+ const run = await this.send(source.agentId, source.conversationId, message, {
1962
+ context: {
1963
+ protocol: "foundry-control",
1964
+ steeredFromRunId: runId,
1965
+ steeringMode: "interrupt-and-restart"
1966
+ }
1967
+ });
1968
+ this.observability.append({
1969
+ type: "run.steered",
1970
+ category: "system",
1971
+ agent: source.agent,
1972
+ runId: run.id,
1973
+ data: {
1974
+ fromRunId: runId,
1975
+ toRunId: run.id,
1976
+ agentId: source.agentId,
1977
+ conversationId: source.conversationId,
1978
+ interrupted,
1979
+ mode: "interrupt-and-restart"
1980
+ }
1981
+ });
1982
+ return { fromRunId: runId, interrupted, run };
1983
+ }
1984
+ listApprovals(filter = {}) {
1985
+ return listFoundryApprovals(this.approvalDirectory, filter);
1986
+ }
1987
+ async resolveApproval(approvalId, decision) {
1988
+ const approval = await resolveFoundryApproval(
1989
+ this.approvalDirectory,
1990
+ approvalId,
1991
+ decision
1992
+ );
1993
+ this.observability.append({
1994
+ type: "approval.resolved",
1995
+ category: "system",
1996
+ agent: approval.definitionId,
1997
+ runId: approval.runId,
1998
+ data: {
1999
+ approvalId: approval.id,
2000
+ toolName: approval.toolName,
2001
+ decision,
2002
+ status: approval.status
2003
+ }
2004
+ });
2005
+ return approval;
2006
+ }
1848
2007
  capabilityManifest(definitionId) {
1849
2008
  if (!this.compositionByDefinition.has(definitionId)) {
1850
2009
  throw new Error(`Foundry agent definition "${definitionId}" was not found.`);
@@ -2116,12 +2275,27 @@ var FoundryRuntime = class _FoundryRuntime {
2116
2275
  }
2117
2276
  }
2118
2277
  for (const { agent, playbooks: matched } of matchedByAgent.values()) {
2119
- const conversationId = `transmission:${route.id}:${agent.id}:${input.threadKey}`;
2120
- let conversation = await Effect5.runPromise(this.data.getConversation(conversationId));
2278
+ const conversationScope = input.conversationScope ?? "route";
2279
+ const conversationKey = input.conversationKey?.trim() || input.threadKey;
2280
+ const conversationId = conversationScope === "agent" ? `transmission:agent:${agent.id}:${encodeURIComponent(conversationKey)}` : `transmission:${route.id}:${agent.id}:${encodeURIComponent(conversationKey)}`;
2281
+ const keyedConversations = conversationScope === "agent" ? (await Effect5.runPromise(this.data.listConversations(agent.id))).filter(
2282
+ (candidate) => candidate.context.conversationKey === conversationKey
2283
+ ) : [];
2284
+ if (keyedConversations.length > 1) {
2285
+ throw new Error(
2286
+ `Agent "${agent.id}" has multiple conversations for key "${conversationKey}".`
2287
+ );
2288
+ }
2289
+ let conversation = keyedConversations[0] ?? await Effect5.runPromise(this.data.getConversation(conversationId));
2121
2290
  if (!conversation) {
2122
2291
  conversation = await this.createConversation(agent.id, {
2123
2292
  id: conversationId,
2124
- context: { routeId: route.id, threadKey: input.threadKey }
2293
+ context: {
2294
+ routeId: route.id,
2295
+ threadKey: input.threadKey,
2296
+ conversationKey,
2297
+ conversationScope
2298
+ }
2125
2299
  });
2126
2300
  }
2127
2301
  const serializationContext = {
@@ -2189,8 +2363,11 @@ var FoundryRuntime = class _FoundryRuntime {
2189
2363
  `Outbound route "${route.id}" does not belong to transmission "${input.transmissionId}".`
2190
2364
  );
2191
2365
  }
2366
+ const agent = await Effect5.runPromise(this.data.getAgent(input.agentId));
2367
+ if (!agent) {
2368
+ throw new Error(`Agent instance "${input.agentId}" does not exist.`);
2369
+ }
2192
2370
  if (input.applicationId) {
2193
- const agent = await Effect5.runPromise(this.data.getAgent(input.agentId));
2194
2371
  const application = agent ? this.compositionByDefinition.get(agent.definitionId)?.capabilities.applications.find(
2195
2372
  (candidate) => candidate.id === input.applicationId
2196
2373
  ) : void 0;
@@ -2219,10 +2396,22 @@ var FoundryRuntime = class _FoundryRuntime {
2219
2396
  return yield* (yield* AccountDirectory).get(route.accountId);
2220
2397
  })) : void 0;
2221
2398
  const payload = await Schema3.decodeUnknownPromise(transmission.outbound.input)(input.payload);
2399
+ const accountSessions = this.byRoute.get(agent.definitionId)?.definition.accountSessions;
2400
+ const sourceRun = account && accountSessions ? await this.getRun(input.runId) : null;
2222
2401
  const output = await Effect5.runPromise(transmission.outbound.adapter.deliver(payload, {
2223
2402
  route,
2224
2403
  ...account ? { account } : {},
2225
- grant
2404
+ grant,
2405
+ signal: input.signal ?? new AbortController().signal,
2406
+ ...account && accountSessions ? {
2407
+ withAccountSession: (operation, use) => accountSessions.withSession({
2408
+ accountId: account.id,
2409
+ operation,
2410
+ agentId: agent.id,
2411
+ conversationId: sourceRun?.conversationId ?? `egress:${input.runId}`,
2412
+ workspaceId: agent.workspaceId
2413
+ }, use)
2414
+ } : {}
2226
2415
  }));
2227
2416
  const validated = await Schema3.decodeUnknownPromise(transmission.outbound.output)(output);
2228
2417
  this.observability.append({
@@ -2251,8 +2440,8 @@ var FoundryRuntime = class _FoundryRuntime {
2251
2440
  environment,
2252
2441
  activations,
2253
2442
  agents: this.agents.length,
2254
- capabilities: this.registry.files.length,
2255
- surfaces: this.registry.nativeFiles.length
2443
+ capabilities: this.registry.capabilities.tools.length + this.registry.capabilities.applications.length + this.registry.capabilities.mcp.length + this.registry.capabilities.memory.length,
2444
+ surfaces: this.registry.native.layers.length + this.registry.native.subscribers.length
2256
2445
  };
2257
2446
  }
2258
2447
  async runsFromIds(runIds) {
@@ -2720,26 +2909,78 @@ var FoundryRuntime = class _FoundryRuntime {
2720
2909
  });
2721
2910
  }
2722
2911
  }
2723
- async enqueueCoreRequest(definitionId, request, runAt) {
2912
+ async enqueueCoreRequest(definitionId, request) {
2724
2913
  const discovered = this.byRoute.get(definitionId);
2725
2914
  if (!discovered) throw new Error(`Foundry agent definition "${definitionId}" was not found.`);
2726
2915
  const agent = await Effect5.runPromise(this.data.getAgent(request.agentId));
2727
2916
  const conversation = await Effect5.runPromise(this.data.getConversation(request.conversationId));
2728
2917
  if (!agent || !conversation) throw new Error("Core command references an unknown agent instance or conversation.");
2729
- const runId = await this.signalRunner.triggerSignal(
2918
+ return this.signalRunner.triggerSignal(
2730
2919
  discovered.executionName,
2731
2920
  await this.executionEnvelope(request)
2732
2921
  );
2733
- if (runAt) {
2734
- const date = new Date(runAt);
2735
- if (Number.isNaN(date.getTime())) throw new Error(`Invalid future run date "${runAt}".`);
2736
- await this.signalRunner.getAdapter().updateRun(runId, { nextRunAt: date });
2737
- }
2738
- return runId;
2739
2922
  }
2740
2923
  async executeCoreCommand(command, parentRunId) {
2741
2924
  if (command.type === "transmit") {
2742
- await this.deliverOutbound(command, parentRunId);
2925
+ const request2 = await getFoundryCoreCommandRequest(
2926
+ this.coreCommandDirectory,
2927
+ command.id
2928
+ );
2929
+ if (!request2) {
2930
+ if (command.payload && typeof command.payload === "object" && command.payload.privateRequest === true) {
2931
+ throw new Error("Foundry transmission private request is missing or invalid.");
2932
+ }
2933
+ await this.deliverOutbound(command, parentRunId);
2934
+ return;
2935
+ }
2936
+ const executable = request2.command ?? command;
2937
+ const claimed = await claimFoundryCoreCommandRequest(
2938
+ this.coreCommandDirectory,
2939
+ command.id
2940
+ );
2941
+ if (!claimed) return;
2942
+ const delivery = new AbortController();
2943
+ const monitoring = new AbortController();
2944
+ const monitor = monitorFoundryCoreCommandRequest(
2945
+ this.coreCommandDirectory,
2946
+ request2,
2947
+ delivery,
2948
+ monitoring.signal
2949
+ );
2950
+ try {
2951
+ if (request2.runId !== parentRunId) {
2952
+ throw new Error("Foundry transmission call does not belong to this run.");
2953
+ }
2954
+ if (executable.id !== command.id || executable.definitionId !== command.definitionId || executable.agentId !== command.agentId || executable.conversationId !== command.conversationId || executable.workspaceId !== command.workspaceId || executable.routeId !== command.routeId || executable.applicationId !== command.applicationId || executable.transmissionId !== command.transmissionId) {
2955
+ throw new Error("Foundry transmission command metadata does not match its private request.");
2956
+ }
2957
+ if (Date.parse(request2.expiresAt) <= Date.now()) {
2958
+ throw new Error("Foundry transmission call expired before delivery.");
2959
+ }
2960
+ const output = await this.deliverOutbound(
2961
+ executable,
2962
+ parentRunId,
2963
+ delivery.signal
2964
+ );
2965
+ await settleFoundryCoreCommandRequest(
2966
+ this.coreCommandDirectory,
2967
+ command.id,
2968
+ { status: "success", output }
2969
+ );
2970
+ } catch (cause) {
2971
+ await settleFoundryCoreCommandRequest(
2972
+ this.coreCommandDirectory,
2973
+ command.id,
2974
+ {
2975
+ status: "error",
2976
+ error: cause instanceof Error ? cause.message : String(cause)
2977
+ }
2978
+ );
2979
+ throw cause;
2980
+ } finally {
2981
+ monitoring.abort();
2982
+ await monitor;
2983
+ }
2743
2984
  return;
2744
2985
  }
2745
2986
  if (command.type === "playbook.sync") {
@@ -2754,6 +2995,14 @@ var FoundryRuntime = class _FoundryRuntime {
2754
2995
  await this.updateScheduledActivation(command, parentRunId);
2755
2996
  return;
2756
2997
  }
2998
+ if (command.type === "schedule.pause") {
2999
+ await this.pauseScheduledActivation(command, parentRunId);
3000
+ return;
3001
+ }
3002
+ if (command.type === "schedule.resume") {
3003
+ await this.resumeScheduledActivation(command, parentRunId);
3004
+ return;
3005
+ }
2757
3006
  if (command.type === "schedule.cancel") {
2758
3007
  await this.cancelScheduledActivation(command, parentRunId);
2759
3008
  return;
@@ -2809,8 +3058,7 @@ var FoundryRuntime = class _FoundryRuntime {
2809
3058
  }
2810
3059
  const runId = await this.enqueueCoreRequest(
2811
3060
  command.definitionId,
2812
- request,
2813
- void 0
3061
+ request
2814
3062
  );
2815
3063
  this.observability.append({
2816
3064
  type: "core.command.accepted",
@@ -2878,6 +3126,11 @@ var FoundryRuntime = class _FoundryRuntime {
2878
3126
  return `activation/${activationId.replaceAll("_", "-")}`;
2879
3127
  }
2880
3128
  async disarmActivation(activation) {
3129
+ const timer = this.activationTimers.get(activation.id);
3130
+ if (timer) {
3131
+ clearTimeout(timer);
3132
+ this.activationTimers.delete(activation.id);
3133
+ }
2881
3134
  if (activation.timing.kind === "every" || activation.timing.kind === "cron") {
2882
3135
  await this.scheduleAdapter.delete(this.backendScheduleId(activation.id));
2883
3136
  } else if (activation.lastRunId) {
@@ -2946,13 +3199,13 @@ var FoundryRuntime = class _FoundryRuntime {
2946
3199
  const updated = {
2947
3200
  ...existing,
2948
3201
  ...command.patch,
2949
- status: "pending",
3202
+ status: existing.status === "paused" ? "paused" : "pending",
2950
3203
  createdByRunId: parentRunId,
2951
3204
  lastRunId: void 0,
2952
3205
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2953
3206
  };
2954
3207
  await Effect5.runPromise(this.data.putActivation(updated));
2955
- await this.materializeActivation(updated);
3208
+ if (updated.status !== "paused") await this.materializeActivation(updated);
2956
3209
  this.observability.append({
2957
3210
  type: "scheduled-action.updated",
2958
3211
  category: "activation",
@@ -2961,6 +3214,53 @@ var FoundryRuntime = class _FoundryRuntime {
2961
3214
  data: { commandId: command.id, activationId: updated.id }
2962
3215
  });
2963
3216
  }
3217
+ async pauseScheduledActivation(command, parentRunId) {
3218
+ const existing = await Effect5.runPromise(this.data.getActivation(command.activationId));
3219
+ if (!existing) throw new Error(`Scheduled activation "${command.activationId}" was not found.`);
3220
+ this.assertActivationOwnership(existing, command);
3221
+ if (existing.status === "completed" || existing.status === "cancelled") {
3222
+ throw new Error(`Scheduled activation "${command.activationId}" cannot be paused from status "${existing.status}".`);
3223
+ }
3224
+ if (existing.status !== "paused") {
3225
+ await this.disarmActivation(existing);
3226
+ await Effect5.runPromise(this.data.putActivation({
3227
+ ...existing,
3228
+ status: "paused",
3229
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3230
+ }));
3231
+ }
3232
+ this.observability.append({
3233
+ type: "scheduled-action.paused",
3234
+ category: "activation",
3235
+ agent: command.definitionId,
3236
+ runId: parentRunId,
3237
+ data: { commandId: command.id, activationId: existing.id }
3238
+ });
3239
+ }
3240
+ async resumeScheduledActivation(command, parentRunId) {
3241
+ const existing = await Effect5.runPromise(this.data.getActivation(command.activationId));
3242
+ if (!existing) throw new Error(`Scheduled activation "${command.activationId}" was not found.`);
3243
+ this.assertActivationOwnership(existing, command);
3244
+ if (existing.status !== "paused") {
3245
+ throw new Error(`Scheduled activation "${command.activationId}" is not paused.`);
3246
+ }
3247
+ const resumed = {
3248
+ ...existing,
3249
+ status: "pending",
3250
+ createdByRunId: parentRunId,
3251
+ lastRunId: void 0,
3252
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3253
+ };
3254
+ await Effect5.runPromise(this.data.putActivation(resumed));
3255
+ await this.materializeActivation(resumed);
3256
+ this.observability.append({
3257
+ type: "scheduled-action.resumed",
3258
+ category: "activation",
3259
+ agent: command.definitionId,
3260
+ runId: parentRunId,
3261
+ data: { commandId: command.id, activationId: existing.id }
3262
+ });
3263
+ }
2964
3264
  async cancelScheduledActivation(command, parentRunId) {
2965
3265
  const existing = await Effect5.runPromise(this.data.getActivation(command.activationId));
2966
3266
  if (!existing) throw new Error(`Scheduled activation "${command.activationId}" was not found.`);
@@ -2981,7 +3281,7 @@ var FoundryRuntime = class _FoundryRuntime {
2981
3281
  }
2982
3282
  async reconstructActivations() {
2983
3283
  for (const activation of await Effect5.runPromise(this.data.listActivations())) {
2984
- if (activation.status === "completed" || activation.status === "cancelled") continue;
3284
+ if (activation.status === "paused" || activation.status === "completed" || activation.status === "cancelled") continue;
2985
3285
  await this.materializeActivation(activation);
2986
3286
  }
2987
3287
  }
@@ -3055,18 +3355,37 @@ var FoundryRuntime = class _FoundryRuntime {
3055
3355
  });
3056
3356
  return;
3057
3357
  }
3058
- const runId = await this.enqueueCoreRequest(
3059
- activation.definitionId,
3060
- request,
3061
- activation.timing.at
3062
- );
3063
- const active = {
3064
- ...activation,
3358
+ await this.armOneShotActivation(activation, request);
3359
+ } catch (cause) {
3360
+ this.materializedActivations.delete(activation.id);
3361
+ throw cause;
3362
+ }
3363
+ }
3364
+ async armOneShotActivation(activation, request) {
3365
+ if (activation.timing.kind !== "at") {
3366
+ throw new Error(`Activation "${activation.id}" is not a one-shot activation.`);
3367
+ }
3368
+ const target = new Date(activation.timing.at).getTime();
3369
+ const active = {
3370
+ ...activation,
3371
+ status: "active",
3372
+ lastRunId: void 0,
3373
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3374
+ };
3375
+ await Effect5.runPromise(this.data.putActivation(active));
3376
+ const dispatch = async () => {
3377
+ this.activationTimers.delete(activation.id);
3378
+ if (this.disposed || !this.started) return;
3379
+ const current = await Effect5.runPromise(this.data.getActivation(activation.id));
3380
+ if (!current || current.status === "paused" || current.status === "cancelled" || current.status === "completed") return;
3381
+ const runId = await this.enqueueCoreRequest(activation.definitionId, request);
3382
+ const running = {
3383
+ ...current,
3065
3384
  status: "active",
3066
3385
  lastRunId: runId,
3067
3386
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3068
3387
  };
3069
- await Effect5.runPromise(this.data.putActivation(active));
3388
+ await Effect5.runPromise(this.data.putActivation(running));
3070
3389
  this.observability.append({
3071
3390
  type: "core.command.accepted",
3072
3391
  category: "system",
@@ -3076,8 +3395,10 @@ var FoundryRuntime = class _FoundryRuntime {
3076
3395
  });
3077
3396
  void this.waitForRun(runId).then(async (run) => {
3078
3397
  if (!run || run.status !== "completed" && run.status !== "failed" && run.status !== "cancelled") return;
3398
+ const latest = await Effect5.runPromise(this.data.getActivation(activation.id));
3399
+ if (!latest || latest.status !== "active" || latest.lastRunId !== runId) return;
3079
3400
  await Effect5.runPromise(this.data.putActivation({
3080
- ...active,
3401
+ ...latest,
3081
3402
  status: run.status === "cancelled" ? "cancelled" : "completed",
3082
3403
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3083
3404
  }));
@@ -3090,20 +3411,45 @@ var FoundryRuntime = class _FoundryRuntime {
3090
3411
  data: { error: cause instanceof Error ? cause.message : String(cause) }
3091
3412
  });
3092
3413
  });
3093
- } catch (cause) {
3094
- this.materializedActivations.delete(activation.id);
3095
- throw cause;
3096
- }
3414
+ };
3415
+ const arm = () => {
3416
+ const remaining = target - Date.now();
3417
+ if (remaining <= 0) {
3418
+ void dispatch().catch((cause) => {
3419
+ this.materializedActivations.delete(activation.id);
3420
+ this.observability.append({
3421
+ type: "activation.dispatch.error",
3422
+ category: "activation",
3423
+ agent: activation.definitionId,
3424
+ runId: activation.createdByRunId,
3425
+ data: { error: cause instanceof Error ? cause.message : String(cause) }
3426
+ });
3427
+ });
3428
+ return;
3429
+ }
3430
+ const timer = setTimeout(arm, Math.min(remaining, 2147483647));
3431
+ this.activationTimers.set(activation.id, timer);
3432
+ };
3433
+ this.observability.append({
3434
+ type: "scheduled-action.created",
3435
+ category: "activation",
3436
+ agent: activation.definitionId,
3437
+ runId: activation.createdByRunId,
3438
+ data: { commandId: activation.id, recurring: false, nextRunAt: activation.timing.at }
3439
+ });
3440
+ if (target <= Date.now()) await dispatch();
3441
+ else arm();
3097
3442
  }
3098
- async deliverOutbound(command, parentRunId) {
3099
- await this.dispatchOutbound({
3443
+ async deliverOutbound(command, parentRunId, signal) {
3444
+ return this.dispatchOutbound({
3100
3445
  routeId: command.routeId,
3101
3446
  agentId: command.agentId,
3102
3447
  runId: parentRunId,
3103
3448
  payload: command.payload,
3104
3449
  commandId: command.id,
3105
3450
  ...command.applicationId ? { applicationId: command.applicationId } : {},
3106
- ...command.transmissionId ? { transmissionId: command.transmissionId } : {}
3451
+ ...command.transmissionId ? { transmissionId: command.transmissionId } : {},
3452
+ ...signal ? { signal } : {}
3107
3453
  });
3108
3454
  }
3109
3455
  async executionEnvelope(request) {
@@ -3142,18 +3488,24 @@ var FoundryRuntime = class _FoundryRuntime {
3142
3488
  import {
3143
3489
  createServer
3144
3490
  } from "node:http";
3145
- import { Schema as Schema4 } from "effect";
3491
+ import { createHash, randomUUID as randomUUID3 } from "node:crypto";
3492
+ import { Effect as Effect6, Schema as Schema4 } from "effect";
3146
3493
 
3147
3494
  // src/dashboard-icons.ts
3148
3495
  var PHOSPHOR_ICON_PATHS = {
3149
3496
  overview: "M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-96,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,200,136Zm0,64H152V152h48v48Z",
3150
3497
  agent: "M200,48H136V16a8,8,0,0,0-16,0V48H56A32,32,0,0,0,24,80V192a32,32,0,0,0,32,32H200a32,32,0,0,0,32-32V80A32,32,0,0,0,200,48Zm16,144a16,16,0,0,1-16,16H56a16,16,0,0,1-16-16V80A16,16,0,0,1,56,64H200a16,16,0,0,1,16,16Zm-52-56H92a28,28,0,0,0,0,56h72a28,28,0,0,0,0-56Zm-24,16v24H116V152ZM80,164a12,12,0,0,1,12-12h8v24H92A12,12,0,0,1,80,164Zm84,12h-8V152h8a12,12,0,0,1,0,24ZM72,108a12,12,0,1,1,12,12A12,12,0,0,1,72,108Zm88,0a12,12,0,1,1,12,12A12,12,0,0,1,160,108Z",
3151
3498
  runs: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm48.24-94.78-64-40A8,8,0,0,0,100,88v80a8,8,0,0,0,12.24,6.78l64-40a8,8,0,0,0,0-13.56ZM116,153.57V102.43L156.91,128Z",
3499
+ chat: "M232,128a104,104,0,0,1-112.44,103.67,103.9,103.9,0,0,1-45.09-16.3L40.31,227.76a16,16,0,0,1-20.49-20.49l12.39-34.16A104,104,0,1,1,232,128ZM128,40a88,88,0,0,0-80.34,124,8,8,0,0,1,.26,6.21L34.87,206.13l35.92-13a8,8,0,0,1,6.21.26A88,88,0,1,0,128,40Zm-44,88a12,12,0,1,1-12-12A12,12,0,0,1,84,128Zm56,0a12,12,0,1,1-12-12A12,12,0,0,1,140,128Zm56,0a12,12,0,1,1-12-12A12,12,0,0,1,196,128Z",
3152
3500
  automations: "M232,136.66A104.12,104.12,0,1,1,119.34,24,8,8,0,0,1,120.66,40,88.12,88.12,0,1,0,216,135.34,8,8,0,0,1,232,136.66ZM120,72v56a8,8,0,0,0,8,8h56a8,8,0,0,0,0-16H136V72a8,8,0,0,0-16,0Zm40-24a12,12,0,1,0-12-12A12,12,0,0,0,160,48Zm36,24a12,12,0,1,0-12-12A12,12,0,0,0,196,72Zm24,36a12,12,0,1,0-12-12A12,12,0,0,0,220,108Z",
3153
3501
  integrations: "M237.66,18.34a8,8,0,0,0-11.32,0l-52.4,52.41-5.37-5.38a32.05,32.05,0,0,0-45.26,0L100,88.69l-6.34-6.35A8,8,0,0,0,82.34,93.66L88.69,100,65.37,123.31a32,32,0,0,0,0,45.26l5.38,5.37-52.41,52.4a8,8,0,0,0,11.32,11.32l52.4-52.41,5.37,5.38a32,32,0,0,0,45.26,0L156,167.31l6.34,6.35a8,8,0,0,0,11.32-11.32L167.31,156l23.32-23.31a32,32,0,0,0,0-45.26l-5.38-5.37,52.41-52.4A8,8,0,0,0,237.66,18.34Zm-116.29,161a16,16,0,0,1-22.62,0L76.69,157.25a16,16,0,0,1,0-22.62L100,111.31,144.69,156Zm57.94-57.94L156,144.69,111.31,100l23.32-23.31a16,16,0,0,1,22.62,0l22.06,22A16,16,0,0,1,179.31,121.37ZM88.57,35A8,8,0,0,1,103.43,29l8,20A8,8,0,0,1,96.57,55ZM24.57,93A8,8,0,0,1,35,88.57l20,8A8,8,0,0,1,49,111.43l-20-8A8,8,0,0,1,24.57,93ZM231.43,163a8,8,0,0,1-10.4,4.46l-20-8A8,8,0,1,1,207,144.57l20,8A8,8,0,0,1,231.43,163Zm-64,58.06A8,8,0,0,1,152.57,227l-8-20A8,8,0,0,1,159.43,201Z",
3154
3502
  workspaces: "M224,64H154.67L126.93,43.2a16.12,16.12,0,0,0-9.6-3.2H72A16,16,0,0,0,56,56V72H40A16,16,0,0,0,24,88V200a16,16,0,0,0,16,16H192.89A15.13,15.13,0,0,0,208,200.89V184h16.89A15.13,15.13,0,0,0,240,168.89V80A16,16,0,0,0,224,64ZM192,200H40V88H85.33l29.87,22.4A8,8,0,0,0,120,112h72Zm32-32H208V112a16,16,0,0,0-16-16H122.67L94.93,75.2a16.12,16.12,0,0,0-9.6-3.2H72V56h45.33L147.2,78.4A8,8,0,0,0,152,80h72Z",
3155
3503
  menu: "M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM40,72H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16ZM216,184H40a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Z",
3156
3504
  plus: "M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z",
3505
+ paperclip: "M209.66,122.34l-82.75,82.75a52,52,0,0,1-73.54-73.54l89.76-89.75a36,36,0,0,1,50.91,50.91L104.28,182.46a20,20,0,0,1-28.28-28.28l83.09-83.09a8,8,0,0,1,11.31,11.32L87.31,165.49a4,4,0,0,0,5.66,5.66l89.76-89.75a20,20,0,0,0-28.29-28.29L64.69,142.86a36,36,0,0,0,50.91,50.91L198.34,111a8,8,0,0,1,11.32,11.31Z",
3506
+ send: "M223.69,42.18l-72,176A16,16,0,0,1,137,228h-.94a16,16,0,0,1-14.41-11.5l-20.6-63.4-63.4-20.6A16,16,0,0,1,36,102.31l176-72a16,16,0,0,1,11.69,11.87ZM52.85,116.9l55.85,18.15L195.5,48.25Zm82.1,86.4,48.8-119.3-66.8,66.8Z",
3507
+ stop: "M200,32H56A24,24,0,0,0,32,56V200a24,24,0,0,0,24,24H200a24,24,0,0,0,24-24V56A24,24,0,0,0,200,32Zm8,168a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H200a8,8,0,0,1,8,8Z",
3508
+ redirect: "M229.66,109.66l-48,48a8,8,0,0,1-11.32-11.32L204.69,112H104a64,64,0,0,0-64,64,8,8,0,0,1-16,0,80,80,0,0,1,80-80H204.69l-34.35-34.34a8,8,0,0,1,11.32-11.32l48,48A8,8,0,0,1,229.66,109.66Z",
3157
3509
  close: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z",
3158
3510
  caretRight: "M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z",
3159
3511
  search: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
@@ -3181,7 +3533,7 @@ function renderGloveMark() {
3181
3533
  var ICON_PATHS_JSON = JSON.stringify(PHOSPHOR_ICON_PATHS);
3182
3534
  var DASHBOARD_SCRIPT = String.raw`
3183
3535
  const ICON_PATHS=${ICON_PATHS_JSON};
3184
- const state={manifest:null,instances:[],subscriptions:[],connections:[],runs:[],events:[],health:null,transmissions:[],accounts:[],routes:[],bindings:[],activations:[],conversations:{},workspaces:{},showAllEvents:false,eventCategory:"all",ready:false,cursor:-1,liveAt:null};
3536
+ const state={manifest:null,instances:[],subscriptions:[],connections:[],runs:[],events:[],health:null,transmissions:[],accounts:[],routes:[],bindings:[],activations:[],approvals:[],conversations:{},workspaces:{},chat:{agentId:"",conversationId:"",transcript:null,query:"",draft:"",files:[],sending:false,error:"",optimistic:null,historyOffset:null,renderedConversationId:"",scrollToBottom:false,scrollToTop:false},showAllEvents:false,eventCategory:"all",ready:false,cursor:-1,liveAt:null};
3185
3537
  const $=id=>document.getElementById(id);
3186
3538
  const esc=value=>String(value??"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));
3187
3539
  const attr=value=>esc(value).replace(/\x60/g,"&#96;");
@@ -3189,7 +3541,8 @@ const fmtDate=iso=>iso?new Date(iso).toLocaleString([],{month:"short",day:"numer
3189
3541
  const fmtTime=iso=>iso?new Date(iso).toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"}):"—";
3190
3542
  const short=value=>{const text=String(value??"");return text.length>22?text.slice(0,10)+"…"+text.slice(-7):text};
3191
3543
  const jsonText=value=>JSON.stringify(value,null,2);
3192
- const count=(n,word)=>n+" "+word+(n===1?"":"s");
3544
+ const count=(n,word,plural=word+"s")=>n+" "+(n===1?word:plural);
3545
+ const fmtBytes=value=>{const n=Number(value)||0;if(n<1024)return n+" B";if(n<1048576)return(n/1024).toFixed(1)+" KB";return(n/1048576).toFixed(1)+" MB"};
3193
3546
  const icon=(name,className)=>'<svg class="'+(className||"icon")+'" data-phosphor="'+name+'" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true"><path d="'+ICON_PATHS[name]+'"/></svg>';
3194
3547
 
3195
3548
  /* ---- Elapsed time. Operators read runs in "how long ago" and "how long did it take". ---- */
@@ -3258,12 +3611,14 @@ async function copyValue(button){
3258
3611
  actively using -- scroll, focus, caret, expanded rows -- survive it. ---- */
3259
3612
  function captureView(){
3260
3613
  const active=document.activeElement;
3614
+ const chatScroll=$("message-scroll");
3261
3615
  return{
3262
3616
  scroll:window.scrollY,
3263
3617
  focus:active&&active.id?active.id:null,
3264
3618
  caret:active&&typeof active.selectionStart==="number"?active.selectionStart:null,
3265
3619
  open:[...document.querySelectorAll("[data-event-key].open")].map(node=>node.dataset.eventKey),
3266
- details:[...document.querySelectorAll("details[data-persist]")].filter(node=>node.open).map(node=>node.dataset.persist)
3620
+ details:[...document.querySelectorAll("details[data-persist]")].filter(node=>node.open).map(node=>node.dataset.persist),
3621
+ chatScroll:chatScroll?{top:chatScroll.scrollTop,nearBottom:chatScroll.scrollHeight-chatScroll.scrollTop-chatScroll.clientHeight<80}:null
3267
3622
  };
3268
3623
  }
3269
3624
  function restoreView(view){
@@ -3278,6 +3633,7 @@ function restoreView(view){
3278
3633
  }
3279
3634
  }
3280
3635
  if(view.scroll)window.scrollTo(0,view.scroll);
3636
+ if(view.chatScroll){const scroll=$("message-scroll");if(scroll)scroll.scrollTop=view.chatScroll.nearBottom?scroll.scrollHeight:view.chatScroll.top}
3281
3637
  }
3282
3638
  function skeleton(rows){
3283
3639
  let html="";for(let index=0;index<(rows||5);index++)html+='<div class="skeleton-row"><span style="width:'+(46+index*7%34)+'%"></span><span style="width:'+(16+index*5%14)+'%"></span></div>';
@@ -3302,6 +3658,12 @@ function dataLinks(){
3302
3658
  document.querySelectorAll("[data-link]").forEach(node=>node.onclick=event=>{if(event.metaKey||event.ctrlKey||event.shiftKey||event.altKey)return;event.preventDefault();go(node.getAttribute("href"))});
3303
3659
  document.querySelectorAll("[data-go]").forEach(node=>node.onclick=event=>{if(event.target.closest("a,button"))return;go(node.dataset.go)});
3304
3660
  document.querySelectorAll("[data-copy]").forEach(node=>node.onclick=event=>{event.preventDefault();event.stopPropagation();void copyValue(node)});
3661
+ document.querySelectorAll("[data-approval-decision]").forEach(node=>node.onclick=event=>{
3662
+ event.preventDefault();event.stopPropagation();
3663
+ const row=node.closest("[data-approval]");
3664
+ row?.querySelectorAll("button").forEach(button=>button.disabled=true);
3665
+ void resolveApproval(node.dataset.approvalId,node.dataset.approvalDecision);
3666
+ });
3305
3667
  syncCursor();
3306
3668
  }
3307
3669
  /* ---- Row cursor. j/k walk a list and Enter opens it, without reaching for the mouse. ---- */
@@ -3332,12 +3694,20 @@ function definition(id){return state.manifest?.agents?.agents?.find(x=>x.id===id
3332
3694
  function eventsFor(runId){return state.events.filter(event=>event.runId===runId)}
3333
3695
  function isKeyRunEvent(event){
3334
3696
  const type=event.type;
3335
- if(/^run\./.test(type)||/^scheduled-action\./.test(type))return true;
3697
+ if(/(?:^|\.)foundry\.guidance\.state$/.test(type))return true;
3698
+ if(/^run\./.test(type)||/^scheduled-action\./.test(type)||/^(?:agent\.)?foundry\.approval\./.test(type)||/^approval\./.test(type))return true;
3336
3699
  return /(?:playbooks\.composed|definition\.schedules\.loaded|application\.transmission-tools\.mounted|working-environment\.(?:mounted|snapshot\.saved)|repl\.mounted|subscriber\.mounted|layer\.mounted|installation\.(?:started|completed|failed)|definition\.memory\.mounted|definition\.inboxes\.loaded|tool_use(?:_result)?|model_response_complete)$/.test(type);
3337
3700
  }
3338
3701
  function activeRuns(){return state.runs.filter(run=>run.status==="pending"||run.status==="running")}
3339
3702
  function failureRuns(){return state.runs.filter(run=>run.status==="failed")}
3340
- function recent(array){return[...array].sort((a,b)=>String(b.createdAt||b.timestamp||b.updatedAt).localeCompare(String(a.createdAt||a.timestamp||a.updatedAt)))}
3703
+ function pendingApprovals(runId){return state.approvals.filter(item=>item.status==="pending"&&(!runId||item.runId===runId))}
3704
+ function recent(array){return[...array].sort((a,b)=>String(b.updatedAt||b.createdAt||b.timestamp).localeCompare(String(a.updatedAt||a.createdAt||a.timestamp)))}
3705
+ function approvalRail(approvals,heading){
3706
+ if(!approvals.length)return"";
3707
+ return'<section class="approval-rail" aria-label="Effect approvals"><header><div><span class="approval-kicker">Decision required</span><h2>'+esc(heading||"An agent is waiting for you")+'</h2><p>Review the exact tool input. Nothing runs unless you approve it.</p></div><span class="approval-count">'+count(approvals.length,"request")+'</span></header><div class="approval-list">'
3708
+ +approvals.map(item=>'<article class="approval-item" data-approval="'+attr(item.id)+'"><div class="approval-context"><span class="approval-tool">'+esc(item.toolName)+'</span><a class="approval-run" href="/runs/'+attr(item.runId)+'" data-link>Run '+esc(short(item.runId))+'</a></div><pre class="approval-input">'+esc(jsonText(item.toolInput))+'</pre><div class="approval-foot"><span>Requested '+relCell(item.requestedAt)+' · expires '+relCell(item.expiresAt)+'</span><div class="approval-actions"><button class="button" type="button" data-approval-id="'+attr(item.id)+'" data-approval-decision="deny">Deny</button><button class="button approve" type="button" data-approval-id="'+attr(item.id)+'" data-approval-decision="approve">Approve this input</button></div></div></article>').join("")
3709
+ +'</div></section>';
3710
+ }
3341
3711
  function explainRun(run){
3342
3712
  if(run.status==="failed")return run.error||"The runtime could not complete this run.";
3343
3713
  const source=run.input?.source?.kind;
@@ -3348,14 +3718,18 @@ function explainRun(run){
3348
3718
  if(run.status==="completed")return"The agent completed the requested work.";
3349
3719
  return"The agent is processing a direct message.";
3350
3720
  }
3351
- function navState(section){const key=section==="overview"?"overview":section;document.querySelectorAll("[data-nav]").forEach(node=>node.classList.toggle("active",node.dataset.nav===key));$("sidebar").classList.remove("open")}
3721
+ function navState(section){
3722
+ const key=section==="overview"?"overview":section;document.querySelectorAll("[data-nav]").forEach(node=>node.classList.toggle("active",node.dataset.nav===key));$("sidebar").classList.remove("open");
3723
+ const action=$("new-run");const chatting=section==="chat";action.innerHTML=icon("plus")+' <span>'+(chatting?"New conversation":"New run")+'</span>';action.onclick=chatting?()=>{const agent=chatAgent();if(agent)void createChatConversation(agent.id);else toast("Create an agent instance first")}:()=>openRunDrawer();
3724
+ }
3352
3725
  function crumbs(items){$("breadcrumbs").innerHTML=items.map((item,index)=>index===items.length-1?'<span>'+esc(item.label)+'</span>':'<a href="'+attr(item.href)+'" data-link>'+esc(item.label)+'</a><span class="crumb-separator">'+icon("caretRight")+'</span>').join("")}
3353
3726
 
3354
3727
  function renderOverview(){
3355
3728
  crumbs([{label:"Overview"}]);
3356
- const recentRuns=recent(state.runs).slice(0,6);const definitions=state.manifest?.agents?.agents||[];const live=activeRuns();
3729
+ const recentRuns=recent(state.runs).slice(0,6);const definitions=state.manifest?.agents?.agents||[];const live=activeRuns();const waiting=pendingApprovals();const attention=failureRuns().length+waiting.length;
3357
3730
  let html=pageHead("Runtime at a glance","What is happening now","Start here. Foundry separates code-defined agents from their runtime instances, then records every invocation as a run.",'<a class="button" href="/agents" data-link>Browse agents</a><button class="button primary" data-new-run>Start a run</button>');
3358
- html+='<div class="grid cols-4">'+metric("Agent definitions",definitions.length,"Discovered from the agents folder",true)+metric("Runtime instances",state.instances.length,"Persisted agent identities")+metric("Active work",live.length,live.length?"Runs currently pending or running":"No work in flight")+metric("Attention needed",failureRuns().length,"Failed runs in retained history")+'</div>';
3731
+ html+=approvalRail(waiting,"Agents are waiting for a decision");
3732
+ html+='<div class="grid cols-4">'+metric("Agent definitions",definitions.length,"Discovered from the agents folder",true)+metric("Runtime instances",state.instances.length,"Persisted agent identities")+metric("Active work",live.length,live.length?"Runs currently pending or running":"No work in flight")+metric("Attention needed",attention,waiting.length?count(waiting.length,"effect")+" waiting for approval":"Failed runs in retained history")+'</div>';
3359
3733
  html+='<div class="grid cols-2" style="margin-top:16px">';
3360
3734
  const activity=recentRuns.map(run=>'<tr class="clickable'+(run.status==="failed"?" row-failed":"")+(isLiveRun(run)?" row-live":"")+'" data-go="/runs/'+attr(run.id)+'"><td><span class="primary-cell">'+esc(run.agent)+'</span><span class="secondary">'+esc(explainRun(run))+'</span></td><td>'+status(run.status)+'</td><td>'+idCell(run.id)+'</td><td>'+durCell(run)+'</td><td>'+relCell(run.createdAt)+'</td></tr>').join("");
3361
3735
  html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Agent</th><th>Status</th><th>Run</th><th>Duration</th><th>Started</th></tr></thead><tbody>'+activity+'</tbody></table></div>'+(activity?"":(state.ready?empty("No runs yet","Start a run to see its progress and trace here."):skeleton(4)))+'<div class="card-foot"><a class="link inline-icon" href="/runs" data-link>View all runs '+icon("external")+'</a></div>',count(state.runs.length,"run"),"span-2");
@@ -3391,7 +3765,7 @@ function renderDefinition(id){
3391
3765
  html+='</div>';$("content").innerHTML=html;document.querySelectorAll("[data-new-run]").forEach(x=>x.onclick=()=>openRunDrawer(id));document.querySelectorAll("[data-create-instance]").forEach(x=>x.onclick=()=>void createInstance(id));dataLinks();
3392
3766
  }
3393
3767
 
3394
- async function ensureConversations(agentId){if(!state.conversations[agentId])state.conversations[agentId]=await api("/api/conversations?agent="+encodeURIComponent(agentId))}
3768
+ async function ensureConversations(agentId,force=false){if(force||!state.conversations[agentId])state.conversations[agentId]=await api("/api/conversations?agent="+encodeURIComponent(agentId))}
3395
3769
  function renderInstance(id){
3396
3770
  const instance=state.instances.find(x=>x.id===id);if(!instance)return renderNotFound("Agent instance");
3397
3771
  crumbs([{label:"Agents",href:"/agents"},{label:instance.definitionId,href:"/agents/"+instance.definitionId},{label:short(id)}]);
@@ -3399,7 +3773,7 @@ function renderInstance(id){
3399
3773
  let html=pageHead("Runtime instance",short(id),"Persisted runtime data assembled from "+instance.definitionId+". This identity can be updated without changing the code definition.",'<button class="button" data-copy="'+attr(id)+'">'+icon("copy")+'<span>Copy instance id</span></button><button class="button primary" data-instance-run>Send message</button>');
3400
3774
  html+='<div class="detail-strip"><div><label>Definition</label><strong><a class="link" href="/agents/'+attr(instance.definitionId)+'" data-link>'+esc(instance.definitionId)+'</a></strong></div><div><label>Workspace</label><strong><a class="link mono" href="/workspaces/'+attr(instance.workspaceId)+'" data-link>'+esc(instance.workspaceId)+'</a></strong></div><div><label>Conversations</label><strong>'+conversations.length+'</strong></div><div><label>Last updated</label><strong>'+relCell(instance.updatedAt)+'</strong></div></div>';
3401
3775
  html+='<div class="grid cols-2">';
3402
- const convRows=recent(conversations).map(c=>'<tr><td><span class="primary-cell">'+esc(c.title||"Untitled conversation")+'</span>'+idCell(c.id)+'</td><td>'+relCell(c.updatedAt)+'</td></tr>').join("");
3776
+ const convRows=recent(conversations).map(c=>'<tr class="clickable" data-go="'+attr(chatPath(id,c.id))+'"><td><span class="primary-cell">'+esc(c.title||"Untitled conversation")+'</span>'+idCell(c.id)+'</td><td>'+relCell(c.updatedAt)+'</td></tr>').join("");
3403
3777
  html+=card("Conversations",'<div class="table-wrap"><table class="table"><thead><tr><th>Conversation</th><th>Updated</th></tr></thead><tbody>'+convRows+'</tbody></table></div>'+(convRows?"":empty("No conversations","The first message creates a conversation for this instance.")),count(conversations.length,"conversation"));
3404
3778
  const runRows=runs.slice(0,8).map(run=>'<tr class="clickable'+(run.status==="failed"?" row-failed":"")+(isLiveRun(run)?" row-live":"")+'" data-go="/runs/'+attr(run.id)+'"><td>'+idCell(run.id)+'<span class="secondary">'+esc(explainRun(run))+'</span></td><td>'+status(run.status)+'</td><td>'+durCell(run)+'</td><td>'+relCell(run.createdAt)+'</td></tr>').join("");
3405
3779
  html+=card("Recent runs",'<div class="table-wrap"><table class="table"><thead><tr><th>Run</th><th>Status</th><th>Duration</th><th>Started</th></tr></thead><tbody>'+runRows+'</tbody></table></div>'+(runRows?"":empty("No runs","Send this instance a message to begin."))+(runs.length>8?'<div class="card-foot"><a class="link inline-icon" href="/runs?q='+encodeURIComponent(instance.id)+'" data-link>View all '+count(runs.length,"run")+' '+icon("external")+'</a></div>':""),count(runs.length,"run"));
@@ -3408,6 +3782,137 @@ function renderInstance(id){
3408
3782
  html+='</div>';$("content").innerHTML=html;document.querySelector("[data-instance-run]").onclick=()=>openRunDrawer(instance.definitionId,id);dataLinks();
3409
3783
  }
3410
3784
 
3785
+ /* ---- WebChat. History comes from the agent's StoreAdapter, never UI state. ---- */
3786
+ function chatPath(agentId,conversationId){return"/chat"+(conversationId?"/"+encodeURIComponent(conversationId):"")+"?agent="+encodeURIComponent(agentId)}
3787
+ function chatAgent(){const requested=new URLSearchParams(location.search).get("agent");return state.instances.find(x=>x.id===requested)||state.instances.find(x=>x.id===state.chat.agentId)||state.instances[0]||null}
3788
+ function chatRuns(agentId,conversationId){return recent(state.runs.filter(run=>run.agentId===agentId&&run.conversationId===conversationId))}
3789
+ function activeChatRun(agentId,conversationId){return chatRuns(agentId,conversationId).find(isLiveRun)||null}
3790
+ function liveAssistantMessage(run){
3791
+ if(!run)return null;
3792
+ const text=eventsFor(run.id).filter(event=>event.type==="agent.text_delta"&&typeof event.data?.text==="string").map(event=>event.data.text).join("");
3793
+ return text?{sender:"agent",text}:null;
3794
+ }
3795
+ function messageAttachments(message){return(message.content||[]).filter(part=>part.type!=="text")}
3796
+ function renderChatMessage(message,pending){
3797
+ const internalTools=message.sender==="user"&&(message.tool_results||[]).length>0&&message.text==="tool results";
3798
+ const kind=internalTools?"tool":message.sender;
3799
+ const sender=internalTools?"Tools":message.sender==="user"?"You":"Agent";
3800
+ const attachments=messageAttachments(message);
3801
+ const attachmentHtml=attachments.length?'<div class="message-attachments">'+attachments.map(part=>'<span>'+icon(part.type==="image"?"overview":"paperclip")+'<b>'+esc(part.name||part.type)+'</b><small>'+esc(part.source?.media_type||part.type)+'</small></span>').join("")+'</div>':"";
3802
+ const tools=(message.tool_calls||[]).length?'<details class="message-tools"><summary>'+count(message.tool_calls.length,"tool call")+'</summary><pre>'+esc(jsonText(message.tool_calls))+'</pre></details>':"";
3803
+ const results=(message.tool_results||[]).length?'<details class="message-tools"><summary>'+count(message.tool_results.length,"tool result")+'</summary><pre>'+esc(jsonText(message.tool_results))+'</pre></details>':"";
3804
+ const flags=[message.is_compaction?"compaction":null,message.is_skill_injection?"skill context":null,pending?"sending":null].filter(Boolean);
3805
+ const text=internalTools?"":message.text||"";
3806
+ return'<article class="chat-message '+attr(kind)+(pending?" optimistic":"")+'"><div class="message-avatar">'+(kind==="user"?esc(sender.slice(0,1)):icon(kind==="tool"?"runs":"agent"))+'</div><div class="message-body"><header><strong>'+esc(sender)+'</strong>'+(flags.length?'<span>'+esc(flags.join(" · "))+'</span>':"")+'</header>'+(text?'<div class="message-text">'+esc(text)+'</div>':"")+attachmentHtml+tools+results+'</div></article>';
3807
+ }
3808
+ function renderChatProgress(run){
3809
+ if(!run)return"";
3810
+ const events=eventsFor(run.id).filter(isKeyRunEvent).slice(-4);
3811
+ return'<aside class="chat-progress"><div><span class="pulse-dot"></span><strong>'+esc(run.status==="pending"?"Queued":run.agent+" is working")+'</strong><small>'+esc(events.at(-1)?.type||"The runtime is assembling this turn.")+'</small></div><div class="chat-progress-actions"><a class="button" href="/runs/'+attr(run.id)+'" data-link>Open trace</a><button class="button danger" type="button" data-chat-stop>'+icon("stop")+' Stop</button></div></aside>';
3812
+ }
3813
+ async function loadChat(current){
3814
+ const agent=chatAgent();
3815
+ if(!agent){state.chat.agentId="";state.chat.conversationId="";state.chat.transcript=null;return}
3816
+ const switchedAgent=Boolean(state.chat.agentId&&state.chat.agentId!==agent.id);
3817
+ state.chat.agentId=agent.id;
3818
+ await ensureConversations(agent.id,true);
3819
+ const conversations=recent(state.conversations[agent.id]||[]);
3820
+ const conversationId=current.id||state.chat.conversationId||conversations[0]?.id||"";
3821
+ if(switchedAgent||(state.chat.conversationId&&state.chat.conversationId!==conversationId)){state.chat.draft="";state.chat.files=[];state.chat.error="";state.chat.optimistic=null;state.chat.historyOffset=null}
3822
+ state.chat.conversationId=conversationId;
3823
+ if(!conversationId){state.chat.transcript=null;return}
3824
+ const historyQuery="&limit=100"+(state.chat.historyOffset===null?"":"&offset="+encodeURIComponent(state.chat.historyOffset));
3825
+ const transcript=await api("/api/conversations/"+encodeURIComponent(conversationId)+"/messages?agent="+encodeURIComponent(agent.id)+historyQuery);
3826
+ state.chat.transcript=transcript;
3827
+ const optimistic=state.chat.optimistic;
3828
+ if(optimistic){
3829
+ const run=state.runs.find(item=>item.id===optimistic.runId);
3830
+ if(transcript.messages.length>optimistic.baseCount||(run&&!isLiveRun(run)))state.chat.optimistic=null;
3831
+ }
3832
+ }
3833
+ function renderChat(){
3834
+ crumbs([{label:"Chat"}]);
3835
+ const agent=chatAgent();
3836
+ if(!agent){
3837
+ $("content").innerHTML=pageHead("WebChat","Talk to your agents","A runtime instance owns conversations, installed capabilities, context, and durable history.")+empty("No agent instance exists","Create an instance before opening a conversation.")+'<div class="empty-action"><a class="button primary" href="/agents" data-link>Choose an agent</a></div>';
3838
+ dataLinks();return;
3839
+ }
3840
+ const conversations=recent(state.conversations[agent.id]||[]);
3841
+ const term=state.chat.query.trim().toLowerCase();
3842
+ const visible=conversations.filter(item=>!term||(item.title||item.id).toLowerCase().includes(term));
3843
+ const transcript=state.chat.transcript;
3844
+ const conversation=transcript?.conversation||conversations.find(item=>item.id===state.chat.conversationId);
3845
+ const run=conversation?activeChatRun(agent.id,conversation.id):null;
3846
+ const lastRun=conversation?chatRuns(agent.id,conversation.id)[0]:null;
3847
+ const messages=[...(transcript?.messages||[])];
3848
+ if(state.chat.optimistic)messages.push(state.chat.optimistic.message);
3849
+ const streaming=state.chat.historyOffset===null?liveAssistantMessage(run):null;if(streaming)messages.push(streaming);
3850
+ let html='<section class="chat-shell">';
3851
+ html+='<aside class="session-rail"><div class="session-head"><div><span class="eyebrow">WebChat</span><h1>Conversations</h1></div><button class="button icon-button" type="button" data-new-chat title="New conversation" aria-label="New conversation">'+icon("plus")+'</button></div>';
3852
+ html+='<label class="session-agent"><span>Agent instance</span><select id="chat-agent">'+state.instances.map(item=>'<option value="'+attr(item.id)+'"'+(item.id===agent.id?" selected":"")+'>'+esc(item.definitionId)+" · "+esc(short(item.id))+'</option>').join("")+'</select></label>';
3853
+ html+='<label class="session-search">'+icon("search")+'<input id="chat-search" value="'+attr(state.chat.query)+'" placeholder="Search conversations" /></label>';
3854
+ html+='<div class="session-list">'+(visible.length?visible.map(item=>'<button type="button" class="chat-session'+(item.id===conversation?.id?" active":"")+'" data-chat-conversation="'+attr(item.id)+'"><span>'+esc(item.title||"Untitled conversation")+'</span><small>'+esc(relTime(item.updatedAt))+' · '+esc(short(item.id))+'</small></button>').join(""):empty(term?"No matching conversations":"No conversations yet",term?"Try another search.":"Create one to start talking with this instance."))+'</div></aside>';
3855
+ html+='<section class="conversation-pane">';
3856
+ if(!conversation){
3857
+ html+='<div class="chat-empty">'+icon("chat","chat-empty-icon")+'<h2>Start a conversation</h2><p>Messages route to this exact runtime instance and become part of its durable context.</p><button class="button primary" type="button" data-new-chat>'+icon("plus")+' New conversation</button></div>';
3858
+ }else{
3859
+ html+='<header class="conversation-head"><div><span class="conversation-kicker">'+esc(agent.definitionId)+" · "+esc(short(agent.id))+'</span><h2>'+esc(conversation.title||"Untitled conversation")+'</h2></div><div class="conversation-meta">'+(run?status(run.status):lastRun?status(lastRun.status):'<span class="status">ready</span>')+'<span>'+esc(transcript?.turnCount??0)+' turns</span><span>'+esc(transcript?.tokenCount??0)+' tokens</span><button type="button" class="conversation-rename" data-export-chat>Export</button><button type="button" class="conversation-rename" data-rename-chat>Rename</button></div></header>';
3860
+ if(transcript&&!transcript.persisted)html+='<div class="chat-store-warning">This agent has no conversation store. New turns can run, but history cannot be reopened after the worker exits.</div>';
3861
+ const page=transcript?.page;const paging=page&&(page.hasEarlier||page.hasLater)?'<div class="load-history">'+(page.hasEarlier?'<button class="button" type="button" data-history-page="earlier">Earlier</button>':"")+'<span>Showing '+esc(page.offset+1)+'–'+esc(page.offset+(transcript?.messages?.length||0))+' of '+esc(page.total)+'</span>'+(page.hasLater?'<button class="button" type="button" data-history-page="later">Newer</button>':"")+'</div>':"";
3862
+ html+='<div class="message-scroll" id="message-scroll">'+paging+(messages.length?messages.map((message,index)=>renderChatMessage(message,index===messages.length-1&&Boolean(state.chat.optimistic))).join(""):'<div class="chat-empty compact">'+icon("chat","chat-empty-icon")+'<h2>What should '+esc(agent.definitionId)+' work on?</h2><p>This conversation can include text, images, video, and documents.</p></div>')+'</div>';
3863
+ html+=renderChatProgress(run);
3864
+ html+='<form class="chat-composer" id="chat-form"><div class="selected-files">'+state.chat.files.map((file,index)=>'<span>'+icon("paperclip")+'<b>'+esc(file.name)+'</b><small>'+esc(fmtBytes(file.size))+'</small><button type="button" data-remove-file="'+index+'" aria-label="Remove '+attr(file.name)+'">'+icon("close")+'</button></span>').join("")+'</div><textarea id="chat-message" rows="1" placeholder="Message '+attr(agent.definitionId)+'…" aria-label="Message"'+(state.chat.sending?" disabled":"")+'>'+esc(state.chat.draft)+'</textarea><div class="composer-actions"><label class="attach-button" title="Attach files">'+icon("paperclip")+'<input id="chat-files" type="file" multiple'+(state.chat.sending?" disabled":"")+' /></label><span>Enter to send · Shift + Enter for a new line</span><button class="button primary" type="submit"'+(state.chat.sending?" disabled":"")+'>'+(run?icon("redirect")+' Redirect':icon("send")+' Send')+'</button></div><div class="form-error'+(state.chat.error?"":" hidden")+'" id="chat-error">'+esc(state.chat.error)+'</div></form>';
3865
+ }
3866
+ html+='</section></section>';
3867
+ $("content").innerHTML=html;
3868
+ document.querySelectorAll("[data-new-chat]").forEach(node=>node.onclick=()=>void createChatConversation(agent.id));
3869
+ const selector=$("chat-agent");if(selector)selector.onchange=event=>void selectChatAgent(event.target.value);
3870
+ const search=$("chat-search");if(search)search.oninput=event=>{state.chat.query=event.target.value;renderChat();const next=$("chat-search");if(next){next.focus();next.setSelectionRange(next.value.length,next.value.length)}};
3871
+ document.querySelectorAll("[data-chat-conversation]").forEach(node=>node.onclick=()=>selectChatConversation(agent.id,node.dataset.chatConversation));
3872
+ document.querySelectorAll("[data-remove-file]").forEach(node=>node.onclick=()=>{state.chat.files.splice(Number(node.dataset.removeFile),1);renderChat()});
3873
+ const files=$("chat-files");if(files)files.onchange=event=>{const next=[...event.target.files];const bytes=[...state.chat.files,...next].reduce((total,file)=>total+file.size,0);if(bytes>28*1024*1024){toast("Attachments must total 28 MB or less");return}state.chat.files.push(...next);renderChat()};
3874
+ const form=$("chat-form");if(form)form.onsubmit=event=>void sendChat(event,agent,conversation,run);
3875
+ const message=$("chat-message");if(message){message.oninput=event=>{state.chat.draft=event.target.value};message.onkeydown=event=>{if(event.key==="Enter"&&!event.shiftKey){event.preventDefault();form.requestSubmit()}}}
3876
+ const stop=document.querySelector("[data-chat-stop]");if(stop&&run)stop.onclick=()=>void stopChat(run.id);
3877
+ const rename=document.querySelector("[data-rename-chat]");if(rename)rename.onclick=()=>void renameChat(agent,conversation);
3878
+ const exportButton=document.querySelector("[data-export-chat]");if(exportButton&&transcript)exportButton.onclick=()=>exportChat(transcript);
3879
+ document.querySelectorAll("[data-history-page]").forEach(node=>node.onclick=()=>void pageChatHistory(node.dataset.historyPage));
3880
+ dataLinks();
3881
+ const changed=state.chat.renderedConversationId!==conversation?.id;state.chat.renderedConversationId=conversation?.id||"";
3882
+ requestAnimationFrame(()=>{const scroll=$("message-scroll");if(!scroll)return;if(state.chat.scrollToTop){scroll.scrollTop=0;state.chat.scrollToTop=false}else if(changed||state.chat.scrollToBottom){scroll.scrollTop=scroll.scrollHeight;state.chat.scrollToBottom=false}});
3883
+ }
3884
+ async function selectChatAgent(agentId){state.chat.agentId=agentId;state.chat.conversationId="";state.chat.transcript=null;state.chat.draft="";state.chat.files=[];state.chat.error="";state.chat.optimistic=null;state.chat.historyOffset=null;await ensureConversations(agentId,true);const first=recent(state.conversations[agentId]||[])[0];go(chatPath(agentId,first?.id))}
3885
+ function selectChatConversation(agentId,conversationId){if(conversationId!==state.chat.conversationId){state.chat.draft="";state.chat.files=[];state.chat.error="";state.chat.optimistic=null;state.chat.historyOffset=null}go(chatPath(agentId,conversationId))}
3886
+ async function createChatConversation(agentId){try{const conversation=await api("/api/conversations",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({agentId,title:"New conversation"})});state.conversations[agentId]=[conversation,...(state.conversations[agentId]||[])];state.chat.conversationId=conversation.id;state.chat.transcript={conversation,messages:[],turnCount:0,tokenCount:0,page:{offset:0,limit:100,total:0,hasEarlier:false,hasLater:false},persisted:true};state.chat.draft="";state.chat.files=[];state.chat.error="";state.chat.historyOffset=null;state.chat.scrollToBottom=true;go(chatPath(agentId,conversation.id));setTimeout(()=>$("chat-message")?.focus(),20)}catch(error){toast(error.message)}}
3887
+ async function pageChatHistory(direction){const page=state.chat.transcript?.page;if(!page)return;state.chat.historyOffset=direction==="earlier"?Math.max(0,page.offset-page.limit):(page.offset+page.limit>=page.total?null:Math.min(Math.max(0,page.total-page.limit),page.offset+page.limit));state.chat.scrollToTop=true;try{await loadChat(route());renderChat()}catch(error){toast(error.message)}}
3888
+ async function renameChat(agent,conversation){const title=window.prompt("Conversation name",conversation.title||"");if(title===null||title.trim()===(conversation.title||""))return;try{const updated=await api("/api/conversations/"+encodeURIComponent(conversation.id),{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({agentId:agent.id,title})});state.conversations[agent.id]=(state.conversations[agent.id]||[]).map(item=>item.id===updated.id?updated:item);if(state.chat.transcript)state.chat.transcript={...state.chat.transcript,conversation:updated};toast("Conversation renamed");renderChat()}catch(error){toast(error.message)}}
3889
+ function exportChat(transcript){const title=transcript.conversation.title||transcript.conversation.id;const file=title.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"").slice(0,64)||"foundry-conversation";const blob=new Blob([JSON.stringify(transcript,null,2)+"\n"],{type:"application/json"});const url=URL.createObjectURL(blob);const link=document.createElement("a");link.href=url;link.download=file+".json";document.body.appendChild(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),1000);toast("Conversation exported")}
3890
+ function fileContent(file){return new Promise((resolveFile,rejectFile)=>{const reader=new FileReader();reader.onerror=()=>rejectFile(reader.error||new Error("Could not read "+file.name));reader.onload=()=>{const value=String(reader.result||"");resolveFile({type:file.type.startsWith("image/")?"image":file.type.startsWith("video/")?"video":"document",name:file.name,source:{type:"base64",media_type:file.type||"application/octet-stream",data:value.slice(value.indexOf(",")+1)}})};reader.readAsDataURL(file)})}
3891
+ async function sendChat(event,agent,conversation,activeRun){
3892
+ event.preventDefault();if(state.chat.sending)return;
3893
+ const text=state.chat.draft.trim();if(!text&&!state.chat.files.length)return;
3894
+ state.chat.sending=true;state.chat.error="";state.chat.historyOffset=null;renderChat();
3895
+ try{
3896
+ const parts=await Promise.all(state.chat.files.map(fileContent));
3897
+ const message=parts.length?[...(text?[{type:"text",text}]:[]),...parts]:text;
3898
+ const baseCount=state.chat.transcript?.messages?.length||0;
3899
+ const optimisticMessage={sender:"user",text:text||(parts.length?"[multimodal message]":""),content:parts.length?[...(text?[{type:"text",text}]:[]),...parts]:undefined};
3900
+ let run;
3901
+ if(conversation.title==="New conversation"&&text){
3902
+ const title=text.replace(/\s+/g," ").slice(0,72);
3903
+ const updated=await api("/api/conversations/"+encodeURIComponent(conversation.id),{method:"PATCH",headers:{"content-type":"application/json"},body:JSON.stringify({agentId:agent.id,title})});
3904
+ state.conversations[agent.id]=(state.conversations[agent.id]||[]).map(item=>item.id===updated.id?updated:item);if(state.chat.transcript)state.chat.transcript={...state.chat.transcript,conversation:updated};
3905
+ }
3906
+ if(activeRun){const steered=await api("/api/runs/"+encodeURIComponent(activeRun.id)+"/steer",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({message})});run=steered.run;toast("Direction updated")}
3907
+ else{run=await api("/api/conversations/"+encodeURIComponent(conversation.id)+"/messages",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({agentId:agent.id,message})});toast("Message sent")}
3908
+ state.runs=[...state.runs.filter(item=>item.id!==run.id),run];
3909
+ state.chat.optimistic={message:optimisticMessage,baseCount,runId:run.id};state.chat.draft="";state.chat.files=[];state.chat.scrollToBottom=true;
3910
+ await ensureConversations(agent.id,true);
3911
+ }catch(error){state.chat.error=error.message}
3912
+ finally{state.chat.sending=false;renderChat()}
3913
+ }
3914
+ async function stopChat(runId){try{await cancelRun(runId);toast("Stopping this turn") }catch(error){toast(error.message)}}
3915
+
3411
3916
  /* ---- Run filters live in the URL, so a filtered view is a link you can send. ---- */
3412
3917
  function runQuery(){
3413
3918
  const params=new URLSearchParams(location.search);
@@ -3513,6 +4018,18 @@ function runErrorText(run){
3513
4018
  if(run.error&&typeof run.error==="object")return run.error.message||jsonText(run.error);
3514
4019
  return"The runtime recorded a failure without a message.";
3515
4020
  }
4021
+ function guidanceCard(events){
4022
+ const event=[...events].reverse().find(e=>/(?:^|\.)foundry\.guidance\.state$/.test(e.type));
4023
+ if(!event||!event.data)return "";
4024
+ const data=event.data;
4025
+ const goals=data.goals;
4026
+ const goalBody=goals?'<p><strong>'+esc(goals.status)+'</strong> · revision '+esc(goals.version)+'</p>'+goals.items.map(g=>'<p>'+esc(g.title||g.key)+' · '+esc(g.status)+' · '+esc(g.completed)+' / '+esc(g.total)+' done</p>').join(""):'<p class="muted">No goal set started.</p>';
4027
+ const facts=data.facts;
4028
+ const factBody=facts?'<p><strong>'+count(facts.revisions,"recorded revision")+'</strong></p><p>'+count(facts.claims,"evidence claim")+' · '+count(facts.urgent,"urgent revision")+'</p>':'<p class="muted">Facts not mounted.</p>';
4029
+ const forms=data.forms;
4030
+ const formBody=forms?.length?forms.map(f=>'<p><strong>'+esc(f.definitionId)+'</strong> · '+esc(f.status)+'</p><p>'+count(f.answered,"answered field")+' · '+count(f.pendingHooks,"pending effect batch","pending effect batches")+(f.blockedOn?' · waiting on '+esc(f.blockedOn):'')+'</p>').join(""):'<p class="muted">No form instances started.</p>';
4031
+ return '<section class="card"><div class="card-head"><h2>Conversation guidance</h2><span class="meta">Observed '+esc(fmtTime(event.timestamp))+'</span></div><div class="card-body"><div class="grid cols-3"><div><h3>Goals</h3>'+goalBody+'</div><div><h3>Facts</h3>'+factBody+'</div><div><h3>Forms</h3>'+formBody+'</div></div><p class="muted">Latest state observed by this run, not a live database view. '+count(data.contextProviders||0,"custom context provider")+'. Answer values and fact bodies are not included in this summary.</p></div></section>';
4032
+ }
3516
4033
  function renderRun(id){
3517
4034
  const run=state.runs.find(x=>x.id===id);if(!run)return renderNotFound("Run");
3518
4035
  const events=eventsFor(id);
@@ -3527,6 +4044,8 @@ function renderRun(id){
3527
4044
  const failure=runErrorText(run);
3528
4045
  if(failure)html+='<div class="alert danger"><span class="symbol">'+icon("warning")+'</span><div><b>This run failed'+(run.attempts>1?" after "+count(run.attempts,"attempt"):"")+'.</b><p>'+esc(failure)+'</p></div></div>';
3529
4046
  if(isLiveRun(run))html+='<div class="alert live"><span class="symbol"><i class="pulse-dot"></i></span><div><b>This run is still '+esc(run.status)+'.</b><p>New events stream in below as the runtime records them.</p></div></div>';
4047
+ html+=approvalRail(pendingApprovals(run.id),"This run is waiting for a decision");
4048
+ html+=guidanceCard(events);
3530
4049
  html+='<div class="detail-strip">'
3531
4050
  +'<div><label>Status</label><strong>'+status(run.status)+'</strong></div>'
3532
4051
  +'<div><label>Duration</label><strong>'+durCell(run)+'</strong></div>'
@@ -3618,8 +4137,8 @@ function renderWorkspacePanel(data){
3618
4137
 
3619
4138
  function renderNotFound(label){crumbs([{label:"Not found"}]);$("content").innerHTML=pageHead("Inspector",label+" not found","The requested runtime record does not exist or is no longer retained.",'<a class="button" href="/" data-link>Return to overview</a>');dataLinks()}
3620
4139
  async function loadBase(){
3621
- const endpoints=["/api/manifest","/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/health","/api/transmissions","/api/accounts","/api/routes","/api/bindings","/api/activations"];
3622
- const results=await Promise.all(endpoints.map(url=>api(url)));[state.manifest,state.instances,state.subscriptions,state.connections,state.runs,state.events,state.health,state.transmissions,state.accounts,state.routes,state.bindings,state.activations]=results;renderHealth();
4140
+ const endpoints=["/api/manifest","/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/health","/api/transmissions","/api/accounts","/api/routes","/api/bindings","/api/activations","/api/approvals"];
4141
+ const results=await Promise.all(endpoints.map(url=>api(url)));[state.manifest,state.instances,state.subscriptions,state.connections,state.runs,state.events,state.health,state.transmissions,state.accounts,state.routes,state.bindings,state.activations,state.approvals]=results;renderHealth();
3623
4142
  }
3624
4143
  /* Every observability event used to trigger a full reload and repaint, which
3625
4144
  made a busy run unreadable. Events are coalesced into one refresh instead,
@@ -3640,9 +4159,9 @@ async function refreshActivity(){
3640
4159
  if(refreshing){refreshQueued=true;return}
3641
4160
  refreshing=true;
3642
4161
  try{
3643
- const endpoints=["/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/api/activations","/health"];
4162
+ const endpoints=["/api/agent-instances","/api/playbook-subscriptions","/api/application-connections","/api/runs","/api/events","/api/activations","/api/approvals","/health"];
3644
4163
  const values=await Promise.all(endpoints.map(url=>api(url)));
3645
- [state.instances,state.subscriptions,state.connections,state.runs,state.events,state.activations,state.health]=values;
4164
+ [state.instances,state.subscriptions,state.connections,state.runs,state.events,state.activations,state.approvals,state.health]=values;
3646
4165
  renderHealth();
3647
4166
  $("event-state").textContent="Live updates";
3648
4167
  await navigate(false,true);
@@ -3655,11 +4174,13 @@ async function refreshActivity(){
3655
4174
  function renderHealth(){const ok=Boolean(state.health?.ok);$("runtime-state").classList.toggle("bad",!ok);$("runtime-state").querySelector("span").textContent=ok?"Runtime healthy":"Runtime needs attention"}
3656
4175
  async function navigate(load=true,preserve=false){
3657
4176
  const current=route();navState(current.section);
4177
+ $("content").classList.toggle("chat-content",current.section==="chat");
3658
4178
  const view=preserve?captureView():null;
3659
4179
  try{
3660
4180
  if(load&&current.section==="instances"&&current.id)await ensureConversations(current.id);
3661
4181
  if(load&&current.section==="workspaces"&&current.id)await ensureWorkspace(current.id);
3662
- if(current.section==="overview")renderOverview();else if(current.section==="agents"&&!current.id)renderAgents();else if(current.section==="agents")renderDefinition(current.id);else if(current.section==="instances")renderInstance(current.id);else if(current.section==="runs"&&!current.id)renderRuns();else if(current.section==="runs")renderRun(current.id);else if(current.section==="automations")renderAutomations();else if(current.section==="integrations")renderIntegrations();else if(current.section==="workspaces")renderWorkspaces(current.id);else renderNotFound("Page");
4182
+ if(current.section==="chat")await loadChat(current);
4183
+ if(current.section==="overview")renderOverview();else if(current.section==="agents"&&!current.id)renderAgents();else if(current.section==="agents")renderDefinition(current.id);else if(current.section==="instances")renderInstance(current.id);else if(current.section==="chat")renderChat();else if(current.section==="runs"&&!current.id)renderRuns();else if(current.section==="runs")renderRun(current.id);else if(current.section==="automations")renderAutomations();else if(current.section==="integrations")renderIntegrations();else if(current.section==="workspaces")renderWorkspaces(current.id);else renderNotFound("Page");
3663
4184
  }catch(error){$("content").innerHTML='<div class="form-error">'+esc(error.message)+'</div>';console.error(error)}
3664
4185
  restoreView(view);
3665
4186
  tickTimes();
@@ -3696,9 +4217,16 @@ async function startRun(event){
3696
4217
  finally{setRunSubmitting(false)}
3697
4218
  }
3698
4219
  async function cancelRun(id){try{await api("/api/runs/"+encodeURIComponent(id)+"/cancel",{method:"POST"});toast("Cancellation requested");await refreshActivity()}catch(error){toast(error.message)}}
4220
+ async function resolveApproval(id,decision){
4221
+ try{
4222
+ await api("/api/approvals/"+encodeURIComponent(id),{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({decision})});
4223
+ toast(decision==="approve"?"Approved this exact tool input":"Denied the tool request");
4224
+ await refreshActivity();
4225
+ }catch(error){toast(error.message);await refreshActivity()}
4226
+ }
3699
4227
 
3700
4228
  function searchItems(){
3701
- const items=[{label:"Overview",detail:"Runtime health and recent activity",path:"/"},{label:"Agents",detail:"Definitions and runtime instances",path:"/agents"},{label:"Runs",detail:"Execution history and traces",path:"/runs"},{label:"Automations",detail:"Schedules, sleeps, playbooks, and connections",path:"/automations"},{label:"Integrations",detail:"Transmissions, routes, accounts, and bindings",path:"/integrations"},{label:"Workspaces",detail:"Shared entries, inbox, tasks, and environment",path:"/workspaces"}];
4229
+ const items=[{label:"Overview",detail:"Runtime health and recent activity",path:"/"},{label:"Agents",detail:"Definitions and runtime instances",path:"/agents"},{label:"Chat",detail:"Durable browser conversations with runtime agents",path:"/chat"},{label:"Runs",detail:"Execution history and traces",path:"/runs"},{label:"Automations",detail:"Schedules, sleeps, playbooks, and connections",path:"/automations"},{label:"Integrations",detail:"Transmissions, routes, accounts, and bindings",path:"/integrations"},{label:"Workspaces",detail:"Shared entries, inbox, tasks, and environment",path:"/workspaces"}];
3702
4230
  (state.manifest?.agents?.agents||[]).forEach(x=>items.push({label:x.id,detail:"Agent definition · "+x.description,path:"/agents/"+x.id}));state.instances.forEach(x=>items.push({label:short(x.id),detail:"Agent instance · "+x.definitionId,path:"/instances/"+x.id}));recent(state.runs).slice(0,100).forEach(x=>items.push({label:short(x.id),detail:"Run · "+x.agent+" · "+x.status,path:"/runs/"+x.id}));return items;
3703
4231
  }
3704
4232
  function renderSearch(){const term=$("search-input").value.trim().toLowerCase();const matches=searchItems().filter(x=>!term||(x.label+" "+x.detail).toLowerCase().includes(term)).slice(0,12);$("search-results").innerHTML=matches.map(x=>'<a class="search-result" href="'+attr(x.path)+'" data-search-link><strong>'+esc(x.label)+'</strong><small>'+esc(x.detail)+'</small></a>').join("")||empty("No matches","Try an agent id, instance id, run id, or page name.");document.querySelectorAll("[data-search-link]").forEach(x=>x.onclick=e=>{e.preventDefault();closeSearch();go(x.getAttribute("href"))})}
@@ -3715,7 +4243,7 @@ document.addEventListener("keydown",e=>{
3715
4243
  else if(e.key==="k"){e.preventDefault();moveCursor(-1)}
3716
4244
  else if(e.key==="Enter"||e.key==="o"){if(state.cursor>=0){e.preventDefault();openCursor()}}
3717
4245
  else if(e.key==="/"){const box=$("run-search");if(box){e.preventDefault();box.focus();box.select()}else{e.preventDefault();openSearch()}}
3718
- else if(e.key==="c"){e.preventDefault();openRunDrawer()}
4246
+ else if(e.key==="c"){e.preventDefault();const current=route();if(current.section==="chat"){const agent=chatAgent();if(agent)void createChatConversation(agent.id)}else openRunDrawer()}
3719
4247
  else if(e.key==="r"){e.preventDefault();void refreshActivity()}
3720
4248
  });
3721
4249
  setInterval(tickTimes,1000);
@@ -3760,7 +4288,7 @@ button,input,textarea,select{font:inherit;color:inherit}button,a{touch-action:ma
3760
4288
  .page-head{display:flex;align-items:flex-end;gap:24px;margin-bottom:28px}.eyebrow{display:flex;align-items:center;gap:8px;font:10px var(--mono);color:var(--accent-soft);text-transform:uppercase;letter-spacing:.14em;margin-bottom:12px}.eyebrow:before{content:"";width:18px;height:1px;background:var(--accent-line)}.page-head h1{font-size:34px;font-weight:400;line-height:1.08;letter-spacing:-.035em;margin:0}.page-head p{margin:9px 0 0;color:var(--text-secondary);max-width:690px;font-size:14px;font-weight:300;line-height:1.6}.page-head .actions{margin-left:auto;display:flex;gap:8px;align-items:center}.page-rule{width:42px;height:2px;background:var(--accent);margin-bottom:16px}
3761
4289
  .grid{display:grid;gap:14px}.cols-4{grid-template-columns:repeat(4,1fr)}.cols-3{grid-template-columns:repeat(3,1fr)}.cols-2{grid-template-columns:repeat(2,1fr)}.span-2{grid-column:span 2}.card{background:rgba(15,22,19,.92);border:1px solid var(--border-subtle);border-radius:var(--radius);min-width:0;margin-bottom:14px;overflow:hidden}.card-head{padding:14px 16px;border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;gap:12px;background:rgba(20,29,24,.3)}.card-head h2{font-size:13px;font-weight:500;margin:0}.card-head .meta{margin-left:auto;color:var(--text-tertiary);font:9px var(--mono)}.card-body{padding:16px}.metric{padding:18px;min-height:124px;margin-bottom:0}.metric label{display:block;color:var(--text-tertiary);font:9px var(--mono);text-transform:uppercase;letter-spacing:.12em}.metric strong{display:block;font-size:30px;font-weight:400;letter-spacing:-.035em;margin-top:15px}.metric small{display:block;color:var(--text-secondary);font-size:11px;font-weight:300;margin-top:6px}.metric.accent{border-top:2px solid var(--accent)}
3762
4290
  .table-wrap{overflow:auto}.table{width:100%;border-collapse:collapse;min-width:650px}.table th{padding:11px 15px;text-align:left;color:var(--text-tertiary);font:9px var(--mono);text-transform:uppercase;letter-spacing:.1em;border-bottom:1px solid var(--border-subtle);white-space:nowrap}.table td{padding:13px 15px;border-bottom:1px solid var(--border-subtle);font-size:12px;vertical-align:top}.table tr:last-child td{border-bottom:0}.table tbody tr.clickable{cursor:pointer}.table tbody tr.clickable:hover{background:var(--accent-glow)}.primary-cell{font-weight:500;color:var(--text-primary)}.secondary{display:block;color:var(--text-tertiary);font:9px/1.55 var(--mono);margin-top:4px;overflow-wrap:anywhere}
3763
- .status{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 8px;background:var(--bg-surface-2);color:var(--text-secondary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;border:1px solid var(--border)}.status:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--text-tertiary)}.status.completed,.status.connected,.status.active,.status.enabled,.status.resolved{background:rgba(140,196,148,.08);color:var(--success);border-color:rgba(140,196,148,.18)}.status.completed:before,.status.connected:before,.status.active:before,.status.enabled:before,.status.resolved:before{background:var(--success)}.status.running,.status.pending,.status.connecting,.status.reconnecting,.status.in-progress{background:rgba(131,179,226,.08);color:var(--network);border-color:rgba(131,179,226,.18)}.status.running:before,.status.pending:before,.status.connecting:before,.status.reconnecting:before,.status.in-progress:before{background:var(--network)}.status.failed,.status.cancelled,.status.disconnected,.status.dismissed{background:rgba(226,155,168,.08);color:var(--danger);border-color:rgba(226,155,168,.18)}.status.failed:before,.status.cancelled:before,.status.disconnected:before,.status.dismissed:before{background:var(--danger)}
4291
+ .status{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:4px 8px;background:var(--bg-surface-2);color:var(--text-secondary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.06em;white-space:nowrap;border:1px solid var(--border)}.status:before{content:"";width:5px;height:5px;border-radius:50%;background:var(--text-tertiary)}.status.completed,.status.connected,.status.active,.status.enabled,.status.resolved{background:rgba(140,196,148,.08);color:var(--success);border-color:rgba(140,196,148,.18)}.status.completed:before,.status.connected:before,.status.active:before,.status.enabled:before,.status.resolved:before{background:var(--success)}.status.running,.status.pending,.status.connecting,.status.reconnecting,.status.in-progress{background:rgba(131,179,226,.08);color:var(--network);border-color:rgba(131,179,226,.18)}.status.running:before,.status.pending:before,.status.connecting:before,.status.reconnecting:before,.status.in-progress:before{background:var(--network)}.status.paused{background:rgba(226,187,131,.08);color:var(--warning);border-color:rgba(226,187,131,.18)}.status.paused:before{background:var(--warning)}.status.failed,.status.cancelled,.status.disconnected,.status.dismissed{background:rgba(226,155,168,.08);color:var(--danger);border-color:rgba(226,155,168,.18)}.status.failed:before,.status.cancelled:before,.status.disconnected:before,.status.dismissed:before{background:var(--danger)}
3764
4292
  .tag{display:inline-block;border:1px solid var(--border);border-radius:4px;padding:3px 6px;font:8px var(--mono);color:var(--text-secondary);margin:2px 4px 2px 0;background:var(--bg-surface)}.link{color:var(--accent);text-decoration:none;font-weight:500}.link:hover{text-decoration:underline;text-decoration-color:var(--accent-line)}.inline-icon{display:inline-flex;align-items:center;gap:5px}.inline-icon .icon{width:12px;height:12px}.empty{padding:38px 22px;text-align:center;color:var(--text-secondary);font-size:12px;line-height:1.6}.empty strong{display:block;color:var(--text-primary);font-size:14px;font-weight:500;margin-bottom:5px}.filters{display:flex;gap:8px;margin-bottom:14px}.filters input,.filters select{background:var(--bg-elevated);border:1px solid var(--border);border-radius:7px;padding:10px 12px;font-size:12px}.filters input{min-width:280px}
3765
4293
  .definition-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-bottom:16px}.definition-card{display:block;color:inherit;text-decoration:none;background:var(--bg-elevated);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);padding:19px;min-height:196px;position:relative;overflow:hidden;transition:transform .2s ease,border-color .2s ease,background .2s}.definition-card:before{content:"";position:absolute;inset:0;opacity:0;background:radial-gradient(110% 80% at 100% 0%,var(--accent-dim),transparent 60%);transition:opacity .2s}.definition-card:hover{transform:translateY(-2px);border-color:var(--accent-line);background:var(--bg-surface)}.definition-card:hover:before{opacity:1}.definition-icon{width:31px;height:31px;display:grid;place-items:center;border:1px solid var(--accent-line);border-radius:8px;color:var(--accent);background:var(--accent-dim);margin-bottom:15px;position:relative}.definition-icon .icon{width:17px;height:17px}.definition-open{position:absolute;right:17px;top:17px;color:var(--text-tertiary);transition:color .18s,transform .18s}.definition-open .icon{width:14px;height:14px}.definition-card:hover .definition-open{color:var(--accent);transform:translate(1px,-1px)}.definition-card h3{margin:0;font-size:17px;font-weight:500;position:relative}.definition-card p{color:var(--text-secondary);font-size:12px;font-weight:300;line-height:1.6;margin:9px 0 18px;position:relative}.definition-meta{position:absolute;left:19px;right:19px;bottom:17px;display:flex;justify-content:space-between;align-items:end}.definition-meta b{font:9px var(--mono);color:var(--accent-soft)}
3766
4294
  .cap-list{display:flex;flex-wrap:wrap;gap:6px}.cap{border:1px solid var(--border);background:var(--bg-surface);border-radius:5px;padding:7px 9px;font:9px var(--mono);margin-bottom:6px;color:var(--text-secondary)}.detail-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));border:1px solid var(--border-subtle);border-radius:var(--radius);background:var(--bg-elevated);margin-bottom:14px;overflow:hidden}.detail-strip>div{padding:15px;border-right:1px solid var(--border-subtle);min-width:0}.detail-strip>div:last-child{border-right:0}.detail-strip label{display:block;color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.1em}.detail-strip strong{display:block;margin-top:8px;font-size:12px;font-weight:500;overflow-wrap:anywhere}.json{margin:0;background:#070A09;color:var(--text-secondary);border:1px solid var(--border-subtle);border-radius:7px;padding:14px;font:9px/1.65 var(--mono);white-space:pre-wrap;overflow:auto;max-height:360px}.summary-box{font-size:12px;line-height:1.65;overflow-wrap:anywhere}.summary-box dl{display:grid;grid-template-columns:130px 1fr;gap:9px 14px;margin:0}.summary-box dt{color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.08em}.summary-box dd{margin:0;color:var(--text-secondary)}
@@ -3837,6 +4365,13 @@ tr.cursor,.definition-card.cursor{outline:1px solid var(--accent-soft);outline-o
3837
4365
  .live-chip.pulse i{animation:live-ping .7s ease-out}
3838
4366
  @keyframes live-ping{0%{transform:scale(1);opacity:1}45%{transform:scale(2.1);opacity:.45}100%{transform:scale(1);opacity:1}}
3839
4367
 
4368
+ /* ---- Effect approvals. One warm decision rail breaks the cool runtime surface. ---- */
4369
+ .approval-rail{position:relative;border:1px solid rgba(228,184,121,.28);border-left:3px solid var(--warn);border-radius:var(--radius);background:linear-gradient(100deg,rgba(228,184,121,.075),rgba(15,22,19,.96) 38%);margin-bottom:16px;overflow:hidden;box-shadow:0 16px 36px rgba(0,0,0,.16)}
4370
+ .approval-rail>header{display:flex;align-items:flex-start;gap:20px;padding:16px 18px;border-bottom:1px solid rgba(228,184,121,.16)}
4371
+ .approval-rail h2{margin:4px 0 0;font-size:16px;font-weight:500;letter-spacing:-.015em}.approval-rail header p{margin:5px 0 0;color:var(--text-secondary);font-size:11px;line-height:1.5}.approval-kicker{font:8px var(--mono);text-transform:uppercase;letter-spacing:.15em;color:var(--warn)}.approval-count{margin-left:auto;border:1px solid rgba(228,184,121,.24);border-radius:999px;padding:4px 8px;color:var(--warn);font:8px var(--mono);text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}
4372
+ .approval-list{display:grid}.approval-item{padding:16px 18px;border-top:1px solid var(--border-subtle)}.approval-item:first-child{border-top:0}.approval-context{display:flex;align-items:center;gap:10px;margin-bottom:10px}.approval-tool{font:10px var(--mono);color:var(--text-primary);background:rgba(228,184,121,.08);border:1px solid rgba(228,184,121,.18);border-radius:5px;padding:5px 8px}.approval-run{margin-left:auto;color:var(--text-secondary);font:9px var(--mono);text-decoration:none}.approval-run:hover{color:var(--warn)}
4373
+ .approval-input{margin:0;max-height:220px;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid var(--border-subtle);border-radius:7px;background:#070A09;padding:12px;color:var(--text-secondary);font:9px/1.65 var(--mono)}.approval-foot{display:flex;align-items:center;gap:16px;margin-top:12px;color:var(--text-tertiary);font:9px var(--mono)}.approval-actions{display:flex;gap:8px;margin-left:auto}.button.approve{border-color:var(--warn);background:var(--warn);color:#14110A;font-weight:600}.button.approve:hover{border-color:#F0C98D;background:#F0C98D;transform:translateY(-1px)}.approval-actions button:disabled{opacity:.55;cursor:wait;transform:none}
4374
+
3840
4375
  /* ---- Event trace ---- */
3841
4376
  .event-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 15px;border-bottom:1px solid var(--border-subtle)}
3842
4377
  .event-controls-right{display:flex;align-items:center;gap:12px;margin-left:auto;flex-wrap:wrap}
@@ -3869,12 +4404,39 @@ tr.cursor,.definition-card.cursor{outline:1px solid var(--accent-soft);outline-o
3869
4404
  .button.danger{color:var(--danger);border-color:rgba(226,155,168,.3)}
3870
4405
  .button.danger:hover{background:rgba(226,155,168,.09);border-color:var(--danger)}
3871
4406
 
4407
+ /* ---- WebChat. A focused conversation surface backed by native Glove history. ---- */
4408
+ .content.chat-content{max-width:none;height:calc(100vh - 72px);padding:0;overflow:hidden}
4409
+ .content.chat-content:before{display:none}
4410
+ .chat-shell{display:grid;grid-template-columns:290px minmax(0,1fr);height:100%;background:var(--bg)}
4411
+ .session-rail{min-width:0;border-right:1px solid var(--border-subtle);background:rgba(15,22,19,.7);display:flex;flex-direction:column;overflow:hidden}
4412
+ .session-head{height:76px;padding:18px;display:flex;align-items:center;border-bottom:1px solid var(--border-subtle)}
4413
+ .session-head>div{min-width:0}.session-head .eyebrow{font-size:8px;margin:0 0 5px}.session-head h1{margin:0;font-size:18px;font-weight:500;letter-spacing:-.02em}.session-head .button{margin-left:auto;color:var(--accent);background:var(--accent-dim);border-color:var(--accent-line)}
4414
+ .session-agent{padding:14px 15px 8px}.session-agent>span{display:block;margin-bottom:6px;color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase;letter-spacing:.1em}.session-agent select{width:100%;border:1px solid var(--border);background:var(--bg);border-radius:7px;padding:9px 10px;font-size:11px}
4415
+ .session-search{position:relative;margin:4px 15px 10px;display:flex;align-items:center;color:var(--text-tertiary)}.session-search>.icon{position:absolute;left:10px;width:13px;height:13px}.session-search input{width:100%;border:1px solid var(--border-subtle);background:var(--bg);border-radius:7px;padding:9px 10px 9px 31px;font-size:11px}.session-search input::placeholder{color:var(--text-tertiary)}
4416
+ .session-list{padding:2px 8px 18px;overflow:auto;min-height:0}.chat-session{position:relative;width:100%;border:1px solid transparent;background:transparent;color:var(--text-secondary);padding:11px 12px;border-radius:7px;text-align:left;cursor:pointer;margin:1px 0}.chat-session:hover{background:var(--accent-glow);color:var(--text-primary)}.chat-session.active{background:var(--accent-dim);border-color:var(--accent-line);color:var(--text-primary)}.chat-session.active:before{content:"";position:absolute;left:0;top:10px;bottom:10px;width:2px;background:var(--accent)}.chat-session span{display:block;font-size:12px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-session small{display:block;margin-top:5px;color:var(--text-tertiary);font:8px var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
4417
+ .conversation-pane{min-width:0;min-height:0;display:flex;flex-direction:column;background:radial-gradient(70% 38% at 50% 0%,var(--accent-glow),transparent 75%)}
4418
+ .conversation-head{height:76px;flex:0 0 auto;border-bottom:1px solid var(--border-subtle);padding:14px 22px;display:flex;align-items:center;gap:20px;background:rgba(8,11,10,.5)}.conversation-head>div:first-child{min-width:0}.conversation-kicker{display:block;color:var(--accent-soft);font:8px var(--mono);text-transform:uppercase;letter-spacing:.1em;margin-bottom:5px}.conversation-head h2{margin:0;font-size:17px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.conversation-meta{margin-left:auto;display:flex;align-items:center;gap:10px;color:var(--text-tertiary);font:8px var(--mono);white-space:nowrap}.conversation-rename{border:0;background:transparent;color:var(--text-tertiary);padding:5px 7px;border-radius:5px;font:8px var(--mono);cursor:pointer}.conversation-rename:hover{color:var(--accent);background:var(--accent-dim)}
4419
+ .chat-store-warning{flex:0 0 auto;border-bottom:1px solid rgba(228,184,121,.22);background:rgba(228,184,121,.07);color:var(--warn);padding:9px 20px;font-size:10px;text-align:center}
4420
+ .message-scroll{flex:1;min-height:0;overflow:auto;padding:32px max(24px,calc((100% - 920px)/2)) 22px;scrollbar-gutter:stable}
4421
+ .load-history{display:flex;align-items:center;justify-content:center;gap:10px;margin:-12px auto 24px}.load-history .button{font-size:10px}.load-history>span{color:var(--text-tertiary);font:8px var(--mono)}
4422
+ .chat-message{display:flex;align-items:flex-start;gap:12px;margin:0 auto 24px;max-width:920px}.chat-message.user{flex-direction:row-reverse}.message-avatar{width:29px;height:29px;flex:0 0 auto;border:1px solid var(--border);border-radius:8px;background:var(--bg-surface);color:var(--text-secondary);display:grid;place-items:center;font:10px var(--mono)}.message-avatar .icon{width:16px;height:16px;color:var(--accent)}.chat-message.user .message-avatar{background:var(--accent);border-color:var(--accent);color:var(--bg);font-weight:700}
4423
+ .message-body{min-width:0;max-width:min(780px,82%)}.message-body>header{display:flex;align-items:center;gap:8px;margin:0 2px 6px}.message-body>header strong{font-size:11px;font-weight:600}.message-body>header span{color:var(--text-tertiary);font:8px var(--mono);text-transform:uppercase}.chat-message.user .message-body>header{justify-content:flex-end}.message-text{white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid var(--border-subtle);background:var(--bg-elevated);border-radius:4px 12px 12px 12px;padding:12px 14px;color:var(--text-primary);font-size:13px;font-weight:300;line-height:1.62}.chat-message.user .message-text{border-color:var(--accent-line);background:var(--accent-dim);border-radius:12px 4px 12px 12px}.chat-message.optimistic{opacity:.65}
4424
+ .chat-message.tool .message-avatar .icon{color:var(--network)}.chat-message.tool .message-body>header strong{color:var(--network)}
4425
+ .message-attachments{display:flex;flex-wrap:wrap;gap:7px;margin-top:8px}.message-attachments>span{display:grid;grid-template-columns:18px minmax(0,1fr);column-gap:7px;align-items:center;min-width:160px;border:1px solid var(--border);background:var(--bg-surface);border-radius:7px;padding:8px 10px}.message-attachments .icon{grid-row:1/3;color:var(--accent);width:16px;height:16px}.message-attachments b{font-size:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.message-attachments small{color:var(--text-tertiary);font:8px var(--mono)}
4426
+ .message-tools{margin-top:7px;border:1px solid var(--border-subtle);border-radius:7px;background:#070A09;overflow:hidden}.message-tools summary{padding:8px 10px;color:var(--network);font:9px var(--mono);cursor:pointer}.message-tools pre{margin:0;border-top:1px solid var(--border-subtle);padding:10px;max-height:260px;overflow:auto;color:var(--text-secondary);white-space:pre-wrap;font:8px/1.55 var(--mono)}
4427
+ .chat-progress{flex:0 0 auto;margin:0 max(24px,calc((100% - 920px)/2)) 10px;border:1px solid var(--accent-line);background:linear-gradient(90deg,var(--accent-dim),var(--bg-elevated));border-radius:9px;padding:11px 13px;display:flex;align-items:center;gap:14px}.chat-progress>div:first-child{display:grid;grid-template-columns:17px minmax(0,1fr);align-items:center;min-width:0}.chat-progress .pulse-dot{grid-row:1/3}.chat-progress strong{font-size:11px}.chat-progress small{margin-top:2px;color:var(--text-tertiary);font:8px var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-progress-actions{display:flex;gap:7px;margin-left:auto}.chat-progress .button{padding:7px 9px;font-size:10px}
4428
+ .chat-composer{flex:0 0 auto;margin:0 max(24px,calc((100% - 920px)/2)) 20px;border:1px solid var(--border-strong);background:var(--bg-elevated);border-radius:11px;box-shadow:0 18px 42px rgba(0,0,0,.24);overflow:hidden}.chat-composer textarea{display:block;width:100%;min-height:56px;max-height:180px;resize:vertical;border:0;background:transparent;padding:14px 15px 8px;font-size:13px;line-height:1.5}.chat-composer textarea:focus{outline:0}.composer-actions{display:flex;align-items:center;gap:10px;padding:6px 8px 8px}.composer-actions>span{color:var(--text-tertiary);font:8px var(--mono)}.composer-actions>.button{margin-left:auto}.attach-button{width:31px;height:31px;border:1px solid var(--border);border-radius:7px;color:var(--text-secondary);display:grid;place-items:center;cursor:pointer}.attach-button:hover{border-color:var(--border-strong);color:var(--accent);background:var(--accent-dim)}.attach-button .icon{width:15px;height:15px}.attach-button input{display:none}
4429
+ .selected-files{display:flex;flex-wrap:wrap;gap:5px;padding:8px 10px 0}.selected-files:empty{display:none}.selected-files>span{display:flex;align-items:center;gap:6px;max-width:260px;border:1px solid var(--border);border-radius:6px;background:var(--bg-surface);padding:5px 6px;color:var(--text-secondary)}.selected-files .icon{width:12px;height:12px;color:var(--accent)}.selected-files b{font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.selected-files small{color:var(--text-tertiary);font:8px var(--mono);white-space:nowrap}.selected-files button{border:0;background:transparent;color:var(--text-tertiary);padding:2px;cursor:pointer}.selected-files button .icon{color:inherit;width:10px;height:10px}.chat-composer .form-error{margin:0;border-width:1px 0 0;border-radius:0}
4430
+ .chat-empty{margin:auto;max-width:430px;padding:40px 24px;text-align:center}.chat-empty.compact{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center}.chat-empty-icon{width:36px;height:36px;color:var(--accent);margin:0 auto 17px}.chat-empty h2{margin:0;font-size:20px;font-weight:400;letter-spacing:-.02em}.chat-empty p{margin:9px 0 20px;color:var(--text-secondary);font-size:12px;line-height:1.6}.empty-action{text-align:center;margin-top:-24px}
4431
+
3872
4432
  @media(max-width:760px){
3873
4433
  .toolbar-right{margin-left:0;width:100%}
3874
4434
  .search-field,.search-field input{width:100%;min-width:0}
3875
4435
  .event-toggle{grid-template-columns:60px 54px minmax(0,1fr) auto!important}
3876
4436
  .event-toggle .event-category{display:none}
4437
+ .approval-rail>header,.approval-foot{display:block}.approval-count{display:inline-flex;margin:10px 0 0}.approval-context{align-items:flex-start}.approval-run{margin-left:0}.approval-actions{margin:12px 0 0}.approval-actions .button{flex:1}
3877
4438
  .hint{display:none}
4439
+ .content.chat-content{height:calc(100vh - 72px);padding:0}.chat-shell{grid-template-columns:1fr;grid-template-rows:auto minmax(0,1fr)}.session-rail{max-height:235px;border-right:0;border-bottom:1px solid var(--border-subtle)}.session-head{height:58px;padding:10px 14px}.session-head .eyebrow{display:none}.session-head h1{font-size:15px}.session-agent{padding:8px 12px 4px}.session-search{margin:4px 12px 7px}.session-list{display:flex;gap:5px;padding:2px 10px 10px;overflow:auto}.chat-session{min-width:180px;margin:0}.conversation-head{height:62px;padding:10px 14px}.conversation-meta span:not(.status){display:none}.message-scroll{padding:20px 13px}.message-body{max-width:88%}.chat-progress{margin:0 10px 8px}.chat-progress-actions a{display:none}.chat-composer{margin:0 10px 10px}.composer-actions>span{display:none}
3878
4440
  }
3879
4441
  @media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}}
3880
4442
  `;
@@ -3883,32 +4445,42 @@ tr.cursor,.definition-card.cursor{outline:1px solid var(--accent-soft);outline-o
3883
4445
  var NAV_ICONS = {
3884
4446
  overview: renderPhosphorIcon("overview"),
3885
4447
  agents: renderPhosphorIcon("agent"),
4448
+ chat: renderPhosphorIcon("chat"),
3886
4449
  runs: renderPhosphorIcon("runs"),
3887
4450
  automations: renderPhosphorIcon("automations"),
3888
4451
  integrations: renderPhosphorIcon("integrations"),
3889
4452
  workspaces: renderPhosphorIcon("workspaces")
3890
4453
  };
3891
- function renderDashboard() {
4454
+ function escapeHtml(value) {
4455
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4456
+ }
4457
+ function renderDashboard(branding = {}) {
4458
+ const name = escapeHtml(branding.name?.trim() || "Glove Foundry");
4459
+ const label = escapeHtml(branding.label?.trim() || "Runtime inspector");
4460
+ const descriptor = escapeHtml(branding.descriptor?.trim() || "Inspector");
4461
+ const accent = /^#[0-9a-f]{6}$/i.test(branding.accent ?? "") ? branding.accent : void 0;
3892
4462
  return `<!doctype html>
3893
4463
  <html lang="en">
3894
4464
  <head>
3895
4465
  <meta charset="utf-8" />
3896
4466
  <meta name="viewport" content="width=device-width,initial-scale=1" />
3897
4467
  <meta name="color-scheme" content="dark" />
3898
- <title>Glove Foundry \xB7 Inspector</title>
4468
+ <title>${name} \xB7 ${descriptor}</title>
3899
4469
  <style>${DASHBOARD_STYLES}</style>
4470
+ ${accent ? `<style>:root{--accent:${accent};--accent-soft:${accent};--accent-strong:${accent}}</style>` : ""}
3900
4471
  </head>
3901
4472
  <body>
3902
4473
  <div class="app">
3903
4474
  <aside class="sidebar" id="sidebar">
3904
4475
  <a class="brand" href="/" data-link>
3905
4476
  ${renderGloveMark()}
3906
- <span><strong>Glove Foundry</strong><small>Runtime inspector</small></span>
4477
+ <span><strong>${name}</strong><small>${label}</small></span>
3907
4478
  </a>
3908
4479
  <nav class="nav">
3909
4480
  <div class="nav-label">Inspect</div>
3910
4481
  <a href="/" data-link data-nav="overview"><span class="nav-icon">${NAV_ICONS.overview}</span>Overview</a>
3911
4482
  <a href="/agents" data-link data-nav="agents"><span class="nav-icon">${NAV_ICONS.agents}</span>Agents</a>
4483
+ <a href="/chat" data-link data-nav="chat"><span class="nav-icon">${NAV_ICONS.chat}</span>Chat</a>
3912
4484
  <a href="/runs" data-link data-nav="runs"><span class="nav-icon">${NAV_ICONS.runs}</span>Runs</a>
3913
4485
  <a href="/automations" data-link data-nav="automations"><span class="nav-icon">${NAV_ICONS.automations}</span>Automations</a>
3914
4486
  <a href="/integrations" data-link data-nav="integrations"><span class="nav-icon">${NAV_ICONS.integrations}</span>Integrations</a>
@@ -3916,7 +4488,7 @@ function renderDashboard() {
3916
4488
  </nav>
3917
4489
  <div class="sidebar-foot">
3918
4490
  <div class="runtime-state" id="runtime-state"><i></i><span>Connecting to runtime\u2026</span></div>
3919
- <button class="shortcut" id="open-search"><span class="shortcut-label">${renderPhosphorIcon("search")}Search Foundry</span><kbd>\u2318K</kbd></button>
4491
+ <button class="shortcut" id="open-search"><span class="shortcut-label">${renderPhosphorIcon("search")}Search ${name}</span><kbd>\u2318K</kbd></button>
3920
4492
  </div>
3921
4493
  </aside>
3922
4494
  <section class="main">
@@ -3952,7 +4524,7 @@ function renderDashboard() {
3952
4524
 
3953
4525
  <div class="search-modal" id="search-modal">
3954
4526
  <div class="search-box" role="dialog" aria-modal="true">
3955
- <div class="search-input-wrap">${renderPhosphorIcon("search")}<input id="search-input" aria-label="Search Foundry" placeholder="Search pages, agents, instances, and runs\u2026" autocomplete="off" /></div>
4527
+ <div class="search-input-wrap">${renderPhosphorIcon("search")}<input id="search-input" aria-label="Search ${name}" placeholder="Search pages, agents, instances, and runs\u2026" autocomplete="off" /></div>
3956
4528
  <div class="search-results" id="search-results"></div>
3957
4529
  </div>
3958
4530
  </div>
@@ -3964,6 +4536,10 @@ function renderDashboard() {
3964
4536
 
3965
4537
  // src/server.ts
3966
4538
  var MAX_BODY_BYTES = 1024 * 1024;
4539
+ function isLoopbackHost(host) {
4540
+ const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
4541
+ return normalized === "localhost" || normalized === "localhost." || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
4542
+ }
3967
4543
  function isFoundryMessageInput(value) {
3968
4544
  if (typeof value === "string") return true;
3969
4545
  if (!Array.isArray(value)) return false;
@@ -3974,6 +4550,7 @@ function isFoundryMessageInput(value) {
3974
4550
  return false;
3975
4551
  }
3976
4552
  if (candidate.text !== void 0 && typeof candidate.text !== "string") return false;
4553
+ if (candidate.name !== void 0 && typeof candidate.name !== "string") return false;
3977
4554
  if (candidate.source === void 0) return true;
3978
4555
  if (!candidate.source || typeof candidate.source !== "object") return false;
3979
4556
  const source = candidate.source;
@@ -3994,14 +4571,14 @@ function json(response, status, value) {
3994
4571
  });
3995
4572
  response.end(JSON.stringify(value));
3996
4573
  }
3997
- async function readJson(request) {
4574
+ async function readJson(request, maxBytes = MAX_BODY_BYTES) {
3998
4575
  const chunks = [];
3999
4576
  let size = 0;
4000
4577
  for await (const chunk of request) {
4001
4578
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4002
4579
  size += buffer.length;
4003
- if (size > MAX_BODY_BYTES) {
4004
- throw new RequestError(413, "Request body exceeds the 1 MB limit.");
4580
+ if (size > maxBytes) {
4581
+ throw new RequestError(413, `Request body exceeds the ${maxBytes}-byte limit.`);
4005
4582
  }
4006
4583
  chunks.push(buffer);
4007
4584
  }
@@ -4088,10 +4665,216 @@ function foundryRequestFrom(value) {
4088
4665
  }
4089
4666
  return body;
4090
4667
  }
4668
+ function openAiChatRequestFrom(value) {
4669
+ if (!value || typeof value !== "object") {
4670
+ throw new RequestError(400, "Chat completion body must be an object.");
4671
+ }
4672
+ const body = value;
4673
+ if (typeof body.model !== "string" || !body.model) {
4674
+ throw new RequestError(400, "model is required and must name a Foundry agent instance.");
4675
+ }
4676
+ if (!Array.isArray(body.messages) || body.messages.length === 0) {
4677
+ throw new RequestError(400, "messages must be a non-empty array.");
4678
+ }
4679
+ const messages = body.messages.map((item) => {
4680
+ if (!item || typeof item !== "object") throw new RequestError(400, "Every message must be an object.");
4681
+ const message = item;
4682
+ if (!["system", "user", "assistant", "tool"].includes(String(message.role))) {
4683
+ throw new RequestError(400, "Every message must have a supported role.");
4684
+ }
4685
+ if (typeof message.content !== "string" && !Array.isArray(message.content)) {
4686
+ throw new RequestError(400, "Every message must have string or array content.");
4687
+ }
4688
+ return message;
4689
+ });
4690
+ if (![...messages].reverse().some((message) => message.role === "user")) {
4691
+ throw new RequestError(400, "messages must include a user message.");
4692
+ }
4693
+ return {
4694
+ model: body.model,
4695
+ messages,
4696
+ stream: body.stream === true,
4697
+ ...typeof body.user === "string" && body.user ? { user: body.user } : {},
4698
+ ...typeof body.conversation_id === "string" && body.conversation_id ? { conversationId: body.conversation_id } : {}
4699
+ };
4700
+ }
4701
+ function openAiPartText(message) {
4702
+ if (typeof message.content === "string") return message.content;
4703
+ return message.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
4704
+ }
4705
+ function openAiCurrentInput(messages, includePrior) {
4706
+ let userIndex = messages.length - 1;
4707
+ while (userIndex >= 0 && messages[userIndex]?.role !== "user") userIndex--;
4708
+ const current = messages[userIndex];
4709
+ const prior = includePrior ? messages.slice(0, userIndex).map(
4710
+ (message) => `<message role="${message.role}">
4711
+ ${openAiPartText(message)}
4712
+ </message>`
4713
+ ).join("\n") : "";
4714
+ if (typeof current.content === "string") {
4715
+ return prior ? `${prior}
4716
+ <current-user-message>
4717
+ ${current.content}
4718
+ </current-user-message>` : current.content;
4719
+ }
4720
+ const parts = [];
4721
+ if (prior) parts.push({ type: "text", text: prior });
4722
+ for (const part of current.content) {
4723
+ if (part.type === "text" && typeof part.text === "string") {
4724
+ parts.push({ type: "text", text: part.text });
4725
+ continue;
4726
+ }
4727
+ if (part.type === "image_url") {
4728
+ const url = typeof part.image_url === "string" ? part.image_url : part.image_url?.url;
4729
+ if (url) parts.push({ type: "image", source: { type: "url", media_type: "image/*", url } });
4730
+ }
4731
+ }
4732
+ if (parts.length === 0) throw new RequestError(400, "The current user message has no supported content.");
4733
+ return parts;
4734
+ }
4735
+ function openAiOutput(value) {
4736
+ if (typeof value === "string") return value;
4737
+ if (value === void 0) return "";
4738
+ return JSON.stringify(value);
4739
+ }
4740
+ function responseContentPart(value) {
4741
+ if (!value || typeof value !== "object") return null;
4742
+ const part = value;
4743
+ if ((part.type === "input_text" || part.type === "text") && typeof part.text === "string") {
4744
+ return { type: "text", text: part.text };
4745
+ }
4746
+ if (part.type === "input_image" || part.type === "image_url") {
4747
+ const image = typeof part.image_url === "string" ? part.image_url : part.image_url && typeof part.image_url === "object" ? part.image_url.url : void 0;
4748
+ if (typeof image === "string" && image) {
4749
+ return { type: "image", source: { type: "url", media_type: "image/*", url: image } };
4750
+ }
4751
+ }
4752
+ if (part.type === "input_file") {
4753
+ if (typeof part.file_url === "string" && part.file_url) {
4754
+ return {
4755
+ type: "document",
4756
+ source: { type: "url", media_type: "application/octet-stream", url: part.file_url }
4757
+ };
4758
+ }
4759
+ if (typeof part.file_data === "string" && part.file_data) {
4760
+ const dataUrl = part.file_data.match(/^data:([^;,]+);base64,(.+)$/s);
4761
+ return {
4762
+ type: "document",
4763
+ source: dataUrl ? { type: "base64", media_type: dataUrl[1], data: dataUrl[2] } : { type: "base64", media_type: "application/octet-stream", data: part.file_data }
4764
+ };
4765
+ }
4766
+ }
4767
+ return null;
4768
+ }
4769
+ function responsesInput(value, instructions) {
4770
+ const prefix = typeof instructions === "string" && instructions.trim() ? `<instructions>
4771
+ ${instructions}
4772
+ </instructions>` : "";
4773
+ if (typeof value === "string") return prefix ? `${prefix}
4774
+ ${value}` : value;
4775
+ if (!Array.isArray(value) || value.length === 0) {
4776
+ throw new RequestError(400, "input must be a non-empty string or array.");
4777
+ }
4778
+ const messages = value.filter((item) => Boolean(item && typeof item === "object"));
4779
+ let userIndex = messages.length - 1;
4780
+ while (userIndex >= 0 && messages[userIndex]?.role !== "user") userIndex--;
4781
+ const selected = userIndex >= 0 ? messages[userIndex] : messages[messages.length - 1];
4782
+ const prior = messages.slice(0, Math.max(userIndex, 0)).map((item) => {
4783
+ const content2 = typeof item.content === "string" ? item.content : Array.isArray(item.content) ? item.content.map((part) => {
4784
+ const normalized = responseContentPart(part);
4785
+ return normalized?.type === "text" ? normalized.text : normalized ? `[${normalized.type}]` : "";
4786
+ }).filter(Boolean).join("\n") : "";
4787
+ return `<message role="${typeof item.role === "string" ? item.role : "user"}">
4788
+ ${content2}
4789
+ </message>`;
4790
+ }).join("\n");
4791
+ const content = selected.content ?? selected;
4792
+ const parts = typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content.map(responseContentPart).filter((part) => part !== null) : [responseContentPart(content)].filter((part) => part !== null);
4793
+ const context = [prefix, prior].filter(Boolean).join("\n");
4794
+ if (context) parts.unshift({ type: "text", text: context });
4795
+ if (parts.length === 0) throw new RequestError(400, "input has no supported text, image, or file content.");
4796
+ return parts;
4797
+ }
4798
+ function openAiResponsesRequestFrom(value) {
4799
+ if (!value || typeof value !== "object") {
4800
+ throw new RequestError(400, "Responses body must be an object.");
4801
+ }
4802
+ const body = value;
4803
+ if (typeof body.model !== "string" || !body.model) {
4804
+ throw new RequestError(400, "model is required and must name a Foundry agent instance.");
4805
+ }
4806
+ return {
4807
+ model: body.model,
4808
+ input: responsesInput(body.input, body.instructions),
4809
+ stream: body.stream === true,
4810
+ store: body.store !== false,
4811
+ ...typeof body.instructions === "string" ? { instructions: body.instructions } : {},
4812
+ ...typeof body.user === "string" && body.user ? { user: body.user } : {},
4813
+ ...typeof body.conversation_id === "string" && body.conversation_id ? { conversationId: body.conversation_id } : {},
4814
+ ...body.context && typeof body.context === "object" ? { context: body.context } : {}
4815
+ };
4816
+ }
4817
+ function foundryControlRunRequestFrom(value) {
4818
+ if (!value || typeof value !== "object") {
4819
+ throw new RequestError(400, "Run body must be an object.");
4820
+ }
4821
+ const body = value;
4822
+ const model = typeof body.model === "string" ? body.model : body.agent_id;
4823
+ const input = body.input ?? body.message;
4824
+ if (typeof model !== "string" || !model) {
4825
+ throw new RequestError(400, "model or agent_id is required.");
4826
+ }
4827
+ if (!isFoundryMessageInput(input)) {
4828
+ throw new RequestError(400, "input or message must be a supported Foundry message.");
4829
+ }
4830
+ return {
4831
+ model,
4832
+ input,
4833
+ ...typeof body.conversation_id === "string" && body.conversation_id ? { conversationId: body.conversation_id } : {},
4834
+ ...typeof body.user === "string" && body.user ? { user: body.user } : {},
4835
+ ...body.context && typeof body.context === "object" ? { context: body.context } : {}
4836
+ };
4837
+ }
4838
+ function foundrySteerInputFrom(value) {
4839
+ if (!value || typeof value !== "object") {
4840
+ throw new RequestError(400, "Steering body must be an object.");
4841
+ }
4842
+ const body = value;
4843
+ const input = body.input ?? body.message ?? body.guidance;
4844
+ if (!isFoundryMessageInput(input)) {
4845
+ throw new RequestError(400, "input, message, or guidance must be a supported Foundry message.");
4846
+ }
4847
+ return input;
4848
+ }
4849
+ function approvalDecisionFrom(value) {
4850
+ if (!value || typeof value !== "object") {
4851
+ throw new RequestError(400, "Approval body must be an object.");
4852
+ }
4853
+ const decision = value.decision;
4854
+ if (decision !== "approve" && decision !== "deny") {
4855
+ throw new RequestError(400, 'decision must be "approve" or "deny".');
4856
+ }
4857
+ return decision;
4858
+ }
4859
+ function approvalFilter(url) {
4860
+ const runId = url.searchParams.get("runId") ?? url.searchParams.get("run_id") ?? void 0;
4861
+ const rawStatus = url.searchParams.get("status");
4862
+ const statuses = ["pending", "approved", "denied", "expired", "cancelled"];
4863
+ if (rawStatus && !statuses.includes(rawStatus)) {
4864
+ throw new RequestError(400, "Unknown approval status filter.");
4865
+ }
4866
+ return {
4867
+ ...runId ? { runId } : {},
4868
+ ...rawStatus ? { status: rawStatus } : {}
4869
+ };
4870
+ }
4091
4871
  var FoundryServer = class {
4092
4872
  constructor(runtime, options = {}) {
4093
4873
  this.runtime = runtime;
4094
4874
  this.options = options;
4875
+ if (options.messageBodyBytes !== void 0 && (!Number.isInteger(options.messageBodyBytes) || options.messageBodyBytes < MAX_BODY_BYTES || options.messageBodyBytes > 128 * 1024 * 1024)) {
4876
+ throw new Error("Foundry messageBodyBytes must be an integer from 1048576 to 134217728.");
4877
+ }
4095
4878
  this.server = createServer((request, response) => {
4096
4879
  void this.handle(request, response);
4097
4880
  });
@@ -4102,6 +4885,11 @@ var FoundryServer = class {
4102
4885
  async listen() {
4103
4886
  const host = this.options.host ?? "127.0.0.1";
4104
4887
  const port = this.options.port ?? 4141;
4888
+ if (!isLoopbackHost(host) && !this.runtime.application.requestAuthorization) {
4889
+ throw new Error(
4890
+ `Foundry refuses to bind ${host} without application.requestAuthorization. Keep the runtime on loopback or provide a user-owned authorization adapter.`
4891
+ );
4892
+ }
4105
4893
  await new Promise((resolve3, reject) => {
4106
4894
  this.server.once("error", reject);
4107
4895
  this.server.listen(port, host, () => {
@@ -4120,19 +4908,46 @@ var FoundryServer = class {
4120
4908
  if (!this.server.listening) return;
4121
4909
  for (const stream of this.eventStreams) stream.end();
4122
4910
  this.eventStreams.clear();
4123
- await new Promise(
4124
- (resolve3, reject) => this.server.close((error) => error ? reject(error) : resolve3())
4125
- );
4911
+ await new Promise((resolve3, reject) => {
4912
+ this.server.close((error) => error ? reject(error) : resolve3());
4913
+ this.server.closeAllConnections();
4914
+ });
4126
4915
  this.addressInfo = null;
4127
4916
  }
4128
4917
  address() {
4129
4918
  return this.addressInfo;
4130
4919
  }
4920
+ async authorizeRequest(request, response, url, method) {
4921
+ const adapter = this.runtime.application.requestAuthorization;
4922
+ if (!adapter) return true;
4923
+ let authorized = false;
4924
+ try {
4925
+ authorized = await Effect6.runPromise(adapter.authorize({
4926
+ method,
4927
+ path: url.pathname,
4928
+ query: url.search,
4929
+ ...typeof request.headers.authorization === "string" ? { authorization: request.headers.authorization } : {},
4930
+ ...typeof request.headers.cookie === "string" ? { cookie: request.headers.cookie } : {},
4931
+ ...request.socket.remoteAddress ? { remoteAddress: request.socket.remoteAddress } : {}
4932
+ }));
4933
+ } catch {
4934
+ }
4935
+ if (authorized) return true;
4936
+ response.writeHead(401, {
4937
+ "content-type": "application/json; charset=utf-8",
4938
+ "cache-control": "no-store",
4939
+ "x-content-type-options": "nosniff",
4940
+ "www-authenticate": adapter.challenge ?? 'Bearer realm="Glove Foundry"'
4941
+ });
4942
+ response.end('{"error":"Foundry authorization is required."}');
4943
+ return false;
4944
+ }
4131
4945
  async handle(request, response) {
4132
4946
  try {
4133
4947
  const url = new URL(request.url ?? "/", "http://foundry.local");
4134
4948
  const method = request.method ?? "GET";
4135
- if (method === "GET" && !url.pathname.startsWith("/api/") && url.pathname !== "/health") {
4949
+ if (!await this.authorizeRequest(request, response, url, method)) return;
4950
+ if (method === "GET" && !url.pathname.startsWith("/api/") && !url.pathname.startsWith("/v1/") && url.pathname !== "/health" && url.pathname !== "/health/detailed") {
4136
4951
  response.writeHead(200, {
4137
4952
  "content-type": "text/html; charset=utf-8",
4138
4953
  "cache-control": "no-store",
@@ -4150,13 +4965,127 @@ var FoundryServer = class {
4150
4965
  "x-content-type-options": "nosniff",
4151
4966
  "x-frame-options": "DENY"
4152
4967
  });
4153
- response.end(renderDashboard());
4968
+ response.end(renderDashboard(this.options.branding));
4154
4969
  return;
4155
4970
  }
4156
4971
  if (method === "GET" && url.pathname === "/health") {
4157
4972
  json(response, 200, await this.runtime.health());
4158
4973
  return;
4159
4974
  }
4975
+ if (method === "GET" && url.pathname === "/health/detailed") {
4976
+ const health = await this.runtime.health();
4977
+ json(response, 200, {
4978
+ ...health,
4979
+ connections: this.runtime.listApplicationConnections(),
4980
+ runs: await this.runtime.listRuns()
4981
+ });
4982
+ return;
4983
+ }
4984
+ if (method === "GET" && url.pathname === "/v1/models") {
4985
+ const instances = await this.runtime.listAgentInstances();
4986
+ json(response, 200, {
4987
+ object: "list",
4988
+ data: instances.map((instance) => ({
4989
+ id: instance.id,
4990
+ object: "model",
4991
+ created: Math.floor(Date.parse(instance.createdAt) / 1e3),
4992
+ owned_by: `glove-foundry:${instance.definitionId}`
4993
+ }))
4994
+ });
4995
+ return;
4996
+ }
4997
+ if (method === "POST" && url.pathname === "/v1/chat/completions") {
4998
+ await this.handleOpenAiChat(request, response, openAiChatRequestFrom(await readJson(request, this.options.messageBodyBytes)));
4999
+ return;
5000
+ }
5001
+ if (method === "POST" && url.pathname === "/v1/responses") {
5002
+ await this.handleOpenAiResponse(request, response, openAiResponsesRequestFrom(await readJson(request, this.options.messageBodyBytes)));
5003
+ return;
5004
+ }
5005
+ if (method === "GET" && url.pathname === "/v1/capabilities") {
5006
+ json(response, 200, {
5007
+ object: "foundry.capabilities",
5008
+ protocols: {
5009
+ native: true,
5010
+ openaiChatCompletions: { streaming: true, multimodalInput: true },
5011
+ openaiResponses: { streaming: true, multimodalInput: true },
5012
+ runControl: {
5013
+ create: true,
5014
+ status: true,
5015
+ events: true,
5016
+ stop: true,
5017
+ steer: true,
5018
+ steeringMode: "interrupt-and-restart"
5019
+ },
5020
+ approvals: { list: true, resolve: true, default: "deny" }
5021
+ },
5022
+ runtime: await this.runtime.health()
5023
+ });
5024
+ return;
5025
+ }
5026
+ if (method === "POST" && url.pathname === "/v1/runs") {
5027
+ const body = foundryControlRunRequestFrom(await readJson(request, this.options.messageBodyBytes));
5028
+ const agent = await this.resolveAgent(body.model);
5029
+ const conversation = await this.resolveConversation(
5030
+ request,
5031
+ agent,
5032
+ "control",
5033
+ body.conversationId ?? body.user
5034
+ );
5035
+ const run = await this.runtime.send(agent.id, conversation.id, body.input, {
5036
+ ...body.context ? { context: { ...body.context, protocol: "foundry-control" } } : {
5037
+ context: { protocol: "foundry-control" }
5038
+ }
5039
+ });
5040
+ response.setHeader("x-foundry-conversation-id", conversation.id);
5041
+ json(response, 202, run);
5042
+ return;
5043
+ }
5044
+ const v1RunEvents = url.pathname.match(/^\/v1\/runs\/([^/]+)\/events$/);
5045
+ if (v1RunEvents && method === "GET") {
5046
+ const runId = decodeURIComponent(v1RunEvents[1]);
5047
+ if (!await this.runtime.getRun(runId)) throw new RequestError(404, "Foundry run was not found.");
5048
+ const filter = { ...eventFilter(url), runId, limit: 5e3 };
5049
+ if (request.headers.accept?.includes("text/event-stream")) {
5050
+ this.streamEvents(request, response, filter, { closeOnTerminalRun: true });
5051
+ } else {
5052
+ json(response, 200, this.runtime.observability.list(filter));
5053
+ }
5054
+ return;
5055
+ }
5056
+ const v1RunStop = url.pathname.match(/^\/v1\/runs\/([^/]+)\/stop$/);
5057
+ if (v1RunStop && method === "POST") {
5058
+ const runId = decodeURIComponent(v1RunStop[1]);
5059
+ if (!await this.runtime.getRun(runId)) throw new RequestError(404, "Foundry run was not found.");
5060
+ json(response, 200, { id: runId, stopped: await this.runtime.cancel(runId) });
5061
+ return;
5062
+ }
5063
+ const v1RunSteer = url.pathname.match(/^\/v1\/runs\/([^/]+)\/steer$/);
5064
+ if (v1RunSteer && method === "POST") {
5065
+ const runId = decodeURIComponent(v1RunSteer[1]);
5066
+ if (!await this.runtime.getRun(runId)) throw new RequestError(404, "Foundry run was not found.");
5067
+ json(response, 202, await this.runtime.steer(runId, foundrySteerInputFrom(await readJson(request, this.options.messageBodyBytes))));
5068
+ return;
5069
+ }
5070
+ const v1Run = url.pathname.match(/^\/v1\/runs\/([^/]+)$/);
5071
+ if (v1Run && method === "GET") {
5072
+ const run = await this.runtime.getRun(decodeURIComponent(v1Run[1]));
5073
+ if (!run) throw new RequestError(404, "Foundry run was not found.");
5074
+ json(response, 200, run);
5075
+ return;
5076
+ }
5077
+ if (method === "GET" && url.pathname === "/v1/approvals") {
5078
+ json(response, 200, await this.runtime.listApprovals(approvalFilter(url)));
5079
+ return;
5080
+ }
5081
+ const v1Approval = url.pathname.match(/^\/v1\/approvals\/([^/]+)$/);
5082
+ if (v1Approval && method === "POST") {
5083
+ json(response, 200, await this.runtime.resolveApproval(
5084
+ decodeURIComponent(v1Approval[1]),
5085
+ approvalDecisionFrom(await readJson(request))
5086
+ ));
5087
+ return;
5088
+ }
4160
5089
  if (method === "GET" && url.pathname === "/api/manifest") {
4161
5090
  const definitions = Object.fromEntries(
4162
5091
  this.runtime.agents.map((agent) => [
@@ -4275,9 +5204,55 @@ var FoundryServer = class {
4275
5204
  }));
4276
5205
  return;
4277
5206
  }
5207
+ const conversationItem = url.pathname.match(/^\/api\/conversations\/([^/]+)$/);
5208
+ if (conversationItem && method === "PATCH") {
5209
+ const body = await readJson(request);
5210
+ if (typeof body.agentId !== "string") throw new RequestError(400, "agentId is required.");
5211
+ if (body.title !== void 0 && typeof body.title !== "string") {
5212
+ throw new RequestError(400, "title must be a string.");
5213
+ }
5214
+ if (typeof body.title === "string" && (!body.title.trim() || body.title.trim().length > 120)) {
5215
+ throw new RequestError(400, "title must contain 1 to 120 characters.");
5216
+ }
5217
+ if (body.context !== void 0 && (!body.context || typeof body.context !== "object" || Array.isArray(body.context))) {
5218
+ throw new RequestError(400, "context must be an object.");
5219
+ }
5220
+ json(response, 200, await this.runtime.updateConversation(
5221
+ body.agentId,
5222
+ decodeURIComponent(conversationItem[1]),
5223
+ {
5224
+ ...typeof body.title === "string" ? { title: body.title } : {},
5225
+ ...body.context && typeof body.context === "object" ? { context: body.context } : {}
5226
+ }
5227
+ ));
5228
+ return;
5229
+ }
4278
5230
  const conversationMessage = url.pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
5231
+ if (conversationMessage && method === "GET") {
5232
+ const agentId = url.searchParams.get("agent");
5233
+ if (!agentId) throw new RequestError(400, "agent query is required.");
5234
+ const limitValue = url.searchParams.get("limit");
5235
+ const offsetValue = url.searchParams.get("offset");
5236
+ const limit = limitValue === null ? void 0 : Number(limitValue);
5237
+ const offset = offsetValue === null ? void 0 : Number(offsetValue);
5238
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1 || limit > 500)) {
5239
+ throw new RequestError(400, "limit must be an integer from 1 to 500.");
5240
+ }
5241
+ if (offset !== void 0 && (!Number.isInteger(offset) || offset < 0)) {
5242
+ throw new RequestError(400, "offset must be a non-negative integer.");
5243
+ }
5244
+ json(response, 200, await this.runtime.conversationTranscript(
5245
+ agentId,
5246
+ decodeURIComponent(conversationMessage[1]),
5247
+ {
5248
+ ...offset !== void 0 ? { offset } : {},
5249
+ ...limit !== void 0 ? { limit } : {}
5250
+ }
5251
+ ));
5252
+ return;
5253
+ }
4279
5254
  if (conversationMessage && method === "POST") {
4280
- const body = await readJson(request);
5255
+ const body = await readJson(request, this.options.messageBodyBytes);
4281
5256
  if (typeof body.agentId !== "string" || !isFoundryMessageInput(body.message)) {
4282
5257
  throw new RequestError(400, "agentId and message are required.");
4283
5258
  }
@@ -4497,6 +5472,18 @@ var FoundryServer = class {
4497
5472
  }
4498
5473
  return;
4499
5474
  }
5475
+ if (url.pathname === "/api/approvals" && method === "GET") {
5476
+ json(response, 200, await this.runtime.listApprovals(approvalFilter(url)));
5477
+ return;
5478
+ }
5479
+ const approvalMatch = url.pathname.match(/^\/api\/approvals\/([^/]+)$/);
5480
+ if (approvalMatch && method === "POST") {
5481
+ json(response, 200, await this.runtime.resolveApproval(
5482
+ decodeURIComponent(approvalMatch[1]),
5483
+ approvalDecisionFrom(await readJson(request))
5484
+ ));
5485
+ return;
5486
+ }
4500
5487
  const transmissionFire = url.pathname.match(/^\/api\/transmissions\/([^/]+)\/fire$/);
4501
5488
  if (transmissionFire && method === "POST") {
4502
5489
  const body = await readJson(request);
@@ -4507,6 +5494,8 @@ var FoundryServer = class {
4507
5494
  routeId: decodeURIComponent(transmissionFire[1]),
4508
5495
  eventId: body.eventId,
4509
5496
  threadKey: body.threadKey,
5497
+ ...typeof body.conversationKey === "string" ? { conversationKey: body.conversationKey } : {},
5498
+ ...body.conversationScope === "route" || body.conversationScope === "agent" ? { conversationScope: body.conversationScope } : {},
4510
5499
  raw: body.raw
4511
5500
  }));
4512
5501
  return;
@@ -4537,6 +5526,22 @@ var FoundryServer = class {
4537
5526
  });
4538
5527
  return;
4539
5528
  }
5529
+ const steerMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/steer$/);
5530
+ if (steerMatch && method === "POST") {
5531
+ const runId = decodeURIComponent(steerMatch[1]);
5532
+ if (!await this.runtime.getRun(runId)) {
5533
+ throw new RequestError(404, "Foundry run was not found.");
5534
+ }
5535
+ json(
5536
+ response,
5537
+ 202,
5538
+ await this.runtime.steer(
5539
+ runId,
5540
+ foundrySteerInputFrom(await readJson(request, this.options.messageBodyBytes))
5541
+ )
5542
+ );
5543
+ return;
5544
+ }
4540
5545
  const eventsMatch = url.pathname.match(/^\/api\/runs\/([^/]+)\/events$/);
4541
5546
  if (eventsMatch && method === "GET") {
4542
5547
  json(
@@ -4564,35 +5569,306 @@ var FoundryServer = class {
4564
5569
  else response.end();
4565
5570
  }
4566
5571
  }
4567
- streamEvents(request, response, filter) {
5572
+ async handleOpenAiChat(request, response, body) {
5573
+ const agent = await this.resolveAgent(body.model);
5574
+ const requestedConversation = request.headers["x-foundry-conversation-id"] ?? body.conversationId ?? body.user;
5575
+ const conversations = await this.runtime.listConversations(agent.id);
5576
+ const conversation = await this.resolveConversation(request, agent, "openai", requestedConversation);
5577
+ const existing = conversations.some((item) => item.id === conversation.id);
5578
+ const run = await this.runtime.send(
5579
+ agent.id,
5580
+ conversation.id,
5581
+ openAiCurrentInput(body.messages, !existing),
5582
+ { context: { protocol: "openai-chat-completions" } }
5583
+ );
5584
+ const completionId = `chatcmpl-${run.id}`;
5585
+ const created = Math.floor(Date.now() / 1e3);
5586
+ response.setHeader("x-foundry-conversation-id", conversation.id);
5587
+ if (!body.stream) {
5588
+ const completed = await this.runtime.waitForRun(run.id, { timeoutMs: run.timeoutMs });
5589
+ if (!completed || completed.status !== "completed") {
5590
+ throw new RequestError(502, completed?.error ?? "Foundry did not complete the chat request.");
5591
+ }
5592
+ json(response, 200, {
5593
+ id: completionId,
5594
+ object: "chat.completion",
5595
+ created,
5596
+ model: agent.id,
5597
+ choices: [{
5598
+ index: 0,
5599
+ message: { role: "assistant", content: openAiOutput(completed.output?.value) },
5600
+ finish_reason: "stop"
5601
+ }],
5602
+ system_fingerprint: "glove-foundry"
5603
+ });
5604
+ return;
5605
+ }
4568
5606
  response.writeHead(200, {
4569
5607
  "content-type": "text/event-stream; charset=utf-8",
4570
5608
  "cache-control": "no-cache, no-transform",
4571
5609
  connection: "keep-alive",
4572
- "x-accel-buffering": "no"
5610
+ "x-accel-buffering": "no",
5611
+ "x-foundry-conversation-id": conversation.id
4573
5612
  });
4574
5613
  this.eventStreams.add(response);
4575
- response.write(": glove-foundry\n\n");
4576
- for (const event of this.runtime.observability.list(filter)) {
4577
- response.write(`id: ${event.sequence}
4578
- data: ${JSON.stringify(event)}
5614
+ const seen = /* @__PURE__ */ new Set();
5615
+ let emittedText = false;
5616
+ const chunk = (delta, finishReason = null) => {
5617
+ response.write(`data: ${JSON.stringify({
5618
+ id: completionId,
5619
+ object: "chat.completion.chunk",
5620
+ created,
5621
+ model: agent.id,
5622
+ choices: [{ index: 0, delta, finish_reason: finishReason }]
5623
+ })}
4579
5624
 
4580
5625
  `);
5626
+ };
5627
+ chunk({ role: "assistant" });
5628
+ const emitEvent = (event) => {
5629
+ if (event.runId !== run.id || seen.has(event.id)) return;
5630
+ seen.add(event.id);
5631
+ if (event.type.endsWith("text_delta")) {
5632
+ const data2 = event.data;
5633
+ if (typeof data2.text === "string" && data2.text) {
5634
+ emittedText = true;
5635
+ chunk({ content: data2.text });
5636
+ }
5637
+ }
5638
+ };
5639
+ const unsubscribe = this.runtime.observability.subscribe(emitEvent);
5640
+ for (const event of this.runtime.observability.list({ runId: run.id, limit: 5e3 })) emitEvent(event);
5641
+ try {
5642
+ const completed = await this.runtime.waitForRun(run.id, { timeoutMs: run.timeoutMs });
5643
+ if (!completed || completed.status !== "completed") {
5644
+ chunk({ content: `Foundry error: ${completed?.error ?? "run did not complete"}` }, "stop");
5645
+ } else {
5646
+ if (!emittedText) chunk({ content: openAiOutput(completed.output?.value) });
5647
+ chunk({}, "stop");
5648
+ }
5649
+ response.write("data: [DONE]\n\n");
5650
+ response.end();
5651
+ } finally {
5652
+ unsubscribe();
5653
+ this.eventStreams.delete(response);
4581
5654
  }
4582
- const unsubscribe = this.runtime.observability.subscribe((event) => {
4583
- if (matches(event, filter)) {
4584
- response.write(`id: ${event.sequence}
4585
- data: ${JSON.stringify(event)}
5655
+ }
5656
+ async resolveAgent(model) {
5657
+ const instances = await this.runtime.listAgentInstances();
5658
+ const exact = instances.find((instance) => instance.id === model);
5659
+ const byDefinition = instances.filter((instance) => instance.definitionId === model);
5660
+ const agent = exact ?? (byDefinition.length === 1 ? byDefinition[0] : void 0);
5661
+ if (!agent) {
5662
+ throw new RequestError(404, `Foundry model "${model}" does not resolve to one agent instance.`);
5663
+ }
5664
+ return agent;
5665
+ }
5666
+ async resolveConversation(request, agent, protocol, requested) {
5667
+ const header = request.headers["x-foundry-conversation-id"];
5668
+ const key = typeof header === "string" && header ? header : typeof requested === "string" && requested ? requested : void 0;
5669
+ const prefix = protocol === "openai" ? "openai" : protocol;
5670
+ const conversationId = key?.startsWith(`${prefix}-`) && key.length < 128 ? key : key ? `${prefix}-${createHash("sha256").update(`${agent.id}\0${key}`).digest("hex").slice(0, 32)}` : `${prefix}-${randomUUID3()}`;
5671
+ const conversations = await this.runtime.listConversations(agent.id);
5672
+ const existing = conversations.find((conversation) => conversation.id === conversationId);
5673
+ if (existing) return existing;
5674
+ return this.runtime.createConversation(agent.id, {
5675
+ id: conversationId,
5676
+ title: protocol === "control" ? "Hercules control session" : "OpenAI-compatible session",
5677
+ context: { protocol: protocol === "responses" ? "openai-responses" : protocol === "openai" ? "openai-chat-completions" : "foundry-control" }
5678
+ });
5679
+ }
5680
+ responseObject(run, request, conversationId) {
5681
+ const text = openAiOutput(run.output?.value);
5682
+ const status = run.status === "pending" ? "queued" : run.status === "running" ? "in_progress" : run.status;
5683
+ const completed = status === "completed";
5684
+ return {
5685
+ id: `resp_${run.id}`,
5686
+ object: "response",
5687
+ created_at: Math.floor(Date.parse(run.createdAt) / 1e3),
5688
+ completed_at: run.completedAt ? Math.floor(Date.parse(run.completedAt) / 1e3) : null,
5689
+ background: false,
5690
+ status,
5691
+ error: run.error ? { message: run.error, type: "foundry_run_error" } : null,
5692
+ incomplete_details: completed || status === "failed" || status === "cancelled" ? null : { reason: run.status },
5693
+ instructions: request.instructions ?? null,
5694
+ max_output_tokens: null,
5695
+ max_tool_calls: null,
5696
+ model: request.model,
5697
+ output: completed ? [{
5698
+ id: `msg_${run.id}`,
5699
+ type: "message",
5700
+ status: "completed",
5701
+ role: "assistant",
5702
+ content: [{ type: "output_text", text, annotations: [], logprobs: [] }]
5703
+ }] : [],
5704
+ parallel_tool_calls: true,
5705
+ previous_response_id: null,
5706
+ reasoning: { effort: null, summary: null },
5707
+ service_tier: "default",
5708
+ store: request.store,
5709
+ temperature: 1,
5710
+ text: { format: { type: "text" } },
5711
+ tool_choice: "auto",
5712
+ tools: [],
5713
+ top_p: 1,
5714
+ truncation: "disabled",
5715
+ user: request.user ?? null,
5716
+ metadata: {
5717
+ "glove.foundry.run_id": run.id,
5718
+ "glove.foundry.conversation_id": conversationId
5719
+ },
5720
+ usage: completed ? {
5721
+ input_tokens: 0,
5722
+ input_tokens_details: { cached_tokens: 0 },
5723
+ output_tokens: 0,
5724
+ output_tokens_details: { reasoning_tokens: 0 },
5725
+ total_tokens: 0
5726
+ } : null
5727
+ };
5728
+ }
5729
+ async handleOpenAiResponse(request, response, body) {
5730
+ const agent = await this.resolveAgent(body.model);
5731
+ const conversation = await this.resolveConversation(
5732
+ request,
5733
+ agent,
5734
+ "responses",
5735
+ body.conversationId ?? body.user
5736
+ );
5737
+ const run = await this.runtime.send(agent.id, conversation.id, body.input, {
5738
+ context: { ...body.context ?? {}, protocol: "openai-responses" }
5739
+ });
5740
+ response.setHeader("x-foundry-conversation-id", conversation.id);
5741
+ if (!body.stream) {
5742
+ const completed = await this.runtime.waitForRun(run.id, { timeoutMs: run.timeoutMs });
5743
+ if (!completed) throw new RequestError(502, "Foundry did not complete the response request.");
5744
+ json(response, completed.status === "completed" ? 200 : 502, this.responseObject(completed, body, conversation.id));
5745
+ return;
5746
+ }
5747
+ response.writeHead(200, {
5748
+ "content-type": "text/event-stream; charset=utf-8",
5749
+ "cache-control": "no-cache, no-transform",
5750
+ connection: "keep-alive",
5751
+ "x-accel-buffering": "no",
5752
+ "x-foundry-conversation-id": conversation.id
5753
+ });
5754
+ this.eventStreams.add(response);
5755
+ const messageId = `msg_${run.id}`;
5756
+ let sequence = 0;
5757
+ const emit = (type, data2) => {
5758
+ response.write(`event: ${type}
5759
+ data: ${JSON.stringify({ type, sequence_number: sequence++, ...data2 })}
4586
5760
 
4587
5761
  `);
5762
+ };
5763
+ const inProgress = this.responseObject({ ...run, status: "running" }, body, conversation.id);
5764
+ emit("response.created", { response: inProgress });
5765
+ emit("response.in_progress", { response: inProgress });
5766
+ emit("response.output_item.added", {
5767
+ output_index: 0,
5768
+ item: { id: messageId, type: "message", status: "in_progress", role: "assistant", content: [] }
5769
+ });
5770
+ emit("response.content_part.added", {
5771
+ item_id: messageId,
5772
+ output_index: 0,
5773
+ content_index: 0,
5774
+ part: { type: "output_text", text: "", annotations: [] }
5775
+ });
5776
+ const seen = /* @__PURE__ */ new Set();
5777
+ let text = "";
5778
+ const emitEvent = (event) => {
5779
+ if (event.runId !== run.id || seen.has(event.id) || !event.type.endsWith("text_delta")) return;
5780
+ seen.add(event.id);
5781
+ const value = event.data;
5782
+ if (typeof value.text !== "string" || !value.text) return;
5783
+ text += value.text;
5784
+ emit("response.output_text.delta", {
5785
+ item_id: messageId,
5786
+ output_index: 0,
5787
+ content_index: 0,
5788
+ delta: value.text
5789
+ });
5790
+ };
5791
+ const unsubscribe = this.runtime.observability.subscribe(emitEvent);
5792
+ for (const event of this.runtime.observability.list({ runId: run.id, limit: 5e3 })) emitEvent(event);
5793
+ try {
5794
+ const completed = await this.runtime.waitForRun(run.id, { timeoutMs: run.timeoutMs });
5795
+ if (!completed) throw new Error("Foundry did not complete the response request.");
5796
+ const output = openAiOutput(completed.output?.value);
5797
+ if (!text && output) {
5798
+ text = output;
5799
+ emit("response.output_text.delta", {
5800
+ item_id: messageId,
5801
+ output_index: 0,
5802
+ content_index: 0,
5803
+ delta: output
5804
+ });
4588
5805
  }
5806
+ emit("response.output_text.done", {
5807
+ item_id: messageId,
5808
+ output_index: 0,
5809
+ content_index: 0,
5810
+ text
5811
+ });
5812
+ emit("response.content_part.done", {
5813
+ item_id: messageId,
5814
+ output_index: 0,
5815
+ content_index: 0,
5816
+ part: { type: "output_text", text, annotations: [], logprobs: [] }
5817
+ });
5818
+ emit("response.output_item.done", {
5819
+ output_index: 0,
5820
+ item: {
5821
+ id: messageId,
5822
+ type: "message",
5823
+ status: completed.status === "completed" ? "completed" : "incomplete",
5824
+ role: "assistant",
5825
+ content: [{ type: "output_text", text, annotations: [], logprobs: [] }]
5826
+ }
5827
+ });
5828
+ emit(completed.status === "completed" ? "response.completed" : "response.failed", {
5829
+ response: this.responseObject(completed, body, conversation.id)
5830
+ });
5831
+ response.end();
5832
+ } finally {
5833
+ unsubscribe();
5834
+ this.eventStreams.delete(response);
5835
+ }
5836
+ }
5837
+ streamEvents(request, response, filter, options = {}) {
5838
+ response.writeHead(200, {
5839
+ "content-type": "text/event-stream; charset=utf-8",
5840
+ "cache-control": "no-cache, no-transform",
5841
+ connection: "keep-alive",
5842
+ "x-accel-buffering": "no"
4589
5843
  });
4590
- const heartbeat = setInterval(() => response.write(": heartbeat\n\n"), 15e3);
4591
- const close = () => {
4592
- clearInterval(heartbeat);
5844
+ this.eventStreams.add(response);
5845
+ response.write(": glove-foundry\n\n");
5846
+ const seen = /* @__PURE__ */ new Set();
5847
+ let closed = false;
5848
+ let heartbeat;
5849
+ let unsubscribe = () => void 0;
5850
+ const close = (end = false) => {
5851
+ if (closed) return;
5852
+ closed = true;
5853
+ if (heartbeat) clearInterval(heartbeat);
4593
5854
  unsubscribe();
4594
5855
  this.eventStreams.delete(response);
5856
+ if (end && !response.writableEnded) response.end();
4595
5857
  };
5858
+ const emit = (event) => {
5859
+ if (!matches(event, filter) || seen.has(event.id) || closed) return;
5860
+ seen.add(event.id);
5861
+ response.write(`id: ${event.sequence}
5862
+ data: ${JSON.stringify(event)}
5863
+
5864
+ `);
5865
+ if (options.closeOnTerminalRun && ["run.completed", "run.failed", "run.cancelled", "run.timeout", "run.skipped"].includes(event.type)) {
5866
+ queueMicrotask(() => close(true));
5867
+ }
5868
+ };
5869
+ unsubscribe = this.runtime.observability.subscribe(emit);
5870
+ for (const event of this.runtime.observability.list(filter)) emit(event);
5871
+ if (!closed) heartbeat = setInterval(() => response.write(": heartbeat\n\n"), 15e3);
4596
5872
  request.once("close", close);
4597
5873
  response.once("close", close);
4598
5874
  }
@@ -4600,8 +5876,6 @@ data: ${JSON.stringify(event)}
4600
5876
 
4601
5877
  export {
4602
5878
  writeGeneratedTypes,
4603
- composeAgent,
4604
- EMPTY_AGENT_COMPOSITION,
4605
5879
  TransmissionId,
4606
5880
  AgentDefinitionId,
4607
5881
  AgentId,