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

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 (33) hide show
  1. package/README.md +73 -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-B4QY5Z6e.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-aayvtf0J.d.mts → index-BJktlD4E.d.mts} +2 -2
  12. package/dist/{index-BPwkgdo3.d.mts → index-BgKFd9yw.d.mts} +1 -1
  13. package/dist/{index-Cb3AB98n.d.mts → index-CbACw6xZ.d.mts} +2 -2
  14. package/dist/index-CpLDt6Rw.d.mts +118 -0
  15. package/dist/{index-Br-zPxZH.d.mts → index-CseuTR-F.d.mts} +2 -2
  16. package/dist/{index-CGmHXV31.d.mts → index-CzAi2Y_Z.d.mts} +13 -6
  17. package/dist/{index--_LdSEuF.d.mts → index-DVw0zOc_.d.mts} +3 -3
  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 +466 -64
  21. package/dist/knowledge/index.d.mts +1 -1
  22. package/dist/models/index.d.mts +1 -1
  23. package/dist/observability/index.d.mts +2 -2
  24. package/dist/policies/index.d.mts +1 -1
  25. package/dist/runtime/index.d.mts +2 -2
  26. package/dist/sandbox/index.d.mts +1 -1
  27. package/dist/teams/index.d.mts +1 -1
  28. package/dist/testing/index.d.mts +6 -6
  29. package/dist/tools/index.d.mts +2 -2
  30. package/dist/{types-BDVUoAU6.d.mts → types-B8laW1hM.d.mts} +2 -0
  31. package/dist/workflows/index.d.mts +1 -1
  32. package/package.json +1 -1
  33. package/dist/index-D6lnI92z.d.mts +0 -80
package/dist/index.mjs CHANGED
@@ -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
  }
@@ -890,8 +903,9 @@ function addWorkflowBudget(first, second) {
890
903
  */
891
904
  function createAgentEngine(composition, core) {
892
905
  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) => {
906
+ const { activeRuns, activeSessions, runtimeOwner, isClosed, admitStart, markInternalRun, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
907
+ const prepareExecutionBody = async (name, request, internal = false, correlation = {}) => {
908
+ if (!internal && isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
895
909
  const agent = agents.get(name);
896
910
  if (!agent) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${name}" is not registered`);
897
911
  const { modelName, model } = resolveModel(agent, request.model);
@@ -920,6 +934,8 @@ function createAgentEngine(composition, core) {
920
934
  agentName: name,
921
935
  modelName,
922
936
  ...model.metadata === void 0 ? {} : { modelMetadata: structuredClone(model.metadata) },
937
+ ...correlation.parentRunId === void 0 ? {} : { parentRunId: correlation.parentRunId },
938
+ ...correlation.rootRunId === void 0 ? {} : { rootRunId: correlation.rootRunId },
923
939
  status: "running",
924
940
  revision: 0,
925
941
  createdAt: now,
@@ -1010,7 +1026,10 @@ function createAgentEngine(composition, core) {
1010
1026
  const prepareExecution = async (name, request, options = {}) => {
1011
1027
  const release = options.admit === false ? void 0 : await admitStart("agent", name, request);
1012
1028
  try {
1013
- return await prepareExecutionBody(name, request, options.admit === false);
1029
+ return await prepareExecutionBody(name, request, options.admit === false, {
1030
+ ...options.parentRunId === void 0 ? {} : { parentRunId: options.parentRunId },
1031
+ ...options.rootRunId === void 0 ? {} : { rootRunId: options.rootRunId }
1032
+ });
1014
1033
  } finally {
1015
1034
  release?.();
1016
1035
  }
@@ -2053,16 +2072,31 @@ function scopedArtifacts(store, options) {
2053
2072
  const TERMINAL_TYPES = /* @__PURE__ */ new Set([
2054
2073
  "run.completed",
2055
2074
  "run.failed",
2056
- "run.cancelled"
2075
+ "run.cancelled",
2076
+ "workflow.run.completed",
2077
+ "workflow.run.failed",
2078
+ "workflow.run.cancelled",
2079
+ "team.run.completed",
2080
+ "team.run.failed",
2081
+ "team.run.cancelled"
2057
2082
  ]);
2058
2083
  const SENSITIVE_KEY = /api[-_]?key|authorization|cookie|credential|password|secret|token/i;
2084
+ function isTraceTerminalEvent(event) {
2085
+ return TERMINAL_TYPES.has(event.type);
2086
+ }
2059
2087
  function durationMs(startedAt, endedAt) {
2060
2088
  return Math.max(0, Date.parse(endedAt) - Date.parse(startedAt));
2061
2089
  }
2062
2090
  function traceStatus(event) {
2063
- if (event.type === "run.completed") return "completed";
2064
- if (event.type === "run.cancelled") return "cancelled";
2065
- return "failed";
2091
+ switch (event.type) {
2092
+ case "run.completed":
2093
+ case "workflow.run.completed":
2094
+ case "team.run.completed": return "completed";
2095
+ case "run.cancelled":
2096
+ case "workflow.run.cancelled":
2097
+ case "team.run.cancelled": return "cancelled";
2098
+ default: return "failed";
2099
+ }
2066
2100
  }
2067
2101
  function redactKeys(value) {
2068
2102
  if (Array.isArray(value)) return value.map(redactKeys);
@@ -2169,20 +2203,74 @@ function annotation(event) {
2169
2203
  toolName: event.payload.toolName
2170
2204
  }
2171
2205
  };
2206
+ case "workflow.run.paused":
2207
+ case "team.run.paused": {
2208
+ const stepId = event.type === "workflow.run.paused" ? event.payload.stepId : event.payload.delegationId;
2209
+ return {
2210
+ name: event.type,
2211
+ timestamp: event.timestamp,
2212
+ attributes: {
2213
+ stepId,
2214
+ reason: event.payload.reason,
2215
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2216
+ }
2217
+ };
2218
+ }
2219
+ case "workflow.run.resumed":
2220
+ case "team.run.resumed": return {
2221
+ name: event.type,
2222
+ timestamp: event.timestamp
2223
+ };
2224
+ case "team.handoff.created": return {
2225
+ name: event.type,
2226
+ timestamp: event.timestamp,
2227
+ attributes: {
2228
+ delegationId: event.payload.delegationId,
2229
+ from: event.payload.from,
2230
+ to: event.payload.to,
2231
+ reason: event.payload.reason
2232
+ }
2233
+ };
2172
2234
  default: return;
2173
2235
  }
2174
2236
  }
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`);
2237
+ function terminalContent(options, terminal) {
2238
+ if (terminal.type === "run.completed" || terminal.type === "workflow.run.completed" || terminal.type === "team.run.completed") {
2239
+ const output = content(options, "run.output", terminal.payload.output);
2240
+ return output ? [output] : [];
2241
+ }
2242
+ if (terminal.type === "run.failed" || terminal.type === "workflow.run.failed" || terminal.type === "team.run.failed") {
2243
+ const error = content(options, "error.message", terminal.payload.error);
2244
+ return error ? [error] : [];
2245
+ }
2246
+ return [];
2247
+ }
2248
+ function sortedEvents(rawEvents) {
2249
+ return [...rawEvents].sort((left, right) => left.sequence - right.sequence);
2250
+ }
2251
+ function terminalEvent(runId, events) {
2252
+ const terminal = [...events].reverse().find((event) => isTraceTerminalEvent(event));
2253
+ if (!terminal) throw new TypeError(`Run "${runId}" has no terminal event`);
2254
+ return terminal;
2255
+ }
2256
+ function startEvent(events) {
2257
+ const started = events.find((event) => event.type === "run.started" || event.type === "workflow.run.started" || event.type === "team.run.started") ?? events[0];
2258
+ if (!started) throw new TypeError("Run has no events");
2259
+ return started;
2260
+ }
2261
+ function traceKind(run) {
2262
+ if (isTeamRun(run)) return "team";
2263
+ return run.kind === "workflow" ? "workflow" : "agent";
2264
+ }
2265
+ function buildAgentTrace(run, events, modelRegistryName, model, options) {
2266
+ const terminal = terminalEvent(run.id, events);
2267
+ const started = startEvent(events);
2181
2268
  const status = traceStatus(terminal);
2182
2269
  const annotations = [];
2183
2270
  const spans = [];
2184
2271
  const openModels = /* @__PURE__ */ new Map();
2185
2272
  const openTools = /* @__PURE__ */ new Map();
2273
+ const modelSpanByStep = /* @__PURE__ */ new Map();
2186
2274
  const previousUsage = {};
2187
2275
  const closeModel = (step, endedAt, spanStatus) => {
2188
2276
  const open = openModels.get(step);
@@ -2213,6 +2301,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2213
2301
  const spanCost = costFor(options, modelRegistryName, model, usage);
2214
2302
  if (spanCost) span.cost = spanCost;
2215
2303
  spans.push(span);
2304
+ modelSpanByStep.set(step, span.id);
2216
2305
  };
2217
2306
  const findOpenTool = (toolCallId) => {
2218
2307
  const entries = [...openTools.entries()];
@@ -2232,7 +2321,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2232
2321
  const errorContent = error ? content(options, "error.message", error) : void 0;
2233
2322
  spans.push({
2234
2323
  id: event.id,
2235
- parentId: run.id,
2324
+ parentId: modelSpanByStep.get(event.payload.step) ?? run.id,
2236
2325
  kind: "tool",
2237
2326
  name: event.payload.toolName,
2238
2327
  startedAt: event.timestamp,
@@ -2297,19 +2386,15 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2297
2386
  for (const open of [...openTools.values()]) if (open.event.type === "tool.started") closeTool(open.event.payload.toolCallId, terminal.timestamp, status);
2298
2387
  const finalUsage = run.usage ?? previousUsage;
2299
2388
  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
- }
2389
+ const traceContent = terminalContent(options, terminal);
2308
2390
  return {
2309
2391
  schemaVersion: "1",
2392
+ kind: "agent",
2310
2393
  traceId: run.id,
2311
2394
  runId: run.id,
2312
2395
  sessionId: run.sessionId,
2396
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
2397
+ rootRunId: run.rootRunId ?? run.parentRunId ?? run.id,
2313
2398
  agentName: run.agentName,
2314
2399
  modelRegistryName,
2315
2400
  ...model ? { model: { ...model } } : {},
@@ -2329,6 +2414,249 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2329
2414
  ...traceContent.length ? { content: traceContent } : {}
2330
2415
  };
2331
2416
  }
2417
+ function buildCoordinatorTrace(run, events, options) {
2418
+ const terminal = terminalEvent(run.id, events);
2419
+ const started = startEvent(events);
2420
+ const status = traceStatus(terminal);
2421
+ const annotations = [];
2422
+ const spans = [];
2423
+ const openSteps = /* @__PURE__ */ new Map();
2424
+ const stepKey = (scope, stepId) => `${scope}:${stepId}`;
2425
+ const modelCallCount = events.filter(({ type }) => type === "model.completed").length;
2426
+ const toolCallCount = events.filter(({ type }) => type === "tool.completed" || type === "tool.failed").length;
2427
+ const openStep = (key, event, name, attributes) => {
2428
+ openSteps.set(key, {
2429
+ event,
2430
+ name,
2431
+ attributes
2432
+ });
2433
+ };
2434
+ const closeStep = (key, endedAt, spanStatus, error) => {
2435
+ const open = openSteps.get(key);
2436
+ if (!open) return;
2437
+ openSteps.delete(key);
2438
+ const errorContent = error ? content(options, "error.message", error) : void 0;
2439
+ spans.push({
2440
+ id: open.event.id,
2441
+ parentId: run.id,
2442
+ kind: "step",
2443
+ name: open.name,
2444
+ startedAt: open.event.timestamp,
2445
+ endedAt,
2446
+ durationMs: durationMs(open.event.timestamp, endedAt),
2447
+ status: spanStatus,
2448
+ attributes: open.attributes,
2449
+ annotations: [],
2450
+ ...errorContent ? { content: [errorContent] } : {}
2451
+ });
2452
+ };
2453
+ for (const event of events) {
2454
+ const item = annotation(event);
2455
+ if (item) annotations.push(item);
2456
+ switch (event.type) {
2457
+ case "workflow.step.started":
2458
+ openStep(stepKey("workflow", event.payload.stepId), event, event.payload.stepId, {
2459
+ stepId: event.payload.stepId,
2460
+ stepKind: event.payload.kind,
2461
+ ...event.payload.agentName === void 0 ? {} : { agentName: event.payload.agentName },
2462
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2463
+ });
2464
+ break;
2465
+ case "workflow.step.completed":
2466
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "completed");
2467
+ break;
2468
+ case "workflow.step.failed":
2469
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "failed", event.payload.error);
2470
+ break;
2471
+ case "workflow.wait.started":
2472
+ openStep(stepKey("workflow", event.payload.stepId), event, event.payload.stepId, {
2473
+ stepId: event.payload.stepId,
2474
+ stepKind: "wait",
2475
+ waitKind: event.payload.kind,
2476
+ ...event.payload.resumeAt === void 0 ? {} : { resumeAt: event.payload.resumeAt },
2477
+ ...event.payload.eventName === void 0 ? {} : { eventName: event.payload.eventName }
2478
+ });
2479
+ break;
2480
+ case "workflow.wait.completed":
2481
+ closeStep(stepKey("workflow", event.payload.stepId), event.timestamp, "completed");
2482
+ break;
2483
+ case "team.agent.assigned":
2484
+ openStep(stepKey("team", event.payload.delegationId), event, event.payload.delegationId, {
2485
+ stepId: event.payload.delegationId,
2486
+ stepKind: "agent",
2487
+ agentName: event.payload.agentName,
2488
+ action: event.payload.action,
2489
+ ...event.payload.expectedOutput === void 0 ? {} : { expectedOutput: event.payload.expectedOutput },
2490
+ ...event.payload.constraints === void 0 ? {} : { constraints: event.payload.constraints },
2491
+ ...event.payload.childRunId === void 0 ? {} : { childRunId: event.payload.childRunId }
2492
+ });
2493
+ break;
2494
+ case "team.task.completed":
2495
+ closeStep(stepKey("team", event.payload.delegationId), event.timestamp, "completed");
2496
+ break;
2497
+ case "team.task.failed":
2498
+ closeStep(stepKey("team", event.payload.delegationId), event.timestamp, "failed", event.payload.error);
2499
+ break;
2500
+ case "team.merge.started":
2501
+ openStep(stepKey("team", event.payload.stepId), event, event.payload.stepId, {
2502
+ stepId: event.payload.stepId,
2503
+ stepKind: "merge",
2504
+ width: event.payload.width
2505
+ });
2506
+ break;
2507
+ case "team.merge.completed":
2508
+ closeStep(stepKey("team", event.payload.stepId), event.timestamp, "completed");
2509
+ break;
2510
+ case "team.merge.failed":
2511
+ closeStep(stepKey("team", event.payload.stepId), event.timestamp, "failed", event.payload.error);
2512
+ break;
2513
+ }
2514
+ }
2515
+ for (const key of [...openSteps.keys()]) closeStep(key, terminal.timestamp, status);
2516
+ const traceContent = terminalContent(options, terminal);
2517
+ return {
2518
+ schemaVersion: "1",
2519
+ kind: traceKind(run),
2520
+ traceId: run.id,
2521
+ runId: run.id,
2522
+ sessionId: run.sessionId,
2523
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
2524
+ rootRunId: run.rootRunId ?? run.parentRunId ?? run.id,
2525
+ agentName: coordinatorName(run),
2526
+ startedAt: started.timestamp,
2527
+ endedAt: terminal.timestamp,
2528
+ durationMs: durationMs(started.timestamp, terminal.timestamp),
2529
+ status,
2530
+ attributes: {
2531
+ eventCount: events.length,
2532
+ stepCount: spans.filter(({ kind }) => kind === "step").length,
2533
+ modelCallCount,
2534
+ toolCallCount
2535
+ },
2536
+ annotations,
2537
+ spans,
2538
+ ...run.usage && Object.keys(run.usage).length ? { usage: { ...run.usage } } : {},
2539
+ ...traceContent.length ? { content: traceContent } : {}
2540
+ };
2541
+ }
2542
+ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2543
+ const events = sortedEvents(rawEvents);
2544
+ if (isCoordinatorRun(run)) return buildCoordinatorTrace(run, events, options);
2545
+ if (!modelRegistryName) throw new TypeError(`Run "${run.id}" has no model registry name`);
2546
+ return buildAgentTrace(run, events, modelRegistryName, model, options);
2547
+ }
2548
+ //#endregion
2549
+ //#region src/internal/trace-export.ts
2550
+ const DEFAULT_CONCURRENCY = 4;
2551
+ const DEFAULT_MAX_QUEUED = 100;
2552
+ const DEFAULT_TIMEOUT_MS = 3e4;
2553
+ const DEFAULT_MAX_FAILURES = 100;
2554
+ const DEDUPE_WINDOW = 1e4;
2555
+ function withTimeout(promise, timeoutMs, message) {
2556
+ return new Promise((resolve, reject) => {
2557
+ const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
2558
+ promise.then((value) => {
2559
+ clearTimeout(timer);
2560
+ resolve(value);
2561
+ }, (error) => {
2562
+ clearTimeout(timer);
2563
+ reject(error);
2564
+ });
2565
+ });
2566
+ }
2567
+ function withDefault(value, fallback) {
2568
+ return value ?? fallback;
2569
+ }
2570
+ /**
2571
+ * Bounded, deduplicated trace export queue. Jobs include building the trace
2572
+ * from the store, so a slow store or exporter cannot grow memory without limit.
2573
+ */
2574
+ function createTraceExportQueue(options = {}) {
2575
+ const concurrency = withDefault(options.concurrency, DEFAULT_CONCURRENCY);
2576
+ const maxQueued = withDefault(options.maxQueued, DEFAULT_MAX_QUEUED);
2577
+ const timeoutMs = withDefault(options.timeoutMs, DEFAULT_TIMEOUT_MS);
2578
+ const maxFailures = withDefault(options.maxFailures, DEFAULT_MAX_FAILURES);
2579
+ const queue = [];
2580
+ const pending = /* @__PURE__ */ new Set();
2581
+ const exportedRuns = /* @__PURE__ */ new Set();
2582
+ const failures = [];
2583
+ let active = 0;
2584
+ let exported = 0;
2585
+ let failed = 0;
2586
+ let dropped = 0;
2587
+ let unreported = 0;
2588
+ const rememberRun = (runId) => {
2589
+ if (exportedRuns.has(runId)) return false;
2590
+ if (exportedRuns.size >= DEDUPE_WINDOW) {
2591
+ const oldest = exportedRuns.values().next().value;
2592
+ if (oldest !== void 0) exportedRuns.delete(oldest);
2593
+ }
2594
+ exportedRuns.add(runId);
2595
+ return true;
2596
+ };
2597
+ const recordFailure = (error) => {
2598
+ failed += 1;
2599
+ unreported += 1;
2600
+ failures.push(error);
2601
+ if (failures.length > maxFailures) failures.shift();
2602
+ };
2603
+ const startNext = () => {
2604
+ while (active < concurrency && queue.length) {
2605
+ const task = queue.shift();
2606
+ active += 1;
2607
+ let job;
2608
+ job = Promise.resolve().then(task).then(() => {
2609
+ exported += 1;
2610
+ }, (error) => {
2611
+ recordFailure(error);
2612
+ }).finally(() => {
2613
+ active -= 1;
2614
+ pending.delete(job);
2615
+ startNext();
2616
+ });
2617
+ pending.add(job);
2618
+ }
2619
+ };
2620
+ return {
2621
+ timeoutMs,
2622
+ enqueue(runId, task) {
2623
+ if (!rememberRun(runId)) return;
2624
+ if (queue.length >= maxQueued) {
2625
+ dropped += 1;
2626
+ recordFailure(/* @__PURE__ */ new Error(`Observability export queue is full; trace for run "${runId}" was dropped`));
2627
+ return;
2628
+ }
2629
+ queue.push(task);
2630
+ startNext();
2631
+ },
2632
+ async flush(flushOptions) {
2633
+ const timeout = flushOptions?.timeoutMs;
2634
+ if (timeout !== void 0 && (typeof timeout !== "number" || !Number.isSafeInteger(timeout) || timeout <= 0)) throw new Error("Observability flush timeoutMs must be a positive safe integer");
2635
+ const drain = async () => {
2636
+ while (pending.size) await Promise.all([...pending]);
2637
+ };
2638
+ if (timeout === void 0) await drain();
2639
+ else await withTimeout(drain(), timeout, `Observability flush timed out after ${timeout}ms`);
2640
+ if (unreported === 0) return;
2641
+ const errors = [...failures];
2642
+ const omitted = unreported - failures.length;
2643
+ if (omitted > 0) errors.push(/* @__PURE__ */ new Error(`${omitted} additional export failures were not retained`));
2644
+ failures.length = 0;
2645
+ unreported = 0;
2646
+ throw new AggregateError(errors, `Observability export failed (${failed} failure(s), ${dropped} dropped trace(s))`);
2647
+ },
2648
+ stats() {
2649
+ return {
2650
+ exported,
2651
+ failed,
2652
+ dropped,
2653
+ queued: queue.length,
2654
+ active,
2655
+ retainedFailures: failures.length
2656
+ };
2657
+ }
2658
+ };
2659
+ }
2332
2660
  //#endregion
2333
2661
  //#region src/internal/run-core.ts
2334
2662
  /**
@@ -2347,11 +2675,25 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2347
2675
  const activeSessions = /* @__PURE__ */ new Set();
2348
2676
  const internalRuns = /* @__PURE__ */ new Set();
2349
2677
  const pendingTenantStarts = /* @__PURE__ */ new Map();
2678
+ const idleWaiters = /* @__PURE__ */ new Set();
2350
2679
  let pendingStarts = 0;
2351
- const pendingExports = /* @__PURE__ */ new Set();
2352
- const exportFailures = [];
2353
- const exportedRuns = /* @__PURE__ */ new Set();
2680
+ let closed = false;
2681
+ const traceExports = createTraceExportQueue(observability?.exportQueue);
2354
2682
  const runtimeOwner = crypto.randomUUID();
2683
+ const isIdle = () => activeRuns.size === 0 && activeWorkflowRuns.size === 0;
2684
+ const notifyIdle = () => {
2685
+ if (!isIdle()) return;
2686
+ for (const resolve of idleWaiters) resolve();
2687
+ idleWaiters.clear();
2688
+ };
2689
+ const waitForIdle = () => {
2690
+ if (isIdle()) return Promise.resolve();
2691
+ return new Promise((resolve) => idleWaiters.add(resolve));
2692
+ };
2693
+ const closeAdmission = () => {
2694
+ closed = true;
2695
+ };
2696
+ const isClosed = () => closed;
2355
2697
  const unboundedEvents = /* @__PURE__ */ new Set([
2356
2698
  "run.started",
2357
2699
  "run.recovered",
@@ -2403,6 +2745,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2403
2745
  return count;
2404
2746
  };
2405
2747
  const admitStart = async (kind, name, request) => {
2748
+ if (closed) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2406
2749
  if (!admission) return () => {};
2407
2750
  const running = activeTopLevelRuns() + pendingStarts;
2408
2751
  const tenant = tenantOf(request.context);
@@ -2459,35 +2802,30 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2459
2802
  }
2460
2803
  };
2461
2804
  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 () => {
2805
+ if (!observability || !isTraceTerminalEvent(event)) return;
2806
+ traceExports.enqueue(event.runId, async () => {
2470
2807
  const run = await runStore.getRun(event.runId);
2471
2808
  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;
2809
+ const coordinator = isCoordinatorRun(run);
2810
+ let modelRegistryName;
2811
+ let modelMetadata;
2812
+ if (!coordinator) {
2813
+ const agentRun = run;
2814
+ modelRegistryName = agentRun.modelName;
2815
+ modelMetadata = agentRun.modelMetadata;
2816
+ if (modelRegistryName === void 0) {
2817
+ const agent = agents.get(agentRun.agentName);
2818
+ if (!agent) throw new Error(`Agent "${agentRun.agentName}" is not registered`);
2819
+ const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(agentRun.id) : void 0;
2820
+ const resolved = resolveModel(agent, checkpoint?.modelName);
2821
+ modelRegistryName = resolved.modelName;
2822
+ modelMetadata = resolved.model.metadata;
2823
+ }
2483
2824
  }
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);
2825
+ const trace = buildRunTrace(run, await runStore.listEvents(run.id), modelRegistryName, modelMetadata, observability);
2826
+ 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
2827
  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);
2828
+ });
2491
2829
  };
2492
2830
  const notifyObserver = (event) => {
2493
2831
  scheduleTraceExport(event);
@@ -2526,6 +2864,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2526
2864
  state.leaseTimer = void 0;
2527
2865
  state.leaseExpiryTimer = void 0;
2528
2866
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2867
+ notifyIdle();
2529
2868
  };
2530
2869
  const commit = async (state, options = {}) => {
2531
2870
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "This run requires a DurableRunStore", state.run.id);
@@ -2615,6 +2954,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2615
2954
  state.leaseTimer = void 0;
2616
2955
  state.leaseExpiryTimer = void 0;
2617
2956
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2957
+ notifyIdle();
2618
2958
  };
2619
2959
  const commitWorkflow = async (state, update, events = []) => {
2620
2960
  state.commitQueue = state.commitQueue.then(async () => {
@@ -2648,8 +2988,11 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2648
2988
  activeRuns,
2649
2989
  activeWorkflowRuns,
2650
2990
  activeSessions,
2651
- pendingExports,
2652
- exportFailures,
2991
+ isIdle,
2992
+ waitForIdle,
2993
+ closeAdmission,
2994
+ isClosed,
2995
+ traceExports,
2653
2996
  runtimeOwner,
2654
2997
  admitStart,
2655
2998
  markInternalRun,
@@ -2680,7 +3023,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2680
3023
  */
2681
3024
  function createWorkflowEngine(composition, core, agentEngine) {
2682
3025
  const { models, agents, workflows, teams, runStore } = composition;
2683
- const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, admitStart, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
3026
+ const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, isClosed, admitStart, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
2684
3027
  const { prepareExecution, executeAgent, nextEvent, resumeAgent, cancelStoredAgent } = agentEngine;
2685
3028
  const cancelWorkflowExecution = async (state, reason = cancellationReason(state.request.signal)) => {
2686
3029
  if (state.leaseLost) {
@@ -2772,6 +3115,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
2772
3115
  }
2773
3116
  };
2774
3117
  const prepareWorkflowExecutionBody = async (name, request, kind = "workflow") => {
3118
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2775
3119
  const workflow = workflows.get(name);
2776
3120
  const team = kind === "team" ? teams.get(name) : void 0;
2777
3121
  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 +3416,11 @@ function createWorkflowEngine(composition, core, agentEngine) {
3072
3416
  context: mergeExecutionContext(state.request.context, request.context),
3073
3417
  limits: combineLimits(request.limits, remainingWorkflowLimits(state.checkpoint.limits, state.checkpoint.budget)),
3074
3418
  signal: childSignal
3075
- }, { admit: false });
3419
+ }, {
3420
+ admit: false,
3421
+ parentRunId: state.run.id,
3422
+ rootRunId: state.run.rootRunId ?? state.run.id
3423
+ });
3076
3424
  const action = options?.metadata?.teamAction === "handoff" ? "handoff" : "delegate";
3077
3425
  const lifecycleEvents = [];
3078
3426
  if (isTeamRun(state.run) && action === "handoff") lifecycleEvents.push(createEvent(state, "team.handoff.created", {
@@ -3084,11 +3432,13 @@ function createWorkflowEngine(composition, core, agentEngine) {
3084
3432
  const started = createCoordinatorEvent(state, "workflow.step.started", {
3085
3433
  stepId,
3086
3434
  kind: "agent",
3087
- agentName
3435
+ agentName,
3436
+ childRunId: childState.run.id
3088
3437
  }, "team.agent.assigned", {
3089
3438
  delegationId: stepId,
3090
3439
  agentName,
3091
3440
  action,
3441
+ childRunId: childState.run.id,
3092
3442
  ...typeof options?.metadata?.expectedOutput === "string" ? { expectedOutput: options.metadata.expectedOutput } : {},
3093
3443
  ...Array.isArray(options?.metadata?.constraints) ? { constraints: options.metadata.constraints } : {}
3094
3444
  });
@@ -3772,24 +4122,76 @@ function createWorkflowEngine(composition, core, agentEngine) {
3772
4122
  }
3773
4123
  //#endregion
3774
4124
  //#region src/internal/runtime.ts
4125
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
4126
+ const SHUTDOWN_CLEANUP_MS = 5e3;
3775
4127
  function createRuntime(composition) {
3776
4128
  const { models, agents, workflows, teams, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
3777
4129
  const core = createRunCore(composition);
3778
- const { activeRuns, activeWorkflowRuns, activeSessions, pendingExports, exportFailures, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
4130
+ const { activeRuns, activeWorkflowRuns, activeSessions, isIdle, waitForIdle, closeAdmission, isClosed, traceExports, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, cancelExecution, releaseWorkflowExecution, commitWorkflow } = core;
3779
4131
  const agentEngine = createAgentEngine(composition, core);
3780
4132
  const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent } = agentEngine;
3781
4133
  const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution, cancelStoredWorkflow } = createWorkflowEngine(composition, core, agentEngine);
3782
4134
  const resumeRun = async (runId, resolution) => {
4135
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3783
4136
  const run = await runStore.getRun(runId);
3784
4137
  if (run && isCoordinatorRun(run)) return resumeWorkflow(runId, resolution);
3785
4138
  return resumeAgent(runId, resolution);
3786
4139
  };
3787
4140
  const recoverRun = async (runId, actor) => {
4141
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3788
4142
  const run = await runStore.getRun(runId);
3789
4143
  if (!run) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be recovered`, runId);
3790
4144
  if (isCoordinatorRun(run)) return resumeWorkflow(runId, void 0, actor);
3791
4145
  return resumeAgent(runId, void 0, false, actor);
3792
4146
  };
4147
+ const waitForIdleUntil = async (deadline) => {
4148
+ if (isIdle()) return true;
4149
+ const remaining = deadline - Date.now();
4150
+ if (remaining <= 0) return false;
4151
+ return new Promise((resolve) => {
4152
+ let settled = false;
4153
+ let timer;
4154
+ const finish = (idle) => {
4155
+ if (settled) return;
4156
+ settled = true;
4157
+ if (timer) clearTimeout(timer);
4158
+ resolve(idle);
4159
+ };
4160
+ timer = setTimeout(() => finish(false), remaining);
4161
+ waitForIdle().then(() => finish(true));
4162
+ });
4163
+ };
4164
+ const shutdown = async (options = {}) => {
4165
+ const timeoutMs = options.timeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
4166
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0) throw new TypeError("Shutdown timeoutMs must be a non-negative safe integer");
4167
+ closeAdmission();
4168
+ const deadline = Date.now() + timeoutMs;
4169
+ let cleanupDeadline = deadline;
4170
+ let timedOut = false;
4171
+ const cancelled = /* @__PURE__ */ new Set();
4172
+ if (!await waitForIdleUntil(deadline)) {
4173
+ timedOut = true;
4174
+ cleanupDeadline = Date.now() + SHUTDOWN_CLEANUP_MS;
4175
+ for (const { run } of activeWorkflowRuns.values()) cancelled.add(run.id);
4176
+ for (const { run } of activeRuns.values()) cancelled.add(run.id);
4177
+ for (const state of [...activeWorkflowRuns.values(), ...activeRuns.values()]) if (!state.request.signal.aborted) state.controller.abort(new DOMException("Run cancelled", "AbortError"));
4178
+ for (const state of [...activeWorkflowRuns.values()]) if (!state.advancing) await cancelWorkflowExecution(state).catch(() => {});
4179
+ for (const state of [...activeRuns.values()]) if (!state.advancing) await cancelExecution(state).catch(() => {});
4180
+ await waitForIdleUntil(cleanupDeadline);
4181
+ }
4182
+ let exportError;
4183
+ try {
4184
+ await traceExports.flush({ timeoutMs: Math.max(1, cleanupDeadline - Date.now()) });
4185
+ } catch (error) {
4186
+ exportError = error;
4187
+ }
4188
+ return {
4189
+ cancelledRunIds: [...cancelled],
4190
+ timedOut,
4191
+ ...exportError === void 0 ? {} : { exportError },
4192
+ observability: traceExports.stats()
4193
+ };
4194
+ };
3793
4195
  return {
3794
4196
  async startAgent(name, request) {
3795
4197
  const state = await prepareExecution(name, request);
@@ -3921,13 +4323,13 @@ function createRuntime(composition) {
3921
4323
  await runStore.saveSession(session);
3922
4324
  return structuredClone(session);
3923
4325
  },
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
- }
4326
+ flushObservability(options) {
4327
+ return traceExports.flush(options);
4328
+ },
4329
+ getObservabilityStats() {
4330
+ return traceExports.stats();
4331
+ },
4332
+ shutdown
3931
4333
  };
3932
4334
  }
3933
4335
  //#endregion