glove-foundry 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -4
- package/dist/{chunk-P65RT7H5.js → chunk-25BGNRJN.js} +1452 -178
- package/dist/{chunk-CRWY7M66.js → chunk-5LDW2N2E.js} +1086 -91
- package/dist/{chunk-3GCUECPA.js → chunk-GKZLWD5M.js} +66 -2
- package/dist/cli.js +186 -40
- package/dist/{client-CLkZREDr.d.ts → client-D0-pKEZM.d.ts} +365 -12
- package/dist/client.d.ts +4 -1
- package/dist/client.js +1 -1
- package/dist/config.d.ts +15 -1
- package/dist/execution-agent.js +1 -1
- package/dist/index.d.ts +91 -5
- package/dist/index.js +428 -6
- package/docs/architecture.md +51 -0
- package/docs/building-with-foundry.md +336 -4
- package/docs/evaluation-checklist.md +10 -1
- package/docs/guidance.md +146 -0
- package/docs/inspector.md +39 -1
- package/docs/release-verification.md +74 -0
- package/package.json +15 -13
- package/templates/minimal/README.md +30 -2
- package/templates/travel-concierge/README.md +33 -5
|
@@ -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
|
-
|
|
30
|
-
|
|
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-
|
|
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
|
|
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
|
-
|
|
1169
|
+
addUnique(registry.tools, [value], "shared tool");
|
|
1224
1170
|
} else if (config.kind === "application") {
|
|
1225
|
-
|
|
1171
|
+
addUnique(
|
|
1226
1172
|
registry.applications,
|
|
1227
1173
|
[value],
|
|
1228
1174
|
"agent application"
|
|
1229
1175
|
);
|
|
1230
1176
|
} else if (config.kind === "mcp") {
|
|
1231
|
-
|
|
1177
|
+
addUnique(registry.mcp, [value], "MCP");
|
|
1232
1178
|
} else if (config.kind === "memory") {
|
|
1233
|
-
|
|
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
|
-
|
|
1224
|
+
addUnique(native.layers, [value], "layer");
|
|
1279
1225
|
} else {
|
|
1280
|
-
|
|
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
|
|
2120
|
-
|
|
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: {
|
|
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.
|
|
2255
|
-
surfaces: this.registry.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
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(
|
|
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
|
-
...
|
|
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
|
-
}
|
|
3094
|
-
|
|
3095
|
-
|
|
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
|
-
|
|
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 {
|
|
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=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));
|
|
3187
3539
|
const attr=value=>esc(value).replace(/\x60/g,"`");
|
|
@@ -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+" "+
|
|
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(
|
|
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
|
|
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){
|
|
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+=
|
|
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(
|
|
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&¤t.section==="instances"&¤t.id)await ensureConversations(current.id);
|
|
3661
4181
|
if(load&¤t.section==="workspaces"&¤t.id)await ensureWorkspace(current.id);
|
|
3662
|
-
if(current.section==="
|
|
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
|
|
4454
|
+
function escapeHtml(value) {
|
|
4455
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 >
|
|
4004
|
-
throw new RequestError(413,
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
data: ${JSON.stringify(
|
|
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
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
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
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
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,
|