@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/README.md +349 -49
- package/dist/agents/index.d.mts +1 -22
- package/dist/evals/index.d.mts +145 -0
- package/dist/evals/index.mjs +419 -0
- package/dist/fevex-Dz6GBz9o.d.mts +32 -0
- package/dist/index-BzxHfyYV.d.mts +23 -0
- package/dist/index-CZGyhNIQ.d.mts +47 -0
- package/dist/index-C_Zpr13f.d.mts +124 -0
- package/dist/index-DGWOGXzc.d.mts +181 -0
- package/dist/index-QTkd6a7C.d.mts +20 -0
- package/dist/index-ojCY3lwm.d.mts +79 -0
- package/dist/index.d.mts +27 -15
- package/dist/index.mjs +1444 -197
- package/dist/model-stream-u2xMsEaf.mjs +251 -0
- package/dist/models/index.d.mts +24 -5
- package/dist/observability/index.d.mts +2 -0
- package/dist/observability/index.mjs +1 -0
- package/dist/policies/index.d.mts +2 -0
- package/dist/policies/index.mjs +6 -0
- package/dist/runtime/index.d.mts +2 -17
- package/dist/runtime/index.mjs +92 -1
- package/dist/testing/index.d.mts +7 -5
- package/dist/testing/index.mjs +172 -11
- package/dist/tools/index.d.mts +3 -31
- package/package.json +15 -3
- package/dist/index-D_RBJmVt.d.mts +0 -44
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 "./
|
|
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/
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
104
|
-
|
|
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
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
tools.
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
if (agent.model &&
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
if (
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
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 };
|