@fevex/core 0.1.0-alpha.5 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +100 -9
  2. package/dist/agents/index.d.mts +1 -1
  3. package/dist/artifacts/index.d.mts +1 -1
  4. package/dist/channels/index.d.mts +1 -1
  5. package/dist/evals/index.d.mts +4 -4
  6. package/dist/{fevex-B5NxFKcV.d.mts → fevex-CVMLnZsk.d.mts} +41 -12
  7. package/dist/http/index.d.mts +4 -4
  8. package/dist/http/index.mjs +2 -0
  9. package/dist/{index-Dsulk-n6.d.mts → index-1cHr7KB_.d.mts} +3 -3
  10. package/dist/{index-CKQCNvJz.d.mts → index-5p3q2yIp.d.mts} +2 -2
  11. package/dist/index-B0AdFbgr.d.mts +116 -0
  12. package/dist/{index--_LdSEuF.d.mts → index-BTD1c4Ys.d.mts} +3 -3
  13. package/dist/{index-BPwkgdo3.d.mts → index-BgKFd9yw.d.mts} +1 -1
  14. package/dist/{index-Cb3AB98n.d.mts → index-CbACw6xZ.d.mts} +2 -2
  15. package/dist/index-CpLDt6Rw.d.mts +118 -0
  16. package/dist/{index-Br-zPxZH.d.mts → index-CseuTR-F.d.mts} +2 -2
  17. package/dist/{index-CGmHXV31.d.mts → index-CzAi2Y_Z.d.mts} +13 -6
  18. package/dist/{index-BFnWUu7j.d.mts → index-Df84VWmN.d.mts} +1 -1
  19. package/dist/index.d.mts +13 -13
  20. package/dist/index.mjs +569 -78
  21. package/dist/knowledge/index.d.mts +2 -2
  22. package/dist/knowledge/index.mjs +236 -36
  23. package/dist/models/index.d.mts +1 -1
  24. package/dist/observability/index.d.mts +2 -2
  25. package/dist/policies/index.d.mts +1 -1
  26. package/dist/runtime/index.d.mts +2 -2
  27. package/dist/sandbox/index.d.mts +1 -1
  28. package/dist/teams/index.d.mts +1 -1
  29. package/dist/testing/index.d.mts +6 -6
  30. package/dist/tools/index.d.mts +2 -2
  31. package/dist/{types-BDVUoAU6.d.mts → types-B8laW1hM.d.mts} +2 -0
  32. package/dist/workflows/index.d.mts +1 -1
  33. package/package.json +1 -1
  34. package/dist/index-D6lnI92z.d.mts +0 -80
  35. package/dist/index-aayvtf0J.d.mts +0 -70
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { defineAgent } from "./agents/index.mjs";
2
2
  import { a as CatalogedArtifactStore, c as assertArtifactId, i as FileSystemArtifactStore, n as InMemoryArtifactStore, o as ArtifactError, r as InMemoryArtifactCatalog, s as artifactTenantId, t as S3ArtifactStore } from "./artifacts-gUzemET7.mjs";
3
3
  import { n as RunPausedError, t as FevexRunError } from "./run-error-Cr17wPGH.mjs";
4
4
  import { ChannelError, deliverChannelRun, handleChannelInput } from "./channels/index.mjs";
5
- import { InMemoryMemoryStore, createMemoryContextProvider, defineContextProvider, defineSkill } from "./knowledge/index.mjs";
5
+ import { InMemoryMemoryStore, createAgentsMdContextProvider, createMemoryContextProvider, defineContextProvider, defineSkill, isProgressiveSkillProvider, loadSkillFile, loadSkillFiles } from "./knowledge/index.mjs";
6
6
  import { n as teamAsWorkflow, t as defineTeam } from "./teams-Ctz676G4.mjs";
7
7
  import { InMemoryRunStore, isDurableRunStore, isRetainableRunStore, validateListEventsOptions, validatePurgeRunsOptions } from "./runtime/index.mjs";
8
8
  import { FEVEX_JSON_SCHEMA_PROFILE_VERSION, IntegrationError, compileFevexJsonSchema, defineConnection, defineTool, toToolSpec, validateFevexJsonSchemaProfile } from "./tools/index.mjs";
@@ -332,6 +332,18 @@ function createComposition(config) {
332
332
  "error.message"
333
333
  ].includes(kind)) && (content.redact === void 0 || typeof content.redact === "function"), "INVALID_CONFIG", "Fevex config \"observability.content\" is invalid");
334
334
  }
335
+ if (config.observability.exportQueue !== void 0) {
336
+ const queue = config.observability.exportQueue;
337
+ assertConfiguration(isRecord(queue) && [
338
+ "concurrency",
339
+ "maxQueued",
340
+ "timeoutMs",
341
+ "maxFailures"
342
+ ].every((key) => {
343
+ const value = queue[key];
344
+ return value === void 0 || typeof value === "number" && Number.isSafeInteger(value) && value > 0;
345
+ }), "INVALID_CONFIG", "Fevex config \"observability.exportQueue\" must contain positive safe integers");
346
+ }
335
347
  }
336
348
  const models = /* @__PURE__ */ new Map();
337
349
  const agents = /* @__PURE__ */ new Map();
@@ -624,7 +636,8 @@ function createComposition(config) {
624
636
  ...config.observability.content ? { content: {
625
637
  include: [...config.observability.content.include],
626
638
  ...config.observability.content.redact ? { redact: config.observability.content.redact } : {}
627
- } } : {}
639
+ } } : {},
640
+ ...config.observability.exportQueue ? { exportQueue: { ...config.observability.exportQueue } } : {}
628
641
  }
629
642
  };
630
643
  }
@@ -636,6 +649,8 @@ const LEASE_MS = 3e4;
636
649
  const LEASE_RENEW_MS = 1e4;
637
650
  /** Reserved name of the internal elicitation tool exposed to the model. */
638
651
  const ELICIT_TOOL_NAME = "fevex__elicit";
652
+ /** Reserved name of the internal progressive skill loader exposed to the model. */
653
+ const LOAD_SKILL_TOOL_NAME = "fevex__load_skill";
639
654
  //#endregion
640
655
  //#region src/internal/run-helpers.ts
641
656
  function redact(message, secrets) {
@@ -890,8 +905,9 @@ function addWorkflowBudget(first, second) {
890
905
  */
891
906
  function createAgentEngine(composition, core) {
892
907
  const { models, agents, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, artifacts, policies } = composition;
893
- const { activeRuns, activeSessions, runtimeOwner, admitStart, markInternalRun, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
894
- const prepareExecutionBody = async (name, request, internal = false) => {
908
+ const { activeRuns, activeSessions, runtimeOwner, isClosed, admitStart, markInternalRun, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
909
+ const prepareExecutionBody = async (name, request, internal = false, correlation = {}) => {
910
+ if (!internal && isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
895
911
  const agent = agents.get(name);
896
912
  if (!agent) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${name}" is not registered`);
897
913
  const { modelName, model } = resolveModel(agent, request.model);
@@ -920,6 +936,8 @@ function createAgentEngine(composition, core) {
920
936
  agentName: name,
921
937
  modelName,
922
938
  ...model.metadata === void 0 ? {} : { modelMetadata: structuredClone(model.metadata) },
939
+ ...correlation.parentRunId === void 0 ? {} : { parentRunId: correlation.parentRunId },
940
+ ...correlation.rootRunId === void 0 ? {} : { rootRunId: correlation.rootRunId },
923
941
  status: "running",
924
942
  revision: 0,
925
943
  createdAt: now,
@@ -1010,7 +1028,10 @@ function createAgentEngine(composition, core) {
1010
1028
  const prepareExecution = async (name, request, options = {}) => {
1011
1029
  const release = options.admit === false ? void 0 : await admitStart("agent", name, request);
1012
1030
  try {
1013
- return await prepareExecutionBody(name, request, options.admit === false);
1031
+ return await prepareExecutionBody(name, request, options.admit === false, {
1032
+ ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
1033
+ ...options.rootRunId === void 0 ? {} : { rootRunId: options.rootRunId }
1034
+ });
1014
1035
  } finally {
1015
1036
  release?.();
1016
1037
  }
@@ -1160,26 +1181,50 @@ function createAgentEngine(composition, core) {
1160
1181
  description: remote.description
1161
1182
  } : tool, inputSchema);
1162
1183
  }));
1163
- const allTools = currentElicitationMode === "pause" ? [...agentTools, {
1164
- name: ELICIT_TOOL_NAME,
1165
- description: "Request external information required to continue this run. Use prompt for explanatory text shown outside the form. Put short field labels in responseSchema property titles and optional field help in property descriptions.",
1184
+ const progressiveSkills = (agent.skills ?? []).map((skillName) => contextProviders.get(skillName)).filter((provider) => provider !== void 0 && isProgressiveSkillProvider(provider));
1185
+ const skillTool = progressiveSkills.length === 0 ? void 0 : {
1186
+ name: LOAD_SKILL_TOOL_NAME,
1187
+ description: `Load the full instructions for one of this agent's skills. Available skills: ${progressiveSkills.map((skill) => skill.name).join(", ")}. Pass a resource path from the loaded skill to read one resource.`,
1166
1188
  inputSchema: {
1167
1189
  type: "object",
1168
1190
  properties: {
1169
- prompt: {
1191
+ name: {
1170
1192
  type: "string",
1171
- description: "Explanatory text for the user; do not duplicate field labels here."
1172
- },
1173
- responseSchema: {
1174
- type: "object",
1175
- description: "JSON Schema for the expected response. Each property should include a short title for the form label and may include a description for field help."
1193
+ enum: progressiveSkills.map((skill) => skill.name)
1176
1194
  },
1177
- expiresAt: { type: "string" }
1195
+ resource: {
1196
+ type: "string",
1197
+ description: "Optional resource path returned by a previous load."
1198
+ }
1178
1199
  },
1179
- required: ["prompt", "responseSchema"],
1200
+ required: ["name"],
1180
1201
  additionalProperties: false
1181
1202
  }
1182
- }] : agentTools;
1203
+ };
1204
+ const allTools = [
1205
+ ...agentTools,
1206
+ ...skillTool === void 0 ? [] : [skillTool],
1207
+ ...currentElicitationMode === "pause" ? [{
1208
+ name: ELICIT_TOOL_NAME,
1209
+ description: "Request external information required to continue this run. Use prompt for explanatory text shown outside the form. Put short field labels in responseSchema property titles and optional field help in property descriptions.",
1210
+ inputSchema: {
1211
+ type: "object",
1212
+ properties: {
1213
+ prompt: {
1214
+ type: "string",
1215
+ description: "Explanatory text for the user; do not duplicate field labels here."
1216
+ },
1217
+ responseSchema: {
1218
+ type: "object",
1219
+ description: "JSON Schema for the expected response. Each property should include a short title for the form label and may include a description for field help."
1220
+ },
1221
+ expiresAt: { type: "string" }
1222
+ },
1223
+ required: ["prompt", "responseSchema"],
1224
+ additionalProperties: false
1225
+ }
1226
+ }] : []
1227
+ ];
1183
1228
  const effectiveLimits = state.checkpoint?.limits ?? combineLimits(agent.limits, request.limits);
1184
1229
  const maxSteps = effectiveLimits?.maxSteps ?? 8;
1185
1230
  const maxToolCalls = effectiveLimits?.maxToolCalls ?? 16;
@@ -1268,6 +1313,57 @@ function createAgentEngine(composition, core) {
1268
1313
  assertContinuationBudget(name, "maxInputTokens", "inputTokens", effectiveLimits?.maxInputTokens, usage);
1269
1314
  assertContinuationBudget(name, "maxOutputTokens", "outputTokens", effectiveLimits?.maxOutputTokens, usage);
1270
1315
  if (result.toolCalls.length > maxToolCalls - toolCallCount) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
1316
+ const loadSkillCall = progressiveSkills.length === 0 ? void 0 : result.toolCalls.find((call) => call.name === LOAD_SKILL_TOOL_NAME);
1317
+ if (loadSkillCall) {
1318
+ if (result.toolCalls.length !== 1) throw new Error(`${LOAD_SKILL_TOOL_NAME} must be the only tool call in a model turn`);
1319
+ if (!loadSkillCall.id?.trim()) throw new Error("Tool call id cannot be empty");
1320
+ if (seenToolCallIds.has(loadSkillCall.id)) throw new Error(`Tool call id "${loadSkillCall.id}" is duplicated in run "${state.run.id}"`);
1321
+ if (toolCallCount + 1 > maxToolCalls) throw new Error(`Agent "${name}" exceeded maxToolCalls limit of ${maxToolCalls}`);
1322
+ const rawInput = toJsonValue(loadSkillCall.input, `Input for tool "${LOAD_SKILL_TOOL_NAME}" must be JSON-serializable`);
1323
+ const skillInput = readSkillInput(rawInput);
1324
+ const skill = progressiveSkills.find((item) => item.name === skillInput.name);
1325
+ if (!skill) throw new Error(`Skill "${skillInput.name}" is not available to agent "${name}"`);
1326
+ seenToolCallIds.add(loadSkillCall.id);
1327
+ messages.push({
1328
+ role: "assistant",
1329
+ content: result.output === void 0 ? "" : serializeValue(result.output, `Model output for agent "${name}" must be serializable`),
1330
+ toolCalls: [{
1331
+ ...loadSkillCall,
1332
+ input: rawInput
1333
+ }]
1334
+ });
1335
+ yield await emit("tool.started", {
1336
+ step,
1337
+ toolCallId: loadSkillCall.id,
1338
+ toolName: LOAD_SKILL_TOOL_NAME
1339
+ });
1340
+ let blocks;
1341
+ try {
1342
+ blocks = await abortable(() => skill.load(knowledgeContext(state, inputContent), skillInput.resource), request.signal);
1343
+ } catch (error) {
1344
+ yield await emit("tool.failed", {
1345
+ step,
1346
+ toolCallId: loadSkillCall.id,
1347
+ toolName: LOAD_SKILL_TOOL_NAME,
1348
+ error: toErrorMessage(error)
1349
+ });
1350
+ throw new Error(`Skill provider "${skill.name}" failed: ${toErrorMessage(error)}`, { cause: error });
1351
+ }
1352
+ messages.push({
1353
+ role: "tool",
1354
+ name: LOAD_SKILL_TOOL_NAME,
1355
+ toolCallId: loadSkillCall.id,
1356
+ content: blocks.filter((block) => block.content.trim()).map((block) => `[Skill: ${skill.name}/${block.id}]\n${block.content}`).join("\n\n")
1357
+ });
1358
+ yield await emit("tool.completed", {
1359
+ step,
1360
+ toolCallId: loadSkillCall.id,
1361
+ toolName: LOAD_SKILL_TOOL_NAME
1362
+ });
1363
+ toolCallCount += 1;
1364
+ step += 1;
1365
+ continue;
1366
+ }
1271
1367
  const elicitationCall = result.toolCalls.find(isElicitationToolCall);
1272
1368
  if (elicitationCall) {
1273
1369
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "Elicitation requires a DurableRunStore", state.run.id);
@@ -2017,6 +2113,18 @@ function createAgentEngine(composition, core) {
2017
2113
  cancelStoredAgent
2018
2114
  };
2019
2115
  }
2116
+ function readSkillInput(input) {
2117
+ if (typeof input !== "object" || input === null || Array.isArray(input)) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} input must be an object`);
2118
+ const record = input;
2119
+ const name = record.name;
2120
+ const resource = record.resource;
2121
+ if (typeof name !== "string" || !name.trim()) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} name must be a non-empty string`);
2122
+ if (resource !== void 0 && (typeof resource !== "string" || !resource.trim())) throw new TypeError(`${LOAD_SKILL_TOOL_NAME} resource must be a non-empty string`);
2123
+ return {
2124
+ name: name.trim(),
2125
+ ...resource === void 0 ? {} : { resource: resource.trim() }
2126
+ };
2127
+ }
2020
2128
  function scopedArtifacts(store, options) {
2021
2129
  const scopedContext = (value) => {
2022
2130
  const contextTenant = options.request.context?.attributes?.tenantId;
@@ -2053,16 +2161,31 @@ function scopedArtifacts(store, options) {
2053
2161
  const TERMINAL_TYPES = /* @__PURE__ */ new Set([
2054
2162
  "run.completed",
2055
2163
  "run.failed",
2056
- "run.cancelled"
2164
+ "run.cancelled",
2165
+ "workflow.run.completed",
2166
+ "workflow.run.failed",
2167
+ "workflow.run.cancelled",
2168
+ "team.run.completed",
2169
+ "team.run.failed",
2170
+ "team.run.cancelled"
2057
2171
  ]);
2058
2172
  const SENSITIVE_KEY = /api[-_]?key|authorization|cookie|credential|password|secret|token/i;
2173
+ function isTraceTerminalEvent(event) {
2174
+ return TERMINAL_TYPES.has(event.type);
2175
+ }
2059
2176
  function durationMs(startedAt, endedAt) {
2060
2177
  return Math.max(0, Date.parse(endedAt) - Date.parse(startedAt));
2061
2178
  }
2062
2179
  function traceStatus(event) {
2063
- if (event.type === "run.completed") return "completed";
2064
- if (event.type === "run.cancelled") return "cancelled";
2065
- return "failed";
2180
+ switch (event.type) {
2181
+ case "run.completed":
2182
+ case "workflow.run.completed":
2183
+ case "team.run.completed": return "completed";
2184
+ case "run.cancelled":
2185
+ case "workflow.run.cancelled":
2186
+ case "team.run.cancelled": return "cancelled";
2187
+ default: return "failed";
2188
+ }
2066
2189
  }
2067
2190
  function redactKeys(value) {
2068
2191
  if (Array.isArray(value)) return value.map(redactKeys);
@@ -2169,20 +2292,74 @@ function annotation(event) {
2169
2292
  toolName: event.payload.toolName
2170
2293
  }
2171
2294
  };
2295
+ case "workflow.run.paused":
2296
+ case "team.run.paused": {
2297
+ const stepId = event.type === "workflow.run.paused" ? event.payload.stepId : event.payload.delegationId;
2298
+ return {
2299
+ name: event.type,
2300
+ timestamp: event.timestamp,
2301
+ attributes: {
2302
+ stepId,
2303
+ reason: event.payload.reason,
2304
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2305
+ }
2306
+ };
2307
+ }
2308
+ case "workflow.run.resumed":
2309
+ case "team.run.resumed": return {
2310
+ name: event.type,
2311
+ timestamp: event.timestamp
2312
+ };
2313
+ case "team.handoff.created": return {
2314
+ name: event.type,
2315
+ timestamp: event.timestamp,
2316
+ attributes: {
2317
+ delegationId: event.payload.delegationId,
2318
+ from: event.payload.from,
2319
+ to: event.payload.to,
2320
+ reason: event.payload.reason
2321
+ }
2322
+ };
2172
2323
  default: return;
2173
2324
  }
2174
2325
  }
2175
- function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2176
- const events = [...rawEvents].sort((left, right) => left.sequence - right.sequence);
2177
- const terminal = [...events].reverse().find((event) => TERMINAL_TYPES.has(event.type));
2178
- if (!terminal) throw new TypeError(`Run "${run.id}" has no terminal event`);
2179
- const started = events.find((event) => event.type === "run.started") ?? events[0];
2180
- if (!started) throw new TypeError(`Run "${run.id}" has no events`);
2326
+ function terminalContent(options, terminal) {
2327
+ if (terminal.type === "run.completed" || terminal.type === "workflow.run.completed" || terminal.type === "team.run.completed") {
2328
+ const output = content(options, "run.output", terminal.payload.output);
2329
+ return output ? [output] : [];
2330
+ }
2331
+ if (terminal.type === "run.failed" || terminal.type === "workflow.run.failed" || terminal.type === "team.run.failed") {
2332
+ const error = content(options, "error.message", terminal.payload.error);
2333
+ return error ? [error] : [];
2334
+ }
2335
+ return [];
2336
+ }
2337
+ function sortedEvents(rawEvents) {
2338
+ return [...rawEvents].sort((left, right) => left.sequence - right.sequence);
2339
+ }
2340
+ function terminalEvent(runId, events) {
2341
+ const terminal = [...events].reverse().find((event) => isTraceTerminalEvent(event));
2342
+ if (!terminal) throw new TypeError(`Run "${runId}" has no terminal event`);
2343
+ return terminal;
2344
+ }
2345
+ function startEvent(events) {
2346
+ const started = events.find((event) => event.type === "run.started" || event.type === "workflow.run.started" || event.type === "team.run.started") ?? events[0];
2347
+ if (!started) throw new TypeError("Run has no events");
2348
+ return started;
2349
+ }
2350
+ function traceKind(run) {
2351
+ if (isTeamRun(run)) return "team";
2352
+ return run.kind === "workflow" ? "workflow" : "agent";
2353
+ }
2354
+ function buildAgentTrace(run, events, modelRegistryName, model, options) {
2355
+ const terminal = terminalEvent(run.id, events);
2356
+ const started = startEvent(events);
2181
2357
  const status = traceStatus(terminal);
2182
2358
  const annotations = [];
2183
2359
  const spans = [];
2184
2360
  const openModels = /* @__PURE__ */ new Map();
2185
2361
  const openTools = /* @__PURE__ */ new Map();
2362
+ const modelSpanByStep = /* @__PURE__ */ new Map();
2186
2363
  const previousUsage = {};
2187
2364
  const closeModel = (step, endedAt, spanStatus) => {
2188
2365
  const open = openModels.get(step);
@@ -2213,6 +2390,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2213
2390
  const spanCost = costFor(options, modelRegistryName, model, usage);
2214
2391
  if (spanCost) span.cost = spanCost;
2215
2392
  spans.push(span);
2393
+ modelSpanByStep.set(step, span.id);
2216
2394
  };
2217
2395
  const findOpenTool = (toolCallId) => {
2218
2396
  const entries = [...openTools.entries()];
@@ -2232,7 +2410,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2232
2410
  const errorContent = error ? content(options, "error.message", error) : void 0;
2233
2411
  spans.push({
2234
2412
  id: event.id,
2235
- parentId: run.id,
2413
+ parentId: modelSpanByStep.get(event.payload.step) ?? run.id,
2236
2414
  kind: "tool",
2237
2415
  name: event.payload.toolName,
2238
2416
  startedAt: event.timestamp,
@@ -2297,19 +2475,15 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2297
2475
  for (const open of [...openTools.values()]) if (open.event.type === "tool.started") closeTool(open.event.payload.toolCallId, terminal.timestamp, status);
2298
2476
  const finalUsage = run.usage ?? previousUsage;
2299
2477
  const totalCost = costFor(options, modelRegistryName, model, finalUsage);
2300
- const traceContent = [];
2301
- if (terminal.type === "run.completed") {
2302
- const output = content(options, "run.output", terminal.payload.output);
2303
- if (output) traceContent.push(output);
2304
- } else if (terminal.type === "run.failed") {
2305
- const error = content(options, "error.message", terminal.payload.error);
2306
- if (error) traceContent.push(error);
2307
- }
2478
+ const traceContent = terminalContent(options, terminal);
2308
2479
  return {
2309
2480
  schemaVersion: "1",
2481
+ kind: "agent",
2310
2482
  traceId: run.id,
2311
2483
  runId: run.id,
2312
2484
  sessionId: run.sessionId,
2485
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
2486
+ rootRunId: run.rootRunId ?? run.parentRunId ?? run.id,
2313
2487
  agentName: run.agentName,
2314
2488
  modelRegistryName,
2315
2489
  ...model ? { model: { ...model } } : {},
@@ -2329,6 +2503,249 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2329
2503
  ...traceContent.length ? { content: traceContent } : {}
2330
2504
  };
2331
2505
  }
2506
+ function buildCoordinatorTrace(run, events, options) {
2507
+ const terminal = terminalEvent(run.id, events);
2508
+ const started = startEvent(events);
2509
+ const status = traceStatus(terminal);
2510
+ const annotations = [];
2511
+ const spans = [];
2512
+ const openSteps = /* @__PURE__ */ new Map();
2513
+ const stepKey = (scope, stepId) => `${scope}:${stepId}`;
2514
+ const modelCallCount = events.filter(({ type }) => type === "model.completed").length;
2515
+ const toolCallCount = events.filter(({ type }) => type === "tool.completed" || type === "tool.failed").length;
2516
+ const openStep = (key, event, name, attributes) => {
2517
+ openSteps.set(key, {
2518
+ event,
2519
+ name,
2520
+ attributes
2521
+ });
2522
+ };
2523
+ const closeStep = (key, endedAt, spanStatus, error) => {
2524
+ const open = openSteps.get(key);
2525
+ if (!open) return;
2526
+ openSteps.delete(key);
2527
+ const errorContent = error ? content(options, "error.message", error) : void 0;
2528
+ spans.push({
2529
+ id: open.event.id,
2530
+ parentId: run.id,
2531
+ kind: "step",
2532
+ name: open.name,
2533
+ startedAt: open.event.timestamp,
2534
+ endedAt,
2535
+ durationMs: durationMs(open.event.timestamp, endedAt),
2536
+ status: spanStatus,
2537
+ attributes: open.attributes,
2538
+ annotations: [],
2539
+ ...errorContent ? { content: [errorContent] } : {}
2540
+ });
2541
+ };
2542
+ for (const event of events) {
2543
+ const item = annotation(event);
2544
+ if (item) annotations.push(item);
2545
+ switch (event.type) {
2546
+ case "workflow.step.started":
2547
+ openStep(stepKey("workflow", event.payload.stepId), event, event.payload.stepId, {
2548
+ stepId: event.payload.stepId,
2549
+ stepKind: event.payload.kind,
2550
+ ...event.payload.agentName === void 0 ? {} : { agentName: event.payload.agentName },
2551
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2552
+ });
2553
+ break;
2554
+ case "workflow.step.completed":
2555
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "completed");
2556
+ break;
2557
+ case "workflow.step.failed":
2558
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "failed", event.payload.error);
2559
+ break;
2560
+ case "workflow.wait.started":
2561
+ openStep(stepKey("workflow", event.payload.stepId), event, event.payload.stepId, {
2562
+ stepId: event.payload.stepId,
2563
+ stepKind: "wait",
2564
+ waitKind: event.payload.kind,
2565
+ ...event.payload.resumeAt === void 0 ? {} : { resumeAt: event.payload.resumeAt },
2566
+ ...event.payload.eventName === void 0 ? {} : { eventName: event.payload.eventName }
2567
+ });
2568
+ break;
2569
+ case "workflow.wait.completed":
2570
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "completed");
2571
+ break;
2572
+ case "team.agent.assigned":
2573
+ openStep(stepKey("team", event.payload.delegationId), event, event.payload.delegationId, {
2574
+ stepId: event.payload.delegationId,
2575
+ stepKind: "agent",
2576
+ agentName: event.payload.agentName,
2577
+ action: event.payload.action,
2578
+ ...event.payload.expectedOutput === void 0 ? {} : { expectedOutput: event.payload.expectedOutput },
2579
+ ...event.payload.constraints === void 0 ? {} : { constraints: event.payload.constraints },
2580
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2581
+ });
2582
+ break;
2583
+ case "team.task.completed":
2584
+ closeStep(stepKey("team", event.payload.delegationId), event.timestamp, "completed");
2585
+ break;
2586
+ case "team.task.failed":
2587
+ closeStep(stepKey("team", event.payload.delegationId), event.timestamp, "failed", event.payload.error);
2588
+ break;
2589
+ case "team.merge.started":
2590
+ openStep(stepKey("team", event.payload.stepId), event, event.payload.stepId, {
2591
+ stepId: event.payload.stepId,
2592
+ stepKind: "merge",
2593
+ width: event.payload.width
2594
+ });
2595
+ break;
2596
+ case "team.merge.completed":
2597
+ closeStep(stepKey("team", event.payload.stepId), event.timestamp, "completed");
2598
+ break;
2599
+ case "team.merge.failed":
2600
+ closeStep(stepKey("team", event.payload.stepId), event.timestamp, "failed", event.payload.error);
2601
+ break;
2602
+ }
2603
+ }
2604
+ for (const key of [...openSteps.keys()]) closeStep(key, terminal.timestamp, status);
2605
+ const traceContent = terminalContent(options, terminal);
2606
+ return {
2607
+ schemaVersion: "1",
2608
+ kind: traceKind(run),
2609
+ traceId: run.id,
2610
+ runId: run.id,
2611
+ sessionId: run.sessionId,
2612
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
2613
+ rootRunId: run.rootRunId ?? run.parentRunId ?? run.id,
2614
+ agentName: coordinatorName(run),
2615
+ startedAt: started.timestamp,
2616
+ endedAt: terminal.timestamp,
2617
+ durationMs: durationMs(started.timestamp, terminal.timestamp),
2618
+ status,
2619
+ attributes: {
2620
+ eventCount: events.length,
2621
+ stepCount: spans.filter(({ kind }) => kind === "step").length,
2622
+ modelCallCount,
2623
+ toolCallCount
2624
+ },
2625
+ annotations,
2626
+ spans,
2627
+ ...run.usage && Object.keys(run.usage).length ? { usage: { ...run.usage } } : {},
2628
+ ...traceContent.length ? { content: traceContent } : {}
2629
+ };
2630
+ }
2631
+ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2632
+ const events = sortedEvents(rawEvents);
2633
+ if (isCoordinatorRun(run)) return buildCoordinatorTrace(run, events, options);
2634
+ if (!modelRegistryName) throw new TypeError(`Run "${run.id}" has no model registry name`);
2635
+ return buildAgentTrace(run, events, modelRegistryName, model, options);
2636
+ }
2637
+ //#endregion
2638
+ //#region src/internal/trace-export.ts
2639
+ const DEFAULT_CONCURRENCY = 4;
2640
+ const DEFAULT_MAX_QUEUED = 100;
2641
+ const DEFAULT_TIMEOUT_MS = 3e4;
2642
+ const DEFAULT_MAX_FAILURES = 100;
2643
+ const DEDUPE_WINDOW = 1e4;
2644
+ function withTimeout(promise, timeoutMs, message) {
2645
+ return new Promise((resolve, reject) => {
2646
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
2647
+ promise.then((value) => {
2648
+ clearTimeout(timer);
2649
+ resolve(value);
2650
+ }, (error) => {
2651
+ clearTimeout(timer);
2652
+ reject(error);
2653
+ });
2654
+ });
2655
+ }
2656
+ function withDefault(value, fallback) {
2657
+ return value ?? fallback;
2658
+ }
2659
+ /**
2660
+ * Bounded, deduplicated trace export queue. Jobs include building the trace
2661
+ * from the store, so a slow store or exporter cannot grow memory without limit.
2662
+ */
2663
+ function createTraceExportQueue(options = {}) {
2664
+ const concurrency = withDefault(options.concurrency, DEFAULT_CONCURRENCY);
2665
+ const maxQueued = withDefault(options.maxQueued, DEFAULT_MAX_QUEUED);
2666
+ const timeoutMs = withDefault(options.timeoutMs, DEFAULT_TIMEOUT_MS);
2667
+ const maxFailures = withDefault(options.maxFailures, DEFAULT_MAX_FAILURES);
2668
+ const queue = [];
2669
+ const pending = /* @__PURE__ */ new Set();
2670
+ const exportedRuns = /* @__PURE__ */ new Set();
2671
+ const failures = [];
2672
+ let active = 0;
2673
+ let exported = 0;
2674
+ let failed = 0;
2675
+ let dropped = 0;
2676
+ let unreported = 0;
2677
+ const rememberRun = (runId) => {
2678
+ if (exportedRuns.has(runId)) return false;
2679
+ if (exportedRuns.size >= DEDUPE_WINDOW) {
2680
+ const oldest = exportedRuns.values().next().value;
2681
+ if (oldest !== void 0) exportedRuns.delete(oldest);
2682
+ }
2683
+ exportedRuns.add(runId);
2684
+ return true;
2685
+ };
2686
+ const recordFailure = (error) => {
2687
+ failed += 1;
2688
+ unreported += 1;
2689
+ failures.push(error);
2690
+ if (failures.length > maxFailures) failures.shift();
2691
+ };
2692
+ const startNext = () => {
2693
+ while (active < concurrency && queue.length) {
2694
+ const task = queue.shift();
2695
+ active += 1;
2696
+ let job;
2697
+ job = Promise.resolve().then(task).then(() => {
2698
+ exported += 1;
2699
+ }, (error) => {
2700
+ recordFailure(error);
2701
+ }).finally(() => {
2702
+ active -= 1;
2703
+ pending.delete(job);
2704
+ startNext();
2705
+ });
2706
+ pending.add(job);
2707
+ }
2708
+ };
2709
+ return {
2710
+ timeoutMs,
2711
+ enqueue(runId, task) {
2712
+ if (!rememberRun(runId)) return;
2713
+ if (queue.length >= maxQueued) {
2714
+ dropped += 1;
2715
+ recordFailure(/* @__PURE__ */ new Error(`Observability export queue is full; trace for run "${runId}" was dropped`));
2716
+ return;
2717
+ }
2718
+ queue.push(task);
2719
+ startNext();
2720
+ },
2721
+ async flush(flushOptions) {
2722
+ const timeout = flushOptions?.timeoutMs;
2723
+ if (timeout !== void 0 && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout <= 0)) throw new Error("Observability flush timeoutMs must be a positive safe integer");
2724
+ const drain = async () => {
2725
+ while (pending.size) await Promise.all([...pending]);
2726
+ };
2727
+ if (timeout === void 0) await drain();
2728
+ else await withTimeout(drain(), timeout, `Observability flush timed out after ${timeout}ms`);
2729
+ if (unreported === 0) return;
2730
+ const errors = [...failures];
2731
+ const omitted = unreported - failures.length;
2732
+ if (omitted > 0) errors.push(/* @__PURE__ */ new Error(`${omitted} additional export failures were not retained`));
2733
+ failures.length = 0;
2734
+ unreported = 0;
2735
+ throw new AggregateError(errors, `Observability export failed (${failed} failure(s), ${dropped} dropped trace(s))`);
2736
+ },
2737
+ stats() {
2738
+ return {
2739
+ exported,
2740
+ failed,
2741
+ dropped,
2742
+ queued: queue.length,
2743
+ active,
2744
+ retainedFailures: failures.length
2745
+ };
2746
+ }
2747
+ };
2748
+ }
2332
2749
  //#endregion
2333
2750
  //#region src/internal/run-core.ts
2334
2751
  /**
@@ -2347,11 +2764,25 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2347
2764
  const activeSessions = /* @__PURE__ */ new Set();
2348
2765
  const internalRuns = /* @__PURE__ */ new Set();
2349
2766
  const pendingTenantStarts = /* @__PURE__ */ new Map();
2767
+ const idleWaiters = /* @__PURE__ */ new Set();
2350
2768
  let pendingStarts = 0;
2351
- const pendingExports = /* @__PURE__ */ new Set();
2352
- const exportFailures = [];
2353
- const exportedRuns = /* @__PURE__ */ new Set();
2769
+ let closed = false;
2770
+ const traceExports = createTraceExportQueue(observability?.exportQueue);
2354
2771
  const runtimeOwner = crypto.randomUUID();
2772
+ const isIdle = () => activeRuns.size === 0 && activeWorkflowRuns.size === 0;
2773
+ const notifyIdle = () => {
2774
+ if (!isIdle()) return;
2775
+ for (const resolve of idleWaiters) resolve();
2776
+ idleWaiters.clear();
2777
+ };
2778
+ const waitForIdle = () => {
2779
+ if (isIdle()) return Promise.resolve();
2780
+ return new Promise((resolve) => idleWaiters.add(resolve));
2781
+ };
2782
+ const closeAdmission = () => {
2783
+ closed = true;
2784
+ };
2785
+ const isClosed = () => closed;
2355
2786
  const unboundedEvents = /* @__PURE__ */ new Set([
2356
2787
  "run.started",
2357
2788
  "run.recovered",
@@ -2403,6 +2834,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2403
2834
  return count;
2404
2835
  };
2405
2836
  const admitStart = async (kind, name, request) => {
2837
+ if (closed) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2406
2838
  if (!admission) return () => {};
2407
2839
  const running = activeTopLevelRuns() + pendingStarts;
2408
2840
  const tenant = tenantOf(request.context);
@@ -2459,35 +2891,30 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2459
2891
  }
2460
2892
  };
2461
2893
  const scheduleTraceExport = (event) => {
2462
- if (!observability || ![
2463
- "run.completed",
2464
- "run.failed",
2465
- "run.cancelled"
2466
- ].includes(event.type) || exportedRuns.has(event.runId)) return;
2467
- exportedRuns.add(event.runId);
2468
- let task;
2469
- task = (async () => {
2894
+ if (!observability || !isTraceTerminalEvent(event)) return;
2895
+ traceExports.enqueue(event.runId, async () => {
2470
2896
  const run = await runStore.getRun(event.runId);
2471
2897
  if (!run) throw new Error(`Run "${event.runId}" does not exist`);
2472
- if (isCoordinatorRun(run)) return;
2473
- const agentRun = run;
2474
- let modelRegistryName = agentRun.modelName;
2475
- let modelMetadata = agentRun.modelMetadata;
2476
- if (modelRegistryName === void 0) {
2477
- const agent = agents.get(agentRun.agentName);
2478
- if (!agent) throw new Error(`Agent "${agentRun.agentName}" is not registered`);
2479
- const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(agentRun.id) : void 0;
2480
- const resolved = resolveModel(agent, checkpoint?.modelName);
2481
- modelRegistryName = resolved.modelName;
2482
- modelMetadata = resolved.model.metadata;
2898
+ const coordinator = isCoordinatorRun(run);
2899
+ let modelRegistryName;
2900
+ let modelMetadata;
2901
+ if (!coordinator) {
2902
+ const agentRun = run;
2903
+ modelRegistryName = agentRun.modelName;
2904
+ modelMetadata = agentRun.modelMetadata;
2905
+ if (modelRegistryName === void 0) {
2906
+ const agent = agents.get(agentRun.agentName);
2907
+ if (!agent) throw new Error(`Agent "${agentRun.agentName}" is not registered`);
2908
+ const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(agentRun.id) : void 0;
2909
+ const resolved = resolveModel(agent, checkpoint?.modelName);
2910
+ modelRegistryName = resolved.modelName;
2911
+ modelMetadata = resolved.model.metadata;
2912
+ }
2483
2913
  }
2484
- const trace = buildRunTrace(agentRun, await runStore.listEvents(agentRun.id), modelRegistryName, modelMetadata, observability);
2485
- const failures = (await Promise.allSettled(observability.exporters.map((exporter) => Promise.resolve().then(() => exporter.export(trace))))).filter((result) => result.status === "rejected").map(({ reason }) => reason);
2914
+ const trace = buildRunTrace(run, await runStore.listEvents(run.id), modelRegistryName, modelMetadata, observability);
2915
+ const failures = (await Promise.allSettled(observability.exporters.map((exporter) => withTimeout(Promise.resolve().then(() => exporter.export(trace)), traceExports.timeoutMs, `Trace exporter timed out after ${traceExports.timeoutMs}ms for run "${run.id}"`)))).filter((result) => result.status === "rejected").map(({ reason }) => reason);
2486
2916
  if (failures.length) throw new AggregateError(failures, "Trace export failed");
2487
- })().catch((error) => {
2488
- exportFailures.push(error);
2489
- }).finally(() => pendingExports.delete(task));
2490
- pendingExports.add(task);
2917
+ });
2491
2918
  };
2492
2919
  const notifyObserver = (event) => {
2493
2920
  scheduleTraceExport(event);
@@ -2526,6 +2953,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2526
2953
  state.leaseTimer = void 0;
2527
2954
  state.leaseExpiryTimer = void 0;
2528
2955
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2956
+ notifyIdle();
2529
2957
  };
2530
2958
  const commit = async (state, options = {}) => {
2531
2959
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "This run requires a DurableRunStore", state.run.id);
@@ -2615,6 +3043,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2615
3043
  state.leaseTimer = void 0;
2616
3044
  state.leaseExpiryTimer = void 0;
2617
3045
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
3046
+ notifyIdle();
2618
3047
  };
2619
3048
  const commitWorkflow = async (state, update, events = []) => {
2620
3049
  state.commitQueue = state.commitQueue.then(async () => {
@@ -2648,8 +3077,11 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2648
3077
  activeRuns,
2649
3078
  activeWorkflowRuns,
2650
3079
  activeSessions,
2651
- pendingExports,
2652
- exportFailures,
3080
+ isIdle,
3081
+ waitForIdle,
3082
+ closeAdmission,
3083
+ isClosed,
3084
+ traceExports,
2653
3085
  runtimeOwner,
2654
3086
  admitStart,
2655
3087
  markInternalRun,
@@ -2680,7 +3112,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2680
3112
  */
2681
3113
  function createWorkflowEngine(composition, core, agentEngine) {
2682
3114
  const { models, agents, workflows, teams, runStore } = composition;
2683
- const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, admitStart, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
3115
+ const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, isClosed, admitStart, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
2684
3116
  const { prepareExecution, executeAgent, nextEvent, resumeAgent, cancelStoredAgent } = agentEngine;
2685
3117
  const cancelWorkflowExecution = async (state, reason = cancellationReason(state.request.signal)) => {
2686
3118
  if (state.leaseLost) {
@@ -2772,6 +3204,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
2772
3204
  }
2773
3205
  };
2774
3206
  const prepareWorkflowExecutionBody = async (name, request, kind = "workflow") => {
3207
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2775
3208
  const workflow = workflows.get(name);
2776
3209
  const team = kind === "team" ? teams.get(name) : void 0;
2777
3210
  if (!workflow || kind === "team" && !team || kind === "workflow" && teams.has(name)) throw new FevexRunError(kind === "team" ? "TEAM_NOT_FOUND" : "WORKFLOW_NOT_FOUND", `${kind === "team" ? "Team" : "Workflow"} "${name}" is not registered`);
@@ -3072,7 +3505,11 @@ function createWorkflowEngine(composition, core, agentEngine) {
3072
3505
  context: mergeExecutionContext(state.request.context, request.context),
3073
3506
  limits: combineLimits(request.limits, remainingWorkflowLimits(state.checkpoint.limits, state.checkpoint.budget)),
3074
3507
  signal: childSignal
3075
- }, { admit: false });
3508
+ }, {
3509
+ admit: false,
3510
+ parentRunId: state.run.id,
3511
+ rootRunId: state.run.rootRunId ?? state.run.id
3512
+ });
3076
3513
  const action = options?.metadata?.teamAction === "handoff" ? "handoff" : "delegate";
3077
3514
  const lifecycleEvents = [];
3078
3515
  if (isTeamRun(state.run) && action === "handoff") lifecycleEvents.push(createEvent(state, "team.handoff.created", {
@@ -3084,11 +3521,13 @@ function createWorkflowEngine(composition, core, agentEngine) {
3084
3521
  const started = createCoordinatorEvent(state, "workflow.step.started", {
3085
3522
  stepId,
3086
3523
  kind: "agent",
3087
- agentName
3524
+ agentName,
3525
+ childRunId: childState.run.id
3088
3526
  }, "team.agent.assigned", {
3089
3527
  delegationId: stepId,
3090
3528
  agentName,
3091
3529
  action,
3530
+ childRunId: childState.run.id,
3092
3531
  ...typeof options?.metadata?.expectedOutput === "string" ? { expectedOutput: options.metadata.expectedOutput } : {},
3093
3532
  ...Array.isArray(options?.metadata?.constraints) ? { constraints: options.metadata.constraints } : {}
3094
3533
  });
@@ -3772,24 +4211,76 @@ function createWorkflowEngine(composition, core, agentEngine) {
3772
4211
  }
3773
4212
  //#endregion
3774
4213
  //#region src/internal/runtime.ts
4214
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
4215
+ const SHUTDOWN_CLEANUP_MS = 5e3;
3775
4216
  function createRuntime(composition) {
3776
4217
  const { models, agents, workflows, teams, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
3777
4218
  const core = createRunCore(composition);
3778
- const { activeRuns, activeWorkflowRuns, activeSessions, pendingExports, exportFailures, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
4219
+ const { activeRuns, activeWorkflowRuns, activeSessions, isIdle, waitForIdle, closeAdmission, isClosed, traceExports, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
3779
4220
  const agentEngine = createAgentEngine(composition, core);
3780
4221
  const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent } = agentEngine;
3781
4222
  const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution, cancelStoredWorkflow } = createWorkflowEngine(composition, core, agentEngine);
3782
4223
  const resumeRun = async (runId, resolution) => {
4224
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3783
4225
  const run = await runStore.getRun(runId);
3784
4226
  if (run && isCoordinatorRun(run)) return resumeWorkflow(runId, resolution);
3785
4227
  return resumeAgent(runId, resolution);
3786
4228
  };
3787
4229
  const recoverRun = async (runId, actor) => {
4230
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3788
4231
  const run = await runStore.getRun(runId);
3789
4232
  if (!run) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be recovered`, runId);
3790
4233
  if (isCoordinatorRun(run)) return resumeWorkflow(runId, void 0, actor);
3791
4234
  return resumeAgent(runId, void 0, false, actor);
3792
4235
  };
4236
+ const waitForIdleUntil = async (deadline) => {
4237
+ if (isIdle()) return true;
4238
+ const remaining = deadline - Date.now();
4239
+ if (remaining <= 0) return false;
4240
+ return new Promise((resolve) => {
4241
+ let settled = false;
4242
+ let timer;
4243
+ const finish = (idle) => {
4244
+ if (settled) return;
4245
+ settled = true;
4246
+ if (timer) clearTimeout(timer);
4247
+ resolve(idle);
4248
+ };
4249
+ timer = setTimeout(() => finish(false), remaining);
4250
+ waitForIdle().then(() => finish(true));
4251
+ });
4252
+ };
4253
+ const shutdown = async (options = {}) => {
4254
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
4255
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) throw new TypeError("Shutdown timeoutMs must be a non-negative safe integer");
4256
+ closeAdmission();
4257
+ const deadline = Date.now() + timeoutMs;
4258
+ let cleanupDeadline = deadline;
4259
+ let timedOut = false;
4260
+ const cancelled = /* @__PURE__ */ new Set();
4261
+ if (!await waitForIdleUntil(deadline)) {
4262
+ timedOut = true;
4263
+ cleanupDeadline = Date.now() + SHUTDOWN_CLEANUP_MS;
4264
+ for (const { run } of activeWorkflowRuns.values()) cancelled.add(run.id);
4265
+ for (const { run } of activeRuns.values()) cancelled.add(run.id);
4266
+ for (const state of [...activeWorkflowRuns.values(), ...activeRuns.values()]) if (!state.request.signal.aborted) state.controller.abort(new DOMException("Run cancelled", "AbortError"));
4267
+ for (const state of [...activeWorkflowRuns.values()]) if (!state.advancing) await cancelWorkflowExecution(state).catch(() => {});
4268
+ for (const state of [...activeRuns.values()]) if (!state.advancing) await cancelExecution(state).catch(() => {});
4269
+ await waitForIdleUntil(cleanupDeadline);
4270
+ }
4271
+ let exportError;
4272
+ try {
4273
+ await traceExports.flush({ timeoutMs: Math.max(1, cleanupDeadline - Date.now()) });
4274
+ } catch (error) {
4275
+ exportError = error;
4276
+ }
4277
+ return {
4278
+ cancelledRunIds: [...cancelled],
4279
+ timedOut,
4280
+ ...exportError === void 0 ? {} : { exportError },
4281
+ observability: traceExports.stats()
4282
+ };
4283
+ };
3793
4284
  return {
3794
4285
  async startAgent(name, request) {
3795
4286
  const state = await prepareExecution(name, request);
@@ -3921,13 +4412,13 @@ function createRuntime(composition) {
3921
4412
  await runStore.saveSession(session);
3922
4413
  return structuredClone(session);
3923
4414
  },
3924
- async flushObservability() {
3925
- while (pendingExports.size) await Promise.all([...pendingExports]);
3926
- if (exportFailures.length) {
3927
- const failures = exportFailures.splice(0);
3928
- throw new AggregateError(failures, "Observability export failed");
3929
- }
3930
- }
4415
+ flushObservability(options) {
4416
+ return traceExports.flush(options);
4417
+ },
4418
+ getObservabilityStats() {
4419
+ return traceExports.stats();
4420
+ },
4421
+ shutdown
3931
4422
  };
3932
4423
  }
3933
4424
  //#endregion
@@ -3942,4 +4433,4 @@ function createFevex(config) {
3942
4433
  return createRuntime(createComposition(config));
3943
4434
  }
3944
4435
  //#endregion
3945
- export { ArtifactError, CatalogedArtifactStore, ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, FileSystemArtifactStore, InMemoryArtifactCatalog, InMemoryArtifactStore, InMemoryMemoryStore, InMemoryRunStore, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, S3ArtifactStore, SCHEMA_NOT_TRANSPORTABLE, SandboxError, artifactTenantId, assertArtifactId, compileFevexJsonSchema, createFevex, createLocalSandbox, createMemoryContextProvider, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, deliverChannelRun, handleChannelInput, isDurableRunStore, isRetainableRunStore, sandboxResultToJson, toToolSpec, validateFevexJsonSchemaProfile, validateListEventsOptions, validatePurgeRunsOptions };
4436
+ export { ArtifactError, CatalogedArtifactStore, ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, FileSystemArtifactStore, InMemoryArtifactCatalog, InMemoryArtifactStore, InMemoryMemoryStore, InMemoryRunStore, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, S3ArtifactStore, SCHEMA_NOT_TRANSPORTABLE, SandboxError, artifactTenantId, assertArtifactId, compileFevexJsonSchema, createAgentsMdContextProvider, createFevex, createLocalSandbox, createMemoryContextProvider, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, deliverChannelRun, handleChannelInput, isDurableRunStore, isProgressiveSkillProvider, isRetainableRunStore, loadSkillFile, loadSkillFiles, sandboxResultToJson, toToolSpec, validateFevexJsonSchemaProfile, validateListEventsOptions, validatePurgeRunsOptions };