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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,55 +1,25 @@
1
- import { defineTool, toToolSpec } from "./tools/index.mjs";
2
1
  import { defineAgent } from "./agents/index.mjs";
2
+ import { InMemoryRunStore, isDurableRunStore } from "./runtime/index.mjs";
3
+ 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";
4
+ import { defineTool, toToolSpec } from "./tools/index.mjs";
3
5
  import "./models/index.mjs";
4
- import "./runtime/index.mjs";
6
+ import { definePolicy } from "./policies/index.mjs";
7
+ //#region src/configuration-error.ts
8
+ var FevexConfigurationError = class extends Error {
9
+ code;
10
+ constructor(code, message) {
11
+ super(message);
12
+ this.code = code;
13
+ this.name = "FevexConfigurationError";
14
+ }
15
+ };
16
+ //#endregion
5
17
  //#region src/core/index.ts
6
18
  const SCHEMA_NOT_TRANSPORTABLE = "SCHEMA_NOT_TRANSPORTABLE";
19
+ const PROVIDER_SCHEMA_UNSUPPORTED = "PROVIDER_SCHEMA_UNSUPPORTED";
20
+ const PROVIDER_REASONING_UNSUPPORTED = "PROVIDER_REASONING_UNSUPPORTED";
7
21
  //#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
- }
22
+ //#region src/internal/schemas.ts
53
23
  function isStandardSchema(value) {
54
24
  if (typeof value !== "object" || value === null || !("~standard" in value)) return false;
55
25
  const standard = value["~standard"];
@@ -91,183 +61,1460 @@ async function validateSchema(schema, value, errorMessage) {
91
61
  }
92
62
  return result.value;
93
63
  }
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);
101
- }
64
+ //#endregion
65
+ //#region src/internal/configuration.ts
66
+ const REASONING_EFFORTS = /* @__PURE__ */ new Set([
67
+ "provider-default",
68
+ "none",
69
+ "minimal",
70
+ "low",
71
+ "medium",
72
+ "high"
73
+ ]);
74
+ function isRecord(value) {
75
+ return typeof value === "object" && value !== null && !Array.isArray(value);
102
76
  }
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);
109
- return {
110
- ...inputTokens !== void 0 ? { inputTokens } : {},
111
- ...outputTokens !== void 0 ? { outputTokens } : {},
112
- ...totalTokens !== void 0 ? { totalTokens } : {}
113
- };
77
+ function configurationError(code, message) {
78
+ return new FevexConfigurationError(code, message);
114
79
  }
115
- function createFevex(config) {
116
- const models = new Map(Object.entries(config.models));
80
+ function assertConfiguration(condition, code, message) {
81
+ if (!condition) throw configurationError(code, message);
82
+ }
83
+ function isModelGateway(value) {
84
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.stream === "function";
85
+ }
86
+ function isRunStore(value) {
87
+ return (typeof value === "object" && value !== null || typeof value === "function") && [
88
+ "getRun",
89
+ "saveRun",
90
+ "getSession",
91
+ "saveSession",
92
+ "appendEvent",
93
+ "listEvents"
94
+ ].every((method) => typeof value[method] === "function");
95
+ }
96
+ function assertLimit(value, name, minimum, agentName, allowFalse = false) {
97
+ if (value === void 0 || allowFalse && value === false) return;
98
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum) throw configurationError("INVALID_AGENT", `Agent "${agentName}" limit "${name}" must be ${minimum === 0 ? "a non-negative" : "a positive"} integer${allowFalse ? " or false" : ""}`);
99
+ }
100
+ function createComposition(config) {
101
+ assertConfiguration(isRecord(config), "INVALID_CONFIG", "Fevex config must be an object");
102
+ assertConfiguration(isRecord(config.models), "INVALID_CONFIG", "Fevex config \"models\" must be an object");
103
+ assertConfiguration(Array.isArray(config.agents), "INVALID_CONFIG", "Fevex config \"agents\" must be an array");
104
+ assertConfiguration(config.tools === void 0 || Array.isArray(config.tools), "INVALID_CONFIG", "Fevex config \"tools\" must be an array");
105
+ assertConfiguration(config.onEvent === void 0 || typeof config.onEvent === "function", "INVALID_CONFIG", "Fevex config \"onEvent\" must be a function");
106
+ assertConfiguration(config.runStore === void 0 || isRunStore(config.runStore), "INVALID_CONFIG", "Fevex config \"runStore\" must implement RunStore");
107
+ assertConfiguration(config.credentialStore === void 0 || typeof config.credentialStore?.resolve === "function", "INVALID_CONFIG", "Fevex config \"credentialStore\" must implement resolve");
108
+ assertConfiguration(config.policies === void 0 || Array.isArray(config.policies), "INVALID_CONFIG", "Fevex config \"policies\" must be an array");
109
+ if (config.observability !== void 0) {
110
+ assertConfiguration(isRecord(config.observability), "INVALID_CONFIG", "Fevex config \"observability\" must be an object");
111
+ 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");
112
+ assertConfiguration(config.observability.calculateCost === void 0 || typeof config.observability.calculateCost === "function", "INVALID_CONFIG", "Fevex config \"observability.calculateCost\" must be a function");
113
+ if (config.observability.content !== void 0) {
114
+ const content = config.observability.content;
115
+ assertConfiguration(isRecord(content) && Array.isArray(content.include) && content.include.every((kind) => [
116
+ "run.output",
117
+ "model.output",
118
+ "error.message"
119
+ ].includes(kind)) && (content.redact === void 0 || typeof content.redact === "function"), "INVALID_CONFIG", "Fevex config \"observability.content\" is invalid");
120
+ }
121
+ }
122
+ const models = /* @__PURE__ */ new Map();
117
123
  const agents = /* @__PURE__ */ new Map();
118
124
  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`);
125
+ for (const [name, model] of Object.entries(config.models)) {
126
+ assertConfiguration(name.trim(), "INVALID_MODEL", "Model name cannot be empty");
127
+ assertConfiguration(isModelGateway(model), "INVALID_MODEL", `Model "${name}" must implement stream`);
128
+ 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`);
129
+ models.set(name, model);
122
130
  }
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);
131
+ for (const [index, definition] of (config.tools ?? []).entries()) {
132
+ assertConfiguration(isRecord(definition), "INVALID_TOOL", `Tool at index ${index} must be an object`);
133
+ assertConfiguration(typeof definition.name === "string", "INVALID_TOOL", "Tool name must be a string");
134
+ const name = definition.name;
135
+ assertConfiguration(name.trim(), "INVALID_TOOL", "Tool name cannot be empty");
136
+ assertConfiguration(!tools.has(name), "DUPLICATE_TOOL", `Tool "${name}" is duplicated`);
137
+ assertConfiguration(definition.description === void 0 || typeof definition.description === "string", "INVALID_TOOL", `Tool "${name}" description must be a string`);
138
+ assertConfiguration(typeof definition.execute === "function", "INVALID_TOOL", `Tool "${name}" must implement execute`);
139
+ assertConfiguration(definition.risk === void 0 || [
140
+ "read",
141
+ "write",
142
+ "sensitive",
143
+ "destructive"
144
+ ].includes(definition.risk), "INVALID_TOOL", `Tool "${name}" risk is invalid`);
145
+ assertConfiguration(definition.approval === void 0 || definition.approval === "never" || definition.approval === "required", "INVALID_TOOL", `Tool "${name}" approval is invalid`);
146
+ assertConfiguration(definition.idempotency === void 0 || definition.idempotency === "none" || definition.idempotency === "keyed", "INVALID_TOOL", `Tool "${name}" idempotency is invalid`);
147
+ 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`);
148
+ 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`);
149
+ if (definition.inputSchema !== void 0) assertConfiguration(isStandardSchema(definition.inputSchema), "INVALID_TOOL", `Input schema for tool "${name}" must implement Standard Schema`);
150
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_TOOL", `Output schema for tool "${name}" must implement Standard Schema`);
151
+ tools.set(name, {
152
+ name,
153
+ ...definition.description === void 0 ? {} : { description: definition.description },
154
+ ...definition.inputSchema === void 0 ? {} : { inputSchema: definition.inputSchema },
155
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
156
+ ...definition.risk === void 0 ? {} : { risk: definition.risk },
157
+ ...definition.approval === void 0 ? {} : { approval: definition.approval },
158
+ ...definition.idempotency === void 0 ? {} : { idempotency: definition.idempotency },
159
+ ...definition.retry === void 0 ? {} : { retry: { ...definition.retry } },
160
+ ...definition.credentials === void 0 ? {} : { credentials: [...definition.credentials] },
161
+ execute: definition.execute
162
+ });
163
+ }
164
+ const policies = [];
165
+ for (const [index, policy] of (config.policies ?? []).entries()) {
166
+ assertConfiguration(isRecord(policy) && typeof policy.name === "string" && Boolean(policy.name.trim()) && typeof policy.authorize === "function", "INVALID_CONFIG", `Policy at index ${index} must have a name and authorize`);
167
+ policies.push({
168
+ name: policy.name,
169
+ authorize: policy.authorize
170
+ });
129
171
  }
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);
172
+ for (const [index, definition] of config.agents.entries()) {
173
+ assertConfiguration(isRecord(definition), "INVALID_AGENT", `Agent at index ${index} must be an object`);
174
+ assertConfiguration(typeof definition.name === "string", "INVALID_AGENT", "Agent name must be a string");
175
+ const name = definition.name;
176
+ assertConfiguration(name.trim(), "INVALID_AGENT", "Agent name cannot be empty");
177
+ assertConfiguration(typeof definition.instructions === "string", "INVALID_AGENT", `Agent "${name}" instructions must be a string`);
178
+ assertConfiguration(definition.instructions.trim(), "INVALID_AGENT", `Agent "${name}" instructions cannot be empty`);
179
+ assertConfiguration(!agents.has(name), "DUPLICATE_AGENT", `Agent "${name}" is duplicated`);
180
+ const model = definition.model;
181
+ if (model !== void 0) if (typeof model === "string") assertConfiguration(model.trim(), "INVALID_MODEL", `Model name required by agent "${name}" cannot be empty`);
182
+ else assertConfiguration(isModelGateway(model), "INVALID_MODEL", `Model for agent "${name}" must implement stream`);
183
+ let agentTools;
184
+ if (definition.tools !== void 0) {
185
+ assertConfiguration(Array.isArray(definition.tools), "INVALID_AGENT", `Agent "${name}" tools must be an array`);
186
+ agentTools = [];
187
+ const seenTools = /* @__PURE__ */ new Set();
188
+ for (const [toolIndex, toolName] of definition.tools.entries()) {
189
+ assertConfiguration(typeof toolName === "string" && Boolean(toolName.trim()), "INVALID_AGENT", `Tool name at index ${toolIndex} for agent "${name}" must be a non-empty string`);
190
+ assertConfiguration(!seenTools.has(toolName), "DUPLICATE_TOOL", `Tool "${toolName}" is duplicated in agent "${name}"`);
191
+ seenTools.add(toolName);
192
+ agentTools.push(toolName);
193
+ }
194
+ }
195
+ assertConfiguration(definition.reasoning === void 0 || typeof definition.reasoning === "string" && REASONING_EFFORTS.has(definition.reasoning), "INVALID_AGENT", `Agent "${name}" reasoning is invalid`);
196
+ assertConfiguration(definition.modelOptions === void 0 || isRecord(definition.modelOptions), "INVALID_AGENT", `Agent "${name}" modelOptions must be an object`);
197
+ assertConfiguration(definition.limits === void 0 || isRecord(definition.limits), "INVALID_AGENT", `Agent "${name}" limits must be an object`);
198
+ if (definition.outputSchema !== void 0) assertConfiguration(isStandardSchema(definition.outputSchema), "INVALID_AGENT", `Output schema for agent "${name}" must implement Standard Schema`);
199
+ const rawLimits = definition.limits;
200
+ assertLimit(rawLimits?.maxSteps, "maxSteps", 1, name);
201
+ assertLimit(rawLimits?.maxToolCalls, "maxToolCalls", 0, name);
202
+ assertLimit(rawLimits?.maxInputTokens, "maxInputTokens", 1, name, true);
203
+ assertLimit(rawLimits?.maxOutputTokens, "maxOutputTokens", 1, name, true);
204
+ agents.set(name, {
205
+ name,
206
+ instructions: definition.instructions,
207
+ ...model === void 0 ? {} : { model },
208
+ ...agentTools === void 0 ? {} : { tools: agentTools },
209
+ ...definition.reasoning === void 0 ? {} : { reasoning: definition.reasoning },
210
+ ...definition.modelOptions === void 0 ? {} : { modelOptions: { ...definition.modelOptions } },
211
+ ...definition.outputSchema === void 0 ? {} : { outputSchema: definition.outputSchema },
212
+ ...rawLimits === void 0 ? {} : { limits: {
213
+ ...rawLimits.maxSteps === void 0 ? {} : { maxSteps: rawLimits.maxSteps },
214
+ ...rawLimits.maxToolCalls === void 0 ? {} : { maxToolCalls: rawLimits.maxToolCalls },
215
+ ...rawLimits.maxInputTokens === void 0 ? {} : { maxInputTokens: rawLimits.maxInputTokens },
216
+ ...rawLimits.maxOutputTokens === void 0 ? {} : { maxOutputTokens: rawLimits.maxOutputTokens }
217
+ } }
218
+ });
136
219
  }
137
220
  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`);
221
+ if (typeof agent.model === "string" && !models.has(agent.model)) throw configurationError("MISSING_MODEL", `Model "${agent.model}" required by agent "${agent.name}" is not registered`);
222
+ if (agent.model === void 0 && !models.has("default")) throw configurationError("MISSING_MODEL", `Default model "default" required by agent "${agent.name}" is not registered`);
223
+ for (const toolName of agent.tools ?? []) if (!tools.has(toolName)) throw configurationError("MISSING_TOOL", `Tool "${toolName}" required by agent "${agent.name}" is not registered`);
224
+ }
225
+ return {
226
+ models,
227
+ agents,
228
+ tools,
229
+ runStore: config.runStore ?? new InMemoryRunStore(),
230
+ credentialStore: config.credentialStore,
231
+ policies,
232
+ onEvent: config.onEvent,
233
+ observability: config.observability === void 0 ? void 0 : {
234
+ exporters: [...config.observability.exporters],
235
+ ...config.observability.calculateCost ? { calculateCost: config.observability.calculateCost } : {},
236
+ ...config.observability.content ? { content: {
237
+ include: [...config.observability.content.include],
238
+ ...config.observability.content.redact ? { redact: config.observability.content.redact } : {}
239
+ } } : {}
240
+ }
241
+ };
242
+ }
243
+ //#endregion
244
+ //#region src/run-error.ts
245
+ var FevexRunError = class extends Error {
246
+ code;
247
+ runId;
248
+ name = "FevexRunError";
249
+ constructor(code, message, runId, options) {
250
+ super(message, options);
251
+ this.code = code;
252
+ this.runId = runId;
142
253
  }
143
- return { async runAgent(name, request) {
254
+ };
255
+ var RunPausedError = class extends FevexRunError {
256
+ runId;
257
+ pause;
258
+ name = "RunPausedError";
259
+ constructor(runId, pause) {
260
+ super("RUN_PAUSED", `Run "${runId}" is paused`, runId);
261
+ this.runId = runId;
262
+ this.pause = pause;
263
+ }
264
+ };
265
+ //#endregion
266
+ //#region src/internal/observability.ts
267
+ const TERMINAL_TYPES = /* @__PURE__ */ new Set([
268
+ "run.completed",
269
+ "run.failed",
270
+ "run.cancelled"
271
+ ]);
272
+ const SENSITIVE_KEY = /api[-_]?key|authorization|cookie|credential|password|secret|token/i;
273
+ function durationMs(startedAt, endedAt) {
274
+ return Math.max(0, Date.parse(endedAt) - Date.parse(startedAt));
275
+ }
276
+ function traceStatus(event) {
277
+ if (event.type === "run.completed") return "completed";
278
+ if (event.type === "run.cancelled") return "cancelled";
279
+ return "failed";
280
+ }
281
+ function redactKeys(value) {
282
+ if (Array.isArray(value)) return value.map(redactKeys);
283
+ if (value === null || typeof value !== "object") return value;
284
+ const result = {};
285
+ for (const [key, item] of Object.entries(value)) result[key] = SENSITIVE_KEY.test(key) ? "[REDACTED]" : redactKeys(item);
286
+ return result;
287
+ }
288
+ function content(options, kind, value) {
289
+ if (!options.content?.include.includes(kind)) return void 0;
290
+ let parsed = value;
291
+ if (typeof value === "string") try {
292
+ parsed = JSON.parse(value);
293
+ } catch {}
294
+ const redacted = redactKeys(parsed);
295
+ const safe = options.content.redact ? options.content.redact({
296
+ kind,
297
+ value: redacted
298
+ }) : redacted;
299
+ return safe === void 0 ? void 0 : {
300
+ kind,
301
+ value: toJsonValue(safe, `TraceRedactor output for "${kind}" must be JSON-serializable`)
302
+ };
303
+ }
304
+ function usageFrom(value) {
305
+ if (!value) return void 0;
306
+ const usage = {};
307
+ if (typeof value.inputTokens === "number") usage.inputTokens = value.inputTokens;
308
+ if (typeof value.outputTokens === "number") usage.outputTokens = value.outputTokens;
309
+ if (typeof value.totalTokens === "number") usage.totalTokens = value.totalTokens;
310
+ return Object.keys(usage).length ? usage : void 0;
311
+ }
312
+ function subtractUsage(current, previous) {
313
+ if (!current) return void 0;
314
+ const usage = {};
315
+ for (const key of [
316
+ "inputTokens",
317
+ "outputTokens",
318
+ "totalTokens"
319
+ ]) if (current[key] !== void 0) usage[key] = Math.max(0, current[key] - (previous[key] ?? 0));
320
+ return Object.keys(usage).length ? usage : void 0;
321
+ }
322
+ function costFor(options, modelRegistryName, model, usage) {
323
+ if (!options.calculateCost || !usage) return void 0;
324
+ const cost = options.calculateCost({
325
+ modelRegistryName,
326
+ model,
327
+ usage
328
+ });
329
+ if (!cost) return void 0;
330
+ if (!Number.isFinite(cost.amount) || cost.amount < 0 || !cost.currency.trim()) throw new TypeError("CostCalculator must return a non-negative amount and currency");
331
+ return {
332
+ amount: cost.amount,
333
+ currency: cost.currency
334
+ };
335
+ }
336
+ function annotation(event) {
337
+ switch (event.type) {
338
+ case "run.paused": return {
339
+ name: event.type,
340
+ timestamp: event.timestamp,
341
+ attributes: {
342
+ reason: event.payload.reason,
343
+ toolCallId: event.payload.toolCallId
344
+ }
345
+ };
346
+ case "run.resumed": return {
347
+ name: event.type,
348
+ timestamp: event.timestamp
349
+ };
350
+ case "approval.requested": return {
351
+ name: event.type,
352
+ timestamp: event.timestamp,
353
+ attributes: {
354
+ approvalId: event.payload.approvalId,
355
+ toolCallId: event.payload.toolCallId,
356
+ toolName: event.payload.toolName
357
+ }
358
+ };
359
+ case "approval.resolved": return {
360
+ name: event.type,
361
+ timestamp: event.timestamp,
362
+ attributes: {
363
+ approvalId: event.payload.approvalId,
364
+ toolCallId: event.payload.toolCallId,
365
+ decision: event.payload.decision
366
+ }
367
+ };
368
+ case "tool.retrying": return {
369
+ name: event.type,
370
+ timestamp: event.timestamp,
371
+ attributes: {
372
+ toolCallId: event.payload.toolCallId,
373
+ toolName: event.payload.toolName,
374
+ attempt: event.payload.attempt,
375
+ delayMs: event.payload.delayMs
376
+ }
377
+ };
378
+ case "tool.execution_unknown": return {
379
+ name: event.type,
380
+ timestamp: event.timestamp,
381
+ attributes: {
382
+ toolCallId: event.payload.toolCallId,
383
+ toolName: event.payload.toolName
384
+ }
385
+ };
386
+ default: return;
387
+ }
388
+ }
389
+ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
390
+ const events = [...rawEvents].sort((left, right) => left.sequence - right.sequence);
391
+ const terminal = [...events].reverse().find((event) => TERMINAL_TYPES.has(event.type));
392
+ if (!terminal) throw new TypeError(`Run "${run.id}" has no terminal event`);
393
+ const started = events.find((event) => event.type === "run.started") ?? events[0];
394
+ if (!started) throw new TypeError(`Run "${run.id}" has no events`);
395
+ const status = traceStatus(terminal);
396
+ const annotations = [];
397
+ const spans = [];
398
+ const openModels = /* @__PURE__ */ new Map();
399
+ const openTools = /* @__PURE__ */ new Map();
400
+ const previousUsage = {};
401
+ const closeModel = (step, endedAt, spanStatus) => {
402
+ const open = openModels.get(step);
403
+ if (!open) return;
404
+ openModels.delete(step);
405
+ const completed = events.find((event) => event.type === "model.completed" && event.payload.step === step && event.sequence >= open.event.sequence);
406
+ const cumulative = completed?.type === "model.completed" ? usageFrom(completed.payload.usage) : void 0;
407
+ const usage = subtractUsage(cumulative, previousUsage);
408
+ if (cumulative) Object.assign(previousUsage, cumulative);
409
+ const spanContent = open.output.length ? content(options, "model.output", open.output.join("")) : void 0;
410
+ const span = {
411
+ id: open.event.id,
412
+ parentId: run.id,
413
+ kind: "model",
414
+ name: `model step ${step}`,
415
+ startedAt: open.event.timestamp,
416
+ endedAt,
417
+ durationMs: durationMs(open.event.timestamp, endedAt),
418
+ status: spanStatus,
419
+ attributes: {
420
+ step,
421
+ modelRegistryName
422
+ },
423
+ annotations: open.annotations,
424
+ ...usage ? { usage } : {},
425
+ ...spanContent ? { content: [spanContent] } : {}
426
+ };
427
+ const spanCost = costFor(options, modelRegistryName, model, usage);
428
+ if (spanCost) span.cost = spanCost;
429
+ spans.push(span);
430
+ };
431
+ const findOpenTool = (toolCallId) => {
432
+ const entries = [...openTools.entries()];
433
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
434
+ const entry = entries[index];
435
+ const event = entry[1].event;
436
+ if (event.type === "tool.started" && event.payload.toolCallId === toolCallId) return entry;
437
+ }
438
+ };
439
+ const closeTool = (toolCallId, endedAt, spanStatus, error) => {
440
+ const entry = findOpenTool(toolCallId);
441
+ if (!entry) return;
442
+ const [key, open] = entry;
443
+ openTools.delete(key);
444
+ const event = open.event;
445
+ if (event.type !== "tool.started") return;
446
+ const errorContent = error ? content(options, "error.message", error) : void 0;
447
+ spans.push({
448
+ id: event.id,
449
+ parentId: run.id,
450
+ kind: "tool",
451
+ name: event.payload.toolName,
452
+ startedAt: event.timestamp,
453
+ endedAt,
454
+ durationMs: durationMs(event.timestamp, endedAt),
455
+ status: spanStatus,
456
+ attributes: {
457
+ step: event.payload.step,
458
+ toolCallId,
459
+ toolName: event.payload.toolName,
460
+ attempt: event.payload.attempt ?? 1
461
+ },
462
+ annotations: open.annotations,
463
+ ...errorContent ? { content: [errorContent] } : {}
464
+ });
465
+ };
466
+ for (const event of events) {
467
+ const item = annotation(event);
468
+ if (item) annotations.push(item);
469
+ switch (event.type) {
470
+ case "model.started":
471
+ openModels.set(event.payload.step, {
472
+ event,
473
+ annotations: [],
474
+ output: []
475
+ });
476
+ break;
477
+ case "model.output.delta":
478
+ openModels.get(event.payload.step)?.output.push(event.payload.delta);
479
+ break;
480
+ case "model.completed":
481
+ closeModel(event.payload.step, event.timestamp, "completed");
482
+ break;
483
+ case "tool.started": {
484
+ const attempt = event.payload.attempt ?? 1;
485
+ openTools.set(`${event.payload.toolCallId}:${attempt}`, {
486
+ event,
487
+ annotations: [],
488
+ output: []
489
+ });
490
+ break;
491
+ }
492
+ case "tool.retrying": {
493
+ const open = findOpenTool(event.payload.toolCallId)?.[1];
494
+ const retry = annotation(event);
495
+ if (open && retry) open.annotations.push(retry);
496
+ closeTool(event.payload.toolCallId, event.timestamp, "failed", event.payload.error);
497
+ break;
498
+ }
499
+ case "tool.completed":
500
+ closeTool(event.payload.toolCallId, event.timestamp, "completed");
501
+ break;
502
+ case "tool.failed":
503
+ closeTool(event.payload.toolCallId, event.timestamp, "failed", event.payload.error);
504
+ break;
505
+ case "tool.execution_unknown":
506
+ closeTool(event.payload.toolCallId, event.timestamp, "failed");
507
+ break;
508
+ }
509
+ }
510
+ for (const step of [...openModels.keys()]) closeModel(step, terminal.timestamp, status);
511
+ for (const open of [...openTools.values()]) if (open.event.type === "tool.started") closeTool(open.event.payload.toolCallId, terminal.timestamp, status);
512
+ const finalUsage = run.usage ?? previousUsage;
513
+ const totalCost = costFor(options, modelRegistryName, model, finalUsage);
514
+ const traceContent = [];
515
+ if (terminal.type === "run.completed") {
516
+ const output = content(options, "run.output", terminal.payload.output);
517
+ if (output) traceContent.push(output);
518
+ } else if (terminal.type === "run.failed") {
519
+ const error = content(options, "error.message", terminal.payload.error);
520
+ if (error) traceContent.push(error);
521
+ }
522
+ return {
523
+ schemaVersion: "1",
524
+ traceId: run.id,
525
+ runId: run.id,
526
+ sessionId: run.sessionId,
527
+ agentName: run.agentName,
528
+ modelRegistryName,
529
+ ...model ? { model: { ...model } } : {},
530
+ startedAt: started.timestamp,
531
+ endedAt: terminal.timestamp,
532
+ durationMs: durationMs(started.timestamp, terminal.timestamp),
533
+ status,
534
+ attributes: {
535
+ eventCount: events.length,
536
+ modelCallCount: spans.filter(({ kind }) => kind === "model").length,
537
+ toolCallCount: spans.filter(({ kind }) => kind === "tool").length
538
+ },
539
+ annotations,
540
+ spans,
541
+ ...Object.keys(finalUsage).length ? { usage: { ...finalUsage } } : {},
542
+ ...totalCost ? { cost: totalCost } : {},
543
+ ...traceContent.length ? { content: traceContent } : {}
544
+ };
545
+ }
546
+ //#endregion
547
+ //#region src/internal/runtime.ts
548
+ const LEASE_MS = 3e4;
549
+ const LEASE_RENEW_MS = 1e4;
550
+ function redact(message, secrets) {
551
+ return secrets.reduce((safe, secret) => secret ? safe.split(secret).join("[REDACTED]") : safe, message);
552
+ }
553
+ function containsSecret(value, secrets) {
554
+ if (typeof value === "string") return secrets.some((secret) => secret && value.includes(secret));
555
+ if (Array.isArray(value)) return value.some((item) => containsSecret(item, secrets));
556
+ if (value && typeof value === "object") return Object.values(value).some((item) => containsSecret(item, secrets));
557
+ return false;
558
+ }
559
+ function canonicalize(value) {
560
+ if (Array.isArray(value)) return value.map(canonicalize);
561
+ if (typeof value !== "object" || value === null) return value;
562
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalize(item)]));
563
+ }
564
+ async function definitionHash(name, modelName, agent, tools) {
565
+ const portable = JSON.stringify(canonicalize({
566
+ name,
567
+ modelName,
568
+ instructions: agent.instructions,
569
+ tools: (agent.tools ?? []).map((toolName) => {
570
+ const tool = tools.get(toolName);
571
+ return {
572
+ name: tool.name,
573
+ description: tool.description,
574
+ inputSchema: tool.inputSchema === void 0 ? void 0 : toTransportableSchema(tool.inputSchema, "input", `Input schema for tool "${tool.name}" is not transportable`),
575
+ outputSchema: tool.outputSchema === void 0 ? void 0 : toTransportableSchema(tool.outputSchema, "output", `Output schema for tool "${tool.name}" is not transportable`),
576
+ risk: tool.risk ?? "read",
577
+ approval: tool.approval ?? "never",
578
+ idempotency: tool.idempotency ?? "none",
579
+ retry: tool.retry,
580
+ credentials: tool.credentials ?? []
581
+ };
582
+ }),
583
+ reasoning: agent.reasoning,
584
+ modelOptions: agent.modelOptions,
585
+ limits: agent.limits,
586
+ outputSchema: agent.outputSchema === void 0 ? void 0 : toTransportableSchema(agent.outputSchema, "output", `Output schema for agent "${name}" is not transportable`)
587
+ }));
588
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(portable));
589
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
590
+ }
591
+ function createRuntime({ models, agents, tools, runStore, credentialStore, policies, onEvent, observability }) {
592
+ const activeRuns = /* @__PURE__ */ new Map();
593
+ const activeSessions = /* @__PURE__ */ new Set();
594
+ const pendingExports = /* @__PURE__ */ new Set();
595
+ const exportFailures = [];
596
+ const exportedRuns = /* @__PURE__ */ new Set();
597
+ const runtimeOwner = crypto.randomUUID();
598
+ const scheduleTraceExport = (event) => {
599
+ if (!observability || ![
600
+ "run.completed",
601
+ "run.failed",
602
+ "run.cancelled"
603
+ ].includes(event.type) || exportedRuns.has(event.runId)) return;
604
+ exportedRuns.add(event.runId);
605
+ let task;
606
+ task = (async () => {
607
+ const run = await runStore.getRun(event.runId);
608
+ if (!run) throw new Error(`Run "${event.runId}" does not exist`);
609
+ const agent = agents.get(run.agentName);
610
+ if (!agent) throw new Error(`Agent "${run.agentName}" is not registered`);
611
+ const modelRegistryName = typeof agent.model === "string" ? agent.model : "default";
612
+ const gateway = typeof agent.model === "string" ? models.get(agent.model) : agent.model ?? models.get("default");
613
+ const trace = buildRunTrace(run, await runStore.listEvents(run.id), modelRegistryName, gateway?.metadata, observability);
614
+ const failures = (await Promise.allSettled(observability.exporters.map((exporter) => Promise.resolve().then(() => exporter.export(trace))))).filter((result) => result.status === "rejected").map(({ reason }) => reason);
615
+ if (failures.length) throw new AggregateError(failures, "Trace export failed");
616
+ })().catch((error) => {
617
+ exportFailures.push(error);
618
+ }).finally(() => pendingExports.delete(task));
619
+ pendingExports.add(task);
620
+ };
621
+ const notifyObserver = (event) => {
622
+ scheduleTraceExport(event);
623
+ if (!onEvent) return;
624
+ try {
625
+ Promise.resolve(onEvent(event)).catch(() => {});
626
+ } catch {}
627
+ };
628
+ const createEvent = (state, type, payload) => ({
629
+ id: crypto.randomUUID(),
630
+ sequence: state.eventSequence += 1,
631
+ type,
632
+ runId: state.run.id,
633
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
634
+ ...payload === void 0 ? {} : { payload }
635
+ });
636
+ const emitEvent = async (state, type, payload) => {
637
+ const event = createEvent(state, type, payload);
638
+ await runStore.appendEvent(event);
639
+ notifyObserver(event);
640
+ return event;
641
+ };
642
+ const releaseExecution = async (state) => {
643
+ activeRuns.delete(state.run.id);
644
+ activeSessions.delete(state.session.id);
645
+ if (state.leaseTimer) clearInterval(state.leaseTimer);
646
+ if (state.leaseOwner && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.leaseOwner).catch(() => {});
647
+ };
648
+ const commit = async (state, options = {}) => {
649
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "This run requires a DurableRunStore", state.run.id);
650
+ state.run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
651
+ if (!await runStore.commitExecution({
652
+ expectedRevision: state.run.revision,
653
+ run: state.run,
654
+ ...options
655
+ })) throw new FevexRunError("RUN_CONFLICT", `Run "${state.run.id}" was modified`, state.run.id);
656
+ for (const event of options.events ?? []) notifyObserver(event);
657
+ };
658
+ const startLease = (state, store) => {
659
+ state.leaseTimer = setInterval(() => {
660
+ if (!state.leaseOwner || state.request.signal.aborted) return;
661
+ const expiresAt = new Date(Date.now() + LEASE_MS).toISOString();
662
+ store.renewLease({
663
+ runId: state.run.id,
664
+ ownerId: state.leaseOwner,
665
+ expiresAt
666
+ }).then((renewed) => {
667
+ if (!renewed) state.controller.abort(/* @__PURE__ */ new Error("Run lease was lost"));
668
+ });
669
+ }, LEASE_RENEW_MS);
670
+ };
671
+ const cancelExecution = async (state, reason = cancellationReason(state.request.signal)) => {
672
+ if (state.run.status === "completed" || state.run.status === "failed" || state.run.status === "cancelled") return;
673
+ Object.assign(state.run, {
674
+ status: "cancelled",
675
+ pause: void 0,
676
+ error: reason,
677
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
678
+ });
679
+ const event = createEvent(state, "run.cancelled", { reason });
680
+ if (isDurableRunStore(runStore)) await commit(state, {
681
+ checkpoint: null,
682
+ events: [event]
683
+ });
684
+ else {
685
+ await runStore.saveRun(state.run);
686
+ await runStore.appendEvent(event);
687
+ notifyObserver(event);
688
+ }
689
+ await releaseExecution(state);
690
+ return event;
691
+ };
692
+ const prepareExecution = async (name, request) => {
693
+ if (!agents.has(name)) throw new Error(`Agent "${name}" is not registered`);
694
+ const now = (/* @__PURE__ */ new Date()).toISOString();
695
+ let session;
696
+ if (request.sessionId === void 0) {
697
+ session = {
698
+ id: crypto.randomUUID(),
699
+ history: [],
700
+ createdAt: now,
701
+ updatedAt: now
702
+ };
703
+ await runStore.saveSession(session);
704
+ } else {
705
+ const stored = await runStore.getSession(request.sessionId);
706
+ if (!stored) throw new Error(`Session "${request.sessionId}" does not exist`);
707
+ session = stored;
708
+ }
709
+ if (activeSessions.has(session.id)) throw new Error(`Session "${session.id}" already has an active run`);
710
+ activeSessions.add(session.id);
711
+ const controller = new AbortController();
712
+ const signal = request.signal ? AbortSignal.any([controller.signal, request.signal]) : controller.signal;
713
+ const run = {
714
+ id: crypto.randomUUID(),
715
+ sessionId: session.id,
716
+ agentName: name,
717
+ status: "running",
718
+ revision: 0,
719
+ createdAt: now,
720
+ updatedAt: now
721
+ };
722
+ const state = {
723
+ run,
724
+ session,
725
+ request: {
726
+ ...request,
727
+ sessionId: session.id,
728
+ signal
729
+ },
730
+ controller,
731
+ eventSequence: 0,
732
+ advancing: false
733
+ };
734
+ try {
735
+ await runStore.saveRun(run);
736
+ } catch (error) {
737
+ activeSessions.delete(session.id);
738
+ throw error;
739
+ }
740
+ activeRuns.set(run.id, state);
741
+ return state;
742
+ };
743
+ const authorize = async (state, tool, input, action, context = state.request.context) => {
744
+ let approval = false;
745
+ for (const policy of policies) {
746
+ const decision = await policy.authorize({
747
+ runId: state.run.id,
748
+ agentName: state.run.agentName,
749
+ toolName: tool.name,
750
+ risk: tool.risk ?? "read",
751
+ input,
752
+ action,
753
+ context
754
+ });
755
+ if (decision === "deny") throw new FevexRunError("POLICY_DENIED", `Policy "${policy.name}" denied tool "${tool.name}"`, state.run.id);
756
+ if (decision === "require_approval") approval = true;
757
+ }
758
+ return approval ? "require_approval" : "allow";
759
+ };
760
+ const durableCheckpoint = async (state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex) => {
761
+ const agent = agents.get(state.run.agentName);
762
+ if (agent.model !== void 0 && typeof agent.model !== "string") throw new FevexRunError("RUN_NOT_RESUMABLE", "Durable continuation requires a named model", state.run.id);
763
+ if (state.request.outputSchema !== void 0) throw new FevexRunError("RUN_NOT_RESUMABLE", "Durable runs must declare outputSchema on the agent", state.run.id);
764
+ let serializedState;
765
+ if (providerState !== void 0) {
766
+ if (!model.stateCodec) throw new FevexRunError("RUN_NOT_RESUMABLE", "ModelGateway must provide stateCodec for durable continuation", state.run.id);
767
+ serializedState = toJsonValue(model.stateCodec.serialize(providerState), "Model provider state must be JSON-serializable");
768
+ }
769
+ return {
770
+ runId: state.run.id,
771
+ definitionHash: await definitionHash(state.run.agentName, modelName, agent, tools),
772
+ messages: structuredClone(messages),
773
+ inputContent,
774
+ context: state.request.context,
775
+ usage,
776
+ providerState: serializedState,
777
+ step,
778
+ toolCallCount,
779
+ seenToolCallIds: [...seenToolCallIds],
780
+ pendingTools: structuredClone(pendingTools),
781
+ pendingIndex
782
+ };
783
+ };
784
+ async function* executeAgent(name, state) {
785
+ const { request } = state;
144
786
  const agent = agents.get(name);
145
- if (!agent) throw new Error(`Agent "${name}" is not registered`);
787
+ const modelName = typeof agent.model === "string" ? agent.model : "default";
146
788
  const model = typeof agent.model === "string" ? models.get(agent.model) : agent.model ?? models.get("default");
147
- const runId = globalThis.crypto.randomUUID();
148
789
  const events = [];
149
- const createEvent = (type, payload) => ({
150
- type,
151
- runId,
152
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
153
- ...payload === void 0 ? {} : { payload }
154
- });
155
- const emit = (type, payload) => {
156
- const event = createEvent(type, payload);
157
- config.onEvent?.(event);
158
- events.push(event);
159
- };
160
- const emitFailure = (type, payload) => {
161
- const event = createEvent(type, payload);
162
- try {
163
- config.onEvent?.(event);
164
- } catch {}
790
+ const emit = async (type, payload) => {
791
+ const event = await emitEvent(state, type, payload);
165
792
  events.push(event);
793
+ return event;
166
794
  };
795
+ let inputContent;
796
+ let messages;
797
+ let usage;
798
+ let providerState;
799
+ let step;
800
+ let toolCallCount;
801
+ let seenToolCallIds;
802
+ let pendingTools;
803
+ let pendingIndex;
167
804
  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;
805
+ if (state.checkpoint) {
806
+ const checkpoint = state.checkpoint;
807
+ const hash = await definitionHash(name, modelName, agent, tools);
808
+ if (checkpoint.definitionHash !== hash) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for agent "${name}" changed`, state.run.id);
809
+ inputContent = checkpoint.inputContent;
810
+ messages = checkpoint.messages;
811
+ usage = checkpoint.usage;
812
+ providerState = checkpoint.providerState === void 0 ? void 0 : model.stateCodec?.restore(checkpoint.providerState);
813
+ step = checkpoint.step;
814
+ toolCallCount = checkpoint.toolCallCount;
815
+ seenToolCallIds = new Set(checkpoint.seenToolCallIds);
816
+ pendingTools = checkpoint.pendingTools;
817
+ pendingIndex = checkpoint.pendingIndex;
818
+ } else {
819
+ yield await emit("run.started", void 0);
820
+ request.signal.throwIfAborted();
821
+ if (request.outputSchema !== void 0) assertStandardSchema(request.outputSchema, `Output schema for request to agent "${name}" must implement Standard Schema`);
822
+ inputContent = serializeValue(request.input, "Run input must be a string or JSON-serializable value");
823
+ messages = [
824
+ {
825
+ role: "system",
826
+ content: agent.instructions
827
+ },
828
+ ...state.session.history,
829
+ {
830
+ role: "user",
831
+ content: inputContent
832
+ }
833
+ ];
834
+ step = 1;
835
+ toolCallCount = 0;
836
+ seenToolCallIds = /* @__PURE__ */ new Set();
837
+ pendingTools = [];
838
+ pendingIndex = 0;
839
+ }
840
+ const activeOutputSchema = request.outputSchema ?? agent.outputSchema;
179
841
  const outputSchema = activeOutputSchema === void 0 ? void 0 : toTransportableSchema(activeOutputSchema, "output", `Output schema for agent "${name}" is not transportable`);
180
842
  const agentTools = (agent.tools ?? []).map((toolName) => {
181
843
  const tool = tools.get(toolName);
182
844
  return toToolSpec(tool, tool.inputSchema === void 0 ? void 0 : toTransportableSchema(tool.inputSchema, "input", `Input schema for tool "${tool.name}" is not transportable`));
183
845
  });
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
191
- });
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
- };
846
+ const maxSteps = agent.limits?.maxSteps ?? 8;
847
+ const maxToolCalls = agent.limits?.maxToolCalls ?? 16;
848
+ while (step <= maxSteps) {
849
+ request.signal.throwIfAborted();
850
+ if (pendingTools.length === 0) {
851
+ const hasToolBudget = step < maxSteps && toolCallCount < maxToolCalls;
852
+ yield await emit("model.started", { step });
853
+ const modelStream = readModelStream(model, {
854
+ messages: [...messages],
855
+ tools: hasToolBudget && agentTools.length ? agentTools : void 0,
856
+ reasoning: agent.reasoning,
857
+ modelOptions: agent.modelOptions,
858
+ outputSchema,
859
+ ...remainingOutputTokens(agent.limits?.maxOutputTokens, usage) === void 0 ? {} : { maxOutputTokens: remainingOutputTokens(agent.limits?.maxOutputTokens, usage) },
860
+ ...providerState === void 0 ? {} : { providerState },
861
+ signal: request.signal
862
+ });
863
+ let modelResult = await modelStream.next();
864
+ while (!modelResult.done) {
865
+ yield await emit("model.output.delta", {
866
+ step,
867
+ delta: modelResult.value
868
+ });
869
+ modelResult = await modelStream.next();
870
+ }
871
+ const result = modelResult.value;
872
+ providerState = result.providerState;
873
+ usage = addUsage(usage, result.usage);
874
+ const usagePayload = eventUsage(usage);
875
+ yield await emit("model.completed", {
876
+ step,
877
+ ...usagePayload === void 0 ? {} : { usage: usagePayload }
878
+ });
879
+ assertTokenBudget(name, "maxInputTokens", "inputTokens", agent.limits?.maxInputTokens, result.usage, usage);
880
+ assertTokenBudget(name, "maxOutputTokens", "outputTokens", agent.limits?.maxOutputTokens, result.usage, usage);
881
+ if (!result.toolCalls?.length) {
882
+ if (result.output === void 0) throw new Error(`Model for agent "${name}" returned no output`);
883
+ 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`);
884
+ state.session.history.push({
885
+ role: "user",
886
+ content: inputContent
887
+ }, {
888
+ role: "assistant",
889
+ content: serializeValue(output, "Output must be serializable")
890
+ });
891
+ state.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
892
+ Object.assign(state.run, {
893
+ status: "completed",
894
+ pause: void 0,
895
+ output,
896
+ ...usage === void 0 ? {} : { usage },
897
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
898
+ });
899
+ const payload = { output };
900
+ const finalUsage = eventUsage(usage);
901
+ if (finalUsage) payload.usage = finalUsage;
902
+ const completed = createEvent(state, "run.completed", payload);
903
+ if (isDurableRunStore(runStore)) await commit(state, {
904
+ checkpoint: null,
905
+ session: state.session,
906
+ events: [completed]
907
+ });
908
+ else {
909
+ await runStore.saveSession(state.session);
910
+ await runStore.saveRun(state.run);
911
+ await runStore.appendEvent(completed);
912
+ notifyObserver(completed);
913
+ }
914
+ events.push(completed);
915
+ await releaseExecution(state);
916
+ yield completed;
917
+ return {
918
+ runId: state.run.id,
919
+ sessionId: state.session.id,
920
+ output,
921
+ events,
922
+ usage
923
+ };
924
+ }
925
+ if (!hasToolBudget) throw new Error(step >= maxSteps ? `Agent "${name}" reached maxSteps limit of ${maxSteps}` : `Agent "${name}" reached maxToolCalls limit of ${maxToolCalls}`);
926
+ assertContinuationBudget(name, "maxInputTokens", "inputTokens", agent.limits?.maxInputTokens, usage);
927
+ assertContinuationBudget(name, "maxOutputTokens", "outputTokens", agent.limits?.maxOutputTokens, usage);
928
+ if (result.toolCalls.length > maxToolCalls - toolCallCount) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
929
+ const batch = /* @__PURE__ */ new Set();
930
+ pendingTools = [];
931
+ for (const call of result.toolCalls) {
932
+ if (!call.id?.trim()) throw new Error("Tool call id cannot be empty");
933
+ if (!call.name?.trim()) throw new Error("Tool call name cannot be empty");
934
+ if (seenToolCallIds.has(call.id) || batch.has(call.id)) throw new Error(`Tool call id "${call.id}" is duplicated in run "${state.run.id}"`);
935
+ batch.add(call.id);
936
+ const tool = agent.tools?.includes(call.name) ? tools.get(call.name) : void 0;
937
+ if (!tool) throw new Error(`Tool "${call.name}" is not available to agent "${name}"`);
938
+ const rawInput = toJsonValue(call.input, `Input for tool "${call.name}" must be JSON-serializable`);
939
+ let validated;
940
+ try {
941
+ validated = tool.inputSchema === void 0 ? rawInput : await validateSchema(tool.inputSchema, rawInput, `Input for tool "${call.name}" does not match inputSchema`);
942
+ } catch (error) {
943
+ yield await emit("tool.failed", {
944
+ step,
945
+ toolCallId: call.id,
946
+ toolName: call.name,
947
+ error: toErrorMessage(error)
948
+ });
949
+ throw error;
950
+ }
951
+ pendingTools.push({
952
+ call: {
953
+ ...call,
954
+ input: rawInput
955
+ },
956
+ input: toJsonValue(validated, `Input for tool "${call.name}" must be serializable`),
957
+ idempotencyKey: crypto.randomUUID(),
958
+ attempt: 0
959
+ });
960
+ }
961
+ for (const id of batch) seenToolCallIds.add(id);
962
+ messages.push({
963
+ role: "assistant",
964
+ content: result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be serializable`),
965
+ toolCalls: pendingTools.map(({ call }) => call)
966
+ });
967
+ pendingIndex = 0;
968
+ }
969
+ while (pendingIndex < pendingTools.length) {
970
+ request.signal.throwIfAborted();
971
+ const pending = pendingTools[pendingIndex];
972
+ const tool = tools.get(pending.call.name);
973
+ const durable = isDurableRunStore(runStore) ? runStore : void 0;
974
+ if (!durable && (tool.approval === "required" || tool.idempotency === "keyed")) throw new FevexRunError("DURABLE_STORE_REQUIRED", `Tool "${tool.name}" requires a DurableRunStore`, state.run.id);
975
+ const policy = await authorize(state, tool, pending.input, "tool.execute");
976
+ if ((tool.approval === "required" || policy === "require_approval") && state.approvedToolCallId !== pending.call.id) {
977
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
978
+ const approval = {
979
+ id: crypto.randomUUID(),
980
+ toolCallId: pending.call.id,
981
+ toolName: pending.call.name,
982
+ input: pending.input,
983
+ risk: tool.risk ?? "read",
984
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
985
+ };
986
+ state.run.status = "paused";
987
+ state.run.pause = {
988
+ type: "approval",
989
+ approval
990
+ };
991
+ const requested = createEvent(state, "approval.requested", {
992
+ approvalId: approval.id,
993
+ toolCallId: approval.toolCallId,
994
+ toolName: approval.toolName
995
+ });
996
+ const paused = createEvent(state, "run.paused", {
997
+ reason: "approval",
998
+ toolCallId: pending.call.id
999
+ });
1000
+ await commit(state, {
1001
+ checkpoint,
1002
+ events: [requested, paused]
1003
+ });
1004
+ events.push(requested, paused);
1005
+ await releaseExecution(state);
1006
+ yield requested;
1007
+ yield paused;
1008
+ return void 0;
1009
+ }
1010
+ state.approvedToolCallId = void 0;
1011
+ if (Boolean(durable && (state.checkpoint || tool.risk && tool.risk !== "read" || tool.idempotency === "keyed" || tool.retry || tool.credentials?.length || policies.length))) {
1012
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1013
+ await commit(state, { checkpoint });
1014
+ state.checkpoint = checkpoint;
1015
+ }
1016
+ let record = durable ? await durable.getToolExecution(state.run.id, pending.call.id) : void 0;
1017
+ if (record?.status === "completed") {
1018
+ messages.push({
1019
+ role: "tool",
1020
+ name: pending.call.name,
1021
+ toolCallId: pending.call.id,
1022
+ content: serializeJsonValue(record.output)
1023
+ });
1024
+ toolCallCount += 1;
1025
+ pendingIndex += 1;
1026
+ continue;
1027
+ }
1028
+ if (record?.status === "started" && tool.idempotency !== "keyed" && state.forcedRetryToolCallId !== pending.call.id) {
1029
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1030
+ state.run.status = "paused";
1031
+ state.run.pause = {
1032
+ type: "tool_execution_unknown",
1033
+ toolCallId: pending.call.id,
1034
+ toolName: pending.call.name,
1035
+ input: pending.input
1036
+ };
1037
+ const unknown = createEvent(state, "tool.execution_unknown", {
1038
+ step,
1039
+ toolCallId: pending.call.id,
1040
+ toolName: pending.call.name
1041
+ });
1042
+ const paused = createEvent(state, "run.paused", {
1043
+ reason: "tool_execution_unknown",
1044
+ toolCallId: pending.call.id
1045
+ });
1046
+ await commit(state, {
1047
+ checkpoint,
1048
+ events: [unknown, paused]
1049
+ });
1050
+ events.push(unknown, paused);
1051
+ await releaseExecution(state);
1052
+ yield unknown;
1053
+ yield paused;
1054
+ return void 0;
1055
+ }
1056
+ state.forcedRetryToolCallId = void 0;
1057
+ const maxAttempts = tool.retry?.maxAttempts ?? 1;
1058
+ let output;
1059
+ let attempt = record?.attempt ?? 0;
1060
+ while (attempt < maxAttempts) {
1061
+ if (attempt > 0) {
1062
+ if (await authorize(state, tool, pending.input, "tool.execute") === "require_approval") {
1063
+ const checkpoint = await durableCheckpoint(state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex);
1064
+ const approval = {
1065
+ id: crypto.randomUUID(),
1066
+ toolCallId: pending.call.id,
1067
+ toolName: pending.call.name,
1068
+ input: pending.input,
1069
+ risk: tool.risk ?? "read",
1070
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
1071
+ };
1072
+ state.run.status = "paused";
1073
+ state.run.pause = {
1074
+ type: "approval",
1075
+ approval
1076
+ };
1077
+ const requested = createEvent(state, "approval.requested", {
1078
+ approvalId: approval.id,
1079
+ toolCallId: approval.toolCallId,
1080
+ toolName: approval.toolName
1081
+ });
1082
+ const paused = createEvent(state, "run.paused", {
1083
+ reason: "approval",
1084
+ toolCallId: pending.call.id
1085
+ });
1086
+ await commit(state, {
1087
+ checkpoint,
1088
+ events: [requested, paused]
1089
+ });
1090
+ events.push(requested, paused);
1091
+ await releaseExecution(state);
1092
+ yield requested;
1093
+ yield paused;
1094
+ return void 0;
1095
+ }
1096
+ }
1097
+ attempt += 1;
1098
+ const secrets = [];
1099
+ const resolvedCredentials = /* @__PURE__ */ new Map();
1100
+ for (const credentialName of tool.credentials ?? []) {
1101
+ const value = await credentialStore?.resolve({
1102
+ name: credentialName,
1103
+ namespace: request.context?.namespace,
1104
+ actor: request.context?.actor
1105
+ });
1106
+ if (!value) throw new FevexRunError("CREDENTIAL_NOT_FOUND", `Credential "${credentialName}" was not found`, state.run.id);
1107
+ resolvedCredentials.set(credentialName, value);
1108
+ secrets.push(value);
1109
+ }
1110
+ const getCredential = async (credentialName) => {
1111
+ if (!tool.credentials?.includes(credentialName)) throw new FevexRunError("CREDENTIAL_NOT_FOUND", `Credential "${credentialName}" is not declared for tool "${tool.name}"`, state.run.id);
1112
+ return resolvedCredentials.get(credentialName);
1113
+ };
1114
+ record = {
1115
+ runId: state.run.id,
1116
+ toolCallId: pending.call.id,
1117
+ toolName: pending.call.name,
1118
+ input: pending.input,
1119
+ status: "started",
1120
+ attempt,
1121
+ idempotencyKey: pending.idempotencyKey,
1122
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1123
+ };
1124
+ const started = createEvent(state, "tool.started", {
1125
+ step,
1126
+ toolCallId: pending.call.id,
1127
+ toolName: pending.call.name,
1128
+ ...attempt > 1 ? { attempt } : {}
1129
+ });
1130
+ if (durable) await commit(state, {
1131
+ toolExecution: record,
1132
+ events: [started]
1133
+ });
1134
+ else {
1135
+ await runStore.appendEvent(started);
1136
+ notifyObserver(started);
1137
+ }
1138
+ events.push(started);
1139
+ yield started;
1140
+ try {
1141
+ const raw = await abortable(() => tool.execute(pending.input, {
1142
+ runId: state.run.id,
1143
+ toolCallId: pending.call.id,
1144
+ attempt,
1145
+ idempotencyKey: pending.idempotencyKey,
1146
+ getCredential,
1147
+ context: request.context,
1148
+ signal: request.signal
1149
+ }), request.signal);
1150
+ 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`);
1151
+ if (containsSecret(output, secrets)) throw new Error(`Output from tool "${tool.name}" contains a credential`);
1152
+ record = {
1153
+ ...record,
1154
+ status: "completed",
1155
+ output,
1156
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1157
+ };
1158
+ const completed = createEvent(state, "tool.completed", {
1159
+ step,
1160
+ toolCallId: pending.call.id,
1161
+ toolName: pending.call.name,
1162
+ ...attempt > 1 ? { attempt } : {}
1163
+ });
1164
+ if (durable) await commit(state, {
1165
+ toolExecution: record,
1166
+ events: [completed]
1167
+ });
1168
+ else {
1169
+ await runStore.appendEvent(completed);
1170
+ notifyObserver(completed);
1171
+ }
1172
+ events.push(completed);
1173
+ yield completed;
1174
+ break;
1175
+ } catch (error) {
1176
+ if (request.signal.aborted) throw error;
1177
+ const safeError = redact(toErrorMessage(error), secrets);
1178
+ record = {
1179
+ ...record,
1180
+ status: "failed",
1181
+ error: safeError,
1182
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1183
+ };
1184
+ if (durable) await commit(state, { toolExecution: record });
1185
+ if (attempt >= maxAttempts || tool.idempotency !== "keyed") {
1186
+ yield await emit("tool.failed", {
1187
+ step,
1188
+ toolCallId: pending.call.id,
1189
+ toolName: pending.call.name,
1190
+ error: safeError
1191
+ });
1192
+ throw secrets.length ? new Error(safeError, { cause: error }) : error;
1193
+ }
1194
+ const delayMs = Math.min(tool.retry.backoffMs * 2 ** (attempt - 1), tool.retry.maxBackoffMs ?? Number.MAX_SAFE_INTEGER);
1195
+ yield await emit("tool.retrying", {
1196
+ step,
1197
+ toolCallId: pending.call.id,
1198
+ toolName: pending.call.name,
1199
+ attempt: attempt + 1,
1200
+ delayMs,
1201
+ error: safeError
1202
+ });
1203
+ if (delayMs) await abortable(() => new Promise((resolve) => setTimeout(resolve, delayMs)), request.signal);
1204
+ }
1205
+ }
1206
+ if (output === void 0) throw new FevexRunError("TOOL_EXECUTION_UNKNOWN", `Tool "${tool.name}" exhausted its configured attempts`, state.run.id);
1207
+ messages.push({
1208
+ role: "tool",
1209
+ name: pending.call.name,
1210
+ toolCallId: pending.call.id,
1211
+ content: serializeJsonValue(output)
1212
+ });
1213
+ toolCallCount += 1;
1214
+ pendingIndex += 1;
1215
+ }
1216
+ pendingTools = [];
1217
+ pendingIndex = 0;
1218
+ step += 1;
202
1219
  }
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
1220
+ throw new Error(`Agent "${name}" reached maxSteps limit of ${maxSteps}`);
1221
+ } catch (error) {
1222
+ if (request.signal.aborted) {
1223
+ const event = await cancelExecution(state);
1224
+ if (event) {
1225
+ events.push(event);
1226
+ yield event;
1227
+ }
1228
+ } else if (state.run.status === "running") {
1229
+ Object.assign(state.run, {
1230
+ status: "failed",
1231
+ error: toErrorMessage(error),
1232
+ ...usage === void 0 ? {} : { usage },
1233
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
220
1234
  });
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)
1235
+ const failed = createEvent(state, "run.failed", { error: toErrorMessage(error) });
1236
+ if (isDurableRunStore(runStore)) await commit(state, {
1237
+ checkpoint: null,
1238
+ events: [failed]
227
1239
  });
228
- throw error;
1240
+ else {
1241
+ await runStore.saveRun(state.run);
1242
+ await runStore.appendEvent(failed);
1243
+ notifyObserver(failed);
1244
+ }
1245
+ events.push(failed);
1246
+ await releaseExecution(state);
1247
+ yield failed;
229
1248
  }
230
- emit("tool.completed", {
231
- toolCallId: toolCall.id,
232
- toolName: toolCall.name
233
- });
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
248
- }
249
- ],
250
- tools: void 0,
251
- reasoning: agent.reasoning,
252
- modelOptions: agent.modelOptions,
253
- outputSchema,
254
- signal: request.signal
255
- });
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");
261
- return {
262
- output,
263
- events,
264
- usage: addUsage(result.usage, finalResult.usage)
1249
+ throw error;
1250
+ }
1251
+ }
1252
+ const nextEvent = async (state, execution) => {
1253
+ state.advancing = true;
1254
+ try {
1255
+ return await execution.next();
1256
+ } finally {
1257
+ state.advancing = false;
1258
+ }
1259
+ };
1260
+ const drainExecution = async (name, state, throwOnPause) => {
1261
+ const execution = executeAgent(name, state);
1262
+ while (true) {
1263
+ const next = await nextEvent(state, execution);
1264
+ if (next.done) {
1265
+ if (next.value === void 0 && throwOnPause) {
1266
+ const run = await runStore.getRun(state.run.id);
1267
+ if (run?.pause) throw new RunPausedError(run.id, run.pause);
1268
+ }
1269
+ return next.value;
1270
+ }
1271
+ }
1272
+ };
1273
+ const resume = async (runId, resolution) => {
1274
+ if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "resumeRun requires DurableRunStore", runId);
1275
+ const run = await runStore.getRun(runId);
1276
+ const checkpoint = await runStore.getCheckpoint(runId);
1277
+ if (!run || !checkpoint) throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
1278
+ if (run.status !== "paused" && run.status !== "running") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" is terminal`, runId);
1279
+ const agent = agents.get(run.agentName);
1280
+ if (!agent) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Agent "${run.agentName}" is unavailable`, runId);
1281
+ const modelName = typeof agent.model === "string" ? agent.model : "default";
1282
+ const model = typeof agent.model === "string" ? models.get(agent.model) : agent.model ?? models.get("default");
1283
+ if (!model || checkpoint.definitionHash !== await definitionHash(run.agentName, modelName, agent, tools)) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Definition for agent "${run.agentName}" changed`, runId);
1284
+ if (checkpoint.providerState !== void 0 && !model.stateCodec) throw new FevexRunError("RUN_NOT_RESUMABLE", "ModelGateway no longer provides its stateCodec", runId);
1285
+ const session = await runStore.getSession(run.sessionId);
1286
+ if (!session) throw new Error(`Session "${run.sessionId}" does not exist`);
1287
+ if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" is active`, runId);
1288
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
1289
+ if (!await runStore.acquireLease({
1290
+ runId,
1291
+ ownerId,
1292
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
1293
+ })) throw new FevexRunError("RUN_CONFLICT", `Run "${runId}" is leased`, runId);
1294
+ try {
1295
+ const controller = new AbortController();
1296
+ const state = {
1297
+ run,
1298
+ session,
1299
+ request: {
1300
+ input: "",
1301
+ sessionId: session.id,
1302
+ context: checkpoint.context,
1303
+ signal: controller.signal
1304
+ },
1305
+ controller,
1306
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
1307
+ advancing: false,
1308
+ checkpoint,
1309
+ leaseOwner: ownerId,
1310
+ ...resolution.type === "approval" ? { approvedToolCallId: run.pause?.type === "approval" ? run.pause.approval.toolCallId : void 0 } : {
1311
+ approvedToolCallId: resolution.toolCallId,
1312
+ ...resolution.decision === "retry" ? { forcedRetryToolCallId: resolution.toolCallId } : {}
1313
+ }
265
1314
  };
1315
+ if (!resolution.actor?.id?.trim()) {
1316
+ await runStore.releaseLease(runId, ownerId);
1317
+ throw new FevexRunError("APPROVAL_INVALID", "Resolution actor is required", runId);
1318
+ }
1319
+ if (resolution.type === "approval") {
1320
+ if (run.pause?.type !== "approval" || run.pause.approval.id !== resolution.approvalId) {
1321
+ await runStore.releaseLease(runId, ownerId);
1322
+ throw new FevexRunError("APPROVAL_INVALID", "Approval does not match the run", runId);
1323
+ }
1324
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1325
+ const tool = pending && tools.get(pending.call.name);
1326
+ if (!pending || !tool) {
1327
+ await runStore.releaseLease(runId, ownerId);
1328
+ throw new FevexRunError("APPROVAL_INVALID", "Approval tool is unavailable", runId);
1329
+ }
1330
+ if (resolution.decision === "approve") await authorize(state, tool, pending.input, "approval.resolve", {
1331
+ ...state.request.context,
1332
+ actor: resolution.actor
1333
+ });
1334
+ if (resolution.decision === "reject") {
1335
+ const toolCallId = run.pause.approval.toolCallId;
1336
+ run.status = "cancelled";
1337
+ run.pause = void 0;
1338
+ run.error = "approval_rejected";
1339
+ const resolved = createEvent(state, "approval.resolved", {
1340
+ approvalId: resolution.approvalId,
1341
+ toolCallId,
1342
+ decision: "reject",
1343
+ actorId: resolution.actor.id
1344
+ });
1345
+ const cancelled = createEvent(state, "run.cancelled", { reason: "approval_rejected" });
1346
+ await commit(state, {
1347
+ checkpoint: null,
1348
+ events: [resolved, cancelled]
1349
+ });
1350
+ await releaseExecution(state);
1351
+ return structuredClone(run);
1352
+ }
1353
+ } else {
1354
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1355
+ const record = await runStore.getToolExecution(runId, resolution.toolCallId);
1356
+ const matchesUnknownPause = run.pause?.type === "tool_execution_unknown" && run.pause.toolCallId === resolution.toolCallId;
1357
+ const matchesInterruptedAttempt = run.status === "running" && pending?.call.id === resolution.toolCallId && (record?.status === "started" || record?.status === "completed");
1358
+ if (!matchesUnknownPause && !matchesInterruptedAttempt) {
1359
+ await runStore.releaseLease(runId, ownerId);
1360
+ throw new FevexRunError("RUN_NOT_RESUMABLE", "Tool execution resolution does not match a pending attempt", runId);
1361
+ }
1362
+ }
1363
+ if (resolution.type === "tool_execution" && resolution.decision === "use_output") {
1364
+ const pending = checkpoint.pendingTools[checkpoint.pendingIndex];
1365
+ const tool = tools.get(pending.call.name);
1366
+ if (!tool) {
1367
+ await runStore.releaseLease(runId, ownerId);
1368
+ throw new FevexRunError("RUN_DEFINITION_CHANGED", "Pending tool is unavailable", runId);
1369
+ }
1370
+ 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");
1371
+ const current = await runStore.getToolExecution(runId, resolution.toolCallId);
1372
+ await commit(state, { toolExecution: {
1373
+ runId,
1374
+ toolCallId: resolution.toolCallId,
1375
+ toolName: pending.call.name,
1376
+ input: pending.input,
1377
+ status: "completed",
1378
+ attempt: current?.attempt ?? 1,
1379
+ idempotencyKey: current?.idempotencyKey ?? pending.idempotencyKey,
1380
+ output,
1381
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1382
+ } });
1383
+ }
1384
+ const approval = run.pause?.type === "approval" ? run.pause.approval : void 0;
1385
+ run.status = "running";
1386
+ run.pause = void 0;
1387
+ const resumeEvents = [createEvent(state, "run.resumed", void 0)];
1388
+ if (approval && resolution.type === "approval") resumeEvents.push(createEvent(state, "approval.resolved", {
1389
+ approvalId: approval.id,
1390
+ toolCallId: approval.toolCallId,
1391
+ decision: "approve",
1392
+ actorId: resolution.actor.id
1393
+ }));
1394
+ await commit(state, {
1395
+ checkpoint,
1396
+ events: resumeEvents
1397
+ });
1398
+ activeRuns.set(runId, state);
1399
+ activeSessions.add(session.id);
1400
+ startLease(state, runStore);
1401
+ drainExecution(run.agentName, state, false).catch(() => {});
1402
+ return structuredClone(run);
266
1403
  } catch (error) {
267
- emitFailure("run.failed", { error: toErrorMessage(error) });
1404
+ await runStore.releaseLease(runId, ownerId).catch(() => {});
268
1405
  throw error;
269
1406
  }
270
- } };
1407
+ };
1408
+ return {
1409
+ async startAgent(name, request) {
1410
+ const state = await prepareExecution(name, request);
1411
+ drainExecution(name, state, false).catch(() => {});
1412
+ return structuredClone(state.run);
1413
+ },
1414
+ async runAgent(name, request) {
1415
+ const state = await prepareExecution(name, request);
1416
+ return await drainExecution(name, state, true);
1417
+ },
1418
+ streamAgent(name, request) {
1419
+ return { async *[Symbol.asyncIterator]() {
1420
+ const state = await prepareExecution(name, request);
1421
+ const execution = executeAgent(name, state);
1422
+ let finished = false;
1423
+ try {
1424
+ while (true) {
1425
+ const next = await nextEvent(state, execution);
1426
+ if (next.done) {
1427
+ finished = true;
1428
+ return;
1429
+ }
1430
+ yield next.value;
1431
+ }
1432
+ } finally {
1433
+ if (!finished && state.run.status === "running") {
1434
+ state.controller.abort(new DOMException("Stream abandoned", "AbortError"));
1435
+ try {
1436
+ while (state.run.status === "running") if ((await nextEvent(state, execution)).done) break;
1437
+ } catch {}
1438
+ if (state.run.status === "running") await cancelExecution(state);
1439
+ }
1440
+ await execution.return(void 0);
1441
+ }
1442
+ } };
1443
+ },
1444
+ getRun(runId) {
1445
+ return runStore.getRun(runId);
1446
+ },
1447
+ listEvents(runId, options) {
1448
+ return runStore.listEvents(runId, options);
1449
+ },
1450
+ async cancelRun(runId) {
1451
+ const active = activeRuns.get(runId);
1452
+ if (active && !active.request.signal.aborted) {
1453
+ active.controller.abort(new DOMException("Run cancelled", "AbortError"));
1454
+ if (!active.advancing) await cancelExecution(active);
1455
+ return true;
1456
+ }
1457
+ const run = await runStore.getRun(runId);
1458
+ if (run?.status !== "paused") return false;
1459
+ if (!isDurableRunStore(runStore)) return false;
1460
+ const session = await runStore.getSession(run.sessionId);
1461
+ if (!session) return false;
1462
+ const state = {
1463
+ run,
1464
+ session,
1465
+ request: {
1466
+ input: "",
1467
+ sessionId: session.id,
1468
+ signal: new AbortController().signal
1469
+ },
1470
+ controller: new AbortController(),
1471
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
1472
+ advancing: false
1473
+ };
1474
+ run.status = "cancelled";
1475
+ run.pause = void 0;
1476
+ run.error = "aborted";
1477
+ const cancelled = createEvent(state, "run.cancelled", { reason: "aborted" });
1478
+ try {
1479
+ await commit(state, {
1480
+ checkpoint: null,
1481
+ events: [cancelled]
1482
+ });
1483
+ return true;
1484
+ } catch (error) {
1485
+ if (error instanceof FevexRunError && error.code === "RUN_CONFLICT" && (await runStore.getRun(runId))?.status !== "paused") return false;
1486
+ throw error;
1487
+ }
1488
+ },
1489
+ resumeRun(runId, resolution) {
1490
+ return resume(runId, resolution);
1491
+ },
1492
+ async compactSession(sessionId, summary) {
1493
+ if (typeof summary !== "string" || !summary.trim()) throw new Error("Session summary must be a non-empty string");
1494
+ if (activeSessions.has(sessionId)) throw new Error(`Session "${sessionId}" has an active run`);
1495
+ const session = await runStore.getSession(sessionId);
1496
+ if (!session) throw new Error(`Session "${sessionId}" does not exist`);
1497
+ session.history = [{
1498
+ role: "system",
1499
+ content: summary
1500
+ }];
1501
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1502
+ await runStore.saveSession(session);
1503
+ return structuredClone(session);
1504
+ },
1505
+ async flushObservability() {
1506
+ while (pendingExports.size) await Promise.all([...pendingExports]);
1507
+ if (exportFailures.length) {
1508
+ const failures = exportFailures.splice(0);
1509
+ throw new AggregateError(failures, "Observability export failed");
1510
+ }
1511
+ }
1512
+ };
1513
+ }
1514
+ //#endregion
1515
+ //#region src/fevex.ts
1516
+ function createFevex(config) {
1517
+ return createRuntime(createComposition(config));
271
1518
  }
272
1519
  //#endregion
273
- export { SCHEMA_NOT_TRANSPORTABLE, createFevex, defineAgent, defineTool, toToolSpec };
1520
+ export { FevexConfigurationError, FevexRunError, InMemoryRunStore, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, SCHEMA_NOT_TRANSPORTABLE, createFevex, defineAgent, definePolicy, defineTool, isDurableRunStore, toToolSpec };