@fevex/core 0.1.0-alpha.0 → 0.1.0-alpha.2

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.
Files changed (47) hide show
  1. package/README.md +770 -52
  2. package/dist/agents/index.d.mts +1 -22
  3. package/dist/agents/index.mjs +1 -1
  4. package/dist/channels/index.d.mts +2 -0
  5. package/dist/channels/index.mjs +150 -0
  6. package/dist/evals/index.d.mts +153 -0
  7. package/dist/evals/index.mjs +422 -0
  8. package/dist/fevex-Ddyi6z6L.d.mts +60 -0
  9. package/dist/http/index.d.mts +90 -0
  10. package/dist/http/index.mjs +470 -0
  11. package/dist/index-B4PIkqOU.d.mts +80 -0
  12. package/dist/index-Bg9rr8Qe.d.mts +50 -0
  13. package/dist/index-Bgcxgy66.d.mts +82 -0
  14. package/dist/index-CTIwrVf9.d.mts +51 -0
  15. package/dist/index-CqOjvtQ9.d.mts +132 -0
  16. package/dist/index-CrOId2px.d.mts +66 -0
  17. package/dist/index-DeXUtyd9.d.mts +70 -0
  18. package/dist/index-DviHtsLo.d.mts +58 -0
  19. package/dist/index-Dx_LotWZ.d.mts +425 -0
  20. package/dist/index-dkFyTNh4.d.mts +21 -0
  21. package/dist/index.d.mts +39 -15
  22. package/dist/index.mjs +3505 -194
  23. package/dist/knowledge/index.d.mts +2 -0
  24. package/dist/knowledge/index.mjs +89 -0
  25. package/dist/model-stream-u2xMsEaf.mjs +251 -0
  26. package/dist/models/index.d.mts +2 -27
  27. package/dist/observability/index.d.mts +2 -0
  28. package/dist/observability/index.mjs +1 -0
  29. package/dist/policies/index.d.mts +2 -0
  30. package/dist/policies/index.mjs +6 -0
  31. package/dist/run-error-Cr17wPGH.mjs +23 -0
  32. package/dist/runtime/index.d.mts +2 -17
  33. package/dist/runtime/index.mjs +141 -1
  34. package/dist/sandbox/index.d.mts +2 -0
  35. package/dist/sandbox/index.mjs +122 -0
  36. package/dist/teams/index.d.mts +2 -0
  37. package/dist/teams/index.mjs +2 -0
  38. package/dist/teams-Ctz676G4.mjs +82 -0
  39. package/dist/testing/index.d.mts +34 -8
  40. package/dist/testing/index.mjs +572 -23
  41. package/dist/tools/index.d.mts +3 -31
  42. package/dist/tools/index.mjs +222 -2
  43. package/dist/types-CuoRx12Q.d.mts +333 -0
  44. package/dist/workflows/index.d.mts +2 -0
  45. package/dist/workflows/index.mjs +6 -0
  46. package/package.json +39 -3
  47. package/dist/index-D_RBJmVt.d.mts +0 -44
package/dist/index.mjs CHANGED
@@ -1,63 +1,36 @@
1
- import { defineTool, toToolSpec } from "./tools/index.mjs";
2
1
  import { defineAgent } from "./agents/index.mjs";
2
+ import { ChannelError, handleChannelInput } from "./channels/index.mjs";
3
+ import { InMemoryMemoryStore, createMemoryContextProvider, defineContextProvider, defineSkill } from "./knowledge/index.mjs";
4
+ import { n as teamAsWorkflow, t as defineTeam } from "./teams-Ctz676G4.mjs";
5
+ import { n as RunPausedError, t as FevexRunError } from "./run-error-Cr17wPGH.mjs";
6
+ import { InMemoryRunStore, isDurableRunStore, validateListEventsOptions } from "./runtime/index.mjs";
7
+ import { FEVEX_JSON_SCHEMA_PROFILE_VERSION, IntegrationError, compileFevexJsonSchema, defineConnection, defineTool, toToolSpec, validateFevexJsonSchemaProfile } from "./tools/index.mjs";
8
+ import { a as assertTokenBudget, c as remainingOutputTokens, d as serializeValue, f as toJsonValue, i as assertContinuationBudget, l as toErrorMessage, n as abortable, o as cancellationReason, r as addUsage, s as eventUsage, t as readModelStream, u as serializeJsonValue } from "./model-stream-u2xMsEaf.mjs";
3
9
  import "./models/index.mjs";
4
- import "./runtime/index.mjs";
5
- //#region src/core/index.ts
10
+ import { definePolicy } from "./policies/index.mjs";
11
+ import { SandboxError, createLocalSandbox, sandboxResultToJson } from "./sandbox/index.mjs";
12
+ import { defineWorkflow } from "./workflows/index.mjs";
13
+ //#region src/configuration-error.ts
14
+ var FevexConfigurationError = class extends Error {
15
+ code;
16
+ constructor(code, message) {
17
+ super(message);
18
+ this.code = code;
19
+ this.name = "FevexConfigurationError";
20
+ }
21
+ };
22
+ //#endregion
23
+ //#region src/core/types.ts
6
24
  const SCHEMA_NOT_TRANSPORTABLE = "SCHEMA_NOT_TRANSPORTABLE";
25
+ const PROVIDER_SCHEMA_UNSUPPORTED = "PROVIDER_SCHEMA_UNSUPPORTED";
26
+ const PROVIDER_REASONING_UNSUPPORTED = "PROVIDER_REASONING_UNSUPPORTED";
7
27
  //#endregion
8
- //#region src/index.ts
9
- const invalidJsonValue = Symbol("invalid-json-value");
10
- function normalizeJsonValue(value, seen = /* @__PURE__ */ new WeakSet()) {
11
- if (value === null || typeof value === "string" || typeof value === "boolean") return value;
12
- if (typeof value === "number") return Number.isFinite(value) ? value : invalidJsonValue;
13
- if (Array.isArray(value)) {
14
- const normalized = [];
15
- for (const item of value) {
16
- const normalizedItem = normalizeJsonValue(item, seen);
17
- if (normalizedItem === invalidJsonValue) return invalidJsonValue;
18
- normalized.push(normalizedItem);
19
- }
20
- return normalized;
21
- }
22
- if (typeof value !== "object" || value === void 0) return invalidJsonValue;
23
- const prototype = Object.getPrototypeOf(value);
24
- if (prototype !== null && prototype !== Object.prototype) return invalidJsonValue;
25
- if (seen.has(value)) return invalidJsonValue;
26
- seen.add(value);
27
- const normalized = {};
28
- for (const [key, item] of Object.entries(value)) {
29
- if (item === void 0) continue;
30
- const normalizedItem = normalizeJsonValue(item, seen);
31
- if (normalizedItem === invalidJsonValue) return invalidJsonValue;
32
- normalized[key] = normalizedItem;
33
- }
34
- seen.delete(value);
35
- return normalized;
36
- }
37
- function toJsonValue(value, errorMessage) {
38
- let normalized;
39
- try {
40
- normalized = normalizeJsonValue(value);
41
- } catch {
42
- throw new TypeError(errorMessage);
43
- }
44
- if (normalized === invalidJsonValue) throw new TypeError(errorMessage);
45
- return normalized;
46
- }
47
- function serializeJsonValue(value) {
48
- return typeof value === "string" ? value : JSON.stringify(value);
49
- }
50
- function serializeValue(value, errorMessage) {
51
- return serializeJsonValue(toJsonValue(value, errorMessage));
52
- }
28
+ //#region src/internal/schemas.ts
53
29
  function isStandardSchema(value) {
54
30
  if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
55
31
  const standard = value["~standard"];
56
32
  return typeof standard === "object" && standard !== null && standard.version === 1 && typeof standard.validate === "function";
57
33
  }
58
- function assertStandardSchema(value, errorMessage) {
59
- if (!isStandardSchema(value)) throw new TypeError(errorMessage);
60
- }
61
34
  function isStandardJsonSchema(value) {
62
35
  if (!isStandardSchema(value)) return false;
63
36
  const jsonSchema = value["~standard"].jsonSchema;
@@ -91,183 +64,3521 @@ async function validateSchema(schema, value, errorMessage) {
91
64
  }
92
65
  return result.value;
93
66
  }
94
- function toErrorMessage(error) {
95
- if (error instanceof Error) return error.message;
96
- if (typeof error === "string") return error;
97
- try {
98
- return JSON.stringify(error) ?? String(error);
99
- } catch {
100
- return String(error);
67
+ //#endregion
68
+ //#region src/internal/configuration.ts
69
+ const REASONING_EFFORTS = /* @__PURE__ */ new Set([
70
+ "provider-default",
71
+ "none",
72
+ "minimal",
73
+ "low",
74
+ "medium",
75
+ "high",
76
+ "xhigh",
77
+ "max"
78
+ ]);
79
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
80
+ const INTERNAL_TOOL_PREFIX = "fevex__";
81
+ function isRecord(value) {
82
+ return typeof value === "object" && value !== null && !Array.isArray(value);
83
+ }
84
+ function configurationError(code, message) {
85
+ return new FevexConfigurationError(code, message);
86
+ }
87
+ function assertConfiguration(condition, code, message) {
88
+ if (!condition) throw configurationError(code, message);
89
+ }
90
+ function isModelGateway(value) {
91
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.stream === "function";
92
+ }
93
+ function isRunStore(value) {
94
+ return (typeof value === "object" && value !== null || typeof value === "function") && [
95
+ "getRun",
96
+ "saveRun",
97
+ "getSession",
98
+ "saveSession",
99
+ "appendEvent",
100
+ "listEvents"
101
+ ].every((method) => typeof value[method] === "function");
102
+ }
103
+ function isSandbox(value) {
104
+ return isRecord(value) && typeof value.run === "function";
105
+ }
106
+ function isJsonObject(value) {
107
+ return isRecord(value);
108
+ }
109
+ function isToolProvider(value) {
110
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.listTools === "function" && typeof value.callTool === "function";
111
+ }
112
+ function isContextProvider(value) {
113
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.name === "string" && typeof value.read === "function";
114
+ }
115
+ function isMemoryStore(value) {
116
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.search === "function" && typeof value.write === "function";
117
+ }
118
+ function connectionAllowlist(connection) {
119
+ if (connection.allowlist) return connection.allowlist;
120
+ if (connection.tools && Array.isArray(connection.tools.allow)) return connection.tools.allow;
121
+ }
122
+ function connectionMetadata(connection, remoteName) {
123
+ if (!connection.tools || "allow" in connection.tools) return void 0;
124
+ return connection.tools[remoteName];
125
+ }
126
+ function namespacedToolName(connectionName, toolName) {
127
+ return `${connectionName}__${toolName}`;
128
+ }
129
+ function timeoutSignal(parent, timeoutMs) {
130
+ const controller = new AbortController();
131
+ const onAbort = () => controller.abort(parent?.reason);
132
+ const timer = setTimeout(() => {
133
+ controller.abort(new IntegrationError("CONNECTION_TIMEOUT", "timeout", true, "Connection tool call timed out"));
134
+ }, timeoutMs);
135
+ parent?.addEventListener("abort", onAbort, { once: true });
136
+ return {
137
+ signal: controller.signal,
138
+ dispose() {
139
+ clearTimeout(timer);
140
+ parent?.removeEventListener("abort", onAbort);
141
+ }
142
+ };
143
+ }
144
+ function connectionError(error, source) {
145
+ if (error instanceof IntegrationError) {
146
+ if (!source || error.source) return error;
147
+ return new IntegrationError(error.code, error.category, error.retryable, error.safeMessage, {
148
+ cause: error,
149
+ source
150
+ });
101
151
  }
152
+ if (error instanceof DOMException && error.name === "AbortError") return new IntegrationError("CONNECTION_TIMEOUT", "timeout", true, "Connection tool call timed out", {
153
+ cause: error,
154
+ source
155
+ });
156
+ return new IntegrationError("CONNECTION_REMOTE_ERROR", "remote", false, "Connection tool call failed", {
157
+ cause: error,
158
+ source
159
+ });
102
160
  }
103
- function addUsage(first, second) {
104
- if (!first) return second;
105
- if (!second) return first;
106
- const inputTokens = first.inputTokens === void 0 && second.inputTokens === void 0 ? void 0 : (first.inputTokens ?? 0) + (second.inputTokens ?? 0);
107
- const outputTokens = first.outputTokens === void 0 && second.outputTokens === void 0 ? void 0 : (first.outputTokens ?? 0) + (second.outputTokens ?? 0);
108
- const totalTokens = first.totalTokens === void 0 && second.totalTokens === void 0 ? void 0 : (first.totalTokens ?? 0) + (second.totalTokens ?? 0);
161
+ function expandConnection(connection) {
162
+ assertConfiguration(isRecord(connection), "INVALID_CONNECTION", "Connection must be an object");
163
+ const allowlist = connectionAllowlist(connection);
164
+ assertConfiguration(typeof connection.name === "string" && Boolean(connection.name.trim()), "INVALID_CONNECTION", "Connection name must be a non-empty string");
165
+ assertConfiguration(!connection.name.includes("__"), "INVALID_CONNECTION", `Connection "${connection.name}" name cannot contain "__"`);
166
+ assertConfiguration(isToolProvider(connection.provider), "INVALID_CONNECTION", `Connection "${connection.name}" provider must implement listTools and callTool`);
167
+ assertConfiguration(Array.isArray(allowlist) && allowlist.length > 0, "INVALID_CONNECTION", `Connection "${connection.name}" allowlist must be a non-empty array`);
168
+ assertConfiguration(connection.timeoutMs === void 0 || Number.isInteger(connection.timeoutMs) && connection.timeoutMs > 0, "INVALID_CONNECTION", `Connection "${connection.name}" timeoutMs must be a positive integer`);
169
+ const seen = /* @__PURE__ */ new Set();
170
+ const listRemoteTools = async (context) => {
171
+ const tools = await connection.provider.listTools(context);
172
+ return new Map(tools.map((tool) => [tool.name, tool]));
173
+ };
174
+ return allowlist.map((remoteName) => {
175
+ assertConfiguration(typeof remoteName === "string" && Boolean(remoteName.trim()), "INVALID_TOOL", `Connection "${connection.name}" tool names must be non-empty strings`);
176
+ assertConfiguration(!remoteName.includes("__"), "INVALID_TOOL", `Connection "${connection.name}" tool "${remoteName}" cannot contain "__"`);
177
+ assertConfiguration(!seen.has(remoteName), "DUPLICATE_TOOL", `Connection "${connection.name}" tool "${remoteName}" is duplicated`);
178
+ seen.add(remoteName);
179
+ const policy = connectionMetadata(connection, remoteName);
180
+ const source = {
181
+ kind: "connection",
182
+ provider: typeof connection.provider.kind === "string" && connection.provider.kind.trim() ? connection.provider.kind : "custom",
183
+ connectionName: connection.name,
184
+ remoteToolName: remoteName
185
+ };
186
+ assertConfiguration(policy?.inputSchema === void 0 || isJsonObject(policy.inputSchema), "INVALID_TOOL", `Input schema for connection tool "${remoteName}" must be a JSON object`);
187
+ assertConfiguration(policy?.outputSchema === void 0 || isJsonObject(policy.outputSchema), "INVALID_TOOL", `Output schema for connection tool "${remoteName}" must be a JSON object`);
188
+ return {
189
+ name: namespacedToolName(connection.name, remoteName),
190
+ ...policy?.label === void 0 ? {} : { label: policy.label },
191
+ ...policy?.description === void 0 ? {} : { description: policy.description },
192
+ ...policy?.inputSchema === void 0 ? {} : { inputJsonSchema: policy.inputSchema },
193
+ ...policy?.outputSchema === void 0 ? {} : { outputJsonSchema: policy.outputSchema },
194
+ ...policy?.risk === void 0 ? {} : { risk: policy.risk },
195
+ ...policy?.approval === void 0 ? {} : { approval: policy.approval },
196
+ ...policy?.idempotency === void 0 ? {} : { idempotency: policy.idempotency },
197
+ ...policy?.retry === void 0 ? {} : { retry: policy.retry },
198
+ ...policy?.credentials === void 0 ? {} : { credentials: policy.credentials },
199
+ source,
200
+ async resolve(context) {
201
+ try {
202
+ return (await listRemoteTools(context)).get(remoteName);
203
+ } catch (error) {
204
+ throw connectionError(error, source);
205
+ }
206
+ },
207
+ async execute(input, context) {
208
+ const timeout = timeoutSignal(context.signal, connection.timeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS);
209
+ try {
210
+ return await Promise.race([connection.provider.callTool(remoteName, input, {
211
+ runId: context.runId,
212
+ toolCallId: context.toolCallId,
213
+ attempt: context.attempt,
214
+ idempotencyKey: context.idempotencyKey,
215
+ context: context.context,
216
+ signal: timeout.signal
217
+ }), new Promise((_, reject) => {
218
+ timeout.signal.addEventListener("abort", () => {
219
+ reject(timeout.signal.reason ?? new DOMException("Aborted", "AbortError"));
220
+ }, { once: true });
221
+ })]);
222
+ } catch (error) {
223
+ throw connectionError(error, source);
224
+ } finally {
225
+ timeout.dispose();
226
+ }
227
+ }
228
+ };
229
+ });
230
+ }
231
+ function assertLimit(value, name, minimum, ownerName, allowFalse = false, owner = "Agent", code = "INVALID_AGENT") {
232
+ if (value === void 0 || allowFalse && value === false) return;
233
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum) throw configurationError(code, `${owner} "${ownerName}" limit "${name}" must be ${minimum === 0 ? "a non-negative" : "a positive"} integer${allowFalse ? " or false" : ""}`);
234
+ }
235
+ function assertStringArray(value, code, message) {
236
+ assertConfiguration(value === void 0 || Array.isArray(value) && value.every((item) => typeof item === "string" && item.trim()), code, message);
237
+ }
238
+ function assertPositiveInteger(value, code, message) {
239
+ assertConfiguration(value === void 0 || Number.isInteger(value) && value > 0, code, message);
240
+ }
241
+ function assertSandboxCapabilities(value, ownerName) {
242
+ if (value === void 0) return;
243
+ assertConfiguration(isRecord(value), "INVALID_TOOL", `Tool "${ownerName}" sandbox must be an object`);
244
+ const capabilities = value;
245
+ const process = capabilities.process;
246
+ if (process !== void 0) {
247
+ assertConfiguration(isRecord(process), "INVALID_TOOL", `Tool "${ownerName}" sandbox.process must be an object`);
248
+ assertStringArray(process.commands, "INVALID_TOOL", `Tool "${ownerName}" sandbox.process.commands must contain non-empty strings`);
249
+ assertPositiveInteger(process.timeoutMs, "INVALID_TOOL", `Tool "${ownerName}" sandbox.process.timeoutMs must be a positive integer`);
250
+ }
251
+ const filesystem = capabilities.filesystem;
252
+ if (filesystem !== void 0) {
253
+ assertConfiguration(isRecord(filesystem), "INVALID_TOOL", `Tool "${ownerName}" sandbox.filesystem must be an object`);
254
+ assertConfiguration(filesystem.cwd === void 0 || typeof filesystem.cwd === "string", "INVALID_TOOL", `Tool "${ownerName}" sandbox.filesystem.cwd must be a string`);
255
+ assertStringArray(filesystem.read, "INVALID_TOOL", `Tool "${ownerName}" sandbox.filesystem.read must contain non-empty strings`);
256
+ assertStringArray(filesystem.write, "INVALID_TOOL", `Tool "${ownerName}" sandbox.filesystem.write must contain non-empty strings`);
257
+ }
258
+ const network = capabilities.network;
259
+ if (network !== void 0 && network !== false) {
260
+ assertConfiguration(isRecord(network), "INVALID_TOOL", `Tool "${ownerName}" sandbox.network must be false or an object`);
261
+ assertStringArray(network.allow, "INVALID_TOOL", `Tool "${ownerName}" sandbox.network.allow must contain non-empty strings`);
262
+ }
263
+ const resources = capabilities.resources;
264
+ if (resources !== void 0) {
265
+ assertConfiguration(isRecord(resources), "INVALID_TOOL", `Tool "${ownerName}" sandbox.resources must be an object`);
266
+ assertPositiveInteger(resources.timeoutMs, "INVALID_TOOL", `Tool "${ownerName}" sandbox.resources.timeoutMs must be a positive integer`);
267
+ assertPositiveInteger(resources.maxOutputBytes, "INVALID_TOOL", `Tool "${ownerName}" sandbox.resources.maxOutputBytes must be a positive integer`);
268
+ }
269
+ }
270
+ function copySandboxCapabilities(value) {
271
+ if (!value) return void 0;
109
272
  return {
110
- ...inputTokens !== void 0 ? { inputTokens } : {},
111
- ...outputTokens !== void 0 ? { outputTokens } : {},
112
- ...totalTokens !== void 0 ? { totalTokens } : {}
273
+ ...value.process === void 0 ? {} : { process: {
274
+ ...value.process.commands === void 0 ? {} : { commands: [...value.process.commands] },
275
+ ...value.process.timeoutMs === void 0 ? {} : { timeoutMs: value.process.timeoutMs }
276
+ } },
277
+ ...value.filesystem === void 0 ? {} : { filesystem: {
278
+ ...value.filesystem.cwd === void 0 ? {} : { cwd: value.filesystem.cwd },
279
+ ...value.filesystem.read === void 0 ? {} : { read: [...value.filesystem.read] },
280
+ ...value.filesystem.write === void 0 ? {} : { write: [...value.filesystem.write] }
281
+ } },
282
+ ...value.network === void 0 ? {} : { network: value.network === false ? false : { allow: [...value.network.allow ?? []] } },
283
+ ...value.resources === void 0 ? {} : { resources: {
284
+ ...value.resources.timeoutMs === void 0 ? {} : { timeoutMs: value.resources.timeoutMs },
285
+ ...value.resources.maxOutputBytes === void 0 ? {} : { maxOutputBytes: value.resources.maxOutputBytes }
286
+ } }
113
287
  };
114
288
  }
115
- function createFevex(config) {
116
- const models = new Map(Object.entries(config.models));
289
+ function createComposition(config) {
290
+ assertConfiguration(isRecord(config), "INVALID_CONFIG", "Fevex config must be an object");
291
+ assertConfiguration(isRecord(config.models), "INVALID_CONFIG", "Fevex config \"models\" must be an object");
292
+ assertConfiguration(Array.isArray(config.agents), "INVALID_CONFIG", "Fevex config \"agents\" must be an array");
293
+ assertConfiguration(config.workflows === void 0 || Array.isArray(config.workflows), "INVALID_CONFIG", "Fevex config \"workflows\" must be an array");
294
+ assertConfiguration(config.teams === void 0 || Array.isArray(config.teams), "INVALID_CONFIG", "Fevex config \"teams\" must be an array");
295
+ assertConfiguration(config.tools === void 0 || Array.isArray(config.tools), "INVALID_CONFIG", "Fevex config \"tools\" must be an array");
296
+ assertConfiguration(config.connections === void 0 || Array.isArray(config.connections), "INVALID_CONFIG", "Fevex config \"connections\" must be an array");
297
+ assertConfiguration(config.contextProviders === void 0 || Array.isArray(config.contextProviders), "INVALID_CONFIG", "Fevex config \"contextProviders\" must be an array");
298
+ assertConfiguration(config.memoryStore === void 0 || isMemoryStore(config.memoryStore), "INVALID_CONFIG", "Fevex config \"memoryStore\" must implement MemoryStore");
299
+ assertConfiguration(config.onEvent === void 0 || typeof config.onEvent === "function", "INVALID_CONFIG", "Fevex config \"onEvent\" must be a function");
300
+ assertConfiguration(config.runStore === void 0 || isRunStore(config.runStore), "INVALID_CONFIG", "Fevex config \"runStore\" must implement RunStore");
301
+ assertConfiguration(config.credentialStore === void 0 || typeof config.credentialStore?.resolve === "function", "INVALID_CONFIG", "Fevex config \"credentialStore\" must implement resolve");
302
+ assertConfiguration(config.sandbox === void 0 || isSandbox(config.sandbox), "INVALID_CONFIG", "Fevex config \"sandbox\" must implement Sandbox");
303
+ assertConfiguration(config.policies === void 0 || Array.isArray(config.policies), "INVALID_CONFIG", "Fevex config \"policies\" must be an array");
304
+ if (config.observability !== void 0) {
305
+ assertConfiguration(isRecord(config.observability), "INVALID_CONFIG", "Fevex config \"observability\" must be an object");
306
+ assertConfiguration(Array.isArray(config.observability.exporters) && config.observability.exporters.every((exporter) => isRecord(exporter) && typeof exporter.export === "function"), "INVALID_CONFIG", "Fevex config \"observability.exporters\" must contain TraceExporters");
307
+ assertConfiguration(config.observability.calculateCost === void 0 || typeof config.observability.calculateCost === "function", "INVALID_CONFIG", "Fevex config \"observability.calculateCost\" must be a function");
308
+ if (config.observability.content !== void 0) {
309
+ const content = config.observability.content;
310
+ assertConfiguration(isRecord(content) && Array.isArray(content.include) && content.include.every((kind) => [
311
+ "run.output",
312
+ "model.output",
313
+ "error.message"
314
+ ].includes(kind)) && (content.redact === void 0 || typeof content.redact === "function"), "INVALID_CONFIG", "Fevex config \"observability.content\" is invalid");
315
+ }
316
+ }
317
+ const models = /* @__PURE__ */ new Map();
117
318
  const agents = /* @__PURE__ */ new Map();
319
+ const workflows = /* @__PURE__ */ new Map();
320
+ const teams = /* @__PURE__ */ new Map();
118
321
  const tools = /* @__PURE__ */ new Map();
119
- for (const [name, model] of models) {
120
- if (!name.trim()) throw new Error("Model name cannot be empty");
121
- if (!model || typeof model.generate !== "function") throw new Error(`Model "${name}" must implement generate`);
322
+ const contextProviders = /* @__PURE__ */ new Map();
323
+ for (const [name, model] of Object.entries(config.models)) {
324
+ assertConfiguration(name.trim(), "INVALID_MODEL", "Model name cannot be empty");
325
+ assertConfiguration(isModelGateway(model), "INVALID_MODEL", `Model "${name}" must implement stream`);
326
+ assertConfiguration(model.stateCodec === void 0 || isRecord(model.stateCodec) && typeof model.stateCodec.serialize === "function" && typeof model.stateCodec.restore === "function", "INVALID_MODEL", `Model "${name}" stateCodec must implement serialize and restore`);
327
+ models.set(name, model);
328
+ }
329
+ for (const [index, provider] of (config.contextProviders ?? []).entries()) {
330
+ assertConfiguration(isContextProvider(provider), "INVALID_CONTEXT_PROVIDER", `Context provider at index ${index} must have a name and read`);
331
+ const name = provider.name;
332
+ assertConfiguration(name.trim(), "INVALID_CONTEXT_PROVIDER", "Context provider name cannot be empty");
333
+ assertConfiguration(!contextProviders.has(name), "DUPLICATE_CONTEXT_PROVIDER", `Context provider "${name}" is duplicated`);
334
+ contextProviders.set(name, provider);
122
335
  }
123
- for (const tool of config.tools ?? []) {
124
- if (!tool.name.trim()) throw new Error("Tool name cannot be empty");
125
- if (tools.has(tool.name)) throw new Error(`Tool "${tool.name}" is duplicated`);
126
- if (tool.inputSchema !== void 0) assertStandardSchema(tool.inputSchema, `Input schema for tool "${tool.name}" must implement Standard Schema`);
127
- if (tool.outputSchema !== void 0) assertStandardSchema(tool.outputSchema, `Output schema for tool "${tool.name}" must implement Standard Schema`);
128
- tools.set(tool.name, tool);
336
+ const expandedTools = [...config.tools ?? [], ...(config.connections ?? []).flatMap((connection) => expandConnection(connection))];
337
+ for (const [index, definition] of expandedTools.entries()) {
338
+ assertConfiguration(isRecord(definition), "INVALID_TOOL", `Tool at index ${index} must be an object`);
339
+ assertConfiguration(typeof definition.name === "string", "INVALID_TOOL", "Tool name must be a string");
340
+ const name = definition.name;
341
+ assertConfiguration(name.trim(), "INVALID_TOOL", "Tool name cannot be empty");
342
+ assertConfiguration(!name.startsWith(INTERNAL_TOOL_PREFIX), "INVALID_TOOL", `Tool "${name}" uses reserved prefix "${INTERNAL_TOOL_PREFIX}"`);
343
+ assertConfiguration(!tools.has(name), "DUPLICATE_TOOL", `Tool "${name}" is duplicated`);
344
+ assertConfiguration(definition.description === void 0 || typeof definition.description === "string", "INVALID_TOOL", `Tool "${name}" description must be a string`);
345
+ assertConfiguration(typeof definition.execute === "function", "INVALID_TOOL", `Tool "${name}" must implement execute`);
346
+ assertConfiguration(definition.risk === void 0 || [
347
+ "read",
348
+ "write",
349
+ "sensitive",
350
+ "destructive"
351
+ ].includes(definition.risk), "INVALID_TOOL", `Tool "${name}" risk is invalid`);
352
+ assertConfiguration(definition.approval === void 0 || definition.approval === "never" || definition.approval === "required", "INVALID_TOOL", `Tool "${name}" approval is invalid`);
353
+ assertConfiguration(definition.idempotency === void 0 || definition.idempotency === "none" || definition.idempotency === "keyed", "INVALID_TOOL", `Tool "${name}" idempotency is invalid`);
354
+ assertConfiguration(definition.credentials === void 0 || Array.isArray(definition.credentials) && definition.credentials.every((value) => typeof value === "string" && value.trim()), "INVALID_TOOL", `Tool "${name}" credentials must be non-empty strings`);
355
+ assertSandboxCapabilities(definition.sandbox, name);
356
+ assertConfiguration(definition.sandbox === void 0 || config.sandbox !== void 0, "INVALID_TOOL", `Tool "${name}" requires Fevex config "sandbox"`);
357
+ if (definition.retry !== void 0) assertConfiguration(isRecord(definition.retry) && Number.isInteger(definition.retry.maxAttempts) && definition.retry.maxAttempts >= 1 && Number.isInteger(definition.retry.backoffMs) && definition.retry.backoffMs >= 0 && (definition.retry.maxBackoffMs === void 0 || Number.isInteger(definition.retry.maxBackoffMs) && definition.retry.maxBackoffMs >= 0), "INVALID_TOOL", `Tool "${name}" retry is invalid`);
358
+ if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_TOOL", `Input schema for tool "${name}" must implement Standard Schema`);
359
+ if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_AGENT", `Input schema for agent "${name}" must implement Standard Schema`);
360
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_TOOL", `Output schema for tool "${name}" must implement Standard Schema`);
361
+ if (definition.inputJsonSchema !== void 0) assertConfiguration(isJsonObject(definition.inputJsonSchema), "INVALID_TOOL", `Input JSON schema for tool "${name}" must be an object`);
362
+ if (definition.outputJsonSchema !== void 0) assertConfiguration(isJsonObject(definition.outputJsonSchema), "INVALID_TOOL", `Output JSON schema for tool "${name}" must be an object`);
363
+ tools.set(name, {
364
+ name,
365
+ ...definition.label === void 0 ? {} : { label: definition.label },
366
+ ...definition.description === void 0 ? {} : { description: definition.description },
367
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
368
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
369
+ ...definition.inputJsonSchema === void 0 ? {} : { inputJsonSchema: definition.inputJsonSchema },
370
+ ...definition.outputJsonSchema === void 0 ? {} : { outputJsonSchema: definition.outputJsonSchema },
371
+ ...definition.risk === void 0 ? {} : { risk: definition.risk },
372
+ ...definition.approval === void 0 ? {} : { approval: definition.approval },
373
+ ...definition.idempotency === void 0 ? {} : { idempotency: definition.idempotency },
374
+ ...definition.retry === void 0 ? {} : { retry: { ...definition.retry } },
375
+ ...definition.credentials === void 0 ? {} : { credentials: [...definition.credentials] },
376
+ ...definition.sandbox === void 0 ? {} : { sandbox: copySandboxCapabilities(definition.sandbox) },
377
+ ...definition.source === void 0 ? {} : { source: { ...definition.source } },
378
+ ...definition.resolve === void 0 ? {} : { resolve: definition.resolve },
379
+ execute: definition.execute
380
+ });
129
381
  }
130
- for (const agent of config.agents) {
131
- if (!agent.name.trim()) throw new Error("Agent name cannot be empty");
132
- if (!agent.instructions.trim()) throw new Error(`Agent "${agent.name}" instructions cannot be empty`);
133
- if (agents.has(agent.name)) throw new Error(`Agent "${agent.name}" is duplicated`);
134
- if (agent.outputSchema !== void 0) assertStandardSchema(agent.outputSchema, `Output schema for agent "${agent.name}" must implement Standard Schema`);
135
- agents.set(agent.name, agent);
382
+ const policies = [];
383
+ for (const [index, policy] of (config.policies ?? []).entries()) {
384
+ assertConfiguration(isRecord(policy) && typeof policy.name === "string" && Boolean(policy.name.trim()) && typeof policy.authorize === "function", "INVALID_POLICY", `Policy at index ${index} must have a name and authorize`);
385
+ policies.push({
386
+ name: policy.name,
387
+ authorize: policy.authorize
388
+ });
389
+ }
390
+ for (const [index, definition] of config.agents.entries()) {
391
+ assertConfiguration(isRecord(definition), "INVALID_AGENT", `Agent at index ${index} must be an object`);
392
+ assertConfiguration(typeof definition.name === "string", "INVALID_AGENT", "Agent name must be a string");
393
+ const name = definition.name;
394
+ assertConfiguration(name.trim(), "INVALID_AGENT", "Agent name cannot be empty");
395
+ assertConfiguration(typeof definition.instructions === "string", "INVALID_AGENT", `Agent "${name}" instructions must be a string`);
396
+ assertConfiguration(definition.instructions.trim(), "INVALID_AGENT", `Agent "${name}" instructions cannot be empty`);
397
+ assertConfiguration(!agents.has(name), "DUPLICATE_AGENT", `Agent "${name}" is duplicated`);
398
+ const model = definition.model;
399
+ if (model !== void 0) if (typeof model === "string") assertConfiguration(model.trim(), "INVALID_MODEL", `Model name required by agent "${name}" cannot be empty`);
400
+ else assertConfiguration(isModelGateway(model), "INVALID_MODEL", `Model for agent "${name}" must implement stream`);
401
+ let agentTools;
402
+ if (definition.tools !== void 0) {
403
+ assertConfiguration(Array.isArray(definition.tools), "INVALID_AGENT", `Agent "${name}" tools must be an array`);
404
+ agentTools = [];
405
+ const seenTools = /* @__PURE__ */ new Set();
406
+ for (const [toolIndex, toolName] of definition.tools.entries()) {
407
+ assertConfiguration(typeof toolName === "string" && Boolean(toolName.trim()), "INVALID_AGENT", `Tool name at index ${toolIndex} for agent "${name}" must be a non-empty string`);
408
+ assertConfiguration(!seenTools.has(toolName), "DUPLICATE_TOOL", `Tool "${toolName}" is duplicated in agent "${name}"`);
409
+ seenTools.add(toolName);
410
+ agentTools.push(toolName);
411
+ }
412
+ }
413
+ let agentContext;
414
+ if (definition.context !== void 0) {
415
+ assertConfiguration(Array.isArray(definition.context), "INVALID_AGENT", `Agent "${name}" context must be an array`);
416
+ agentContext = [];
417
+ const seenContext = /* @__PURE__ */ new Set();
418
+ for (const [contextIndex, providerName] of definition.context.entries()) {
419
+ assertConfiguration(typeof providerName === "string" && Boolean(providerName.trim()), "INVALID_AGENT", `Context provider name at index ${contextIndex} for agent "${name}" must be a non-empty string`);
420
+ assertConfiguration(!seenContext.has(providerName), "INVALID_AGENT", `Context provider "${providerName}" is duplicated in agent "${name}"`);
421
+ seenContext.add(providerName);
422
+ agentContext.push(providerName);
423
+ }
424
+ }
425
+ let agentSkills;
426
+ if (definition.skills !== void 0) {
427
+ assertConfiguration(Array.isArray(definition.skills), "INVALID_AGENT", `Agent "${name}" skills must be an array`);
428
+ agentSkills = [];
429
+ const seenSkills = /* @__PURE__ */ new Set();
430
+ for (const [skillIndex, skillName] of definition.skills.entries()) {
431
+ assertConfiguration(typeof skillName === "string" && Boolean(skillName.trim()), "INVALID_AGENT", `Skill name at index ${skillIndex} for agent "${name}" must be a non-empty string`);
432
+ assertConfiguration(!seenSkills.has(skillName), "INVALID_AGENT", `Skill "${skillName}" is duplicated in agent "${name}"`);
433
+ seenSkills.add(skillName);
434
+ agentSkills.push(skillName);
435
+ }
436
+ }
437
+ assertConfiguration(definition.memory === void 0 || isRecord(definition.memory), "INVALID_AGENT", `Agent "${name}" memory must be an object`);
438
+ assertConfiguration(definition.memory?.read === void 0 || typeof definition.memory.read === "boolean", "INVALID_AGENT", `Agent "${name}" memory.read must be a boolean`);
439
+ assertConfiguration(definition.memory?.write === void 0 || typeof definition.memory.write === "boolean", "INVALID_AGENT", `Agent "${name}" memory.write must be a boolean`);
440
+ assertConfiguration(definition.memory?.limit === void 0 || Number.isInteger(definition.memory.limit) && definition.memory.limit > 0, "INVALID_AGENT", `Agent "${name}" memory.limit must be a positive integer`);
441
+ assertConfiguration(definition.reasoning === void 0 || typeof definition.reasoning === "string" && REASONING_EFFORTS.has(definition.reasoning), "INVALID_AGENT", `Agent "${name}" reasoning is invalid`);
442
+ assertConfiguration(definition.modelOptions === void 0 || isRecord(definition.modelOptions), "INVALID_AGENT", `Agent "${name}" modelOptions must be an object`);
443
+ assertConfiguration(definition.elicitation === void 0 || definition.elicitation === "pause" || definition.elicitation === "forbid", "INVALID_AGENT", `Agent "${name}" elicitation is invalid`);
444
+ assertConfiguration(definition.approvalMode === void 0 || definition.approvalMode === "pause" || definition.approvalMode === "deny", "INVALID_AGENT", `Agent "${name}" approvalMode is invalid`);
445
+ assertConfiguration(definition.toolChoice === void 0 || definition.toolChoice === "auto" || definition.toolChoice === "required" || definition.toolChoice === "none" || isRecord(definition.toolChoice) && typeof definition.toolChoice.name === "string" && Boolean(definition.toolChoice.name.trim()), "INVALID_AGENT", `Agent "${name}" toolChoice is invalid`);
446
+ assertConfiguration(definition.limits === void 0 || isRecord(definition.limits), "INVALID_AGENT", `Agent "${name}" limits must be an object`);
447
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_AGENT", `Output schema for agent "${name}" must implement Standard Schema`);
448
+ const rawLimits = definition.limits;
449
+ assertLimit(rawLimits?.maxSteps, "maxSteps", 1, name);
450
+ assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name);
451
+ assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true);
452
+ assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true);
453
+ agents.set(name, {
454
+ name,
455
+ instructions: definition.instructions,
456
+ ...model === void 0 ? {} : { model },
457
+ ...agentTools === void 0 ? {} : { tools: agentTools },
458
+ ...agentContext === void 0 ? {} : { context: agentContext },
459
+ ...agentSkills === void 0 ? {} : { skills: agentSkills },
460
+ ...definition.memory === void 0 ? {} : { memory: {
461
+ ...definition.memory.read === void 0 ? {} : { read: definition.memory.read },
462
+ ...definition.memory.write === void 0 ? {} : { write: definition.memory.write },
463
+ ...definition.memory.limit === void 0 ? {} : { limit: definition.memory.limit }
464
+ } },
465
+ ...definition.reasoning === void 0 ? {} : { reasoning: definition.reasoning },
466
+ ...definition.modelOptions === void 0 ? {} : { modelOptions: { ...definition.modelOptions } },
467
+ ...definition.toolChoice === void 0 ? {} : { toolChoice: typeof definition.toolChoice === "string" ? definition.toolChoice : { name: definition.toolChoice.name } },
468
+ ...definition.elicitation === void 0 ? {} : { elicitation: definition.elicitation },
469
+ ...definition.approvalMode === void 0 ? {} : { approvalMode: definition.approvalMode },
470
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
471
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
472
+ ...rawLimits === void 0 ? {} : { limits: {
473
+ ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
474
+ ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
475
+ ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
476
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
477
+ } }
478
+ });
136
479
  }
137
480
  for (const agent of agents.values()) {
138
- if (typeof agent.model === "string" && !models.has(agent.model)) throw new Error(`Model "${agent.model}" required by agent "${agent.name}" is not registered`);
139
- if (agent.model && typeof agent.model !== "string" && typeof agent.model.generate !== "function") throw new Error(`Model for agent "${agent.name}" must implement generate`);
140
- if (!agent.model && !models.has("default")) throw new Error(`Default model "default" required by agent "${agent.name}" is not registered`);
141
- for (const toolName of agent.tools ?? []) if (!tools.has(toolName)) throw new Error(`Tool "${toolName}" required by agent "${agent.name}" is not registered`);
481
+ if (typeof agent.model === "string" && !models.has(agent.model)) throw configurationError("MISSING_MODEL", `Model "${agent.model}" required by agent "${agent.name}" is not registered`);
482
+ if (agent.model === void 0 && !models.has("default")) throw configurationError("MISSING_MODEL", `Default model "default" required by agent "${agent.name}" is not registered`);
483
+ for (const toolName of agent.tools ?? []) if (!tools.has(toolName)) throw configurationError("MISSING_TOOL", `Tool "${toolName}" required by agent "${agent.name}" is not registered`);
484
+ if (typeof agent.toolChoice === "object" && !agent.tools?.includes(agent.toolChoice.name)) throw configurationError("MISSING_TOOL", `Tool "${agent.toolChoice.name}" required by agent "${agent.name}" toolChoice is not registered`);
485
+ for (const providerName of [...agent.context ?? [], ...agent.skills ?? []]) if (!contextProviders.has(providerName)) throw configurationError("INVALID_AGENT", `Context provider "${providerName}" required by agent "${agent.name}" is not registered`);
142
486
  }
143
- return { async runAgent(name, request) {
144
- const agent = agents.get(name);
145
- if (!agent) throw new Error(`Agent "${name}" is not registered`);
146
- const model = typeof agent.model === "string" ? models.get(agent.model) : agent.model ?? models.get("default");
147
- const runId = globalThis.crypto.randomUUID();
148
- const events = [];
149
- const createEvent = (type, payload) => ({
150
- type,
151
- runId,
152
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
153
- ...payload === void 0 ? {} : { payload }
487
+ for (const [index, definition] of (config.workflows ?? []).entries()) {
488
+ assertConfiguration(isRecord(definition), "INVALID_WORKFLOW", `Workflow at index ${index} must be an object`);
489
+ assertConfiguration(typeof definition.name === "string", "INVALID_WORKFLOW", "Workflow name must be a string");
490
+ const name = definition.name;
491
+ assertConfiguration(name.trim(), "INVALID_WORKFLOW", "Workflow name cannot be empty");
492
+ assertConfiguration(typeof definition.run === "function", "INVALID_WORKFLOW", `Workflow "${name}" must implement run`);
493
+ assertConfiguration(definition.version === void 0 || typeof definition.version === "string" && Boolean(definition.version.trim()), "INVALID_WORKFLOW", `Workflow "${name}" version must be a non-empty string`);
494
+ assertConfiguration(definition.limits === void 0 || isRecord(definition.limits), "INVALID_WORKFLOW", `Workflow "${name}" limits must be an object`);
495
+ if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_WORKFLOW", `Input schema for workflow "${name}" must implement Standard Schema`);
496
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_WORKFLOW", `Output schema for workflow "${name}" must implement Standard Schema`);
497
+ assertConfiguration(definition.events === void 0 || isRecord(definition.events), "INVALID_WORKFLOW", `Workflow "${name}" events must be an object`);
498
+ const workflowEvents = {};
499
+ for (const [eventName, rawEvent] of Object.entries(definition.events ?? {})) {
500
+ assertConfiguration(Boolean(eventName.trim()) && isRecord(rawEvent), "INVALID_WORKFLOW", `Workflow "${name}" event definitions must be named objects`);
501
+ assertConfiguration(rawEvent.requireActor === void 0 || typeof rawEvent.requireActor === "boolean", "INVALID_WORKFLOW", `Workflow "${name}" event "${eventName}" requireActor must be a boolean`);
502
+ assertConfiguration(rawEvent.payloadSchema === void 0 || isStandardSchema(rawEvent.payloadSchema), "INVALID_WORKFLOW", `Payload schema for workflow "${name}" event "${eventName}" must implement Standard Schema`);
503
+ workflowEvents[eventName] = {
504
+ ...rawEvent.payloadSchema === void 0 ? {} : { payloadSchema: rawEvent.payloadSchema },
505
+ ...rawEvent.requireActor === void 0 ? {} : { requireActor: rawEvent.requireActor }
506
+ };
507
+ }
508
+ const rawLimits = definition.limits;
509
+ assertLimit(rawLimits?.maxSteps, "maxSteps", 1, name, false, "Workflow", "INVALID_WORKFLOW");
510
+ assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name, false, "Workflow", "INVALID_WORKFLOW");
511
+ assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true, "Workflow", "INVALID_WORKFLOW");
512
+ assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true, "Workflow", "INVALID_WORKFLOW");
513
+ assertConfiguration(!workflows.has(name), "DUPLICATE_WORKFLOW", `Workflow "${name}" is duplicated`);
514
+ workflows.set(name, {
515
+ name,
516
+ version: definition.version ?? "1",
517
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
518
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
519
+ ...definition.events === void 0 ? {} : { events: workflowEvents },
520
+ ...rawLimits === void 0 ? {} : { limits: {
521
+ ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
522
+ ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
523
+ ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
524
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
525
+ } },
526
+ run: definition.run
154
527
  });
155
- const emit = (type, payload) => {
156
- const event = createEvent(type, payload);
157
- config.onEvent?.(event);
158
- events.push(event);
528
+ }
529
+ for (const [index, definition] of (config.teams ?? []).entries()) {
530
+ assertConfiguration(isRecord(definition), "INVALID_TEAM", `Team at index ${index} must be an object`);
531
+ assertConfiguration(typeof definition.name === "string", "INVALID_TEAM", "Team name must be a string");
532
+ const name = definition.name;
533
+ assertConfiguration(name.trim(), "INVALID_TEAM", "Team name cannot be empty");
534
+ assertConfiguration(typeof definition.supervisor === "string" && Boolean(definition.supervisor.trim()), "INVALID_TEAM", `Team "${name}" supervisor must be a non-empty string`);
535
+ assertConfiguration(typeof definition.run === "function", "INVALID_TEAM", `Team "${name}" must implement run`);
536
+ assertConfiguration(definition.version === void 0 || typeof definition.version === "string" && Boolean(definition.version.trim()), "INVALID_TEAM", `Team "${name}" version must be a non-empty string`);
537
+ assertConfiguration(Array.isArray(definition.members), "INVALID_TEAM", `Team "${name}" members must be an array`);
538
+ const members = [];
539
+ const memberNames = /* @__PURE__ */ new Set();
540
+ for (const [memberIndex, rawMember] of definition.members.entries()) {
541
+ assertConfiguration(isRecord(rawMember) && typeof rawMember.agent === "string" && Boolean(rawMember.agent.trim()) && typeof rawMember.role === "string" && Boolean(rawMember.role.trim()) && (rawMember.description === void 0 || typeof rawMember.description === "string"), "INVALID_TEAM", `Team "${name}" member at index ${memberIndex} is invalid`);
542
+ assertConfiguration(!memberNames.has(rawMember.agent), "INVALID_TEAM", `Team "${name}" member "${rawMember.agent}" is duplicated`);
543
+ memberNames.add(rawMember.agent);
544
+ members.push({
545
+ agent: rawMember.agent,
546
+ role: rawMember.role,
547
+ ...rawMember.description === void 0 ? {} : { description: rawMember.description }
548
+ });
549
+ }
550
+ const supervisor = definition.supervisor;
551
+ assertConfiguration(agents.has(supervisor), "INVALID_TEAM", `Supervisor "${supervisor}" required by team "${name}" is not registered`);
552
+ for (const member of members) assertConfiguration(agents.has(member.agent), "INVALID_TEAM", `Agent "${member.agent}" required by team "${name}" is not registered`);
553
+ if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_TEAM", `Input schema for team "${name}" must implement Standard Schema`);
554
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_TEAM", `Output schema for team "${name}" must implement Standard Schema`);
555
+ assertConfiguration(definition.limits === void 0 || isRecord(definition.limits), "INVALID_TEAM", `Team "${name}" limits must be an object`);
556
+ const rawLimits = definition.limits;
557
+ assertLimit(rawLimits?.maxDelegations, "maxDelegations", 1, name, false, "Team", "INVALID_TEAM");
558
+ assertLimit(rawLimits?.maxParallel, "maxParallel", 1, name, false, "Team", "INVALID_TEAM");
559
+ assertLimit(rawLimits?.maxSteps, "maxSteps", 1, name, false, "Team", "INVALID_TEAM");
560
+ assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name, false, "Team", "INVALID_TEAM");
561
+ assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true, "Team", "INVALID_TEAM");
562
+ assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true, "Team", "INVALID_TEAM");
563
+ assertConfiguration(!teams.has(name), "DUPLICATE_TEAM", `Team "${name}" is duplicated`);
564
+ assertConfiguration(!workflows.has(name), "INVALID_TEAM", `Team "${name}" conflicts with a workflow of the same name`);
565
+ const team = {
566
+ name,
567
+ version: definition.version ?? "1",
568
+ supervisor,
569
+ members,
570
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
571
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
572
+ ...rawLimits === void 0 ? {} : { limits: {
573
+ ...rawLimits.maxDelegations === void 0 ? {} : { maxDelegations: rawLimits.maxDelegations },
574
+ ...rawLimits.maxParallel === void 0 ? {} : { maxParallel: rawLimits.maxParallel },
575
+ ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
576
+ ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
577
+ ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
578
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
579
+ } },
580
+ run: definition.run
159
581
  };
160
- const emitFailure = (type, payload) => {
161
- const event = createEvent(type, payload);
162
- try {
163
- config.onEvent?.(event);
164
- } catch {}
582
+ teams.set(name, team);
583
+ workflows.set(name, teamAsWorkflow(team));
584
+ }
585
+ const usesMemory = [...agents.values()].some((agent) => agent.memory && (agent.memory.read !== false || agent.memory.write === true));
586
+ return {
587
+ models,
588
+ agents,
589
+ workflows,
590
+ teams,
591
+ tools,
592
+ contextProviders,
593
+ memoryStore: config.memoryStore ?? (usesMemory ? new InMemoryMemoryStore() : void 0),
594
+ runStore: config.runStore ?? new InMemoryRunStore(),
595
+ credentialStore: config.credentialStore,
596
+ sandbox: config.sandbox,
597
+ policies,
598
+ onEvent: config.onEvent,
599
+ observability: config.observability === void 0 ? void 0 : {
600
+ exporters: [...config.observability.exporters],
601
+ ...config.observability.calculateCost ? { calculateCost: config.observability.calculateCost } : {},
602
+ ...config.observability.content ? { content: {
603
+ include: [...config.observability.content.include],
604
+ ...config.observability.content.redact ? { redact: config.observability.content.redact } : {}
605
+ } } : {}
606
+ }
607
+ };
608
+ }
609
+ //#endregion
610
+ //#region src/internal/runtime-constants.ts
611
+ /** How long a run lease stays valid before it must be renewed. */
612
+ const LEASE_MS = 3e4;
613
+ /** How often a running execution renews its lease. */
614
+ const LEASE_RENEW_MS = 1e4;
615
+ /** Reserved name of the internal elicitation tool exposed to the model. */
616
+ const ELICIT_TOOL_NAME = "fevex__elicit";
617
+ //#endregion
618
+ //#region src/internal/run-helpers.ts
619
+ function redact(message, secrets) {
620
+ return secrets.reduce((safe, secret) => secret ? safe.split(secret).join("[REDACTED]") : safe, message);
621
+ }
622
+ function containsSecret(value, secrets) {
623
+ if (typeof value === "string") return secrets.some((secret) => secret && value.includes(secret));
624
+ if (Array.isArray(value)) return value.some((item) => containsSecret(item, secrets));
625
+ if (value && typeof value === "object") return Object.values(value).some((item) => containsSecret(item, secrets));
626
+ return false;
627
+ }
628
+ function effectiveElicitationMode(agent, request) {
629
+ return request.elicitation ?? agent.elicitation ?? "forbid";
630
+ }
631
+ function effectiveApprovalMode(agent, request) {
632
+ return request.approvalMode ?? agent.approvalMode ?? "pause";
633
+ }
634
+ function effectiveToolChoice(agent, request) {
635
+ return request.toolChoice ?? agent.toolChoice ?? "auto";
636
+ }
637
+ function isElicitationToolCall(call) {
638
+ return call.name === ELICIT_TOOL_NAME;
639
+ }
640
+ function readElicitationInput(input) {
641
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new TypeError("Elicitation input must be an object");
642
+ const prompt = input.prompt;
643
+ const responseSchema = input.responseSchema;
644
+ const expiresAt = input.expiresAt;
645
+ if (typeof prompt !== "string" || !prompt.trim()) throw new TypeError("Elicitation prompt must be a non-empty string");
646
+ if (typeof responseSchema !== "object" || responseSchema === null || Array.isArray(responseSchema)) throw new TypeError("Elicitation responseSchema must be an object");
647
+ validateFevexJsonSchemaProfile(responseSchema);
648
+ if (expiresAt !== void 0) {
649
+ if (typeof expiresAt !== "string" || !Number.isFinite(Date.parse(expiresAt))) throw new TypeError("Elicitation expiresAt must be a valid ISO date string");
650
+ }
651
+ return {
652
+ prompt,
653
+ responseSchema,
654
+ ...expiresAt === void 0 ? {} : { expiresAt }
655
+ };
656
+ }
657
+ function isCoordinatorRun(run) {
658
+ return run.kind === "workflow" || run.kind === "team";
659
+ }
660
+ function isTeamRun(run) {
661
+ return run.kind === "team";
662
+ }
663
+ function coordinatorName(run) {
664
+ return run.kind === "team" ? run.teamName : run.workflowName;
665
+ }
666
+ var WorkflowChildPausedError = class extends Error {
667
+ stepId;
668
+ paused;
669
+ constructor(stepId, paused) {
670
+ super(paused.message, { cause: paused });
671
+ this.stepId = stepId;
672
+ this.paused = paused;
673
+ }
674
+ };
675
+ function pauseMatchesResolution(pause, resolution) {
676
+ if (pause.type === "elicitation" && resolution.type === "elicitation") return pause.request.id === resolution.requestId;
677
+ if (pause.type === "approval" && resolution.type === "approval") return pause.approval.id === resolution.approvalId;
678
+ if (pause.type === "tool_execution_unknown" && resolution.type === "tool_execution") return pause.toolCallId === resolution.toolCallId;
679
+ return false;
680
+ }
681
+ function mergeExecutionContext(parent, child) {
682
+ if (!parent) return child;
683
+ if (!child) return parent;
684
+ return {
685
+ ...parent,
686
+ ...child,
687
+ actor: parent.actor ?? child.actor,
688
+ ...parent.attributes || child.attributes ? { attributes: {
689
+ ...parent.attributes,
690
+ ...child.attributes
691
+ } } : {},
692
+ ...parent.prompt || child.prompt ? { prompt: {
693
+ ...parent.prompt,
694
+ ...child.prompt
695
+ } } : {}
696
+ };
697
+ }
698
+ function systemBlock(label, provider, block) {
699
+ return {
700
+ role: "system",
701
+ content: `[${label}: ${provider}/${block.id}]\n${block.content}`
702
+ };
703
+ }
704
+ async function readMemoryMessages(agent, context, memoryStore) {
705
+ if (!agent.memory || agent.memory.read === false || !memoryStore) return [];
706
+ try {
707
+ return (await abortable(() => memoryStore.search({
708
+ query: context.input,
709
+ limit: agent.memory?.limit ?? 5,
710
+ agentName: context.agentName,
711
+ sessionId: context.sessionId,
712
+ namespace: context.context?.namespace,
713
+ actor: context.context?.actor
714
+ }, context), context.signal)).filter((record) => record.content.trim()).map((record) => ({
715
+ role: "system",
716
+ content: `[Memory: ${record.id}]\n${record.content}`
717
+ }));
718
+ } catch (cause) {
719
+ throw new Error("Memory search failed", { cause });
720
+ }
721
+ }
722
+ const toolSourcePayload = (tool, error) => {
723
+ const source = error instanceof IntegrationError ? error.source ?? tool.source : tool.source;
724
+ return source === void 0 ? {} : { source };
725
+ };
726
+ const toolLabelPayload = (tool) => tool.label === void 0 ? {} : { toolLabel: tool.label };
727
+ const toolErrorPayload = (tool, error) => ({
728
+ ...toolSourcePayload(tool, error),
729
+ ...error instanceof IntegrationError ? {
730
+ errorCode: error.code,
731
+ retryable: error.retryable
732
+ } : {}
733
+ });
734
+ const runErrorPayload = (error) => {
735
+ if (!(error instanceof IntegrationError)) return {};
736
+ return {
737
+ errorCode: error.code,
738
+ retryable: error.retryable,
739
+ ...error.source === void 0 ? {} : { source: error.source }
740
+ };
741
+ };
742
+ //#endregion
743
+ //#region src/internal/definition-hash.ts
744
+ function canonicalize(value) {
745
+ if (Array.isArray(value)) return value.map(canonicalize);
746
+ if (typeof value !== "object" || value === null) return value;
747
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalize(item)]));
748
+ }
749
+ async function transportableTool(tool) {
750
+ return {
751
+ name: tool.name,
752
+ description: tool.description,
753
+ inputSchema: tool.inputJsonSchema ?? (tool.inputSchema === void 0 ? void 0 : toTransportableSchema(tool.inputSchema, "input", `Input schema for tool "${tool.name}" is not transportable`)),
754
+ outputSchema: tool.outputJsonSchema ?? (tool.outputSchema === void 0 ? void 0 : toTransportableSchema(tool.outputSchema, "output", `Output schema for tool "${tool.name}" is not transportable`)),
755
+ risk: tool.risk ?? "read",
756
+ approval: tool.approval ?? "never",
757
+ idempotency: tool.idempotency ?? "none",
758
+ retry: tool.retry,
759
+ credentials: tool.credentials ?? [],
760
+ sandbox: tool.sandbox,
761
+ source: tool.source
762
+ };
763
+ }
764
+ async function sha256(value) {
765
+ const encoded = new TextEncoder().encode(JSON.stringify(canonicalize(value)));
766
+ const digest = await crypto.subtle.digest("SHA-256", encoded);
767
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
768
+ }
769
+ async function definitionHash(name, modelName, agent, tools) {
770
+ const toolDefinitions = await Promise.all((agent.tools ?? []).map((toolName) => transportableTool(tools.get(toolName))));
771
+ return sha256({
772
+ name,
773
+ modelName,
774
+ instructions: agent.instructions,
775
+ context: agent.context,
776
+ memory: agent.memory,
777
+ skills: agent.skills,
778
+ tools: toolDefinitions,
779
+ reasoning: agent.reasoning,
780
+ modelOptions: agent.modelOptions,
781
+ toolChoice: agent.toolChoice,
782
+ elicitation: agent.elicitation,
783
+ approvalMode: agent.approvalMode,
784
+ limits: agent.limits,
785
+ inputSchema: agent.inputSchema === void 0 ? void 0 : toTransportableSchema(agent.inputSchema, "input", `Input schema for agent "${name}" is not transportable`),
786
+ outputSchema: agent.outputSchema === void 0 ? void 0 : toTransportableSchema(agent.outputSchema, "output", `Output schema for agent "${name}" is not transportable`)
787
+ });
788
+ }
789
+ function workflowDefinitionHash(name, workflow) {
790
+ return sha256({
791
+ name,
792
+ version: workflow.version ?? "1",
793
+ limits: workflow.limits,
794
+ inputSchema: workflow.inputSchema === void 0 ? void 0 : toTransportableSchema(workflow.inputSchema, "input", `Input schema for workflow "${name}" is not transportable`),
795
+ outputSchema: workflow.outputSchema === void 0 ? void 0 : toTransportableSchema(workflow.outputSchema, "output", `Output schema for workflow "${name}" is not transportable`),
796
+ events: Object.fromEntries(Object.entries(workflow.events ?? {}).map(([eventName, event]) => [eventName, {
797
+ requireActor: event.requireActor ?? false,
798
+ payloadSchema: event.payloadSchema === void 0 ? void 0 : toTransportableSchema(event.payloadSchema, "input", `Payload schema for workflow "${name}" event "${eventName}" is not transportable`)
799
+ }]))
800
+ });
801
+ }
802
+ function teamDefinitionHash(name, team) {
803
+ return sha256({
804
+ name,
805
+ version: team.version ?? "1",
806
+ supervisor: team.supervisor,
807
+ members: team.members,
808
+ limits: team.limits,
809
+ inputSchema: team.inputSchema === void 0 ? void 0 : toTransportableSchema(team.inputSchema, "input", `Input schema for team "${name}" is not transportable`),
810
+ outputSchema: team.outputSchema === void 0 ? void 0 : toTransportableSchema(team.outputSchema, "output", `Output schema for team "${name}" is not transportable`)
811
+ });
812
+ }
813
+ //#endregion
814
+ //#region src/internal/workflow-budget.ts
815
+ function minLimit(first, second) {
816
+ if (first === void 0) return second;
817
+ if (second === void 0) return first;
818
+ if (first === false) return second;
819
+ if (second === false) return first;
820
+ return Math.min(first, second);
821
+ }
822
+ function combineLimits(first, second) {
823
+ if (!first) return second;
824
+ if (!second) return first;
825
+ return {
826
+ ...minLimit(first.maxSteps, second.maxSteps) === void 0 ? {} : { maxSteps: minLimit(first.maxSteps, second.maxSteps) },
827
+ ...minLimit(first.maxToolCalls, second.maxToolCalls) === void 0 ? {} : { maxToolCalls: minLimit(first.maxToolCalls, second.maxToolCalls) },
828
+ ...minLimit(first.maxInputTokens, second.maxInputTokens) === void 0 ? {} : { maxInputTokens: minLimit(first.maxInputTokens, second.maxInputTokens) },
829
+ ...minLimit(first.maxOutputTokens, second.maxOutputTokens) === void 0 ? {} : { maxOutputTokens: minLimit(first.maxOutputTokens, second.maxOutputTokens) }
830
+ };
831
+ }
832
+ function subtractLimit(limit, used) {
833
+ if (limit === void 0 || limit === false) return limit;
834
+ return Math.max(0, limit - (used ?? 0));
835
+ }
836
+ function remainingWorkflowLimits(limits, budget) {
837
+ if (!limits) return void 0;
838
+ return {
839
+ ...limits.maxSteps === void 0 ? {} : { maxSteps: subtractLimit(limits.maxSteps, budget?.steps) },
840
+ ...limits.maxToolCalls === void 0 ? {} : { maxToolCalls: subtractLimit(limits.maxToolCalls, budget?.toolCalls) },
841
+ ...limits.maxInputTokens === void 0 ? {} : { maxInputTokens: subtractLimit(limits.maxInputTokens, budget?.usage?.inputTokens) },
842
+ ...limits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: subtractLimit(limits.maxOutputTokens, budget?.usage?.outputTokens) }
843
+ };
844
+ }
845
+ function assertWorkflowBudget(workflowName, limits, budget) {
846
+ if (!limits || !budget) return;
847
+ if (limits.maxSteps !== void 0 && budget.steps > limits.maxSteps) throw new Error(`Workflow "${workflowName}" exceeded maxSteps limit of ${limits.maxSteps}`);
848
+ if (limits.maxToolCalls !== void 0 && budget.toolCalls > limits.maxToolCalls) throw new Error(`Workflow "${workflowName}" exceeded maxToolCalls limit of ${limits.maxToolCalls}`);
849
+ assertTokenBudget(`workflow:${workflowName}`, "maxInputTokens", "inputTokens", limits.maxInputTokens, budget.usage, budget.usage);
850
+ assertTokenBudget(`workflow:${workflowName}`, "maxOutputTokens", "outputTokens", limits.maxOutputTokens, budget.usage, budget.usage);
851
+ }
852
+ function addWorkflowBudget(first, second) {
853
+ return {
854
+ usage: addUsage(first?.usage, second.usage),
855
+ steps: (first?.steps ?? 0) + second.steps,
856
+ toolCalls: (first?.toolCalls ?? 0) + second.toolCalls
857
+ };
858
+ }
859
+ //#endregion
860
+ //#region src/internal/agent-engine.ts
861
+ /**
862
+ * Everything that runs a single agent: preparation, the model/tool loop,
863
+ * checkpointing, knowledge and memory, resume and out-of-band cancellation.
864
+ *
865
+ * Depends only on {@link RunCore}. It must never reach into the workflow
866
+ * engine — that direction is what keeps the two separable.
867
+ */
868
+ function createAgentEngine(composition, core) {
869
+ const { models, agents, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
870
+ const { activeRuns, activeSessions, runtimeOwner, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
871
+ const prepareExecution = async (name, request) => {
872
+ const agent = agents.get(name);
873
+ if (!agent) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${name}" is not registered`);
874
+ const { modelName } = resolveModel(agent, request.model);
875
+ const now = (/* @__PURE__ */ new Date()).toISOString();
876
+ let session;
877
+ const newSession = request.sessionId === void 0;
878
+ if (newSession) session = {
879
+ id: crypto.randomUUID(),
880
+ history: [],
881
+ createdAt: now,
882
+ updatedAt: now
883
+ };
884
+ else {
885
+ const stored = await runStore.getSession(request.sessionId);
886
+ if (!stored) throw new FevexRunError("SESSION_NOT_FOUND", `Session "${request.sessionId}" does not exist`);
887
+ session = stored;
888
+ }
889
+ if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" already has an active run`);
890
+ activeSessions.add(session.id);
891
+ const controller = new AbortController();
892
+ const signal = request.signal ? AbortSignal.any([controller.signal, request.signal]) : controller.signal;
893
+ const run = {
894
+ id: crypto.randomUUID(),
895
+ sessionId: session.id,
896
+ agentName: name,
897
+ status: "running",
898
+ revision: 0,
899
+ createdAt: now,
900
+ updatedAt: now
901
+ };
902
+ const state = {
903
+ run,
904
+ session,
905
+ request: {
906
+ ...request,
907
+ sessionId: session.id,
908
+ signal
909
+ },
910
+ controller,
911
+ eventSequence: 0,
912
+ advancing: false
913
+ };
914
+ try {
915
+ if (isDurableRunStore(runStore)) {
916
+ const inputContent = serializeValue(agent.inputSchema === void 0 ? request.input : await abortable(() => validateSchema(agent.inputSchema, request.input, `Input for agent "${name}" does not match inputSchema`), signal), "Run input must be a string or JSON-serializable value");
917
+ const knowledge = await readKnowledgeMessages(state, agent, inputContent);
918
+ const messages = [
919
+ {
920
+ role: "system",
921
+ content: agent.instructions
922
+ },
923
+ ...knowledge.skills,
924
+ ...session.history,
925
+ ...knowledge.context,
926
+ ...knowledge.memory,
927
+ {
928
+ role: "user",
929
+ content: inputContent
930
+ }
931
+ ];
932
+ const checkpoint = {
933
+ version: 2,
934
+ runId: run.id,
935
+ ...request.model !== void 0 || typeof agent.model === "string" ? { modelName } : {},
936
+ ...request.reasoning === void 0 ? {} : { reasoning: request.reasoning },
937
+ definitionHash: await definitionHash(name, modelName, agent, tools),
938
+ limits: combineLimits(agent.limits, request.limits),
939
+ messages,
940
+ inputContent,
941
+ context: request.context,
942
+ step: 1,
943
+ toolCallCount: 0,
944
+ seenToolCallIds: [],
945
+ pendingTools: [],
946
+ pendingIndex: 0,
947
+ effectiveElicitationMode: effectiveElicitationMode(agent, request),
948
+ effectiveApprovalMode: effectiveApprovalMode(agent, request),
949
+ effectiveToolChoice: effectiveToolChoice(agent, request)
950
+ };
951
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
952
+ const lease = {
953
+ generation: 0,
954
+ runId: run.id,
955
+ ownerId,
956
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
957
+ };
958
+ const started = createEvent(state, "run.started", void 0);
959
+ if (!await runStore.createExecution({
960
+ run,
961
+ checkpoint,
962
+ session,
963
+ lease,
964
+ events: [started]
965
+ })) throw new FevexRunError("RUN_CONFLICT", `Run or session "${session.id}" is active or was modified`, run.id);
966
+ state.checkpoint = checkpoint;
967
+ state.lease = lease;
968
+ state.initialEvents = [started];
969
+ notifyObserver(started);
970
+ startLease(state, runStore);
971
+ } else {
972
+ if (newSession) await runStore.saveSession(session);
973
+ await runStore.saveRun(run);
974
+ }
975
+ } catch (error) {
976
+ activeSessions.delete(session.id);
977
+ throw error;
978
+ }
979
+ activeRuns.set(run.id, state);
980
+ return state;
981
+ };
982
+ const durableCheckpoint = async (state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex) => {
983
+ const agent = agents.get(state.run.agentName);
984
+ if (agent.model !== void 0 && typeof agent.model !== "string") throw new FevexRunError("RUN_NOT_RESUMABLE", "Durable continuation requires a named model", state.run.id);
985
+ let serializedState;
986
+ if (providerState !== void 0) {
987
+ if (!model.stateCodec) throw new FevexRunError("RUN_NOT_RESUMABLE", "ModelGateway must provide stateCodec for durable continuation", state.run.id);
988
+ serializedState = toJsonValue(model.stateCodec.serialize(providerState), "Model provider state must be JSON-serializable");
989
+ }
990
+ return {
991
+ version: 2,
992
+ runId: state.run.id,
993
+ modelName,
994
+ ...state.request.reasoning === void 0 ? {} : { reasoning: state.request.reasoning },
995
+ definitionHash: await definitionHash(state.run.agentName, modelName, agent, tools),
996
+ limits: state.checkpoint?.limits ?? combineLimits(agent.limits, state.request.limits),
997
+ messages: structuredClone(messages),
998
+ inputContent,
999
+ context: state.request.context,
1000
+ usage,
1001
+ providerState: serializedState,
1002
+ step,
1003
+ toolCallCount,
1004
+ seenToolCallIds: [...seenToolCallIds],
1005
+ pendingTools: structuredClone(pendingTools),
1006
+ pendingIndex,
1007
+ effectiveElicitationMode: state.checkpoint?.effectiveElicitationMode ?? effectiveElicitationMode(agent, state.request),
1008
+ effectiveApprovalMode: state.checkpoint?.effectiveApprovalMode ?? effectiveApprovalMode(agent, state.request),
1009
+ effectiveToolChoice: state.checkpoint?.effectiveToolChoice ?? effectiveToolChoice(agent, state.request)
1010
+ };
1011
+ };
1012
+ const knowledgeContext = (state, input) => ({
1013
+ agentName: state.run.agentName,
1014
+ input,
1015
+ sessionId: state.session.id,
1016
+ ...state.request.context === void 0 ? {} : { context: state.request.context },
1017
+ signal: state.request.signal
1018
+ });
1019
+ const readProvider = async (providerName, label, context) => {
1020
+ try {
1021
+ const provider = contextProviders.get(providerName);
1022
+ return (await abortable(() => provider.read(context), context.signal)).filter((block) => block.content.trim()).map((block) => systemBlock(label, providerName, block));
1023
+ } catch (cause) {
1024
+ throw new Error(`${label} provider "${providerName}" failed`, { cause });
1025
+ }
1026
+ };
1027
+ const readKnowledgeMessages = async (state, agent, input) => {
1028
+ const context = knowledgeContext(state, input);
1029
+ return {
1030
+ skills: (await Promise.all((agent.skills ?? []).map((name) => readProvider(name, "Skill", context)))).flat(),
1031
+ context: (await Promise.all((agent.context ?? []).map((name) => readProvider(name, "Context", context)))).flat(),
1032
+ memory: await readMemoryMessages(agent, context, memoryStore)
1033
+ };
1034
+ };
1035
+ const writeMemory = async (state, agent, input, output) => {
1036
+ if (!agent.memory?.write || !memoryStore) return;
1037
+ const context = knowledgeContext(state, input);
1038
+ try {
1039
+ await abortable(() => memoryStore.write({
1040
+ content: `User: ${input}\nAssistant: ${serializeJsonValue(output)}`,
1041
+ agentName: state.run.agentName,
1042
+ sessionId: state.session.id,
1043
+ namespace: state.request.context?.namespace,
1044
+ actor: state.request.context?.actor,
1045
+ metadata: { runId: state.run.id }
1046
+ }, context), state.request.signal);
1047
+ } catch (cause) {
1048
+ throw new Error("Memory write failed", { cause });
1049
+ }
1050
+ };
1051
+ async function* executeAgent(name, state) {
1052
+ const { request } = state;
1053
+ const agent = agents.get(name);
1054
+ const { modelName, model } = resolveModel(agent, state.checkpoint?.modelName ?? request.model);
1055
+ const events = [...state.initialEvents ?? []];
1056
+ const firstEvents = state.initialEvents ?? [];
1057
+ state.initialEvents = void 0;
1058
+ const emit = async (type, payload) => {
1059
+ const event = await emitEvent(state, type, payload);
165
1060
  events.push(event);
1061
+ return event;
166
1062
  };
1063
+ let inputContent;
1064
+ let messages;
1065
+ let usage;
1066
+ let providerState;
1067
+ let step;
1068
+ let toolCallCount;
1069
+ let seenToolCallIds;
1070
+ let pendingTools;
1071
+ let pendingIndex;
167
1072
  try {
168
- emit("run.started");
169
- request.signal?.throwIfAborted();
170
- if (request.outputSchema !== void 0) assertStandardSchema(request.outputSchema, `Output schema for request to agent "${name}" must implement Standard Schema`);
171
- const messages = [{
172
- role: "system",
173
- content: agent.instructions
174
- }, {
175
- role: "user",
176
- content: serializeValue(request.input, "Run input must be a string or JSON-serializable value")
177
- }];
178
- const activeOutputSchema = request.outputSchema !== void 0 ? request.outputSchema : agent.outputSchema;
1073
+ for (const event of firstEvents) yield event;
1074
+ if (state.checkpoint) {
1075
+ const checkpoint = state.checkpoint;
1076
+ if (checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${state.run.id}" is unsupported`, state.run.id);
1077
+ const hash = await definitionHash(name, modelName, agent, tools);
1078
+ if (checkpoint.definitionHash !== hash) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for agent "${name}" changed`, state.run.id);
1079
+ inputContent = checkpoint.inputContent;
1080
+ messages = checkpoint.messages;
1081
+ usage = checkpoint.usage;
1082
+ providerState = checkpoint.providerState === void 0 ? void 0 : model.stateCodec?.restore(checkpoint.providerState);
1083
+ step = checkpoint.step;
1084
+ toolCallCount = checkpoint.toolCallCount;
1085
+ seenToolCallIds = new Set(checkpoint.seenToolCallIds);
1086
+ pendingTools = checkpoint.pendingTools;
1087
+ pendingIndex = checkpoint.pendingIndex;
1088
+ } else {
1089
+ yield await emit("run.started", void 0);
1090
+ request.signal.throwIfAborted();
1091
+ inputContent = serializeValue(agent.inputSchema === void 0 ? request.input : await abortable(() => validateSchema(agent.inputSchema, request.input, `Input for agent "${name}" does not match inputSchema`), request.signal), "Run input must be a string or JSON-serializable value");
1092
+ const knowledge = await readKnowledgeMessages(state, agent, inputContent);
1093
+ messages = [
1094
+ {
1095
+ role: "system",
1096
+ content: agent.instructions
1097
+ },
1098
+ ...knowledge.skills,
1099
+ ...state.session.history,
1100
+ ...knowledge.context,
1101
+ ...knowledge.memory,
1102
+ {
1103
+ role: "user",
1104
+ content: inputContent
1105
+ }
1106
+ ];
1107
+ step = 1;
1108
+ toolCallCount = 0;
1109
+ seenToolCallIds = /* @__PURE__ */ new Set();
1110
+ pendingTools = [];
1111
+ pendingIndex = 0;
1112
+ }
1113
+ const activeOutputSchema = agent.outputSchema;
179
1114
  const outputSchema = activeOutputSchema === void 0 ? void 0 : toTransportableSchema(activeOutputSchema, "output", `Output schema for agent "${name}" is not transportable`);
180
- const agentTools = (agent.tools ?? []).map((toolName) => {
1115
+ const currentElicitationMode = state.checkpoint?.effectiveElicitationMode ?? effectiveElicitationMode(agent, request);
1116
+ const currentApprovalMode = state.checkpoint?.effectiveApprovalMode ?? effectiveApprovalMode(agent, request);
1117
+ const currentToolChoice = state.checkpoint?.effectiveToolChoice ?? effectiveToolChoice(agent, request);
1118
+ const agentTools = await Promise.all((agent.tools ?? []).map(async (toolName) => {
181
1119
  const tool = tools.get(toolName);
182
- return toToolSpec(tool, tool.inputSchema === void 0 ? void 0 : toTransportableSchema(tool.inputSchema, "input", `Input schema for tool "${tool.name}" is not transportable`));
1120
+ const remote = await tool.resolve?.({ context: request.context });
1121
+ const inputSchema = tool.inputJsonSchema ?? remote?.inputSchema ?? (tool.inputSchema === void 0 ? void 0 : toTransportableSchema(tool.inputSchema, "input", `Input schema for tool "${tool.name}" is not transportable`));
1122
+ return toToolSpec(tool.description === void 0 && remote?.description !== void 0 ? {
1123
+ ...tool,
1124
+ description: remote.description
1125
+ } : tool, inputSchema);
1126
+ }));
1127
+ const allTools = currentElicitationMode === "pause" ? [...agentTools, {
1128
+ name: ELICIT_TOOL_NAME,
1129
+ description: "Request external information required to continue this run. Use prompt for explanatory text shown outside the form. Put short field labels in responseSchema property titles and optional field help in property descriptions.",
1130
+ inputSchema: {
1131
+ type: "object",
1132
+ properties: {
1133
+ prompt: {
1134
+ type: "string",
1135
+ description: "Explanatory text for the user; do not duplicate field labels here."
1136
+ },
1137
+ responseSchema: {
1138
+ type: "object",
1139
+ description: "JSON Schema for the expected response. Each property should include a short title for the form label and may include a description for field help."
1140
+ },
1141
+ expiresAt: { type: "string" }
1142
+ },
1143
+ required: ["prompt", "responseSchema"],
1144
+ additionalProperties: false
1145
+ }
1146
+ }] : agentTools;
1147
+ const effectiveLimits = state.checkpoint?.limits ?? combineLimits(agent.limits, request.limits);
1148
+ const maxSteps = effectiveLimits?.maxSteps ?? 8;
1149
+ const maxToolCalls = effectiveLimits?.maxToolCalls ?? 16;
1150
+ if (typeof currentToolChoice === "object" && !allTools.some((tool) => tool.name === currentToolChoice.name)) throw new Error(`Tool "${currentToolChoice.name}" is not available to agent "${name}"`);
1151
+ while (step <= maxSteps) {
1152
+ request.signal.throwIfAborted();
1153
+ if (pendingTools.length === 0) {
1154
+ const hasToolBudget = step < maxSteps && toolCallCount < maxToolCalls;
1155
+ const modelToolChoice = hasToolBudget && allTools.length ? currentToolChoice === "auto" ? void 0 : currentToolChoice : void 0;
1156
+ yield await emit("model.started", { step });
1157
+ const modelStream = readModelStream(model, {
1158
+ messages: [...messages],
1159
+ tools: hasToolBudget && allTools.length ? allTools : void 0,
1160
+ ...modelToolChoice === void 0 ? {} : { toolChoice: modelToolChoice },
1161
+ reasoning: state.request.reasoning ?? agent.reasoning,
1162
+ modelOptions: agent.modelOptions,
1163
+ outputSchema,
1164
+ ...remainingOutputTokens(effectiveLimits?.maxOutputTokens, usage) === void 0 ? {} : { maxOutputTokens: remainingOutputTokens(effectiveLimits?.maxOutputTokens, usage) },
1165
+ ...providerState === void 0 ? {} : { providerState },
1166
+ signal: request.signal
1167
+ });
1168
+ let modelResult = await modelStream.next();
1169
+ while (!modelResult.done) {
1170
+ yield await emit("model.output.delta", {
1171
+ step,
1172
+ delta: modelResult.value
1173
+ });
1174
+ modelResult = await modelStream.next();
1175
+ }
1176
+ const result = modelResult.value;
1177
+ providerState = result.providerState;
1178
+ usage = addUsage(usage, result.usage);
1179
+ const usagePayload = eventUsage(usage);
1180
+ yield await emit("model.completed", {
1181
+ step,
1182
+ ...usagePayload === void 0 ? {} : { usage: usagePayload }
1183
+ });
1184
+ assertTokenBudget(name, "maxInputTokens", "inputTokens", effectiveLimits?.maxInputTokens, result.usage, usage);
1185
+ assertTokenBudget(name, "maxOutputTokens", "outputTokens", effectiveLimits?.maxOutputTokens, result.usage, usage);
1186
+ if (!result.toolCalls?.length) {
1187
+ if (result.output === void 0) throw new Error(`Model for agent "${name}" returned no output`);
1188
+ const output = toJsonValue(activeOutputSchema === void 0 ? result.output : await abortable(() => validateSchema(activeOutputSchema, result.output, `Output from agent "${name}" does not match outputSchema`), request.signal), `Output from agent "${name}" must be JSON-serializable`);
1189
+ await writeMemory(state, agent, inputContent, output);
1190
+ state.session.history.push({
1191
+ role: "user",
1192
+ content: inputContent
1193
+ }, {
1194
+ role: "assistant",
1195
+ content: serializeValue(output, "Output must be serializable")
1196
+ });
1197
+ state.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1198
+ Object.assign(state.run, {
1199
+ status: "completed",
1200
+ pause: void 0,
1201
+ output,
1202
+ ...usage === void 0 ? {} : { usage },
1203
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1204
+ });
1205
+ const payload = { output };
1206
+ const finalUsage = eventUsage(usage);
1207
+ if (finalUsage) payload.usage = finalUsage;
1208
+ const completed = createEvent(state, "run.completed", payload);
1209
+ if (isDurableRunStore(runStore)) await commit(state, {
1210
+ checkpoint: null,
1211
+ session: state.session,
1212
+ events: [completed]
1213
+ });
1214
+ else {
1215
+ await runStore.saveSession(state.session);
1216
+ await runStore.saveRun(state.run);
1217
+ await runStore.appendEvent(completed);
1218
+ notifyObserver(completed);
1219
+ }
1220
+ events.push(completed);
1221
+ await releaseExecution(state);
1222
+ yield completed;
1223
+ return {
1224
+ runId: state.run.id,
1225
+ sessionId: state.session.id,
1226
+ output,
1227
+ events,
1228
+ usage
1229
+ };
1230
+ }
1231
+ if (!hasToolBudget) throw new Error(step >= maxSteps ? `Agent "${name}" reached maxSteps limit of ${maxSteps}` : `Agent "${name}" reached maxToolCalls limit of ${maxToolCalls}`);
1232
+ assertContinuationBudget(name, "maxInputTokens", "inputTokens", effectiveLimits?.maxInputTokens, usage);
1233
+ assertContinuationBudget(name, "maxOutputTokens", "outputTokens", effectiveLimits?.maxOutputTokens, usage);
1234
+ if (result.toolCalls.length > maxToolCalls - toolCallCount) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
1235
+ const elicitationCall = result.toolCalls.find(isElicitationToolCall);
1236
+ if (elicitationCall) {
1237
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "Elicitation requires a DurableRunStore", state.run.id);
1238
+ if (currentElicitationMode !== "pause") throw new Error(`Tool "${ELICIT_TOOL_NAME}" is not available to agent "${name}"`);
1239
+ if (result.toolCalls.length !== 1) throw new Error(`${ELICIT_TOOL_NAME} must be the only tool call in a model turn`);
1240
+ if (!elicitationCall.id?.trim()) throw new Error("Tool call id cannot be empty");
1241
+ if (seenToolCallIds.has(elicitationCall.id)) throw new Error(`Tool call id "${elicitationCall.id}" is duplicated in run "${state.run.id}"`);
1242
+ const rawInput = toJsonValue(elicitationCall.input, `Input for tool "${ELICIT_TOOL_NAME}" must be JSON-serializable`);
1243
+ const input = readElicitationInput(rawInput);
1244
+ seenToolCallIds.add(elicitationCall.id);
1245
+ messages.push({
1246
+ role: "assistant",
1247
+ content: result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be serializable`),
1248
+ toolCalls: [{
1249
+ ...elicitationCall,
1250
+ input: rawInput
1251
+ }]
1252
+ });
1253
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step + 1, toolCallCount, seenToolCallIds, [], 0);
1254
+ const request = {
1255
+ id: crypto.randomUUID(),
1256
+ toolCallId: elicitationCall.id,
1257
+ prompt: input.prompt,
1258
+ responseSchema: input.responseSchema,
1259
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
1260
+ ...input.expiresAt === void 0 ? {} : { expiresAt: input.expiresAt }
1261
+ };
1262
+ state.run.status = "paused";
1263
+ state.run.pause = {
1264
+ type: "elicitation",
1265
+ request
1266
+ };
1267
+ const requested = createEvent(state, "elicitation.requested", { request });
1268
+ const paused = createEvent(state, "run.paused", {
1269
+ reason: "elicitation",
1270
+ toolCallId: elicitationCall.id
1271
+ });
1272
+ await commit(state, {
1273
+ checkpoint,
1274
+ events: [requested, paused]
1275
+ });
1276
+ events.push(requested, paused);
1277
+ await releaseExecution(state);
1278
+ yield requested;
1279
+ yield paused;
1280
+ return void 0;
1281
+ }
1282
+ const batch = /* @__PURE__ */ new Set();
1283
+ pendingTools = [];
1284
+ for (const call of result.toolCalls) {
1285
+ if (!call.id?.trim()) throw new Error("Tool call id cannot be empty");
1286
+ if (!call.name?.trim()) throw new Error("Tool call name cannot be empty");
1287
+ if (seenToolCallIds.has(call.id) || batch.has(call.id)) throw new Error(`Tool call id "${call.id}" is duplicated in run "${state.run.id}"`);
1288
+ batch.add(call.id);
1289
+ const tool = agent.tools?.includes(call.name) ? tools.get(call.name) : void 0;
1290
+ if (!tool) throw new Error(`Tool "${call.name}" is not available to agent "${name}"`);
1291
+ const rawInput = toJsonValue(call.input, `Input for tool "${call.name}" must be JSON-serializable`);
1292
+ let validated;
1293
+ try {
1294
+ validated = tool.inputSchema === void 0 ? rawInput : await validateSchema(tool.inputSchema, rawInput, `Input for tool "${call.name}" does not match inputSchema`);
1295
+ } catch (error) {
1296
+ yield await emit("tool.failed", {
1297
+ step,
1298
+ toolCallId: call.id,
1299
+ toolName: call.name,
1300
+ error: toErrorMessage(error),
1301
+ ...toolErrorPayload(tool, error)
1302
+ });
1303
+ throw error;
1304
+ }
1305
+ pendingTools.push({
1306
+ call: {
1307
+ ...call,
1308
+ input: rawInput
1309
+ },
1310
+ input: toJsonValue(validated, `Input for tool "${call.name}" must be serializable`),
1311
+ idempotencyKey: crypto.randomUUID(),
1312
+ attempt: 0
1313
+ });
1314
+ }
1315
+ for (const id of batch) seenToolCallIds.add(id);
1316
+ messages.push({
1317
+ role: "assistant",
1318
+ content: result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be serializable`),
1319
+ toolCalls: pendingTools.map(({ call }) => call)
1320
+ });
1321
+ pendingIndex = 0;
1322
+ }
1323
+ while (pendingIndex < pendingTools.length) {
1324
+ request.signal.throwIfAborted();
1325
+ const pending = pendingTools[pendingIndex];
1326
+ const tool = tools.get(pending.call.name);
1327
+ const durable = isDurableRunStore(runStore) ? runStore : void 0;
1328
+ if (!durable && (tool.approval === "required" || tool.idempotency === "keyed")) throw new FevexRunError("DURABLE_STORE_REQUIRED", `Tool "${tool.name}" requires a DurableRunStore`, state.run.id);
1329
+ let record = durable ? await durable.getToolExecution(state.run.id, pending.call.id) : void 0;
1330
+ const policy = await authorize(state, tool, pending.input, "tool.execute");
1331
+ if ((tool.approval === "required" || policy === "require_approval") && state.approvedToolCallId !== pending.call.id && record?.status !== "started" && record?.status !== "completed") {
1332
+ if (currentApprovalMode === "deny") throw new FevexRunError("POLICY_DENIED", `Tool "${tool.name}" requires approval but approvalMode is "deny"`, state.run.id);
1333
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1334
+ const approval = {
1335
+ id: crypto.randomUUID(),
1336
+ toolCallId: pending.call.id,
1337
+ toolName: pending.call.name,
1338
+ ...tool.label === void 0 ? {} : { toolLabel: tool.label },
1339
+ input: pending.input,
1340
+ risk: tool.risk ?? "read",
1341
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
1342
+ };
1343
+ state.run.status = "paused";
1344
+ state.run.pause = {
1345
+ type: "approval",
1346
+ approval
1347
+ };
1348
+ const requested = createEvent(state, "approval.requested", {
1349
+ approvalId: approval.id,
1350
+ toolCallId: approval.toolCallId,
1351
+ toolName: approval.toolName,
1352
+ ...toolLabelPayload(tool),
1353
+ ...toolSourcePayload(tool)
1354
+ });
1355
+ const paused = createEvent(state, "run.paused", {
1356
+ reason: "approval",
1357
+ toolCallId: pending.call.id
1358
+ });
1359
+ await commit(state, {
1360
+ checkpoint,
1361
+ events: [requested, paused]
1362
+ });
1363
+ events.push(requested, paused);
1364
+ await releaseExecution(state);
1365
+ yield requested;
1366
+ yield paused;
1367
+ return void 0;
1368
+ }
1369
+ state.approvedToolCallId = void 0;
1370
+ if (Boolean(durable && (state.checkpoint || tool.risk && tool.risk !== "read" || tool.idempotency === "keyed" || tool.retry || tool.credentials?.length || policies.length))) {
1371
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1372
+ await commit(state, { checkpoint });
1373
+ state.checkpoint = checkpoint;
1374
+ }
1375
+ if (record?.status === "completed") {
1376
+ messages.push({
1377
+ role: "tool",
1378
+ name: pending.call.name,
1379
+ toolCallId: pending.call.id,
1380
+ content: serializeJsonValue(record.output)
1381
+ });
1382
+ toolCallCount += 1;
1383
+ pendingIndex += 1;
1384
+ continue;
1385
+ }
1386
+ if (record?.status === "started" && tool.idempotency !== "keyed" && state.forcedRetryToolCallId !== pending.call.id) {
1387
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1388
+ state.run.status = "paused";
1389
+ state.run.pause = {
1390
+ type: "tool_execution_unknown",
1391
+ toolCallId: pending.call.id,
1392
+ toolName: pending.call.name,
1393
+ input: pending.input
1394
+ };
1395
+ const unknown = createEvent(state, "tool.execution_unknown", {
1396
+ step,
1397
+ toolCallId: pending.call.id,
1398
+ toolName: pending.call.name,
1399
+ ...toolSourcePayload(tool)
1400
+ });
1401
+ const paused = createEvent(state, "run.paused", {
1402
+ reason: "tool_execution_unknown",
1403
+ toolCallId: pending.call.id
1404
+ });
1405
+ await commit(state, {
1406
+ checkpoint,
1407
+ events: [unknown, paused]
1408
+ });
1409
+ events.push(unknown, paused);
1410
+ await releaseExecution(state);
1411
+ yield unknown;
1412
+ yield paused;
1413
+ return void 0;
1414
+ }
1415
+ const forcedRetry = state.forcedRetryToolCallId === pending.call.id;
1416
+ state.forcedRetryToolCallId = void 0;
1417
+ const configuredMaxAttempts = tool.retry?.maxAttempts ?? 1;
1418
+ const maxAttempts = forcedRetry ? Math.max(configuredMaxAttempts, (record?.attempt ?? 0) + 1) : configuredMaxAttempts;
1419
+ let output;
1420
+ let attempt = record?.status === "started" && tool.idempotency === "keyed" ? Math.max(0, record.attempt - 1) : record?.attempt ?? 0;
1421
+ while (attempt < maxAttempts) {
1422
+ if (attempt > 0) {
1423
+ if (await authorize(state, tool, pending.input, "tool.execute") === "require_approval") {
1424
+ if (currentApprovalMode === "deny") throw new FevexRunError("POLICY_DENIED", `Tool "${tool.name}" requires approval but approvalMode is "deny"`, state.run.id);
1425
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1426
+ const approval = {
1427
+ id: crypto.randomUUID(),
1428
+ toolCallId: pending.call.id,
1429
+ toolName: pending.call.name,
1430
+ ...tool.label === void 0 ? {} : { toolLabel: tool.label },
1431
+ input: pending.input,
1432
+ risk: tool.risk ?? "read",
1433
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
1434
+ };
1435
+ state.run.status = "paused";
1436
+ state.run.pause = {
1437
+ type: "approval",
1438
+ approval
1439
+ };
1440
+ const requested = createEvent(state, "approval.requested", {
1441
+ approvalId: approval.id,
1442
+ toolCallId: approval.toolCallId,
1443
+ toolName: approval.toolName,
1444
+ ...toolLabelPayload(tool),
1445
+ ...toolSourcePayload(tool)
1446
+ });
1447
+ const paused = createEvent(state, "run.paused", {
1448
+ reason: "approval",
1449
+ toolCallId: pending.call.id
1450
+ });
1451
+ await commit(state, {
1452
+ checkpoint,
1453
+ events: [requested, paused]
1454
+ });
1455
+ events.push(requested, paused);
1456
+ await releaseExecution(state);
1457
+ yield requested;
1458
+ yield paused;
1459
+ return void 0;
1460
+ }
1461
+ }
1462
+ attempt += 1;
1463
+ const secrets = [];
1464
+ const resolvedCredentials = /* @__PURE__ */ new Map();
1465
+ for (const credentialName of tool.credentials ?? []) {
1466
+ const value = await credentialStore?.resolve({
1467
+ name: credentialName,
1468
+ namespace: request.context?.namespace,
1469
+ actor: request.context?.actor
1470
+ });
1471
+ if (!value) throw new FevexRunError("CREDENTIAL_NOT_FOUND", `Credential "${credentialName}" was not found`, state.run.id);
1472
+ resolvedCredentials.set(credentialName, value);
1473
+ secrets.push(value);
1474
+ }
1475
+ const getCredential = async (credentialName) => {
1476
+ if (!tool.credentials?.includes(credentialName)) throw new FevexRunError("CREDENTIAL_NOT_FOUND", `Credential "${credentialName}" is not declared for tool "${tool.name}"`, state.run.id);
1477
+ return resolvedCredentials.get(credentialName);
1478
+ };
1479
+ const scopedSandbox = tool.sandbox === void 0 ? void 0 : { run: (sandboxRequest) => {
1480
+ if (!sandbox) throw new FevexRunError("SANDBOX_REQUIRED", `Tool "${tool.name}" requires a sandbox`, state.run.id);
1481
+ const signal = sandboxRequest.signal ? AbortSignal.any([request.signal, sandboxRequest.signal]) : request.signal;
1482
+ return sandbox.run({
1483
+ ...sandboxRequest,
1484
+ capabilities: tool.sandbox,
1485
+ runId: state.run.id,
1486
+ toolCallId: pending.call.id,
1487
+ attempt,
1488
+ idempotencyKey: pending.idempotencyKey,
1489
+ context: request.context,
1490
+ signal
1491
+ });
1492
+ } };
1493
+ record = {
1494
+ runId: state.run.id,
1495
+ toolCallId: pending.call.id,
1496
+ toolName: pending.call.name,
1497
+ input: pending.input,
1498
+ status: "started",
1499
+ attempt,
1500
+ idempotencyKey: pending.idempotencyKey,
1501
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1502
+ };
1503
+ const started = createEvent(state, "tool.started", {
1504
+ step,
1505
+ toolCallId: pending.call.id,
1506
+ toolName: pending.call.name,
1507
+ ...toolSourcePayload(tool),
1508
+ ...attempt > 1 ? { attempt } : {}
1509
+ });
1510
+ if (durable) await commit(state, {
1511
+ toolExecution: record,
1512
+ events: [started]
1513
+ });
1514
+ else {
1515
+ await runStore.appendEvent(started);
1516
+ notifyObserver(started);
1517
+ }
1518
+ events.push(started);
1519
+ yield started;
1520
+ try {
1521
+ const raw = await abortable(() => tool.execute(pending.input, {
1522
+ runId: state.run.id,
1523
+ toolCallId: pending.call.id,
1524
+ attempt,
1525
+ idempotencyKey: pending.idempotencyKey,
1526
+ getCredential,
1527
+ ...scopedSandbox === void 0 ? {} : { sandbox: scopedSandbox },
1528
+ context: request.context,
1529
+ signal: request.signal
1530
+ }), request.signal);
1531
+ output = toJsonValue(tool.outputSchema === void 0 ? raw : await validateSchema(tool.outputSchema, raw, `Output from tool "${tool.name}" does not match outputSchema`), `Output from tool "${tool.name}" must be JSON-serializable`);
1532
+ if (containsSecret(output, secrets)) throw new Error(`Output from tool "${tool.name}" contains a credential`);
1533
+ record = {
1534
+ ...record,
1535
+ status: "completed",
1536
+ output,
1537
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1538
+ };
1539
+ const completed = createEvent(state, "tool.completed", {
1540
+ step,
1541
+ toolCallId: pending.call.id,
1542
+ toolName: pending.call.name,
1543
+ ...toolSourcePayload(tool),
1544
+ ...attempt > 1 ? { attempt } : {}
1545
+ });
1546
+ if (durable) await commit(state, {
1547
+ toolExecution: record,
1548
+ events: [completed]
1549
+ });
1550
+ else {
1551
+ await runStore.appendEvent(completed);
1552
+ notifyObserver(completed);
1553
+ }
1554
+ events.push(completed);
1555
+ yield completed;
1556
+ break;
1557
+ } catch (error) {
1558
+ if (request.signal.aborted) throw error;
1559
+ const safeError = redact(toErrorMessage(error), secrets);
1560
+ record = {
1561
+ ...record,
1562
+ status: "failed",
1563
+ error: safeError,
1564
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1565
+ };
1566
+ if (durable) await commit(state, { toolExecution: record });
1567
+ if (attempt >= maxAttempts || tool.idempotency !== "keyed") {
1568
+ yield await emit("tool.failed", {
1569
+ step,
1570
+ toolCallId: pending.call.id,
1571
+ toolName: pending.call.name,
1572
+ error: safeError,
1573
+ ...toolErrorPayload(tool, error)
1574
+ });
1575
+ throw secrets.length ? new Error(safeError, { cause: error }) : error;
1576
+ }
1577
+ const delayMs = Math.min(tool.retry.backoffMs * 2 ** (attempt - 1), tool.retry.maxBackoffMs ?? Number.MAX_SAFE_INTEGER);
1578
+ yield await emit("tool.retrying", {
1579
+ step,
1580
+ toolCallId: pending.call.id,
1581
+ toolName: pending.call.name,
1582
+ attempt: attempt + 1,
1583
+ delayMs,
1584
+ error: safeError,
1585
+ ...toolErrorPayload(tool, error)
1586
+ });
1587
+ if (delayMs) await abortable(() => new Promise((resolve) => setTimeout(resolve, delayMs)), request.signal);
1588
+ }
1589
+ }
1590
+ if (output === void 0) throw new FevexRunError("TOOL_EXECUTION_UNKNOWN", `Tool "${tool.name}" exhausted its configured attempts`, state.run.id);
1591
+ messages.push({
1592
+ role: "tool",
1593
+ name: pending.call.name,
1594
+ toolCallId: pending.call.id,
1595
+ content: serializeJsonValue(output)
1596
+ });
1597
+ toolCallCount += 1;
1598
+ pendingIndex += 1;
1599
+ }
1600
+ pendingTools = [];
1601
+ pendingIndex = 0;
1602
+ step += 1;
1603
+ }
1604
+ throw new Error(`Agent "${name}" reached maxSteps limit of ${maxSteps}`);
1605
+ } catch (error) {
1606
+ if (request.signal.aborted) {
1607
+ const event = await cancelExecution(state);
1608
+ if (event) {
1609
+ events.push(event);
1610
+ yield event;
1611
+ }
1612
+ } else if (state.run.status === "running") {
1613
+ Object.assign(state.run, {
1614
+ status: "failed",
1615
+ error: toErrorMessage(error),
1616
+ ...usage === void 0 ? {} : { usage },
1617
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1618
+ });
1619
+ const failed = createEvent(state, "run.failed", {
1620
+ error: toErrorMessage(error),
1621
+ ...runErrorPayload(error)
1622
+ });
1623
+ if (isDurableRunStore(runStore)) try {
1624
+ await commit(state, {
1625
+ checkpoint: null,
1626
+ events: [failed]
1627
+ });
1628
+ } catch (commitError) {
1629
+ await releaseExecution(state);
1630
+ throw commitError;
1631
+ }
1632
+ else {
1633
+ await runStore.saveRun(state.run);
1634
+ await runStore.appendEvent(failed);
1635
+ notifyObserver(failed);
1636
+ }
1637
+ events.push(failed);
1638
+ await releaseExecution(state);
1639
+ yield failed;
1640
+ } else await releaseExecution(state);
1641
+ throw error;
1642
+ }
1643
+ }
1644
+ const nextEvent = async (state, execution) => {
1645
+ state.advancing = true;
1646
+ try {
1647
+ return await execution.next();
1648
+ } finally {
1649
+ state.advancing = false;
1650
+ }
1651
+ };
1652
+ const drainExecution = async (name, state, throwOnPause) => {
1653
+ const execution = executeAgent(name, state);
1654
+ while (true) {
1655
+ const next = await nextEvent(state, execution);
1656
+ if (next.done) {
1657
+ if (next.value === void 0 && throwOnPause) {
1658
+ const run = await runStore.getRun(state.run.id);
1659
+ if (run?.pause) throw new RunPausedError(run.id, run.pause);
1660
+ }
1661
+ return next.value;
1662
+ }
1663
+ }
1664
+ };
1665
+ const resumeAgent = async (runId, resolution, waitForCompletion = false, recoveryActor) => {
1666
+ if (resolution?.type === "timer" || resolution?.type === "event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
1667
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "resumeRun requires DurableRunStore", runId);
1668
+ const run = await runStore.getRun(runId);
1669
+ const checkpoint = await runStore.getCheckpoint(runId);
1670
+ if (!run || !checkpoint) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be ${resolution ? "resumed" : "recovered"}`, runId);
1671
+ if (checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${runId}" is unsupported`, runId);
1672
+ if (resolution && run.status !== "paused" && run.status !== "running" || !resolution && run.status !== "running") throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" is not ${resolution ? "resumable" : "recoverable"}`, runId);
1673
+ const agent = agents.get(run.agentName);
1674
+ if (!agent) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Agent "${run.agentName}" is unavailable`, runId);
1675
+ const checkpointModel = await resolveCheckpointModel(run.agentName, agent, checkpoint);
1676
+ if (!checkpointModel) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for agent "${run.agentName}" changed`, runId);
1677
+ const { modelName, model } = checkpointModel;
1678
+ if (checkpoint.providerState !== void 0 && !model.stateCodec) throw new FevexRunError("RUN_NOT_RESUMABLE", "ModelGateway no longer provides its stateCodec", runId);
1679
+ const session = await runStore.getSession(run.sessionId);
1680
+ if (!session) throw new Error(`Session "${run.sessionId}" does not exist`);
1681
+ if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" is active`, runId);
1682
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
1683
+ const lease = {
1684
+ generation: 0,
1685
+ runId,
1686
+ ownerId,
1687
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
1688
+ };
1689
+ if (!await runStore.acquireLease(lease)) throw new FevexRunError("RUN_CONFLICT", `Run "${runId}" is leased`, runId);
1690
+ try {
1691
+ const controller = new AbortController();
1692
+ const state = {
1693
+ run,
1694
+ session,
1695
+ request: {
1696
+ input: "",
1697
+ model: modelName,
1698
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
1699
+ sessionId: session.id,
1700
+ context: checkpoint.context,
1701
+ limits: checkpoint.limits,
1702
+ signal: controller.signal
1703
+ },
1704
+ controller,
1705
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
1706
+ advancing: false,
1707
+ checkpoint,
1708
+ lease,
1709
+ ...resolution?.type === "approval" ? { approvedToolCallId: run.pause?.type === "approval" ? run.pause.approval.toolCallId : void 0 } : resolution?.type === "tool_execution" ? {
1710
+ approvedToolCallId: resolution.toolCallId,
1711
+ ...resolution.decision === "retry" ? { forcedRetryToolCallId: resolution.toolCallId } : {}
1712
+ } : {}
1713
+ };
1714
+ if (!resolution) {
1715
+ if (!recoveryActor?.id.trim()) throw new FevexRunError("RUN_NOT_RECOVERABLE", "Recovery actor is required", runId);
1716
+ const recovered = createEvent(state, "run.recovered", { actorId: recoveryActor.id });
1717
+ await commit(state, {
1718
+ checkpoint,
1719
+ events: [recovered]
1720
+ });
1721
+ state.initialEvents = [recovered];
1722
+ activeRuns.set(runId, state);
1723
+ activeSessions.add(session.id);
1724
+ startLease(state, runStore);
1725
+ if (waitForCompletion) await drainExecution(run.agentName, state, false);
1726
+ else drainExecution(run.agentName, state, false).catch(() => {});
1727
+ return structuredClone(run);
1728
+ }
1729
+ if (!resolution.actor?.id?.trim()) {
1730
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1731
+ throw new FevexRunError(resolution.type === "elicitation" ? "ELICITATION_INVALID" : "APPROVAL_INVALID", "Resolution actor is required", runId);
1732
+ }
1733
+ if (resolution.type === "elicitation") {
1734
+ const pause = run.pause;
1735
+ if (pause?.type !== "elicitation" || pause.request.id !== resolution.requestId) {
1736
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1737
+ throw new FevexRunError("ELICITATION_INVALID", "Elicitation does not match the run", runId);
1738
+ }
1739
+ if (pause.request.expiresAt && Date.now() > Date.parse(pause.request.expiresAt)) {
1740
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1741
+ throw new FevexRunError("ELICITATION_INVALID", "Elicitation has expired", runId);
1742
+ }
1743
+ let value;
1744
+ try {
1745
+ value = compileFevexJsonSchema(pause.request.responseSchema).validate(resolution.value);
1746
+ } catch (error) {
1747
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1748
+ throw new FevexRunError("ELICITATION_INVALID", `Elicitation value does not match responseSchema: ${toErrorMessage(error)}`, runId, { cause: error });
1749
+ }
1750
+ checkpoint.messages.push({
1751
+ role: "tool",
1752
+ name: ELICIT_TOOL_NAME,
1753
+ toolCallId: pause.request.toolCallId,
1754
+ content: serializeJsonValue(value)
1755
+ });
1756
+ run.status = "running";
1757
+ run.pause = void 0;
1758
+ const resolved = createEvent(state, "elicitation.resolved", {
1759
+ requestId: pause.request.id,
1760
+ toolCallId: pause.request.toolCallId,
1761
+ actorId: resolution.actor.id
1762
+ });
1763
+ const resumed = createEvent(state, "run.resumed", void 0);
1764
+ await commit(state, {
1765
+ checkpoint,
1766
+ events: [resolved, resumed]
1767
+ });
1768
+ activeRuns.set(runId, state);
1769
+ activeSessions.add(session.id);
1770
+ startLease(state, runStore);
1771
+ if (waitForCompletion) await drainExecution(run.agentName, state, false);
1772
+ else drainExecution(run.agentName, state, false).catch(() => {});
1773
+ return structuredClone(run);
1774
+ }
1775
+ if (resolution.type === "approval") {
1776
+ if (run.pause?.type !== "approval" || run.pause.approval.id !== resolution.approvalId) {
1777
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1778
+ throw new FevexRunError("APPROVAL_INVALID", "Approval does not match the run", runId);
1779
+ }
1780
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1781
+ const tool = pending && tools.get(pending.call.name);
1782
+ if (!pending || !tool) {
1783
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1784
+ throw new FevexRunError("APPROVAL_INVALID", "Approval tool is unavailable", runId);
1785
+ }
1786
+ if (resolution.decision === "approve") await authorize(state, tool, pending.input, "approval.resolve", {
1787
+ ...state.request.context,
1788
+ actor: resolution.actor
1789
+ });
1790
+ if (resolution.decision === "reject") {
1791
+ const approval = run.pause.approval;
1792
+ const toolCallId = approval.toolCallId;
1793
+ run.status = "cancelled";
1794
+ run.pause = void 0;
1795
+ run.error = "approval_rejected";
1796
+ const resolved = createEvent(state, "approval.resolved", {
1797
+ approvalId: resolution.approvalId,
1798
+ toolCallId,
1799
+ decision: "reject",
1800
+ actorId: resolution.actor.id,
1801
+ ...approval.toolLabel === void 0 ? {} : { toolLabel: approval.toolLabel },
1802
+ ...toolSourcePayload(tool)
1803
+ });
1804
+ const cancelled = createEvent(state, "run.cancelled", { reason: "approval_rejected" });
1805
+ await commit(state, {
1806
+ checkpoint: null,
1807
+ events: [resolved, cancelled]
1808
+ });
1809
+ await releaseExecution(state);
1810
+ return structuredClone(run);
1811
+ }
1812
+ } else {
1813
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1814
+ const record = await runStore.getToolExecution(runId, resolution.toolCallId);
1815
+ const matchesUnknownPause = run.pause?.type === "tool_execution_unknown" && run.pause.toolCallId === resolution.toolCallId;
1816
+ const matchesInterruptedAttempt = run.status === "running" && pending?.call.id === resolution.toolCallId && (record?.status === "started" || record?.status === "completed");
1817
+ if (!matchesUnknownPause && !matchesInterruptedAttempt) {
1818
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1819
+ throw new FevexRunError("RUN_NOT_RESUMABLE", "Tool execution resolution does not match a pending attempt", runId);
1820
+ }
1821
+ }
1822
+ if (resolution.type === "tool_execution" && resolution.decision === "use_output") {
1823
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1824
+ const tool = tools.get(pending.call.name);
1825
+ if (!tool) {
1826
+ await runStore.releaseLease(runId, ownerId, lease.generation);
1827
+ throw new FevexRunError("RUN_DEFINITION_CHANGED", "Pending tool is unavailable", runId);
1828
+ }
1829
+ const output = toJsonValue(tool.outputSchema === void 0 ? resolution.output : await validateSchema(tool.outputSchema, resolution.output, `Manual output for tool "${tool.name}" does not match outputSchema`), "Manual tool output must be JSON-serializable");
1830
+ const current = await runStore.getToolExecution(runId, resolution.toolCallId);
1831
+ await commit(state, { toolExecution: {
1832
+ runId,
1833
+ toolCallId: resolution.toolCallId,
1834
+ toolName: pending.call.name,
1835
+ input: pending.input,
1836
+ status: "completed",
1837
+ attempt: current?.attempt ?? 1,
1838
+ idempotencyKey: current?.idempotencyKey ?? pending.idempotencyKey,
1839
+ output,
1840
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1841
+ } });
1842
+ }
1843
+ const approval = run.pause?.type === "approval" ? run.pause.approval : void 0;
1844
+ const approvalTool = approval ? tools.get(approval.toolName) : void 0;
1845
+ run.status = "running";
1846
+ run.pause = void 0;
1847
+ const resumeEvents = [createEvent(state, "run.resumed", void 0)];
1848
+ if (approval && resolution.type === "approval") resumeEvents.push(createEvent(state, "approval.resolved", {
1849
+ approvalId: approval.id,
1850
+ toolCallId: approval.toolCallId,
1851
+ decision: "approve",
1852
+ actorId: resolution.actor.id,
1853
+ ...approval.toolLabel === void 0 ? {} : { toolLabel: approval.toolLabel },
1854
+ ...approvalTool === void 0 ? {} : toolSourcePayload(approvalTool)
1855
+ }));
1856
+ await commit(state, {
1857
+ checkpoint,
1858
+ events: resumeEvents
183
1859
  });
184
- const result = await model.generate({
185
- messages,
186
- tools: agentTools.length > 0 ? agentTools : void 0,
187
- reasoning: agent.reasoning,
188
- modelOptions: agent.modelOptions,
189
- outputSchema,
190
- signal: request.signal
1860
+ activeRuns.set(runId, state);
1861
+ activeSessions.add(session.id);
1862
+ startLease(state, runStore);
1863
+ if (waitForCompletion) await drainExecution(run.agentName, state, false);
1864
+ else drainExecution(run.agentName, state, false).catch(() => {});
1865
+ return structuredClone(run);
1866
+ } catch (error) {
1867
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
1868
+ throw error;
1869
+ }
1870
+ };
1871
+ const cancelStoredAgent = async (runId) => {
1872
+ const active = activeRuns.get(runId);
1873
+ if (active && !active.request.signal.aborted) {
1874
+ active.controller.abort(new DOMException("Run cancelled", "AbortError"));
1875
+ if (!active.advancing) {
1876
+ await cancelExecution(active);
1877
+ return true;
1878
+ }
1879
+ for (let attempt = 0; attempt < 100; attempt += 1) {
1880
+ const current = await runStore.getRun(runId);
1881
+ if (current?.status === "cancelled" || current?.status === "completed" || current?.status === "failed") return true;
1882
+ await new Promise((resolve) => setTimeout(resolve, 10));
1883
+ }
1884
+ return false;
1885
+ }
1886
+ if (!isDurableRunStore(runStore)) return false;
1887
+ const run = await runStore.getRun(runId);
1888
+ if (!run) return false;
1889
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") return true;
1890
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
1891
+ const lease = {
1892
+ generation: 0,
1893
+ runId,
1894
+ ownerId,
1895
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
1896
+ };
1897
+ if (!await runStore.acquireLease(lease)) return false;
1898
+ try {
1899
+ const session = await runStore.getSession(run.sessionId);
1900
+ if (!session) return false;
1901
+ const controller = new AbortController();
1902
+ const state = {
1903
+ run,
1904
+ session,
1905
+ request: {
1906
+ input: "",
1907
+ sessionId: session.id,
1908
+ signal: controller.signal
1909
+ },
1910
+ controller,
1911
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
1912
+ advancing: false,
1913
+ lease
1914
+ };
1915
+ run.status = "cancelled";
1916
+ run.pause = void 0;
1917
+ run.error = "aborted";
1918
+ const cancelled = createEvent(state, "run.cancelled", { reason: "aborted" });
1919
+ await commit(state, {
1920
+ checkpoint: null,
1921
+ events: [cancelled]
191
1922
  });
192
- emit("model.completed");
193
- if (!result.toolCalls?.length) {
194
- if (result.output === void 0) throw new Error(`Model for agent "${name}" returned no output`);
195
- const output = toJsonValue(activeOutputSchema === void 0 ? result.output : await validateSchema(activeOutputSchema, result.output, `Output from agent "${name}" does not match outputSchema`), `Output from agent "${name}" must be JSON-serializable`);
196
- emit("run.completed");
197
- return {
198
- output,
199
- events,
200
- usage: result.usage
201
- };
1923
+ return true;
1924
+ } catch (error) {
1925
+ if (error instanceof FevexRunError && error.code === "RUN_CONFLICT") return false;
1926
+ throw error;
1927
+ } finally {
1928
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
1929
+ }
1930
+ };
1931
+ return {
1932
+ prepareExecution,
1933
+ executeAgent,
1934
+ nextEvent,
1935
+ drainExecution,
1936
+ resumeAgent,
1937
+ cancelStoredAgent
1938
+ };
1939
+ }
1940
+ //#endregion
1941
+ //#region src/internal/observability.ts
1942
+ const TERMINAL_TYPES = /* @__PURE__ */ new Set([
1943
+ "run.completed",
1944
+ "run.failed",
1945
+ "run.cancelled"
1946
+ ]);
1947
+ const SENSITIVE_KEY = /api[-_]?key|authorization|cookie|credential|password|secret|token/i;
1948
+ function durationMs(startedAt, endedAt) {
1949
+ return Math.max(0, Date.parse(endedAt) - Date.parse(startedAt));
1950
+ }
1951
+ function traceStatus(event) {
1952
+ if (event.type === "run.completed") return "completed";
1953
+ if (event.type === "run.cancelled") return "cancelled";
1954
+ return "failed";
1955
+ }
1956
+ function redactKeys(value) {
1957
+ if (Array.isArray(value)) return value.map(redactKeys);
1958
+ if (value === null || typeof value !== "object") return value;
1959
+ const result = {};
1960
+ for (const [key, item] of Object.entries(value)) result[key] = SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactKeys(item);
1961
+ return result;
1962
+ }
1963
+ function content(options, kind, value) {
1964
+ if (!options.content?.include.includes(kind)) return void 0;
1965
+ let parsed = value;
1966
+ if (typeof value === "string") try {
1967
+ parsed = JSON.parse(value);
1968
+ } catch {}
1969
+ const redacted = redactKeys(parsed);
1970
+ const safe = options.content.redact ? options.content.redact({
1971
+ kind,
1972
+ value: redacted
1973
+ }) : redacted;
1974
+ return safe === void 0 ? void 0 : {
1975
+ kind,
1976
+ value: toJsonValue(safe, `TraceRedactor output for "${kind}" must be JSON-serializable`)
1977
+ };
1978
+ }
1979
+ function usageFrom(value) {
1980
+ if (!value) return void 0;
1981
+ const usage = {};
1982
+ if (typeof value.inputTokens === "number") usage.inputTokens = value.inputTokens;
1983
+ if (typeof value.outputTokens === "number") usage.outputTokens = value.outputTokens;
1984
+ if (typeof value.totalTokens === "number") usage.totalTokens = value.totalTokens;
1985
+ return Object.keys(usage).length ? usage : void 0;
1986
+ }
1987
+ function subtractUsage(current, previous) {
1988
+ if (!current) return void 0;
1989
+ const usage = {};
1990
+ for (const key of [
1991
+ "inputTokens",
1992
+ "outputTokens",
1993
+ "totalTokens"
1994
+ ]) if (current[key] !== void 0) usage[key] = Math.max(0, current[key] - (previous[key] ?? 0));
1995
+ return Object.keys(usage).length ? usage : void 0;
1996
+ }
1997
+ function costFor(options, modelRegistryName, model, usage) {
1998
+ if (!options.calculateCost || !usage) return void 0;
1999
+ const cost = options.calculateCost({
2000
+ modelRegistryName,
2001
+ model,
2002
+ usage
2003
+ });
2004
+ if (!cost) return void 0;
2005
+ if (!Number.isFinite(cost.amount) || cost.amount < 0 || !cost.currency.trim()) throw new TypeError("CostCalculator must return a non-negative amount and currency");
2006
+ return {
2007
+ amount: cost.amount,
2008
+ currency: cost.currency
2009
+ };
2010
+ }
2011
+ function annotation(event) {
2012
+ switch (event.type) {
2013
+ case "run.paused": return {
2014
+ name: event.type,
2015
+ timestamp: event.timestamp,
2016
+ attributes: {
2017
+ reason: event.payload.reason,
2018
+ toolCallId: event.payload.toolCallId
202
2019
  }
203
- if (result.toolCalls.length > 1) throw new Error(`Agent "${name}" requested multiple tools, but this runtime supports one tool call`);
204
- const toolCall = result.toolCalls[0];
205
- if (typeof toolCall.id !== "string" || !toolCall.id.trim()) throw new Error("Tool call id cannot be empty");
206
- if (typeof toolCall.name !== "string" || !toolCall.name.trim()) throw new Error("Tool call name cannot be empty");
207
- const assistantContent = result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be JSON-serializable`);
208
- request.signal?.throwIfAborted();
209
- let toolContent;
210
- try {
211
- const tool = agent.tools?.includes(toolCall.name) ? tools.get(toolCall.name) : void 0;
212
- if (!tool) throw new Error(`Tool "${toolCall.name}" is not available to agent "${name}"`);
213
- const rawToolInput = toJsonValue(toolCall.input, `Input for tool "${toolCall.name}" must be JSON-serializable`);
214
- const toolInput = toJsonValue(tool.inputSchema === void 0 ? rawToolInput : await validateSchema(tool.inputSchema, rawToolInput, `Input for tool "${toolCall.name}" does not match inputSchema`), `Input for tool "${toolCall.name}" must be JSON-serializable`);
215
- const rawToolOutput = await tool.execute(toolInput, {
216
- runId,
217
- toolCallId: toolCall.id,
218
- context: request.context,
219
- signal: request.signal
2020
+ };
2021
+ case "run.resumed": return {
2022
+ name: event.type,
2023
+ timestamp: event.timestamp
2024
+ };
2025
+ case "approval.requested": return {
2026
+ name: event.type,
2027
+ timestamp: event.timestamp,
2028
+ attributes: {
2029
+ approvalId: event.payload.approvalId,
2030
+ toolCallId: event.payload.toolCallId,
2031
+ toolName: event.payload.toolName
2032
+ }
2033
+ };
2034
+ case "approval.resolved": return {
2035
+ name: event.type,
2036
+ timestamp: event.timestamp,
2037
+ attributes: {
2038
+ approvalId: event.payload.approvalId,
2039
+ toolCallId: event.payload.toolCallId,
2040
+ decision: event.payload.decision
2041
+ }
2042
+ };
2043
+ case "tool.retrying": return {
2044
+ name: event.type,
2045
+ timestamp: event.timestamp,
2046
+ attributes: {
2047
+ toolCallId: event.payload.toolCallId,
2048
+ toolName: event.payload.toolName,
2049
+ attempt: event.payload.attempt,
2050
+ delayMs: event.payload.delayMs
2051
+ }
2052
+ };
2053
+ case "tool.execution_unknown": return {
2054
+ name: event.type,
2055
+ timestamp: event.timestamp,
2056
+ attributes: {
2057
+ toolCallId: event.payload.toolCallId,
2058
+ toolName: event.payload.toolName
2059
+ }
2060
+ };
2061
+ default: return;
2062
+ }
2063
+ }
2064
+ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2065
+ const events = [...rawEvents].sort((left, right) => left.sequence - right.sequence);
2066
+ const terminal = [...events].reverse().find((event) => TERMINAL_TYPES.has(event.type));
2067
+ if (!terminal) throw new TypeError(`Run "${run.id}" has no terminal event`);
2068
+ const started = events.find((event) => event.type === "run.started") ?? events[0];
2069
+ if (!started) throw new TypeError(`Run "${run.id}" has no events`);
2070
+ const status = traceStatus(terminal);
2071
+ const annotations = [];
2072
+ const spans = [];
2073
+ const openModels = /* @__PURE__ */ new Map();
2074
+ const openTools = /* @__PURE__ */ new Map();
2075
+ const previousUsage = {};
2076
+ const closeModel = (step, endedAt, spanStatus) => {
2077
+ const open = openModels.get(step);
2078
+ if (!open) return;
2079
+ openModels.delete(step);
2080
+ const completed = events.find((event) => event.type === "model.completed" && event.payload.step === step && event.sequence >= open.event.sequence);
2081
+ const cumulative = completed?.type === "model.completed" ? usageFrom(completed.payload.usage) : void 0;
2082
+ const usage = subtractUsage(cumulative, previousUsage);
2083
+ if (cumulative) Object.assign(previousUsage, cumulative);
2084
+ const spanContent = open.output.length ? content(options, "model.output", open.output.join("")) : void 0;
2085
+ const span = {
2086
+ id: open.event.id,
2087
+ parentId: run.id,
2088
+ kind: "model",
2089
+ name: `model step ${step}`,
2090
+ startedAt: open.event.timestamp,
2091
+ endedAt,
2092
+ durationMs: durationMs(open.event.timestamp, endedAt),
2093
+ status: spanStatus,
2094
+ attributes: {
2095
+ step,
2096
+ modelRegistryName
2097
+ },
2098
+ annotations: open.annotations,
2099
+ ...usage ? { usage } : {},
2100
+ ...spanContent ? { content: [spanContent] } : {}
2101
+ };
2102
+ const spanCost = costFor(options, modelRegistryName, model, usage);
2103
+ if (spanCost) span.cost = spanCost;
2104
+ spans.push(span);
2105
+ };
2106
+ const findOpenTool = (toolCallId) => {
2107
+ const entries = [...openTools.entries()];
2108
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
2109
+ const entry = entries[index];
2110
+ const event = entry[1].event;
2111
+ if (event.type === "tool.started" && event.payload.toolCallId === toolCallId) return entry;
2112
+ }
2113
+ };
2114
+ const closeTool = (toolCallId, endedAt, spanStatus, error) => {
2115
+ const entry = findOpenTool(toolCallId);
2116
+ if (!entry) return;
2117
+ const [key, open] = entry;
2118
+ openTools.delete(key);
2119
+ const event = open.event;
2120
+ if (event.type !== "tool.started") return;
2121
+ const errorContent = error ? content(options, "error.message", error) : void 0;
2122
+ spans.push({
2123
+ id: event.id,
2124
+ parentId: run.id,
2125
+ kind: "tool",
2126
+ name: event.payload.toolName,
2127
+ startedAt: event.timestamp,
2128
+ endedAt,
2129
+ durationMs: durationMs(event.timestamp, endedAt),
2130
+ status: spanStatus,
2131
+ attributes: {
2132
+ step: event.payload.step,
2133
+ toolCallId,
2134
+ toolName: event.payload.toolName,
2135
+ attempt: event.payload.attempt ?? 1
2136
+ },
2137
+ annotations: open.annotations,
2138
+ ...errorContent ? { content: [errorContent] } : {}
2139
+ });
2140
+ };
2141
+ for (const event of events) {
2142
+ const item = annotation(event);
2143
+ if (item) annotations.push(item);
2144
+ switch (event.type) {
2145
+ case "model.started":
2146
+ openModels.set(event.payload.step, {
2147
+ event,
2148
+ annotations: [],
2149
+ output: []
220
2150
  });
221
- toolContent = serializeJsonValue(toJsonValue(tool.outputSchema === void 0 ? rawToolOutput : await validateSchema(tool.outputSchema, rawToolOutput, `Output from tool "${toolCall.name}" does not match outputSchema`), `Output from tool "${toolCall.name}" must be JSON-serializable`));
222
- } catch (error) {
223
- emitFailure("tool.failed", {
224
- toolCallId: toolCall.id,
225
- toolName: toolCall.name,
226
- error: toErrorMessage(error)
2151
+ break;
2152
+ case "model.output.delta":
2153
+ openModels.get(event.payload.step)?.output.push(event.payload.delta);
2154
+ break;
2155
+ case "model.completed":
2156
+ closeModel(event.payload.step, event.timestamp, "completed");
2157
+ break;
2158
+ case "tool.started": {
2159
+ const attempt = event.payload.attempt ?? 1;
2160
+ openTools.set(`${event.payload.toolCallId}:${attempt}`, {
2161
+ event,
2162
+ annotations: [],
2163
+ output: []
227
2164
  });
228
- throw error;
2165
+ break;
229
2166
  }
230
- emit("tool.completed", {
231
- toolCallId: toolCall.id,
232
- toolName: toolCall.name
2167
+ case "tool.retrying": {
2168
+ const open = findOpenTool(event.payload.toolCallId)?.[1];
2169
+ const retry = annotation(event);
2170
+ if (open && retry) open.annotations.push(retry);
2171
+ closeTool(event.payload.toolCallId, event.timestamp, "failed", event.payload.error);
2172
+ break;
2173
+ }
2174
+ case "tool.completed":
2175
+ closeTool(event.payload.toolCallId, event.timestamp, "completed");
2176
+ break;
2177
+ case "tool.failed":
2178
+ closeTool(event.payload.toolCallId, event.timestamp, "failed", event.payload.error);
2179
+ break;
2180
+ case "tool.execution_unknown":
2181
+ closeTool(event.payload.toolCallId, event.timestamp, "failed");
2182
+ break;
2183
+ }
2184
+ }
2185
+ for (const step of [...openModels.keys()]) closeModel(step, terminal.timestamp, status);
2186
+ for (const open of [...openTools.values()]) if (open.event.type === "tool.started") closeTool(open.event.payload.toolCallId, terminal.timestamp, status);
2187
+ const finalUsage = run.usage ?? previousUsage;
2188
+ const totalCost = costFor(options, modelRegistryName, model, finalUsage);
2189
+ const traceContent = [];
2190
+ if (terminal.type === "run.completed") {
2191
+ const output = content(options, "run.output", terminal.payload.output);
2192
+ if (output) traceContent.push(output);
2193
+ } else if (terminal.type === "run.failed") {
2194
+ const error = content(options, "error.message", terminal.payload.error);
2195
+ if (error) traceContent.push(error);
2196
+ }
2197
+ return {
2198
+ schemaVersion: "1",
2199
+ traceId: run.id,
2200
+ runId: run.id,
2201
+ sessionId: run.sessionId,
2202
+ agentName: run.agentName,
2203
+ modelRegistryName,
2204
+ ...model ? { model: { ...model } } : {},
2205
+ startedAt: started.timestamp,
2206
+ endedAt: terminal.timestamp,
2207
+ durationMs: durationMs(started.timestamp, terminal.timestamp),
2208
+ status,
2209
+ attributes: {
2210
+ eventCount: events.length,
2211
+ modelCallCount: spans.filter(({ kind }) => kind === "model").length,
2212
+ toolCallCount: spans.filter(({ kind }) => kind === "tool").length
2213
+ },
2214
+ annotations,
2215
+ spans,
2216
+ ...Object.keys(finalUsage).length ? { usage: { ...finalUsage } } : {},
2217
+ ...totalCost ? { cost: totalCost } : {},
2218
+ ...traceContent.length ? { content: traceContent } : {}
2219
+ };
2220
+ }
2221
+ //#endregion
2222
+ //#region src/internal/run-core.ts
2223
+ /**
2224
+ * Shared foundation both engines build on: the registries that enforce one
2225
+ * active run per session, the single event sequencer, the durable commit path,
2226
+ * leases and policy authorization.
2227
+ *
2228
+ * It must be instantiated exactly once per runtime. `createEvent` advances
2229
+ * `state.eventSequence` in place and `runtimeOwner` identifies this process to
2230
+ * the lease table, so a second instance would corrupt event ordering and lease
2231
+ * ownership without raising an error.
2232
+ */
2233
+ function createRunCore({ models, agents, tools, runStore, policies, onEvent, observability }) {
2234
+ const activeRuns = /* @__PURE__ */ new Map();
2235
+ const activeWorkflowRuns = /* @__PURE__ */ new Map();
2236
+ const activeSessions = /* @__PURE__ */ new Set();
2237
+ const pendingExports = /* @__PURE__ */ new Set();
2238
+ const exportFailures = [];
2239
+ const exportedRuns = /* @__PURE__ */ new Set();
2240
+ const runtimeOwner = crypto.randomUUID();
2241
+ const resolveModel = (agent, requestedModel) => {
2242
+ const modelName = requestedModel ?? (typeof agent.model === "string" ? agent.model : "default");
2243
+ if (!modelName.trim()) throw new FevexRunError("MODEL_NOT_FOUND", "Model name cannot be empty");
2244
+ const model = requestedModel !== void 0 || typeof agent.model === "string" ? models.get(modelName) : agent.model ?? models.get(modelName);
2245
+ if (!model) throw new FevexRunError("MODEL_NOT_FOUND", `Model "${modelName}" is not registered`);
2246
+ return {
2247
+ modelName,
2248
+ model
2249
+ };
2250
+ };
2251
+ const resolveCheckpointModel = async (agentName, agent, checkpoint) => {
2252
+ const preferred = resolveModel(agent, checkpoint.modelName);
2253
+ if (checkpoint.definitionHash === await definitionHash(agentName, preferred.modelName, agent, tools)) return preferred;
2254
+ if (checkpoint.modelName !== void 0) return void 0;
2255
+ for (const candidateName of models.keys()) {
2256
+ if (candidateName === preferred.modelName) continue;
2257
+ const candidate = resolveModel(agent, candidateName);
2258
+ if (checkpoint.definitionHash === await definitionHash(agentName, candidate.modelName, agent, tools)) return candidate;
2259
+ }
2260
+ };
2261
+ const scheduleTraceExport = (event) => {
2262
+ if (!observability || ![
2263
+ "run.completed",
2264
+ "run.failed",
2265
+ "run.cancelled"
2266
+ ].includes(event.type) || exportedRuns.has(event.runId)) return;
2267
+ exportedRuns.add(event.runId);
2268
+ let task;
2269
+ task = (async () => {
2270
+ const run = await runStore.getRun(event.runId);
2271
+ if (!run) throw new Error(`Run "${event.runId}" does not exist`);
2272
+ if (isCoordinatorRun(run)) return;
2273
+ const agentRun = run;
2274
+ const agent = agents.get(agentRun.agentName);
2275
+ if (!agent) throw new Error(`Agent "${agentRun.agentName}" is not registered`);
2276
+ const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(agentRun.id) : void 0;
2277
+ const { modelName: modelRegistryName, model: gateway } = resolveModel(agent, checkpoint?.modelName);
2278
+ const trace = buildRunTrace(agentRun, await runStore.listEvents(agentRun.id), modelRegistryName, gateway?.metadata, observability);
2279
+ const failures = (await Promise.allSettled(observability.exporters.map((exporter) => Promise.resolve().then(() => exporter.export(trace))))).filter((result) => result.status === "rejected").map(({ reason }) => reason);
2280
+ if (failures.length) throw new AggregateError(failures, "Trace export failed");
2281
+ })().catch((error) => {
2282
+ exportFailures.push(error);
2283
+ }).finally(() => pendingExports.delete(task));
2284
+ pendingExports.add(task);
2285
+ };
2286
+ const notifyObserver = (event) => {
2287
+ scheduleTraceExport(event);
2288
+ if (!onEvent) return;
2289
+ try {
2290
+ Promise.resolve(onEvent(event)).catch(() => {});
2291
+ } catch {}
2292
+ };
2293
+ const createEvent = (state, type, payload) => ({
2294
+ id: crypto.randomUUID(),
2295
+ sequence: state.eventSequence += 1,
2296
+ type,
2297
+ runId: state.run.id,
2298
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2299
+ ...payload === void 0 ? {} : { payload }
2300
+ });
2301
+ const createCoordinatorEvent = (state, workflowType, workflowPayload, teamType, teamPayload = workflowPayload) => createEvent(state, isTeamRun(state.run) ? teamType : workflowType, isTeamRun(state.run) ? teamPayload : workflowPayload);
2302
+ const emitEvent = async (state, type, payload) => {
2303
+ const event = createEvent(state, type, payload);
2304
+ if (isDurableRunStore(runStore)) await commit(state, { events: [event] });
2305
+ else {
2306
+ await runStore.appendEvent(event);
2307
+ notifyObserver(event);
2308
+ }
2309
+ return event;
2310
+ };
2311
+ const releaseExecution = async (state) => {
2312
+ activeRuns.delete(state.run.id);
2313
+ activeSessions.delete(state.session.id);
2314
+ if (state.leaseTimer) clearInterval(state.leaseTimer);
2315
+ if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2316
+ state.leaseTimer = void 0;
2317
+ state.leaseExpiryTimer = void 0;
2318
+ if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2319
+ };
2320
+ const commit = async (state, options = {}) => {
2321
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "This run requires a DurableRunStore", state.run.id);
2322
+ if (state.leaseLost && state.controller.signal.reason instanceof FevexRunError) throw state.controller.signal.reason;
2323
+ if (!state.lease || state.leaseLost) throw new FevexRunError("RUN_CONFLICT", "Run lease is unavailable", state.run.id);
2324
+ state.run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2325
+ if (!await runStore.commitExecution({
2326
+ expectedRevision: state.run.revision,
2327
+ lease: state.lease,
2328
+ run: state.run,
2329
+ ...options
2330
+ })) {
2331
+ state.leaseLost = true;
2332
+ const error = new FevexRunError("RUN_CONFLICT", `Run "${state.run.id}" ownership or revision was lost`, state.run.id);
2333
+ state.controller.abort(error);
2334
+ throw error;
2335
+ }
2336
+ for (const event of options.events ?? []) notifyObserver(event);
2337
+ };
2338
+ const startLease = (state, store) => {
2339
+ let renewing = false;
2340
+ const loseLease = (cause) => {
2341
+ state.leaseLost = true;
2342
+ if (state.leaseTimer) clearInterval(state.leaseTimer);
2343
+ if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2344
+ state.leaseTimer = void 0;
2345
+ state.leaseExpiryTimer = void 0;
2346
+ state.controller.abort(new FevexRunError("RUN_CONFLICT", `Run lease lost: ${cause instanceof Error ? cause.message : String(cause)}`, state.run.id, { cause }));
2347
+ };
2348
+ const watchExpiry = () => {
2349
+ if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2350
+ state.leaseExpiryTimer = setTimeout(() => loseLease(/* @__PURE__ */ new Error("Lease expired before renewal completed")), Math.max(0, Date.parse(state.lease.expiresAt) - Date.now()));
2351
+ };
2352
+ watchExpiry();
2353
+ state.leaseTimer = setInterval(() => {
2354
+ if (!state.lease || state.leaseLost || state.request.signal.aborted || renewing) return;
2355
+ renewing = true;
2356
+ const lease = {
2357
+ ...state.lease,
2358
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
2359
+ };
2360
+ Promise.resolve().then(() => store.renewLease(lease)).then((renewed) => {
2361
+ if (state.leaseLost || state.request.signal.aborted || !state.leaseTimer) return;
2362
+ if (!renewed) loseLease(/* @__PURE__ */ new Error("Lease ownership changed"));
2363
+ else {
2364
+ state.lease.expiresAt = lease.expiresAt;
2365
+ watchExpiry();
2366
+ }
2367
+ }).catch((error) => {
2368
+ if (state.leaseTimer) loseLease(error);
2369
+ }).finally(() => {
2370
+ renewing = false;
233
2371
  });
234
- request.signal?.throwIfAborted();
235
- const finalResult = await model.generate({
236
- messages: [
237
- ...messages,
238
- {
239
- role: "assistant",
240
- content: assistantContent,
241
- toolCalls: [toolCall]
242
- },
243
- {
244
- role: "tool",
245
- name: toolCall.name,
246
- toolCallId: toolCall.id,
247
- content: toolContent
2372
+ }, LEASE_RENEW_MS);
2373
+ };
2374
+ const cancelExecution = async (state, reason = cancellationReason(state.request.signal)) => {
2375
+ if (state.leaseLost) {
2376
+ await releaseExecution(state);
2377
+ throw state.controller.signal.reason;
2378
+ }
2379
+ if (state.run.status === "completed" || state.run.status === "failed" || state.run.status === "cancelled") return;
2380
+ Object.assign(state.run, {
2381
+ status: "cancelled",
2382
+ pause: void 0,
2383
+ error: reason,
2384
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2385
+ });
2386
+ const event = createEvent(state, "run.cancelled", { reason });
2387
+ if (isDurableRunStore(runStore)) await commit(state, {
2388
+ checkpoint: null,
2389
+ events: [event]
2390
+ });
2391
+ else {
2392
+ await runStore.saveRun(state.run);
2393
+ await runStore.appendEvent(event);
2394
+ notifyObserver(event);
2395
+ }
2396
+ await releaseExecution(state);
2397
+ return event;
2398
+ };
2399
+ const releaseWorkflowExecution = async (state) => {
2400
+ activeWorkflowRuns.delete(state.run.id);
2401
+ activeSessions.delete(state.session.id);
2402
+ if (state.leaseTimer) clearInterval(state.leaseTimer);
2403
+ if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2404
+ state.leaseTimer = void 0;
2405
+ state.leaseExpiryTimer = void 0;
2406
+ if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2407
+ };
2408
+ const commitWorkflow = async (state, update, events = []) => {
2409
+ state.commitQueue = state.commitQueue.then(async () => {
2410
+ if (state.run.status !== "running") throw new FevexRunError("RUN_CONFLICT", `Workflow run "${state.run.id}" is not running`, state.run.id);
2411
+ update();
2412
+ await commit(state, {
2413
+ checkpoint: state.checkpoint,
2414
+ events
2415
+ });
2416
+ });
2417
+ await state.commitQueue;
2418
+ };
2419
+ const authorize = async (state, tool, input, action, context = state.request.context) => {
2420
+ let approval = false;
2421
+ for (const policy of policies) {
2422
+ const decision = await policy.authorize({
2423
+ runId: state.run.id,
2424
+ agentName: state.run.agentName,
2425
+ toolName: tool.name,
2426
+ risk: tool.risk ?? "read",
2427
+ input,
2428
+ action,
2429
+ context
2430
+ });
2431
+ if (decision === "deny") throw new FevexRunError("POLICY_DENIED", `Policy "${policy.name}" denied tool "${tool.name}"`, state.run.id);
2432
+ if (decision === "require_approval") approval = true;
2433
+ }
2434
+ return approval ? "require_approval" : "allow";
2435
+ };
2436
+ return {
2437
+ activeRuns,
2438
+ activeWorkflowRuns,
2439
+ activeSessions,
2440
+ pendingExports,
2441
+ exportFailures,
2442
+ runtimeOwner,
2443
+ resolveModel,
2444
+ resolveCheckpointModel,
2445
+ notifyObserver,
2446
+ createEvent,
2447
+ createCoordinatorEvent,
2448
+ emitEvent,
2449
+ releaseExecution,
2450
+ commit,
2451
+ startLease,
2452
+ cancelExecution,
2453
+ releaseWorkflowExecution,
2454
+ commitWorkflow,
2455
+ authorize
2456
+ };
2457
+ }
2458
+ //#endregion
2459
+ //#region src/internal/workflow-engine.ts
2460
+ /**
2461
+ * Coordination on top of the agent engine: durable workflow and team runs,
2462
+ * the step API, compensation, waits, and resume.
2463
+ *
2464
+ * It receives {@link AgentEngine} explicitly. That is what makes the old
2465
+ * forward reference from workflow cancellation into agent cancellation a
2466
+ * declared dependency instead of a call resolved 2,900 lines later.
2467
+ */
2468
+ function createWorkflowEngine(composition, core, agentEngine) {
2469
+ const { models, agents, workflows, teams, runStore } = composition;
2470
+ const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
2471
+ const { prepareExecution, executeAgent, nextEvent, resumeAgent, cancelStoredAgent } = agentEngine;
2472
+ const cancelWorkflowExecution = async (state, reason = cancellationReason(state.request.signal)) => {
2473
+ if (state.leaseLost) {
2474
+ await releaseWorkflowExecution(state);
2475
+ throw state.controller.signal.reason;
2476
+ }
2477
+ if (state.run.status === "completed" || state.run.status === "failed" || state.run.status === "cancelled") return;
2478
+ const childRunIds = Object.values(state.checkpoint.steps).filter((record) => record.type === "agent" && record.status === "running").map(({ childRunId }) => childRunId);
2479
+ for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId)) throw new FevexRunError("RUN_CONFLICT", `Workflow child "${childRunId}" could not be cancelled`, state.run.id);
2480
+ state.run.status = "cancelled";
2481
+ state.run.pause = void 0;
2482
+ state.run.error = reason;
2483
+ state.run.usage = state.checkpoint.budget?.usage;
2484
+ const event = createCoordinatorEvent(state, "workflow.run.cancelled", { reason }, "team.run.cancelled");
2485
+ await commit(state, {
2486
+ checkpoint: null,
2487
+ events: [event]
2488
+ });
2489
+ await releaseWorkflowExecution(state);
2490
+ return event;
2491
+ };
2492
+ const prepareWorkflowExecution = async (name, request, kind = "workflow") => {
2493
+ const workflow = workflows.get(name);
2494
+ const team = kind === "team" ? teams.get(name) : void 0;
2495
+ if (!workflow || kind === "team" && !team || kind === "workflow" && teams.has(name)) throw new FevexRunError(kind === "team" ? "TEAM_NOT_FOUND" : "WORKFLOW_NOT_FOUND", `${kind === "team" ? "Team" : "Workflow"} "${name}" is not registered`);
2496
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", `${kind === "team" ? "Teams" : "Workflows"} require a DurableRunStore`);
2497
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2498
+ let session;
2499
+ if (request.sessionId === void 0) session = {
2500
+ id: crypto.randomUUID(),
2501
+ history: [],
2502
+ createdAt: now,
2503
+ updatedAt: now
2504
+ };
2505
+ else {
2506
+ const stored = await runStore.getSession(request.sessionId);
2507
+ if (!stored) throw new FevexRunError("SESSION_NOT_FOUND", `Session "${request.sessionId}" does not exist`);
2508
+ session = stored;
2509
+ }
2510
+ if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" already has an active run`);
2511
+ activeSessions.add(session.id);
2512
+ const controller = new AbortController();
2513
+ const signal = request.signal ? AbortSignal.any([controller.signal, request.signal]) : controller.signal;
2514
+ const input = toJsonValue(workflow.inputSchema === void 0 ? request.input : await abortable(() => validateSchema(workflow.inputSchema, request.input, `Input for ${kind} "${name}" does not match inputSchema`), signal), `${kind === "team" ? "Team" : "Workflow"} input must be a string or JSON-serializable value`);
2515
+ const id = crypto.randomUUID();
2516
+ const run = kind === "team" ? {
2517
+ kind: "team",
2518
+ id,
2519
+ sessionId: session.id,
2520
+ teamName: name,
2521
+ status: "running",
2522
+ revision: 0,
2523
+ createdAt: now,
2524
+ updatedAt: now
2525
+ } : {
2526
+ kind: "workflow",
2527
+ id,
2528
+ sessionId: session.id,
2529
+ workflowName: name,
2530
+ status: "running",
2531
+ revision: 0,
2532
+ createdAt: now,
2533
+ updatedAt: now
2534
+ };
2535
+ const checkpoint = kind === "team" ? {
2536
+ version: 2,
2537
+ kind: "team",
2538
+ runId: run.id,
2539
+ teamName: name,
2540
+ ...request.model === void 0 ? {} : { model: request.model },
2541
+ ...request.reasoning === void 0 ? {} : { reasoning: request.reasoning },
2542
+ definitionHash: await teamDefinitionHash(name, team),
2543
+ input,
2544
+ context: request.context,
2545
+ steps: {},
2546
+ limits: combineLimits(workflow.limits, request.limits)
2547
+ } : {
2548
+ version: 2,
2549
+ kind: "workflow",
2550
+ runId: run.id,
2551
+ workflowName: name,
2552
+ ...request.model === void 0 ? {} : { model: request.model },
2553
+ ...request.reasoning === void 0 ? {} : { reasoning: request.reasoning },
2554
+ definitionHash: await workflowDefinitionHash(name, workflow),
2555
+ input,
2556
+ context: request.context,
2557
+ steps: {},
2558
+ limits: combineLimits(workflow.limits, request.limits)
2559
+ };
2560
+ const state = {
2561
+ run,
2562
+ session,
2563
+ request: {
2564
+ ...request,
2565
+ sessionId: session.id,
2566
+ signal
2567
+ },
2568
+ controller,
2569
+ eventSequence: 0,
2570
+ checkpoint,
2571
+ advancing: false,
2572
+ commitQueue: Promise.resolve()
2573
+ };
2574
+ try {
2575
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
2576
+ const lease = {
2577
+ generation: 0,
2578
+ runId: run.id,
2579
+ ownerId,
2580
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
2581
+ };
2582
+ const started = createCoordinatorEvent(state, "workflow.run.started", void 0, "team.run.started");
2583
+ if (!await runStore.createExecution({
2584
+ run,
2585
+ checkpoint,
2586
+ session,
2587
+ lease,
2588
+ events: [started]
2589
+ })) throw new FevexRunError("RUN_CONFLICT", `Run or session "${session.id}" is active or was modified`, run.id);
2590
+ state.lease = lease;
2591
+ state.initialEvents = [started];
2592
+ notifyObserver(started);
2593
+ startLease(state, runStore);
2594
+ } catch (error) {
2595
+ activeSessions.delete(session.id);
2596
+ throw error;
2597
+ }
2598
+ activeWorkflowRuns.set(run.id, state);
2599
+ return state;
2600
+ };
2601
+ async function* executeWorkflow(name, state) {
2602
+ const workflow = workflows.get(name);
2603
+ const events = [];
2604
+ let parallelDepth = 0;
2605
+ const compensationHandlers = /* @__PURE__ */ new Map();
2606
+ const pauseForChild = async (stepId, error) => {
2607
+ if (parallelDepth > 0) throw new WorkflowChildPausedError(stepId, error);
2608
+ const childPause = error.pause;
2609
+ if (childPause.type === "workflow_child" || childPause.type === "workflow_children") throw error;
2610
+ if (childPause.type === "workflow_timer" || childPause.type === "workflow_event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Workflow child "${error.runId}" paused for a workflow wait`, state.run.id);
2611
+ const event = createCoordinatorEvent(state, "workflow.run.paused", {
2612
+ stepId,
2613
+ reason: "child",
2614
+ childRunId: error.runId
2615
+ }, "team.run.paused", {
2616
+ delegationId: stepId,
2617
+ reason: "child",
2618
+ childRunId: error.runId
2619
+ });
2620
+ await commitWorkflow(state, () => {
2621
+ state.run.status = "paused";
2622
+ state.run.pause = {
2623
+ type: "workflow_child",
2624
+ childRunId: error.runId,
2625
+ childPause
2626
+ };
2627
+ }, [event]);
2628
+ events.push(event);
2629
+ await releaseWorkflowExecution(state);
2630
+ throw new RunPausedError(state.run.id, state.run.pause);
2631
+ };
2632
+ const compensationContext = (stepId) => ({
2633
+ runId: state.run.id,
2634
+ sessionId: state.session.id,
2635
+ stepId,
2636
+ context: state.request.context,
2637
+ signal: state.request.signal
2638
+ });
2639
+ const registerCompensation = (stepId, kind, result, options) => {
2640
+ if (!options?.compensate) return;
2641
+ compensationHandlers.set(stepId, {
2642
+ kind,
2643
+ async run() {
2644
+ await options.compensate(structuredClone(result), compensationContext(stepId));
2645
+ }
2646
+ });
2647
+ };
2648
+ const childBudget = async (result) => {
2649
+ const childEvents = await runStore.listEvents(result.runId);
2650
+ return {
2651
+ usage: result.usage,
2652
+ steps: childEvents.filter(({ type }) => type === "model.completed").length,
2653
+ toolCalls: childEvents.filter(({ type }) => type === "tool.completed").length
2654
+ };
2655
+ };
2656
+ const addChildBudget = (budget) => {
2657
+ state.checkpoint.budget = addWorkflowBudget(state.checkpoint.budget, budget);
2658
+ state.run.usage = state.checkpoint.budget?.usage;
2659
+ };
2660
+ const relayChildEvent = async (event, stepId, agentName) => {
2661
+ if (event.type !== "model.started" && event.type !== "model.output.delta" && event.type !== "model.completed" && event.type !== "tool.started" && event.type !== "tool.completed" && event.type !== "tool.failed" && event.type !== "tool.retrying" && event.type !== "tool.execution_unknown" && event.type !== "elicitation.requested" && event.type !== "elicitation.resolved" && event.type !== "approval.requested" && event.type !== "approval.resolved") return;
2662
+ const relayed = createEvent(state, event.type, {
2663
+ ...event.payload,
2664
+ ...isTeamRun(state.run) ? {
2665
+ teamDelegationId: stepId,
2666
+ teamAgentName: agentName
2667
+ } : {
2668
+ workflowStepId: stepId,
2669
+ workflowAgentName: agentName
2670
+ }
2671
+ });
2672
+ await commitWorkflow(state, () => {}, [relayed]);
2673
+ events.push(relayed);
2674
+ };
2675
+ const runCompensations = async () => {
2676
+ const entries = Object.entries(state.checkpoint.steps).reverse();
2677
+ for (const [stepId, record] of entries) {
2678
+ if (record.type !== "agent" && record.type !== "parallel" || record.status !== "completed" || record.compensation?.status !== "pending") continue;
2679
+ const handler = compensationHandlers.get(stepId);
2680
+ if (!handler) throw new Error(`Workflow step "${stepId}" has no compensation handler`);
2681
+ const started = createEvent(state, "workflow.compensation.started", {
2682
+ stepId,
2683
+ kind: handler.kind
2684
+ });
2685
+ await commitWorkflow(state, () => {}, [started]);
2686
+ events.push(started);
2687
+ try {
2688
+ await abortable(() => handler.run(record.result), state.request.signal);
2689
+ const completed = createEvent(state, "workflow.compensation.completed", {
2690
+ stepId,
2691
+ kind: handler.kind
2692
+ });
2693
+ await commitWorkflow(state, () => {
2694
+ record.compensation = { status: "completed" };
2695
+ }, [completed]);
2696
+ events.push(completed);
2697
+ } catch (error) {
2698
+ const failed = createEvent(state, "workflow.compensation.failed", {
2699
+ stepId,
2700
+ kind: handler.kind,
2701
+ error: toErrorMessage(error)
2702
+ });
2703
+ await commitWorkflow(state, () => {
2704
+ record.compensation = {
2705
+ status: "failed",
2706
+ error: toErrorMessage(error)
2707
+ };
2708
+ }, [failed]);
2709
+ events.push(failed);
2710
+ throw error;
2711
+ }
2712
+ }
2713
+ };
2714
+ const completeAgentStep = async (stepId, agentName, childRunId, output, options) => {
2715
+ const child = await runStore.getRun(childRunId);
2716
+ const result = {
2717
+ runId: childRunId,
2718
+ sessionId: child.sessionId,
2719
+ output,
2720
+ ...child.usage === void 0 ? {} : { usage: child.usage }
2721
+ };
2722
+ const budget = await childBudget(result);
2723
+ const event = createCoordinatorEvent(state, "workflow.step.completed", {
2724
+ stepId,
2725
+ kind: "agent"
2726
+ }, "team.task.completed", {
2727
+ delegationId: stepId,
2728
+ agentName,
2729
+ ...eventUsage(child.usage) === void 0 ? {} : { usage: eventUsage(child.usage) }
2730
+ });
2731
+ await commitWorkflow(state, () => {
2732
+ addChildBudget(budget);
2733
+ state.checkpoint.steps[stepId] = {
2734
+ type: "agent",
2735
+ status: "completed",
2736
+ agentName,
2737
+ childRunId,
2738
+ result,
2739
+ ...options?.metadata === void 0 ? {} : { metadata: structuredClone(options.metadata) },
2740
+ ...options?.compensate === void 0 ? {} : { compensation: { status: "pending" } }
2741
+ };
2742
+ }, [event]);
2743
+ if (options?.compensate) registerCompensation(stepId, "agent", result, options);
2744
+ events.push(event);
2745
+ assertWorkflowBudget(name, state.checkpoint.limits, state.checkpoint.budget);
2746
+ return structuredClone(result);
2747
+ };
2748
+ const step = {
2749
+ async agent(stepId, agentName, request, options) {
2750
+ if (!stepId.trim()) throw new Error("Workflow step id cannot be empty");
2751
+ const existing = state.checkpoint.steps[stepId];
2752
+ if (existing?.type === "agent" && existing.status === "completed") {
2753
+ const result = structuredClone(existing.result);
2754
+ registerCompensation(stepId, "agent", result, options);
2755
+ return result;
2756
+ }
2757
+ if (existing?.type === "agent" && existing.status === "running") {
2758
+ let child = await runStore.getRun(existing.childRunId);
2759
+ if (!child) throw new Error(`Child run "${existing.childRunId}" does not exist`);
2760
+ if (child.status === "running") {
2761
+ if (!state.recoveryActor) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Child run "${child.id}" is still running`, state.run.id);
2762
+ child = await resumeAgent(child.id, void 0, true, state.recoveryActor);
248
2763
  }
249
- ],
250
- tools: void 0,
251
- reasoning: agent.reasoning,
252
- modelOptions: agent.modelOptions,
253
- outputSchema,
254
- signal: request.signal
2764
+ if (child.status === "completed") return completeAgentStep(stepId, existing.agentName, existing.childRunId, toJsonValue(child.output, "Child agent output must be JSON-serializable"), options);
2765
+ if (child.status === "paused" && child.pause) return pauseForChild(stepId, new RunPausedError(child.id, child.pause));
2766
+ if (child.status === "failed" || child.status === "cancelled") throw new Error(`Child run "${child.id}" ${child.status}`);
2767
+ }
2768
+ if (existing && existing.type !== "agent") throw new Error(`Workflow step "${stepId}" was already used as ${existing.type}`);
2769
+ if (!agents.has(agentName)) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${agentName}" is not registered`);
2770
+ const childSignal = request.signal ? AbortSignal.any([state.request.signal, request.signal]) : state.request.signal;
2771
+ const childState = await prepareExecution(agentName, {
2772
+ ...request,
2773
+ model: request.model ?? state.request.model,
2774
+ reasoning: request.reasoning ?? state.request.reasoning,
2775
+ context: mergeExecutionContext(state.request.context, request.context),
2776
+ limits: combineLimits(request.limits, remainingWorkflowLimits(state.checkpoint.limits, state.checkpoint.budget)),
2777
+ signal: childSignal
2778
+ });
2779
+ const action = options?.metadata?.teamAction === "handoff" ? "handoff" : "delegate";
2780
+ const lifecycleEvents = [];
2781
+ if (isTeamRun(state.run) && action === "handoff") lifecycleEvents.push(createEvent(state, "team.handoff.created", {
2782
+ delegationId: stepId,
2783
+ from: String(options?.metadata?.from ?? ""),
2784
+ to: agentName,
2785
+ reason: String(options?.metadata?.reason ?? "")
2786
+ }));
2787
+ const started = createCoordinatorEvent(state, "workflow.step.started", {
2788
+ stepId,
2789
+ kind: "agent",
2790
+ agentName
2791
+ }, "team.agent.assigned", {
2792
+ delegationId: stepId,
2793
+ agentName,
2794
+ action,
2795
+ ...typeof options?.metadata?.expectedOutput === "string" ? { expectedOutput: options.metadata.expectedOutput } : {},
2796
+ ...Array.isArray(options?.metadata?.constraints) ? { constraints: options.metadata.constraints } : {}
2797
+ });
2798
+ lifecycleEvents.push(started);
2799
+ await commitWorkflow(state, () => {
2800
+ state.checkpoint.steps[stepId] = {
2801
+ type: "agent",
2802
+ status: "running",
2803
+ agentName,
2804
+ childRunId: childState.run.id,
2805
+ ...options?.metadata === void 0 ? {} : { metadata: structuredClone(options.metadata) }
2806
+ };
2807
+ }, lifecycleEvents);
2808
+ events.push(...lifecycleEvents);
2809
+ try {
2810
+ const execution = executeAgent(agentName, childState);
2811
+ let result;
2812
+ while (true) {
2813
+ const next = await nextEvent(childState, execution);
2814
+ if (next.done) {
2815
+ if (next.value === void 0) {
2816
+ const run = await runStore.getRun(childState.run.id);
2817
+ if (run?.pause) throw new RunPausedError(run.id, run.pause);
2818
+ }
2819
+ result = next.value;
2820
+ break;
2821
+ }
2822
+ await relayChildEvent(next.value, stepId, agentName);
2823
+ }
2824
+ if (!result) throw new Error(`Agent "${agentName}" did not complete`);
2825
+ const normalized = {
2826
+ runId: result.runId,
2827
+ sessionId: result.sessionId,
2828
+ output: result.output,
2829
+ ...result.usage === void 0 ? {} : { usage: result.usage }
2830
+ };
2831
+ const budget = await childBudget(normalized);
2832
+ const completed = createCoordinatorEvent(state, "workflow.step.completed", {
2833
+ stepId,
2834
+ kind: "agent"
2835
+ }, "team.task.completed", {
2836
+ delegationId: stepId,
2837
+ agentName,
2838
+ ...eventUsage(normalized.usage) === void 0 ? {} : { usage: eventUsage(normalized.usage) }
2839
+ });
2840
+ await commitWorkflow(state, () => {
2841
+ addChildBudget(budget);
2842
+ state.checkpoint.steps[stepId] = {
2843
+ type: "agent",
2844
+ status: "completed",
2845
+ agentName,
2846
+ childRunId: normalized.runId,
2847
+ result: structuredClone(normalized),
2848
+ ...options?.metadata === void 0 ? {} : { metadata: structuredClone(options.metadata) },
2849
+ ...options?.compensate === void 0 ? {} : { compensation: { status: "pending" } }
2850
+ };
2851
+ }, [completed]);
2852
+ registerCompensation(stepId, "agent", normalized, options);
2853
+ events.push(completed);
2854
+ assertWorkflowBudget(name, state.checkpoint.limits, state.checkpoint.budget);
2855
+ return normalized;
2856
+ } catch (error) {
2857
+ if (error instanceof RunPausedError) return pauseForChild(stepId, error);
2858
+ const failed = createCoordinatorEvent(state, "workflow.step.failed", {
2859
+ stepId,
2860
+ kind: "agent",
2861
+ error: toErrorMessage(error)
2862
+ }, "team.task.failed", {
2863
+ delegationId: stepId,
2864
+ agentName,
2865
+ error: toErrorMessage(error)
2866
+ });
2867
+ await commitWorkflow(state, () => {}, [failed]);
2868
+ events.push(failed);
2869
+ throw error;
2870
+ }
2871
+ },
2872
+ async parallel(stepId, tasks, options) {
2873
+ if (!stepId.trim()) throw new Error("Workflow step id cannot be empty");
2874
+ const existing = state.checkpoint.steps[stepId];
2875
+ if (existing?.type === "parallel" && existing.status === "completed") {
2876
+ const result = structuredClone(existing.result);
2877
+ registerCompensation(stepId, "parallel", result, options);
2878
+ return result;
2879
+ }
2880
+ if (existing && existing.type !== "parallel") throw new Error(`Workflow step "${stepId}" was already used as ${existing.type}`);
2881
+ const started = createCoordinatorEvent(state, "workflow.step.started", {
2882
+ stepId,
2883
+ kind: "parallel"
2884
+ }, "team.merge.started", {
2885
+ stepId,
2886
+ width: typeof options?.metadata?.width === "number" ? options.metadata.width : Object.keys(tasks).length
2887
+ });
2888
+ await commitWorkflow(state, () => {
2889
+ state.checkpoint.steps[stepId] = {
2890
+ type: "parallel",
2891
+ status: "running",
2892
+ ...options?.metadata === void 0 ? {} : { metadata: structuredClone(options.metadata) }
2893
+ };
2894
+ }, [started]);
2895
+ events.push(started);
2896
+ try {
2897
+ parallelDepth += 1;
2898
+ const taskEntries = Object.entries(tasks);
2899
+ let settled;
2900
+ try {
2901
+ settled = await Promise.allSettled(taskEntries.map(async ([key, task]) => [key, await task()]));
2902
+ } finally {
2903
+ parallelDepth -= 1;
2904
+ }
2905
+ const failures = settled.filter((item) => item.status === "rejected").map(({ reason }) => reason);
2906
+ const pauses = failures.filter((error) => error instanceof WorkflowChildPausedError);
2907
+ const errors = failures.filter((error) => !(error instanceof WorkflowChildPausedError));
2908
+ if (errors.length) {
2909
+ for (const pause of pauses) if (!await cancelStoredAgent(pause.paused.runId)) throw new FevexRunError("RUN_CONFLICT", `Paused workflow child "${pause.paused.runId}" could not be cancelled`, state.run.id);
2910
+ throw errors.length === 1 ? errors[0] : new AggregateError(errors, `Workflow parallel step "${stepId}" failed`);
2911
+ }
2912
+ if (pauses.length) {
2913
+ if (pauses.length > 1) {
2914
+ const event = createCoordinatorEvent(state, "workflow.run.paused", {
2915
+ stepId,
2916
+ reason: "child",
2917
+ childRunId: pauses[0].paused.runId
2918
+ }, "team.run.paused", {
2919
+ delegationId: stepId,
2920
+ reason: "child",
2921
+ childRunId: pauses[0].paused.runId
2922
+ });
2923
+ await commitWorkflow(state, () => {
2924
+ state.run.status = "paused";
2925
+ state.run.pause = {
2926
+ type: "workflow_children",
2927
+ children: pauses.map((pause) => ({
2928
+ stepId: pause.stepId,
2929
+ childRunId: pause.paused.runId,
2930
+ childPause: pause.paused.pause
2931
+ }))
2932
+ };
2933
+ }, [event]);
2934
+ events.push(event);
2935
+ await releaseWorkflowExecution(state);
2936
+ throw new RunPausedError(state.run.id, state.run.pause);
2937
+ }
2938
+ return pauseForChild(pauses[0].stepId, pauses[0].paused);
2939
+ }
2940
+ const entries = settled.map((item) => item.value);
2941
+ const result = toJsonValue(Object.fromEntries(entries), `Workflow parallel step "${stepId}" result must be JSON-serializable`);
2942
+ if (typeof result !== "object" || result === null || Array.isArray(result)) throw new Error(`Workflow parallel step "${stepId}" result must be an object`);
2943
+ const completed = createCoordinatorEvent(state, "workflow.step.completed", {
2944
+ stepId,
2945
+ kind: "parallel"
2946
+ }, "team.merge.completed", { stepId });
2947
+ await commitWorkflow(state, () => {
2948
+ state.checkpoint.steps[stepId] = {
2949
+ type: "parallel",
2950
+ status: "completed",
2951
+ result,
2952
+ ...options?.metadata === void 0 ? {} : { metadata: structuredClone(options.metadata) },
2953
+ ...options?.compensate === void 0 ? {} : { compensation: { status: "pending" } }
2954
+ };
2955
+ }, [completed]);
2956
+ registerCompensation(stepId, "parallel", structuredClone(result), options);
2957
+ events.push(completed);
2958
+ return structuredClone(result);
2959
+ } catch (error) {
2960
+ if (error instanceof RunPausedError) throw error;
2961
+ const failed = createCoordinatorEvent(state, "workflow.step.failed", {
2962
+ stepId,
2963
+ kind: "parallel",
2964
+ error: toErrorMessage(error)
2965
+ }, "team.merge.failed", {
2966
+ stepId,
2967
+ error: toErrorMessage(error)
2968
+ });
2969
+ await commitWorkflow(state, () => {}, [failed]);
2970
+ events.push(failed);
2971
+ throw error;
2972
+ }
2973
+ },
2974
+ async waitUntil(stepId, resumeAt) {
2975
+ if (!stepId.trim()) throw new Error("Workflow step id cannot be empty");
2976
+ const existing = state.checkpoint.steps[stepId];
2977
+ if (existing?.type === "wait" && existing.status === "completed") return;
2978
+ if (existing && existing.type !== "wait") throw new Error(`Workflow step "${stepId}" was already used as ${existing.type}`);
2979
+ const at = resumeAt instanceof Date ? resumeAt : new Date(resumeAt);
2980
+ if (!Number.isFinite(at.getTime())) throw new Error(`Workflow wait "${stepId}" resumeAt must be a valid date`);
2981
+ const wait = {
2982
+ type: "timer",
2983
+ resumeAt: at.toISOString()
2984
+ };
2985
+ if (Date.now() >= at.getTime()) {
2986
+ const started = createEvent(state, "workflow.wait.started", {
2987
+ stepId,
2988
+ kind: "timer",
2989
+ resumeAt: wait.resumeAt
2990
+ });
2991
+ const completed = createEvent(state, "workflow.wait.completed", {
2992
+ stepId,
2993
+ kind: "timer"
2994
+ });
2995
+ await commitWorkflow(state, () => {
2996
+ state.checkpoint.steps[stepId] = {
2997
+ type: "wait",
2998
+ status: "completed",
2999
+ wait
3000
+ };
3001
+ }, [started, completed]);
3002
+ events.push(started, completed);
3003
+ return;
3004
+ }
3005
+ const started = createEvent(state, "workflow.wait.started", {
3006
+ stepId,
3007
+ kind: "timer",
3008
+ resumeAt: wait.resumeAt
3009
+ });
3010
+ const paused = createEvent(state, "workflow.run.paused", {
3011
+ stepId,
3012
+ reason: "timer"
3013
+ });
3014
+ await commitWorkflow(state, () => {
3015
+ state.checkpoint.steps[stepId] = {
3016
+ type: "wait",
3017
+ status: "running",
3018
+ wait
3019
+ };
3020
+ state.run.status = "paused";
3021
+ state.run.pause = {
3022
+ type: "workflow_timer",
3023
+ stepId,
3024
+ resumeAt: wait.resumeAt
3025
+ };
3026
+ }, [started, paused]);
3027
+ events.push(started, paused);
3028
+ await releaseWorkflowExecution(state);
3029
+ throw new RunPausedError(state.run.id, state.run.pause);
3030
+ },
3031
+ async waitForEvent(stepId, eventName) {
3032
+ if (!stepId.trim()) throw new Error("Workflow step id cannot be empty");
3033
+ if (!eventName.trim()) throw new Error("Workflow event name cannot be empty");
3034
+ const definition = workflow.events?.[eventName];
3035
+ if (!definition) throw new Error(`Workflow event "${eventName}" is not declared`);
3036
+ const existing = state.checkpoint.steps[stepId];
3037
+ if (existing?.type === "wait" && existing.status === "completed") return {
3038
+ ...existing.payload === void 0 ? {} : { payload: existing.payload },
3039
+ ...existing.actor === void 0 ? {} : { actor: existing.actor },
3040
+ receivedAt: existing.receivedAt
3041
+ };
3042
+ if (existing && existing.type !== "wait") throw new Error(`Workflow step "${stepId}" was already used as ${existing.type}`);
3043
+ const wait = {
3044
+ type: "event",
3045
+ eventName,
3046
+ requireActor: definition.requireActor ?? false
3047
+ };
3048
+ const started = createEvent(state, "workflow.wait.started", {
3049
+ stepId,
3050
+ kind: "event",
3051
+ eventName
3052
+ });
3053
+ const paused = createEvent(state, "workflow.run.paused", {
3054
+ stepId,
3055
+ reason: "event"
3056
+ });
3057
+ await commitWorkflow(state, () => {
3058
+ state.checkpoint.steps[stepId] = {
3059
+ type: "wait",
3060
+ status: "running",
3061
+ wait
3062
+ };
3063
+ state.run.status = "paused";
3064
+ state.run.pause = {
3065
+ type: "workflow_event",
3066
+ stepId,
3067
+ eventName
3068
+ };
3069
+ }, [started, paused]);
3070
+ events.push(started, paused);
3071
+ await releaseWorkflowExecution(state);
3072
+ throw new RunPausedError(state.run.id, state.run.pause);
3073
+ }
3074
+ };
3075
+ const firstEvents = state.initialEvents ?? [];
3076
+ state.initialEvents = void 0;
3077
+ events.push(...firstEvents);
3078
+ try {
3079
+ for (const event of firstEvents) yield event;
3080
+ if (state.checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${state.run.id}" is unsupported`, state.run.id);
3081
+ const hash = isTeamRun(state.run) ? await teamDefinitionHash(name, teams.get(name)) : await workflowDefinitionHash(name, workflow);
3082
+ if (state.checkpoint.definitionHash !== hash) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for ${(isTeamRun(state.run) ? "Team" : "Workflow").toLowerCase()} "${name}" changed`, state.run.id);
3083
+ state.request.signal.throwIfAborted();
3084
+ const rawOutput = await workflow.run(step, state.checkpoint.input);
3085
+ const output = toJsonValue(workflow.outputSchema === void 0 ? rawOutput : await validateSchema(workflow.outputSchema, rawOutput, `Output from ${isTeamRun(state.run) ? "team" : "workflow"} "${name}" does not match outputSchema`), `Output from ${isTeamRun(state.run) ? "team" : "workflow"} "${name}" must be JSON-serializable`);
3086
+ await state.commitQueue;
3087
+ state.run.status = "completed";
3088
+ state.run.pause = void 0;
3089
+ state.run.output = output;
3090
+ state.run.usage = state.checkpoint.budget?.usage;
3091
+ const completedPayload = {
3092
+ output,
3093
+ ...eventUsage(state.run.usage) === void 0 ? {} : { usage: eventUsage(state.run.usage) }
3094
+ };
3095
+ const completed = createCoordinatorEvent(state, "workflow.run.completed", completedPayload, "team.run.completed");
3096
+ await commit(state, {
3097
+ checkpoint: null,
3098
+ events: [completed]
255
3099
  });
256
- emit("model.completed");
257
- if (finalResult.toolCalls?.length) throw new Error(`Agent "${name}" requested another tool, but this runtime supports one tool call`);
258
- if (finalResult.output === void 0) throw new Error(`Model for agent "${name}" returned no output`);
259
- const output = toJsonValue(activeOutputSchema === void 0 ? finalResult.output : await validateSchema(activeOutputSchema, finalResult.output, `Output from agent "${name}" does not match outputSchema`), `Output from agent "${name}" must be JSON-serializable`);
260
- emit("run.completed");
3100
+ events.push(completed);
3101
+ await releaseWorkflowExecution(state);
3102
+ yield completed;
261
3103
  return {
3104
+ runId: state.run.id,
3105
+ sessionId: state.session.id,
262
3106
  output,
263
3107
  events,
264
- usage: addUsage(result.usage, finalResult.usage)
3108
+ ...state.run.usage === void 0 ? {} : { usage: state.run.usage }
265
3109
  };
266
3110
  } catch (error) {
267
- emitFailure("run.failed", { error: toErrorMessage(error) });
3111
+ let thrown = error;
3112
+ if (state.request.signal.aborted) try {
3113
+ const event = await cancelWorkflowExecution(state);
3114
+ if (event) {
3115
+ events.push(event);
3116
+ yield event;
3117
+ }
3118
+ } catch (cancellationError) {
3119
+ await releaseWorkflowExecution(state);
3120
+ throw cancellationError;
3121
+ }
3122
+ else if (state.run.status === "paused") throw error;
3123
+ else if (error instanceof FevexRunError && error.code === "RUN_CONFLICT") {
3124
+ await releaseWorkflowExecution(state);
3125
+ throw error;
3126
+ } else if (state.run.status === "running") {
3127
+ await state.commitQueue;
3128
+ let failure = error;
3129
+ try {
3130
+ await runCompensations();
3131
+ } catch (compensationError) {
3132
+ failure = new AggregateError([error, compensationError], `Workflow failed: ${toErrorMessage(error)}; compensation failed: ${toErrorMessage(compensationError)}`);
3133
+ thrown = failure;
3134
+ }
3135
+ state.run.status = "failed";
3136
+ state.run.error = toErrorMessage(failure);
3137
+ state.run.usage = state.checkpoint.budget?.usage;
3138
+ const failedPayload = {
3139
+ error: toErrorMessage(failure),
3140
+ ...runErrorPayload(failure)
3141
+ };
3142
+ const failed = createCoordinatorEvent(state, "workflow.run.failed", failedPayload, "team.run.failed");
3143
+ await commit(state, {
3144
+ checkpoint: null,
3145
+ events: [failed]
3146
+ });
3147
+ events.push(failed);
3148
+ await releaseWorkflowExecution(state);
3149
+ yield failed;
3150
+ } else await releaseWorkflowExecution(state);
3151
+ throw thrown;
3152
+ }
3153
+ }
3154
+ const nextWorkflowEvent = async (state, execution) => {
3155
+ state.advancing = true;
3156
+ try {
3157
+ return await execution.next();
3158
+ } finally {
3159
+ state.advancing = false;
3160
+ }
3161
+ };
3162
+ const drainWorkflowExecution = async (name, state, throwOnPause) => {
3163
+ const execution = executeWorkflow(name, state);
3164
+ while (true) {
3165
+ const next = await nextWorkflowEvent(state, execution);
3166
+ if (next.done) {
3167
+ if (next.value === void 0 && throwOnPause) {
3168
+ const run = await runStore.getRun(state.run.id);
3169
+ if (run?.pause) throw new RunPausedError(run.id, run.pause);
3170
+ }
3171
+ return next.value;
3172
+ }
3173
+ }
3174
+ };
3175
+ const resumeWorkflow = async (runId, resolution, recoveryActor) => {
3176
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "resumeRun requires DurableRunStore", runId);
3177
+ const run = await runStore.getRun(runId);
3178
+ const checkpoint = await runStore.getCheckpoint(runId);
3179
+ if (!run || !checkpoint) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be ${resolution ? "resumed" : "recovered"}`, runId);
3180
+ if (checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${runId}" is unsupported`, runId);
3181
+ if (resolution && run.status !== "paused" && run.status !== "running" || !resolution && run.status !== "running") throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" is not ${resolution ? "resumable" : "recoverable"}`, runId);
3182
+ const name = coordinatorName(run);
3183
+ const workflow = workflows.get(name);
3184
+ const team = isTeamRun(run) ? teams.get(name) : void 0;
3185
+ if (!workflow) throw new FevexRunError("RUN_DEFINITION_CHANGED", `${isTeamRun(run) ? "Team" : "Workflow"} "${name}" is unavailable`, runId);
3186
+ const currentHash = isTeamRun(run) ? team && await teamDefinitionHash(name, team) : await workflowDefinitionHash(name, workflow);
3187
+ if (!currentHash || checkpoint.definitionHash !== currentHash) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for ${isTeamRun(run) ? "team" : "workflow"} "${name}" changed`, runId);
3188
+ if (resolution && run.pause?.type !== "workflow_child" && run.pause?.type !== "workflow_children" && run.pause?.type !== "workflow_timer" && run.pause?.type !== "workflow_event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
3189
+ const session = await runStore.getSession(run.sessionId);
3190
+ if (!session) throw new Error(`Session "${run.sessionId}" does not exist`);
3191
+ if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" is active`, runId);
3192
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
3193
+ const lease = {
3194
+ generation: 0,
3195
+ runId,
3196
+ ownerId,
3197
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
3198
+ };
3199
+ if (!await runStore.acquireLease(lease)) throw new FevexRunError("RUN_CONFLICT", `Run "${runId}" is leased`, runId);
3200
+ try {
3201
+ if (!resolution) {
3202
+ if (!recoveryActor?.id.trim()) throw new FevexRunError("RUN_NOT_RECOVERABLE", "Recovery actor is required", runId);
3203
+ const controller = new AbortController();
3204
+ const state = {
3205
+ run,
3206
+ session,
3207
+ request: {
3208
+ input: checkpoint.input,
3209
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
3210
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3211
+ sessionId: session.id,
3212
+ context: checkpoint.context,
3213
+ limits: checkpoint.limits,
3214
+ signal: controller.signal
3215
+ },
3216
+ controller,
3217
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3218
+ checkpoint,
3219
+ advancing: false,
3220
+ lease,
3221
+ commitQueue: Promise.resolve(),
3222
+ recoveryActor
3223
+ };
3224
+ const recovered = createCoordinatorEvent(state, "workflow.run.recovered", { actorId: recoveryActor.id }, "team.run.recovered");
3225
+ await commit(state, {
3226
+ checkpoint,
3227
+ events: [recovered]
3228
+ });
3229
+ state.initialEvents = [recovered];
3230
+ activeWorkflowRuns.set(runId, state);
3231
+ activeSessions.add(session.id);
3232
+ startLease(state, runStore);
3233
+ drainWorkflowExecution(name, state, false).catch(() => {});
3234
+ return structuredClone(run);
3235
+ }
3236
+ const workflowPause = run.pause;
3237
+ if (!workflowPause || workflowPause.type !== "workflow_child" && workflowPause.type !== "workflow_children" && workflowPause.type !== "workflow_timer" && workflowPause.type !== "workflow_event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
3238
+ if (workflowPause.type === "workflow_timer" || workflowPause.type === "workflow_event") {
3239
+ const pause = workflowPause;
3240
+ if (pause.type === "workflow_timer") {
3241
+ if (resolution.type !== "timer") throw new FevexRunError("RUN_NOT_RESUMABLE", "Timer resolution is required", runId);
3242
+ if (Date.now() < Date.parse(pause.resumeAt)) throw new FevexRunError("RUN_NOT_RESUMABLE", "Workflow timer has not elapsed", runId);
3243
+ } else if (resolution.type !== "event" || resolution.eventName !== pause.eventName) throw new FevexRunError("RUN_NOT_RESUMABLE", "Workflow event does not match", runId);
3244
+ const waiting = checkpoint.steps[pause.stepId];
3245
+ if (waiting?.type !== "wait" || waiting.status !== "running") throw new FevexRunError("RUN_NOT_RESUMABLE", "Workflow wait is unavailable", runId);
3246
+ let eventPayload;
3247
+ const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
3248
+ if (resolution.type === "event") {
3249
+ const eventDefinition = workflow.events?.[resolution.eventName];
3250
+ if (!eventDefinition) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Workflow event "${resolution.eventName}" is unavailable`, runId);
3251
+ if (waiting.wait.type !== "event") throw new FevexRunError("RUN_NOT_RESUMABLE", "Workflow wait is unavailable", runId);
3252
+ if (waiting.wait.requireActor && !resolution.actor?.id?.trim()) throw new FevexRunError("APPROVAL_INVALID", "Workflow event actor is required", runId);
3253
+ const validatedPayload = eventDefinition.payloadSchema === void 0 ? resolution.payload : await validateSchema(eventDefinition.payloadSchema, resolution.payload, `Payload for workflow event "${resolution.eventName}" does not match payloadSchema`);
3254
+ if (validatedPayload !== void 0) eventPayload = toJsonValue(validatedPayload, "Workflow event payload must be JSON-serializable");
3255
+ }
3256
+ const controller = new AbortController();
3257
+ const state = {
3258
+ run,
3259
+ session,
3260
+ request: {
3261
+ input: checkpoint.input,
3262
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
3263
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3264
+ sessionId: session.id,
3265
+ context: checkpoint.context,
3266
+ limits: checkpoint.limits,
3267
+ signal: controller.signal
3268
+ },
3269
+ controller,
3270
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3271
+ checkpoint,
3272
+ advancing: false,
3273
+ lease,
3274
+ commitQueue: Promise.resolve()
3275
+ };
3276
+ run.status = "running";
3277
+ run.pause = void 0;
3278
+ const resumed = createCoordinatorEvent(state, "workflow.run.resumed", void 0, "team.run.resumed");
3279
+ const completed = createEvent(state, "workflow.wait.completed", {
3280
+ stepId: pause.stepId,
3281
+ kind: waiting.wait.type,
3282
+ ...eventPayload === void 0 ? {} : { payload: eventPayload },
3283
+ ...resolution.type === "event" && resolution.actor ? { actorId: resolution.actor.id } : {},
3284
+ ...resolution.type === "event" ? { receivedAt } : {}
3285
+ });
3286
+ await commitWorkflow(state, () => {
3287
+ state.checkpoint.steps[pause.stepId] = {
3288
+ type: "wait",
3289
+ status: "completed",
3290
+ wait: waiting.wait,
3291
+ ...eventPayload === void 0 ? {} : { payload: eventPayload },
3292
+ ...resolution.type === "event" && resolution.actor ? { actor: resolution.actor } : {},
3293
+ ...resolution.type === "event" ? { receivedAt } : {}
3294
+ };
3295
+ }, [resumed, completed]);
3296
+ activeWorkflowRuns.set(runId, state);
3297
+ activeSessions.add(session.id);
3298
+ startLease(state, runStore);
3299
+ drainWorkflowExecution(name, state, false).catch(() => {});
3300
+ return structuredClone(run);
3301
+ }
3302
+ if (resolution.type === "timer" || resolution.type === "event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
3303
+ const childRunId = workflowPause.type === "workflow_children" ? workflowPause.children.find(({ childPause }) => pauseMatchesResolution(childPause, resolution))?.childRunId : workflowPause.childRunId;
3304
+ if (!childRunId) throw new FevexRunError("RUN_NOT_RESUMABLE", "Resolution does not match a paused child run", runId);
3305
+ const childRun = await resumeAgent(childRunId, resolution, true);
3306
+ const controller = new AbortController();
3307
+ const state = {
3308
+ run,
3309
+ session,
3310
+ request: {
3311
+ input: checkpoint.input,
3312
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
3313
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3314
+ sessionId: session.id,
3315
+ context: checkpoint.context,
3316
+ limits: checkpoint.limits,
3317
+ signal: controller.signal
3318
+ },
3319
+ controller,
3320
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3321
+ checkpoint,
3322
+ advancing: false,
3323
+ lease,
3324
+ commitQueue: Promise.resolve()
3325
+ };
3326
+ if (childRun.status === "cancelled") {
3327
+ run.status = "cancelled";
3328
+ run.pause = void 0;
3329
+ run.error = childRun.error ?? "aborted";
3330
+ const reason = childRun.error === "approval_rejected" ? "approval_rejected" : "aborted";
3331
+ const cancelled = createCoordinatorEvent(state, "workflow.run.cancelled", { reason }, "team.run.cancelled");
3332
+ await commit(state, {
3333
+ checkpoint: null,
3334
+ events: [cancelled]
3335
+ });
3336
+ await releaseWorkflowExecution(state);
3337
+ return structuredClone(run);
3338
+ }
3339
+ run.status = "running";
3340
+ run.pause = void 0;
3341
+ const resumed = createCoordinatorEvent(state, "workflow.run.resumed", void 0, "team.run.resumed");
3342
+ await commit(state, {
3343
+ checkpoint,
3344
+ events: [resumed]
3345
+ });
3346
+ activeWorkflowRuns.set(runId, state);
3347
+ activeSessions.add(session.id);
3348
+ startLease(state, runStore);
3349
+ drainWorkflowExecution(name, state, false).catch(() => {});
3350
+ return structuredClone(run);
3351
+ } catch (error) {
3352
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
268
3353
  throw error;
269
3354
  }
270
- } };
3355
+ };
3356
+ return {
3357
+ prepareWorkflowExecution,
3358
+ executeWorkflow,
3359
+ nextWorkflowEvent,
3360
+ drainWorkflowExecution,
3361
+ resumeWorkflow,
3362
+ cancelWorkflowExecution
3363
+ };
3364
+ }
3365
+ //#endregion
3366
+ //#region src/internal/runtime.ts
3367
+ function createRuntime(composition) {
3368
+ const { models, agents, workflows, teams, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
3369
+ const core = createRunCore(composition);
3370
+ const { activeRuns, activeWorkflowRuns, activeSessions, pendingExports, exportFailures, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
3371
+ const agentEngine = createAgentEngine(composition, core);
3372
+ const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent } = agentEngine;
3373
+ const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution } = createWorkflowEngine(composition, core, agentEngine);
3374
+ const resumeRun = async (runId, resolution) => {
3375
+ const run = await runStore.getRun(runId);
3376
+ if (run && isCoordinatorRun(run)) return resumeWorkflow(runId, resolution);
3377
+ return resumeAgent(runId, resolution);
3378
+ };
3379
+ const recoverRun = async (runId, actor) => {
3380
+ const run = await runStore.getRun(runId);
3381
+ if (!run) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be recovered`, runId);
3382
+ if (isCoordinatorRun(run)) return resumeWorkflow(runId, void 0, actor);
3383
+ return resumeAgent(runId, void 0, false, actor);
3384
+ };
3385
+ return {
3386
+ async startAgent(name, request) {
3387
+ const state = await prepareExecution(name, request);
3388
+ drainExecution(name, state, false).catch(() => {});
3389
+ return structuredClone(state.run);
3390
+ },
3391
+ async runAgent(name, request) {
3392
+ const state = await prepareExecution(name, request);
3393
+ return await drainExecution(name, state, true);
3394
+ },
3395
+ streamAgent(name, request) {
3396
+ return { async *[Symbol.asyncIterator]() {
3397
+ const state = await prepareExecution(name, request);
3398
+ const execution = executeAgent(name, state);
3399
+ let finished = false;
3400
+ try {
3401
+ while (true) {
3402
+ const next = await nextEvent(state, execution);
3403
+ if (next.done) {
3404
+ finished = true;
3405
+ return;
3406
+ }
3407
+ yield next.value;
3408
+ }
3409
+ } finally {
3410
+ if (!finished && state.run.status === "running") {
3411
+ state.controller.abort(new DOMException("Stream abandoned", "AbortError"));
3412
+ try {
3413
+ while (state.run.status === "running") if ((await nextEvent(state, execution)).done) break;
3414
+ } catch {}
3415
+ if (state.run.status === "running") await cancelExecution(state);
3416
+ }
3417
+ await execution.return(void 0);
3418
+ }
3419
+ } };
3420
+ },
3421
+ async startWorkflow(name, request) {
3422
+ const state = await prepareWorkflowExecution(name, request);
3423
+ drainWorkflowExecution(name, state, false).catch(() => {});
3424
+ return structuredClone(state.run);
3425
+ },
3426
+ async runWorkflow(name, request) {
3427
+ const state = await prepareWorkflowExecution(name, request);
3428
+ return await drainWorkflowExecution(name, state, true);
3429
+ },
3430
+ async startTeam(name, request) {
3431
+ const state = await prepareWorkflowExecution(name, request, "team");
3432
+ drainWorkflowExecution(name, state, false).catch(() => {});
3433
+ return structuredClone(state.run);
3434
+ },
3435
+ async runTeam(name, request) {
3436
+ const state = await prepareWorkflowExecution(name, request, "team");
3437
+ return await drainWorkflowExecution(name, state, true);
3438
+ },
3439
+ streamTeam(name, request) {
3440
+ return { async *[Symbol.asyncIterator]() {
3441
+ const state = await prepareWorkflowExecution(name, request, "team");
3442
+ const execution = executeWorkflow(name, state);
3443
+ let finished = false;
3444
+ try {
3445
+ while (true) {
3446
+ const next = await nextWorkflowEvent(state, execution);
3447
+ if (next.done) {
3448
+ finished = true;
3449
+ return;
3450
+ }
3451
+ yield next.value;
3452
+ }
3453
+ } finally {
3454
+ if (!finished && state.run.status === "running") {
3455
+ state.controller.abort(new DOMException("Stream abandoned", "AbortError"));
3456
+ try {
3457
+ while (state.run.status === "running") if ((await nextWorkflowEvent(state, execution)).done) break;
3458
+ } catch {}
3459
+ if (state.run.status === "running") await cancelWorkflowExecution(state);
3460
+ }
3461
+ await execution.return(void 0);
3462
+ }
3463
+ } };
3464
+ },
3465
+ async getTeamRun(runId) {
3466
+ const run = await runStore.getRun(runId);
3467
+ return run?.kind === "team" ? run : void 0;
3468
+ },
3469
+ getRun(runId) {
3470
+ return runStore.getRun(runId);
3471
+ },
3472
+ listEvents(runId, options) {
3473
+ return runStore.listEvents(runId, options);
3474
+ },
3475
+ async cancelRun(runId) {
3476
+ const activeWorkflow = activeWorkflowRuns.get(runId);
3477
+ if (activeWorkflow && !activeWorkflow.request.signal.aborted) {
3478
+ activeWorkflow.controller.abort(new DOMException("Run cancelled", "AbortError"));
3479
+ if (!activeWorkflow.advancing) await cancelWorkflowExecution(activeWorkflow);
3480
+ return true;
3481
+ }
3482
+ const active = activeRuns.get(runId);
3483
+ if (active && !active.request.signal.aborted) {
3484
+ active.controller.abort(new DOMException("Run cancelled", "AbortError"));
3485
+ if (!active.advancing) await cancelExecution(active);
3486
+ return true;
3487
+ }
3488
+ const run = await runStore.getRun(runId);
3489
+ if (!run || run.status !== "paused" && run.status !== "running") return false;
3490
+ if (!isDurableRunStore(runStore)) return false;
3491
+ const session = await runStore.getSession(run.sessionId);
3492
+ if (!session) return false;
3493
+ if (isCoordinatorRun(run)) {
3494
+ const checkpoint = await runStore.getCheckpoint(runId);
3495
+ if (!checkpoint) return false;
3496
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
3497
+ const lease = {
3498
+ generation: 0,
3499
+ runId,
3500
+ ownerId,
3501
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
3502
+ };
3503
+ if (!await runStore.acquireLease(lease)) return false;
3504
+ try {
3505
+ const childRunIds = Object.values(checkpoint.steps).filter((record) => record.type === "agent" && record.status === "running").map(({ childRunId }) => childRunId);
3506
+ for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId)) return false;
3507
+ const state = {
3508
+ run,
3509
+ session,
3510
+ request: {
3511
+ input: checkpoint.input,
3512
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3513
+ sessionId: session.id,
3514
+ context: checkpoint.context,
3515
+ limits: checkpoint.limits,
3516
+ signal: new AbortController().signal
3517
+ },
3518
+ controller: new AbortController(),
3519
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3520
+ checkpoint,
3521
+ advancing: false,
3522
+ lease,
3523
+ commitQueue: Promise.resolve()
3524
+ };
3525
+ run.status = "cancelled";
3526
+ run.pause = void 0;
3527
+ run.error = "aborted";
3528
+ run.usage = checkpoint.budget?.usage;
3529
+ const cancelled = createCoordinatorEvent(state, "workflow.run.cancelled", { reason: "aborted" }, "team.run.cancelled");
3530
+ await commit(state, {
3531
+ checkpoint: null,
3532
+ events: [cancelled]
3533
+ });
3534
+ return true;
3535
+ } catch (error) {
3536
+ if (error instanceof FevexRunError && error.code === "RUN_CONFLICT" && (await runStore.getRun(runId))?.status !== "paused") return false;
3537
+ throw error;
3538
+ } finally {
3539
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
3540
+ }
3541
+ }
3542
+ return cancelStoredAgent(runId);
3543
+ },
3544
+ resumeRun(runId, resolution) {
3545
+ return resumeRun(runId, resolution);
3546
+ },
3547
+ recoverRun(runId, options) {
3548
+ return recoverRun(runId, options.actor);
3549
+ },
3550
+ async compactSession(sessionId, summary) {
3551
+ if (typeof summary !== "string" || !summary.trim()) throw new Error("Session summary must be a non-empty string");
3552
+ if (activeSessions.has(sessionId)) throw new Error(`Session "${sessionId}" has an active run`);
3553
+ const session = await runStore.getSession(sessionId);
3554
+ if (!session) throw new Error(`Session "${sessionId}" does not exist`);
3555
+ session.history = [{
3556
+ role: "system",
3557
+ content: summary
3558
+ }];
3559
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3560
+ await runStore.saveSession(session);
3561
+ return structuredClone(session);
3562
+ },
3563
+ async flushObservability() {
3564
+ while (pendingExports.size) await Promise.all([...pendingExports]);
3565
+ if (exportFailures.length) {
3566
+ const failures = exportFailures.splice(0);
3567
+ throw new AggregateError(failures, "Observability export failed");
3568
+ }
3569
+ }
3570
+ };
3571
+ }
3572
+ //#endregion
3573
+ //#region src/fevex.ts
3574
+ /**
3575
+ * Validates a configuration snapshot and creates an isolated runtime.
3576
+ *
3577
+ * The runtime does not own external adapters; callers remain responsible for
3578
+ * closing model, connection, store, and exporter resources they provide.
3579
+ */
3580
+ function createFevex(config) {
3581
+ return createRuntime(createComposition(config));
271
3582
  }
272
3583
  //#endregion
273
- export { SCHEMA_NOT_TRANSPORTABLE, createFevex, defineAgent, defineTool, toToolSpec };
3584
+ export { ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, InMemoryMemoryStore, InMemoryRunStore, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, SCHEMA_NOT_TRANSPORTABLE, SandboxError, compileFevexJsonSchema, createFevex, createLocalSandbox, createMemoryContextProvider, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, handleChannelInput, isDurableRunStore, sandboxResultToJson, toToolSpec, validateFevexJsonSchemaProfile, validateListEventsOptions };