@fevex/core 0.1.0-alpha.4 → 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 (42) hide show
  1. package/README.md +240 -11
  2. package/dist/agents/index.d.mts +1 -1
  3. package/dist/artifacts/index.d.mts +2 -0
  4. package/dist/artifacts/index.mjs +2 -0
  5. package/dist/artifacts-gUzemET7.mjs +440 -0
  6. package/dist/channels/index.d.mts +1 -1
  7. package/dist/evals/index.d.mts +4 -4
  8. package/dist/fevex-B4QY5Z6e.d.mts +119 -0
  9. package/dist/http/index.d.mts +65 -5
  10. package/dist/http/index.mjs +238 -16
  11. package/dist/{index-C-IFfCNw.d.mts → index-1cHr7KB_.d.mts} +3 -3
  12. package/dist/{index-CqOjvtQ9.d.mts → index-5p3q2yIp.d.mts} +4 -2
  13. package/dist/{index-hhbgvfIt.d.mts → index-BJktlD4E.d.mts} +2 -2
  14. package/dist/{index-CTIwrVf9.d.mts → index-BgKFd9yw.d.mts} +1 -1
  15. package/dist/{index-dkFyTNh4.d.mts → index-CbACw6xZ.d.mts} +2 -2
  16. package/dist/index-CpLDt6Rw.d.mts +118 -0
  17. package/dist/{index-CknU-glw.d.mts → index-CseuTR-F.d.mts} +2 -2
  18. package/dist/{index-CaE864Kz.d.mts → index-CzAi2Y_Z.d.mts} +45 -6
  19. package/dist/{index-DyyNLuwO.d.mts → index-DVw0zOc_.d.mts} +3 -3
  20. package/dist/{index-CrOId2px.d.mts → index-Df84VWmN.d.mts} +1 -1
  21. package/dist/index.d.mts +13 -13
  22. package/dist/index.mjs +904 -153
  23. package/dist/knowledge/index.d.mts +1 -1
  24. package/dist/model-stream-JQdHDDcj.mjs +131 -0
  25. package/dist/models/index.d.mts +1 -1
  26. package/dist/observability/index.d.mts +2 -2
  27. package/dist/policies/index.d.mts +1 -1
  28. package/dist/{model-stream-u2xMsEaf.mjs → run-support-CmZ6Xo6H.mjs} +41 -130
  29. package/dist/runtime/index.d.mts +2 -2
  30. package/dist/runtime/index.mjs +43 -1
  31. package/dist/sandbox/index.d.mts +1 -1
  32. package/dist/sandbox/index.mjs +1 -1
  33. package/dist/teams/index.d.mts +1 -1
  34. package/dist/testing/index.d.mts +8 -7
  35. package/dist/testing/index.mjs +94 -2
  36. package/dist/tools/index.d.mts +2 -2
  37. package/dist/types-B8laW1hM.d.mts +561 -0
  38. package/dist/workflows/index.d.mts +1 -1
  39. package/package.json +6 -2
  40. package/dist/fevex-BoPbZ-Tw.d.mts +0 -60
  41. package/dist/index-voIZhQXB.d.mts +0 -80
  42. package/dist/types-CuoRx12Q.d.mts +0 -333
package/dist/index.mjs CHANGED
@@ -1,11 +1,13 @@
1
1
  import { defineAgent } from "./agents/index.mjs";
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";
2
3
  import { n as RunPausedError, t as FevexRunError } from "./run-error-Cr17wPGH.mjs";
3
4
  import { ChannelError, deliverChannelRun, handleChannelInput } from "./channels/index.mjs";
4
5
  import { InMemoryMemoryStore, createMemoryContextProvider, defineContextProvider, defineSkill } from "./knowledge/index.mjs";
5
6
  import { n as teamAsWorkflow, t as defineTeam } from "./teams-Ctz676G4.mjs";
6
- import { InMemoryRunStore, isDurableRunStore, validateListEventsOptions } from "./runtime/index.mjs";
7
+ import { InMemoryRunStore, isDurableRunStore, isRetainableRunStore, validateListEventsOptions, validatePurgeRunsOptions } from "./runtime/index.mjs";
7
8
  import { FEVEX_JSON_SCHEMA_PROFILE_VERSION, IntegrationError, compileFevexJsonSchema, defineConnection, defineTool, toToolSpec, validateFevexJsonSchemaProfile } from "./tools/index.mjs";
8
- import { a as assertTokenBudget, c as remainingOutputTokens, d as serializeValue, f as toJsonValue, i as assertContinuationBudget, l as toErrorMessage, n as abortable, o as cancellationReason, r as addUsage, s as eventUsage, t as readModelStream, u as serializeJsonValue } from "./model-stream-u2xMsEaf.mjs";
9
+ import { _ as serializeValue, a as assertTokenBudget, c as eventUsage, d as remainingDeadlineMs, f as remainingOutputTokens, g as serializeJsonValue, h as toErrorMessage, i as assertContinuationBudget, l as isTimeoutError, m as runSignal, n as abortable, o as cancellationReason, p as resumeDeadlineSignal, r as addUsage, s as deadlineAtFromMs, u as minDeadline, v as toJsonValue } from "./run-support-CmZ6Xo6H.mjs";
10
+ import { t as readModelStream } from "./model-stream-JQdHDDcj.mjs";
9
11
  import "./models/index.mjs";
10
12
  import { definePolicy } from "./policies/index.mjs";
11
13
  import { SandboxError, createLocalSandbox, sandboxResultToJson } from "./sandbox/index.mjs";
@@ -103,6 +105,9 @@ function isRunStore(value) {
103
105
  function isSandbox(value) {
104
106
  return isRecord(value) && typeof value.run === "function";
105
107
  }
108
+ function isArtifactStore(value) {
109
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof value.put === "function" && typeof value.get === "function";
110
+ }
106
111
  function isJsonObject(value) {
107
112
  return isRecord(value);
108
113
  }
@@ -300,7 +305,21 @@ function createComposition(config) {
300
305
  assertConfiguration(config.runStore === void 0 || isRunStore(config.runStore), "INVALID_CONFIG", "Fevex config \"runStore\" must implement RunStore");
301
306
  assertConfiguration(config.credentialStore === void 0 || typeof config.credentialStore?.resolve === "function", "INVALID_CONFIG", "Fevex config \"credentialStore\" must implement resolve");
302
307
  assertConfiguration(config.sandbox === void 0 || isSandbox(config.sandbox), "INVALID_CONFIG", "Fevex config \"sandbox\" must implement Sandbox");
308
+ assertConfiguration(config.artifacts === void 0 || isArtifactStore(config.artifacts), "INVALID_CONFIG", "Fevex config \"artifacts\" must implement ArtifactStore");
303
309
  assertConfiguration(config.policies === void 0 || Array.isArray(config.policies), "INVALID_CONFIG", "Fevex config \"policies\" must be an array");
310
+ if (config.admission !== void 0) {
311
+ assertConfiguration(isRecord(config.admission), "INVALID_CONFIG", "Fevex config \"admission\" must be an object");
312
+ for (const name of ["maxActiveRuns", "maxActiveRunsPerTenant"]) {
313
+ const value = config.admission[name];
314
+ assertConfiguration(value === void 0 || typeof value === "number" && Number.isSafeInteger(value) && value > 0, "INVALID_CONFIG", `Fevex config "admission.${name}" must be a positive safe integer`);
315
+ }
316
+ assertConfiguration(config.admission.tenantKey === void 0 || typeof config.admission.tenantKey === "function", "INVALID_CONFIG", "Fevex config \"admission.tenantKey\" must be a function");
317
+ assertConfiguration(config.admission.authorize === void 0 || typeof config.admission.authorize === "function", "INVALID_CONFIG", "Fevex config \"admission.authorize\" must be a function");
318
+ }
319
+ if (config.limits !== void 0) {
320
+ assertConfiguration(isRecord(config.limits), "INVALID_CONFIG", "Fevex config \"limits\" must be an object");
321
+ assertConfiguration(config.limits.maxEventsPerRun === void 0 || typeof config.limits.maxEventsPerRun === "number" && Number.isSafeInteger(config.limits.maxEventsPerRun) && config.limits.maxEventsPerRun > 0, "INVALID_CONFIG", "Fevex config \"limits.maxEventsPerRun\" must be a positive safe integer");
322
+ }
304
323
  if (config.observability !== void 0) {
305
324
  assertConfiguration(isRecord(config.observability), "INVALID_CONFIG", "Fevex config \"observability\" must be an object");
306
325
  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");
@@ -313,6 +332,18 @@ function createComposition(config) {
313
332
  "error.message"
314
333
  ].includes(kind)) && (content.redact === void 0 || typeof content.redact === "function"), "INVALID_CONFIG", "Fevex config \"observability.content\" is invalid");
315
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
+ }
316
347
  }
317
348
  const models = /* @__PURE__ */ new Map();
318
349
  const agents = /* @__PURE__ */ new Map();
@@ -594,15 +625,19 @@ function createComposition(config) {
594
625
  runStore: config.runStore ?? new InMemoryRunStore(),
595
626
  credentialStore: config.credentialStore,
596
627
  sandbox: config.sandbox,
628
+ artifacts: config.artifacts,
597
629
  policies,
598
630
  onEvent: config.onEvent,
631
+ admission: config.admission === void 0 ? void 0 : { ...config.admission },
632
+ limits: config.limits === void 0 ? void 0 : { ...config.limits },
599
633
  observability: config.observability === void 0 ? void 0 : {
600
634
  exporters: [...config.observability.exporters],
601
635
  ...config.observability.calculateCost ? { calculateCost: config.observability.calculateCost } : {},
602
636
  ...config.observability.content ? { content: {
603
637
  include: [...config.observability.content.include],
604
638
  ...config.observability.content.redact ? { redact: config.observability.content.redact } : {}
605
- } } : {}
639
+ } } : {},
640
+ ...config.observability.exportQueue ? { exportQueue: { ...config.observability.exportQueue } } : {}
606
641
  }
607
642
  };
608
643
  }
@@ -732,6 +767,7 @@ const toolErrorPayload = (tool, error) => ({
732
767
  } : {}
733
768
  });
734
769
  const runErrorPayload = (error) => {
770
+ if (error instanceof FevexRunError) return { errorCode: error.code };
735
771
  if (!(error instanceof IntegrationError)) return {};
736
772
  return {
737
773
  errorCode: error.code,
@@ -866,9 +902,10 @@ function addWorkflowBudget(first, second) {
866
902
  * engine — that direction is what keeps the two separable.
867
903
  */
868
904
  function createAgentEngine(composition, core) {
869
- const { models, agents, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
870
- const { activeRuns, activeSessions, runtimeOwner, resolveModel, resolveCheckpointModel, notifyObserver, createEvent, emitEvent, releaseExecution, commit, startLease, cancelExecution, authorize } = core;
871
- const prepareExecution = async (name, request) => {
905
+ const { models, agents, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, artifacts, policies } = composition;
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");
872
909
  const agent = agents.get(name);
873
910
  if (!agent) throw new FevexRunError("AGENT_NOT_FOUND", `Agent "${name}" is not registered`);
874
911
  const { modelName, model } = resolveModel(agent, request.model);
@@ -887,20 +924,25 @@ function createAgentEngine(composition, core) {
887
924
  session = stored;
888
925
  }
889
926
  if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" already has an active run`);
890
- activeSessions.add(session.id);
891
927
  const controller = new AbortController();
892
- const signal = request.signal ? AbortSignal.any([controller.signal, request.signal]) : controller.signal;
928
+ const signal = runSignal(controller, request);
929
+ const deadlineAt = deadlineAtFromMs(request.deadlineMs);
930
+ activeSessions.add(session.id);
893
931
  const run = {
894
932
  id: crypto.randomUUID(),
895
933
  sessionId: session.id,
896
934
  agentName: name,
897
935
  modelName,
898
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 },
899
939
  status: "running",
900
940
  revision: 0,
901
941
  createdAt: now,
902
- updatedAt: now
942
+ updatedAt: now,
943
+ ...deadlineAt === void 0 ? {} : { deadlineAt }
903
944
  };
945
+ if (internal) markInternalRun(run.id);
904
946
  const state = {
905
947
  run,
906
948
  session,
@@ -981,6 +1023,17 @@ function createAgentEngine(composition, core) {
981
1023
  activeRuns.set(run.id, state);
982
1024
  return state;
983
1025
  };
1026
+ const prepareExecution = async (name, request, options = {}) => {
1027
+ const release = options.admit === false ? void 0 : await admitStart("agent", name, request);
1028
+ try {
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
+ });
1033
+ } finally {
1034
+ release?.();
1035
+ }
1036
+ };
984
1037
  const durableCheckpoint = async (state, model, modelName, messages, inputContent, usage, providerState, step, toolCallCount, seenToolCallIds, pendingTools, pendingIndex) => {
985
1038
  const agent = agents.get(state.run.agentName);
986
1039
  if (agent.model !== void 0 && typeof agent.model !== "string") throw new FevexRunError("RUN_NOT_RESUMABLE", "Durable continuation requires a named model", state.run.id);
@@ -1519,6 +1572,20 @@ function createAgentEngine(composition, core) {
1519
1572
  }
1520
1573
  events.push(started);
1521
1574
  yield started;
1575
+ const createdArtifacts = [];
1576
+ const toolArtifacts = artifacts === void 0 ? void 0 : scopedArtifacts(artifacts, {
1577
+ runId: state.run.id,
1578
+ sessionId: state.session.id,
1579
+ request,
1580
+ emit: async (descriptor) => {
1581
+ createdArtifacts.push(await emit("artifact.created", {
1582
+ descriptor,
1583
+ toolCallId: pending.call.id,
1584
+ toolName: pending.call.name,
1585
+ ...toolSourcePayload(tool)
1586
+ }));
1587
+ }
1588
+ });
1522
1589
  try {
1523
1590
  const raw = await abortable(() => tool.execute(pending.input, {
1524
1591
  runId: state.run.id,
@@ -1527,9 +1594,11 @@ function createAgentEngine(composition, core) {
1527
1594
  idempotencyKey: pending.idempotencyKey,
1528
1595
  getCredential,
1529
1596
  ...scopedSandbox === void 0 ? {} : { sandbox: scopedSandbox },
1597
+ ...toolArtifacts === void 0 ? {} : { artifacts: toolArtifacts },
1530
1598
  context: request.context,
1531
1599
  signal: request.signal
1532
1600
  }), request.signal);
1601
+ for (const event of createdArtifacts.splice(0)) yield event;
1533
1602
  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`);
1534
1603
  if (containsSecret(output, secrets)) throw new Error(`Output from tool "${tool.name}" contains a credential`);
1535
1604
  record = {
@@ -1557,6 +1626,7 @@ function createAgentEngine(composition, core) {
1557
1626
  yield completed;
1558
1627
  break;
1559
1628
  } catch (error) {
1629
+ for (const event of createdArtifacts.splice(0)) yield event;
1560
1630
  if (request.signal.aborted) throw error;
1561
1631
  const safeError = redact(toErrorMessage(error), secrets);
1562
1632
  record = {
@@ -1664,7 +1734,7 @@ function createAgentEngine(composition, core) {
1664
1734
  }
1665
1735
  }
1666
1736
  };
1667
- const resumeAgent = async (runId, resolution, waitForCompletion = false, recoveryActor) => {
1737
+ const resumeAgentBody = async (runId, resolution, waitForCompletion = false, recoveryActor) => {
1668
1738
  if (resolution?.type === "timer" || resolution?.type === "event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
1669
1739
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "resumeRun requires DurableRunStore", runId);
1670
1740
  const run = await runStore.getRun(runId);
@@ -1672,6 +1742,11 @@ function createAgentEngine(composition, core) {
1672
1742
  if (!run || !checkpoint) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be ${resolution ? "resumed" : "recovered"}`, runId);
1673
1743
  if (checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${runId}" is unsupported`, runId);
1674
1744
  if (resolution && run.status !== "paused" && run.status !== "running" || !resolution && run.status !== "running") throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" is not ${resolution ? "resumable" : "recoverable"}`, runId);
1745
+ if (run.deadlineAt !== void 0 && Date.parse(run.deadlineAt) <= Date.now()) {
1746
+ if (!await cancelStoredAgent(runId, "timeout")) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" deadline has passed`, runId);
1747
+ return await runStore.getRun(runId);
1748
+ }
1749
+ const deadlineSignal = resumeDeadlineSignal(run.deadlineAt, runId, resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE");
1675
1750
  const agent = agents.get(run.agentName);
1676
1751
  if (!agent) throw new FevexRunError("RUN_DEFINITION_CHANGED", `Agent "${run.agentName}" is unavailable`, runId);
1677
1752
  const checkpointModel = await resolveCheckpointModel(run.agentName, agent, checkpoint);
@@ -1695,6 +1770,7 @@ function createAgentEngine(composition, core) {
1695
1770
  if (!await runStore.acquireLease(lease)) throw new FevexRunError("RUN_CONFLICT", `Run "${runId}" is leased`, runId);
1696
1771
  try {
1697
1772
  const controller = new AbortController();
1773
+ const signal = deadlineSignal ? AbortSignal.any([controller.signal, deadlineSignal]) : controller.signal;
1698
1774
  const state = {
1699
1775
  run,
1700
1776
  session,
@@ -1705,7 +1781,7 @@ function createAgentEngine(composition, core) {
1705
1781
  sessionId: session.id,
1706
1782
  context: checkpoint.context,
1707
1783
  limits: checkpoint.limits,
1708
- signal: controller.signal
1784
+ signal
1709
1785
  },
1710
1786
  controller,
1711
1787
  eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
@@ -1874,10 +1950,27 @@ function createAgentEngine(composition, core) {
1874
1950
  throw error;
1875
1951
  }
1876
1952
  };
1877
- const cancelStoredAgent = async (runId) => {
1953
+ const resumeAgent = async (runId, resolution, waitForCompletion = false, recoveryActor, options = {}) => {
1954
+ let release;
1955
+ if (options.admit !== false) {
1956
+ const run = await runStore.getRun(runId);
1957
+ const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(runId) : void 0;
1958
+ if (run && checkpoint && (run.status === "paused" || run.status === "running")) release = await admitStart("agent", run.agentName, {
1959
+ input: "",
1960
+ ...checkpoint.context === void 0 ? {} : { context: checkpoint.context }
1961
+ });
1962
+ }
1963
+ try {
1964
+ if (options.internal) markInternalRun(runId);
1965
+ return await resumeAgentBody(runId, resolution, waitForCompletion, recoveryActor);
1966
+ } finally {
1967
+ release?.();
1968
+ }
1969
+ };
1970
+ const cancelStoredAgent = async (runId, reason = "aborted") => {
1878
1971
  const active = activeRuns.get(runId);
1879
1972
  if (active && !active.request.signal.aborted) {
1880
- active.controller.abort(new DOMException("Run cancelled", "AbortError"));
1973
+ active.controller.abort(new DOMException(reason === "timeout" ? "Run deadline exceeded" : "Run cancelled", reason === "timeout" ? "TimeoutError" : "AbortError"));
1881
1974
  if (!active.advancing) {
1882
1975
  await cancelExecution(active);
1883
1976
  return true;
@@ -1920,8 +2013,8 @@ function createAgentEngine(composition, core) {
1920
2013
  };
1921
2014
  run.status = "cancelled";
1922
2015
  run.pause = void 0;
1923
- run.error = "aborted";
1924
- const cancelled = createEvent(state, "run.cancelled", { reason: "aborted" });
2016
+ run.error = reason;
2017
+ const cancelled = createEvent(state, "run.cancelled", { reason });
1925
2018
  await commit(state, {
1926
2019
  checkpoint: null,
1927
2020
  events: [cancelled]
@@ -1943,21 +2036,67 @@ function createAgentEngine(composition, core) {
1943
2036
  cancelStoredAgent
1944
2037
  };
1945
2038
  }
2039
+ function scopedArtifacts(store, options) {
2040
+ const scopedContext = (value) => {
2041
+ const contextTenant = options.request.context?.attributes?.tenantId;
2042
+ const tenantId = typeof contextTenant === "string" ? contextTenant : value?.tenantId;
2043
+ const actor = options.request.context?.actor;
2044
+ return {
2045
+ ...value,
2046
+ runId: options.runId,
2047
+ sessionId: options.sessionId,
2048
+ ...tenantId === void 0 ? {} : { tenantId },
2049
+ ...actor === void 0 ? {} : { actor },
2050
+ signal: options.request.signal
2051
+ };
2052
+ };
2053
+ return {
2054
+ async put(input, context) {
2055
+ const descriptor = await store.put(input, scopedContext(context));
2056
+ await options.emit(descriptor);
2057
+ return descriptor;
2058
+ },
2059
+ get(artifactId, context) {
2060
+ return store.get(artifactId, scopedContext(context));
2061
+ },
2062
+ ...store.head === void 0 ? {} : { head(artifactId, context) {
2063
+ return store.head(artifactId, scopedContext(context));
2064
+ } },
2065
+ ...store.delete === void 0 ? {} : { delete(artifactId, context) {
2066
+ return store.delete(artifactId, scopedContext(context));
2067
+ } }
2068
+ };
2069
+ }
1946
2070
  //#endregion
1947
2071
  //#region src/internal/observability.ts
1948
2072
  const TERMINAL_TYPES = /* @__PURE__ */ new Set([
1949
2073
  "run.completed",
1950
2074
  "run.failed",
1951
- "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"
1952
2082
  ]);
1953
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
+ }
1954
2087
  function durationMs(startedAt, endedAt) {
1955
2088
  return Math.max(0, Date.parse(endedAt) - Date.parse(startedAt));
1956
2089
  }
1957
2090
  function traceStatus(event) {
1958
- if (event.type === "run.completed") return "completed";
1959
- if (event.type === "run.cancelled") return "cancelled";
1960
- 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
+ }
1961
2100
  }
1962
2101
  function redactKeys(value) {
1963
2102
  if (Array.isArray(value)) return value.map(redactKeys);
@@ -2064,20 +2203,74 @@ function annotation(event) {
2064
2203
  toolName: event.payload.toolName
2065
2204
  }
2066
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
+ };
2067
2234
  default: return;
2068
2235
  }
2069
2236
  }
2070
- function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2071
- const events = [...rawEvents].sort((left, right) => left.sequence - right.sequence);
2072
- const terminal = [...events].reverse().find((event) => TERMINAL_TYPES.has(event.type));
2073
- if (!terminal) throw new TypeError(`Run "${run.id}" has no terminal event`);
2074
- const started = events.find((event) => event.type === "run.started") ?? events[0];
2075
- 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);
2076
2268
  const status = traceStatus(terminal);
2077
2269
  const annotations = [];
2078
2270
  const spans = [];
2079
2271
  const openModels = /* @__PURE__ */ new Map();
2080
2272
  const openTools = /* @__PURE__ */ new Map();
2273
+ const modelSpanByStep = /* @__PURE__ */ new Map();
2081
2274
  const previousUsage = {};
2082
2275
  const closeModel = (step, endedAt, spanStatus) => {
2083
2276
  const open = openModels.get(step);
@@ -2108,6 +2301,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2108
2301
  const spanCost = costFor(options, modelRegistryName, model, usage);
2109
2302
  if (spanCost) span.cost = spanCost;
2110
2303
  spans.push(span);
2304
+ modelSpanByStep.set(step, span.id);
2111
2305
  };
2112
2306
  const findOpenTool = (toolCallId) => {
2113
2307
  const entries = [...openTools.entries()];
@@ -2127,7 +2321,7 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2127
2321
  const errorContent = error ? content(options, "error.message", error) : void 0;
2128
2322
  spans.push({
2129
2323
  id: event.id,
2130
- parentId: run.id,
2324
+ parentId: modelSpanByStep.get(event.payload.step) ?? run.id,
2131
2325
  kind: "tool",
2132
2326
  name: event.payload.toolName,
2133
2327
  startedAt: event.timestamp,
@@ -2192,19 +2386,15 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2192
2386
  for (const open of [...openTools.values()]) if (open.event.type === "tool.started") closeTool(open.event.payload.toolCallId, terminal.timestamp, status);
2193
2387
  const finalUsage = run.usage ?? previousUsage;
2194
2388
  const totalCost = costFor(options, modelRegistryName, model, finalUsage);
2195
- const traceContent = [];
2196
- if (terminal.type === "run.completed") {
2197
- const output = content(options, "run.output", terminal.payload.output);
2198
- if (output) traceContent.push(output);
2199
- } else if (terminal.type === "run.failed") {
2200
- const error = content(options, "error.message", terminal.payload.error);
2201
- if (error) traceContent.push(error);
2202
- }
2389
+ const traceContent = terminalContent(options, terminal);
2203
2390
  return {
2204
2391
  schemaVersion: "1",
2392
+ kind: "agent",
2205
2393
  traceId: run.id,
2206
2394
  runId: run.id,
2207
2395
  sessionId: run.sessionId,
2396
+ ...run.parentRunId === void 0 ? {} : { parentRunId: run.parentRunId },
2397
+ rootRunId: run.rootRunId ?? run.parentRunId ?? run.id,
2208
2398
  agentName: run.agentName,
2209
2399
  modelRegistryName,
2210
2400
  ...model ? { model: { ...model } } : {},
@@ -2224,6 +2414,249 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2224
2414
  ...traceContent.length ? { content: traceContent } : {}
2225
2415
  };
2226
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
+ }
2227
2660
  //#endregion
2228
2661
  //#region src/internal/run-core.ts
2229
2662
  /**
@@ -2236,14 +2669,118 @@ function buildRunTrace(run, rawEvents, modelRegistryName, model, options) {
2236
2669
  * the lease table, so a second instance would corrupt event ordering and lease
2237
2670
  * ownership without raising an error.
2238
2671
  */
2239
- function createRunCore({ models, agents, tools, runStore, policies, onEvent, observability }) {
2672
+ function createRunCore({ models, agents, tools, runStore, policies, onEvent, observability, admission, limits }) {
2240
2673
  const activeRuns = /* @__PURE__ */ new Map();
2241
2674
  const activeWorkflowRuns = /* @__PURE__ */ new Map();
2242
2675
  const activeSessions = /* @__PURE__ */ new Set();
2243
- const pendingExports = /* @__PURE__ */ new Set();
2244
- const exportFailures = [];
2245
- const exportedRuns = /* @__PURE__ */ new Set();
2676
+ const internalRuns = /* @__PURE__ */ new Set();
2677
+ const pendingTenantStarts = /* @__PURE__ */ new Map();
2678
+ const idleWaiters = /* @__PURE__ */ new Set();
2679
+ let pendingStarts = 0;
2680
+ let closed = false;
2681
+ const traceExports = createTraceExportQueue(observability?.exportQueue);
2246
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;
2697
+ const unboundedEvents = /* @__PURE__ */ new Set([
2698
+ "run.started",
2699
+ "run.recovered",
2700
+ "run.paused",
2701
+ "run.resumed",
2702
+ "elicitation.requested",
2703
+ "elicitation.resolved",
2704
+ "approval.requested",
2705
+ "approval.resolved",
2706
+ "tool.execution_unknown",
2707
+ "run.completed",
2708
+ "run.failed",
2709
+ "run.cancelled",
2710
+ "workflow.run.started",
2711
+ "workflow.run.recovered",
2712
+ "workflow.run.paused",
2713
+ "workflow.run.resumed",
2714
+ "workflow.wait.started",
2715
+ "workflow.wait.completed",
2716
+ "workflow.run.completed",
2717
+ "workflow.run.failed",
2718
+ "workflow.run.cancelled",
2719
+ "team.run.started",
2720
+ "team.run.recovered",
2721
+ "team.run.paused",
2722
+ "team.run.resumed",
2723
+ "team.run.completed",
2724
+ "team.run.failed",
2725
+ "team.run.cancelled"
2726
+ ]);
2727
+ const tenantOf = (context) => {
2728
+ const configured = admission?.tenantKey?.(context);
2729
+ if (configured !== void 0) return configured;
2730
+ const tenantId = context?.attributes?.tenantId;
2731
+ if (typeof tenantId !== "string") return void 0;
2732
+ return tenantId.trim() || void 0;
2733
+ };
2734
+ const countsForAdmission = (runId, state) => !internalRuns.has(runId) && !state.request.signal?.aborted;
2735
+ const activeTopLevelRuns = () => {
2736
+ let count = 0;
2737
+ for (const [runId, state] of activeRuns) if (countsForAdmission(runId, state)) count += 1;
2738
+ for (const [runId, state] of activeWorkflowRuns) if (countsForAdmission(runId, state)) count += 1;
2739
+ return count;
2740
+ };
2741
+ const activeTenantRuns = (tenant) => {
2742
+ let count = 0;
2743
+ for (const [runId, state] of activeRuns) if (countsForAdmission(runId, state) && tenantOf(state.request.context) === tenant) count += 1;
2744
+ for (const [runId, state] of activeWorkflowRuns) if (countsForAdmission(runId, state) && tenantOf(state.request.context) === tenant) count += 1;
2745
+ return count;
2746
+ };
2747
+ const admitStart = async (kind, name, request) => {
2748
+ if (closed) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2749
+ if (!admission) return () => {};
2750
+ const running = activeTopLevelRuns() + pendingStarts;
2751
+ const tenant = tenantOf(request.context);
2752
+ const tenantRuns = tenant === void 0 ? 0 : activeTenantRuns(tenant) + (pendingTenantStarts.get(tenant) ?? 0);
2753
+ if (admission.maxActiveRuns !== void 0 && running >= admission.maxActiveRuns) throw new FevexRunError("ADMISSION_DENIED", `Runtime reached maxActiveRuns limit of ${admission.maxActiveRuns}`);
2754
+ if (admission.maxActiveRunsPerTenant !== void 0 && tenant !== void 0 && tenantRuns >= admission.maxActiveRunsPerTenant) throw new FevexRunError("ADMISSION_DENIED", `Tenant "${tenant}" reached maxActiveRunsPerTenant limit of ${admission.maxActiveRunsPerTenant}`);
2755
+ pendingStarts += 1;
2756
+ if (tenant !== void 0) pendingTenantStarts.set(tenant, (pendingTenantStarts.get(tenant) ?? 0) + 1);
2757
+ let released = false;
2758
+ const release = () => {
2759
+ if (released) return;
2760
+ released = true;
2761
+ pendingStarts = Math.max(0, pendingStarts - 1);
2762
+ if (tenant === void 0) return;
2763
+ const next = (pendingTenantStarts.get(tenant) ?? 1) - 1;
2764
+ if (next <= 0) pendingTenantStarts.delete(tenant);
2765
+ else pendingTenantStarts.set(tenant, next);
2766
+ };
2767
+ try {
2768
+ if (admission.authorize && !await admission.authorize({
2769
+ kind,
2770
+ name,
2771
+ request,
2772
+ activeRuns: running,
2773
+ activeTenantRuns: tenantRuns
2774
+ })) throw new FevexRunError("ADMISSION_DENIED", "Run admission was denied by the host");
2775
+ } catch (error) {
2776
+ release();
2777
+ throw error;
2778
+ }
2779
+ return release;
2780
+ };
2781
+ const markInternalRun = (runId) => {
2782
+ internalRuns.add(runId);
2783
+ };
2247
2784
  const resolveModel = (agent, requestedModel) => {
2248
2785
  const modelName = requestedModel ?? (typeof agent.model === "string" ? agent.model : "default");
2249
2786
  if (!modelName.trim()) throw new FevexRunError("MODEL_NOT_FOUND", "Model name cannot be empty");
@@ -2265,35 +2802,30 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2265
2802
  }
2266
2803
  };
2267
2804
  const scheduleTraceExport = (event) => {
2268
- if (!observability || ![
2269
- "run.completed",
2270
- "run.failed",
2271
- "run.cancelled"
2272
- ].includes(event.type) || exportedRuns.has(event.runId)) return;
2273
- exportedRuns.add(event.runId);
2274
- let task;
2275
- task = (async () => {
2805
+ if (!observability || !isTraceTerminalEvent(event)) return;
2806
+ traceExports.enqueue(event.runId, async () => {
2276
2807
  const run = await runStore.getRun(event.runId);
2277
2808
  if (!run) throw new Error(`Run "${event.runId}" does not exist`);
2278
- if (isCoordinatorRun(run)) return;
2279
- const agentRun = run;
2280
- let modelRegistryName = agentRun.modelName;
2281
- let modelMetadata = agentRun.modelMetadata;
2282
- if (modelRegistryName === void 0) {
2283
- const agent = agents.get(agentRun.agentName);
2284
- if (!agent) throw new Error(`Agent "${agentRun.agentName}" is not registered`);
2285
- const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(agentRun.id) : void 0;
2286
- const resolved = resolveModel(agent, checkpoint?.modelName);
2287
- modelRegistryName = resolved.modelName;
2288
- 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
+ }
2289
2824
  }
2290
- const trace = buildRunTrace(agentRun, await runStore.listEvents(agentRun.id), modelRegistryName, modelMetadata, observability);
2291
- 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);
2292
2827
  if (failures.length) throw new AggregateError(failures, "Trace export failed");
2293
- })().catch((error) => {
2294
- exportFailures.push(error);
2295
- }).finally(() => pendingExports.delete(task));
2296
- pendingExports.add(task);
2828
+ });
2297
2829
  };
2298
2830
  const notifyObserver = (event) => {
2299
2831
  scheduleTraceExport(event);
@@ -2302,14 +2834,17 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2302
2834
  Promise.resolve(onEvent(event)).catch(() => {});
2303
2835
  } catch {}
2304
2836
  };
2305
- const createEvent = (state, type, payload) => ({
2306
- id: crypto.randomUUID(),
2307
- sequence: state.eventSequence += 1,
2308
- type,
2309
- runId: state.run.id,
2310
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2311
- ...payload === void 0 ? {} : { payload }
2312
- });
2837
+ const createEvent = (state, type, payload) => {
2838
+ if (limits?.maxEventsPerRun !== void 0 && !unboundedEvents.has(type) && state.eventSequence >= limits.maxEventsPerRun) throw new FevexRunError("EVENT_LIMIT_EXCEEDED", `Run exceeded maxEventsPerRun limit of ${limits.maxEventsPerRun}`, state.run.id);
2839
+ return {
2840
+ id: crypto.randomUUID(),
2841
+ sequence: state.eventSequence += 1,
2842
+ type,
2843
+ runId: state.run.id,
2844
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2845
+ ...payload === void 0 ? {} : { payload }
2846
+ };
2847
+ };
2313
2848
  const createCoordinatorEvent = (state, workflowType, workflowPayload, teamType, teamPayload = workflowPayload) => createEvent(state, isTeamRun(state.run) ? teamType : workflowType, isTeamRun(state.run) ? teamPayload : workflowPayload);
2314
2849
  const emitEvent = async (state, type, payload) => {
2315
2850
  const event = createEvent(state, type, payload);
@@ -2323,11 +2858,13 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2323
2858
  const releaseExecution = async (state) => {
2324
2859
  activeRuns.delete(state.run.id);
2325
2860
  activeSessions.delete(state.session.id);
2861
+ internalRuns.delete(state.run.id);
2326
2862
  if (state.leaseTimer) clearInterval(state.leaseTimer);
2327
2863
  if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2328
2864
  state.leaseTimer = void 0;
2329
2865
  state.leaseExpiryTimer = void 0;
2330
2866
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2867
+ notifyIdle();
2331
2868
  };
2332
2869
  const commit = async (state, options = {}) => {
2333
2870
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "This run requires a DurableRunStore", state.run.id);
@@ -2411,11 +2948,13 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2411
2948
  const releaseWorkflowExecution = async (state) => {
2412
2949
  activeWorkflowRuns.delete(state.run.id);
2413
2950
  activeSessions.delete(state.session.id);
2951
+ internalRuns.delete(state.run.id);
2414
2952
  if (state.leaseTimer) clearInterval(state.leaseTimer);
2415
2953
  if (state.leaseExpiryTimer) clearTimeout(state.leaseExpiryTimer);
2416
2954
  state.leaseTimer = void 0;
2417
2955
  state.leaseExpiryTimer = void 0;
2418
2956
  if (state.lease && isDurableRunStore(runStore)) await runStore.releaseLease(state.run.id, state.lease.ownerId, state.lease.generation).catch(() => {});
2957
+ notifyIdle();
2419
2958
  };
2420
2959
  const commitWorkflow = async (state, update, events = []) => {
2421
2960
  state.commitQueue = state.commitQueue.then(async () => {
@@ -2449,9 +2988,14 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2449
2988
  activeRuns,
2450
2989
  activeWorkflowRuns,
2451
2990
  activeSessions,
2452
- pendingExports,
2453
- exportFailures,
2991
+ isIdle,
2992
+ waitForIdle,
2993
+ closeAdmission,
2994
+ isClosed,
2995
+ traceExports,
2454
2996
  runtimeOwner,
2997
+ admitStart,
2998
+ markInternalRun,
2455
2999
  resolveModel,
2456
3000
  resolveCheckpointModel,
2457
3001
  notifyObserver,
@@ -2479,7 +3023,7 @@ function createRunCore({ models, agents, tools, runStore, policies, onEvent, obs
2479
3023
  */
2480
3024
  function createWorkflowEngine(composition, core, agentEngine) {
2481
3025
  const { models, agents, workflows, teams, runStore } = composition;
2482
- const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
3026
+ const { activeRuns, activeWorkflowRuns, activeSessions, runtimeOwner, isClosed, admitStart, createEvent, createCoordinatorEvent, notifyObserver, commit, startLease, releaseWorkflowExecution, commitWorkflow } = core;
2483
3027
  const { prepareExecution, executeAgent, nextEvent, resumeAgent, cancelStoredAgent } = agentEngine;
2484
3028
  const cancelWorkflowExecution = async (state, reason = cancellationReason(state.request.signal)) => {
2485
3029
  if (state.leaseLost) {
@@ -2488,7 +3032,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
2488
3032
  }
2489
3033
  if (state.run.status === "completed" || state.run.status === "failed" || state.run.status === "cancelled") return;
2490
3034
  const childRunIds = Object.values(state.checkpoint.steps).filter((record) => record.type === "agent" && record.status === "running").map(({ childRunId }) => childRunId);
2491
- for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId)) throw new FevexRunError("RUN_CONFLICT", `Workflow child "${childRunId}" could not be cancelled`, state.run.id);
3035
+ for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId, reason === "timeout" ? "timeout" : "aborted")) throw new FevexRunError("RUN_CONFLICT", `Workflow child "${childRunId}" could not be cancelled`, state.run.id);
2492
3036
  state.run.status = "cancelled";
2493
3037
  state.run.pause = void 0;
2494
3038
  state.run.error = reason;
@@ -2501,7 +3045,77 @@ function createWorkflowEngine(composition, core, agentEngine) {
2501
3045
  await releaseWorkflowExecution(state);
2502
3046
  return event;
2503
3047
  };
2504
- const prepareWorkflowExecution = async (name, request, kind = "workflow") => {
3048
+ const cancelStoredWorkflow = async (runId, reason = "aborted") => {
3049
+ const active = activeWorkflowRuns.get(runId);
3050
+ if (active && !active.request.signal.aborted) {
3051
+ active.controller.abort(new DOMException(reason === "timeout" ? "Run deadline exceeded" : "Run cancelled", reason === "timeout" ? "TimeoutError" : "AbortError"));
3052
+ if (!active.advancing) {
3053
+ await cancelWorkflowExecution(active);
3054
+ return true;
3055
+ }
3056
+ for (let attempt = 0; attempt < 100; attempt += 1) {
3057
+ const current = await runStore.getRun(runId);
3058
+ if (current?.status === "cancelled" || current?.status === "completed" || current?.status === "failed") return true;
3059
+ await new Promise((resolve) => setTimeout(resolve, 10));
3060
+ }
3061
+ return false;
3062
+ }
3063
+ if (!isDurableRunStore(runStore)) return false;
3064
+ const run = await runStore.getRun(runId);
3065
+ if (!run) return false;
3066
+ if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") return true;
3067
+ const session = await runStore.getSession(run.sessionId);
3068
+ const checkpoint = await runStore.getCheckpoint(runId);
3069
+ if (!session || !checkpoint) return false;
3070
+ const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
3071
+ const lease = {
3072
+ generation: 0,
3073
+ runId,
3074
+ ownerId,
3075
+ expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
3076
+ };
3077
+ if (!await runStore.acquireLease(lease)) return false;
3078
+ try {
3079
+ const childRunIds = Object.values(checkpoint.steps).filter((record) => record.type === "agent" && record.status === "running").map(({ childRunId }) => childRunId);
3080
+ for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId, reason)) return false;
3081
+ const state = {
3082
+ run,
3083
+ session,
3084
+ request: {
3085
+ input: checkpoint.input,
3086
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
3087
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3088
+ sessionId: session.id,
3089
+ context: checkpoint.context,
3090
+ limits: checkpoint.limits,
3091
+ signal: new AbortController().signal
3092
+ },
3093
+ controller: new AbortController(),
3094
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3095
+ checkpoint,
3096
+ advancing: false,
3097
+ lease,
3098
+ commitQueue: Promise.resolve()
3099
+ };
3100
+ run.status = "cancelled";
3101
+ run.pause = void 0;
3102
+ run.error = reason;
3103
+ run.usage = checkpoint.budget?.usage;
3104
+ const cancelled = createCoordinatorEvent(state, "workflow.run.cancelled", { reason }, "team.run.cancelled");
3105
+ await commit(state, {
3106
+ checkpoint: null,
3107
+ events: [cancelled]
3108
+ });
3109
+ return true;
3110
+ } catch (error) {
3111
+ if (error instanceof FevexRunError && error.code === "RUN_CONFLICT" && (await runStore.getRun(runId))?.status !== "paused") return false;
3112
+ throw error;
3113
+ } finally {
3114
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
3115
+ }
3116
+ };
3117
+ const prepareWorkflowExecutionBody = async (name, request, kind = "workflow") => {
3118
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs");
2505
3119
  const workflow = workflows.get(name);
2506
3120
  const team = kind === "team" ? teams.get(name) : void 0;
2507
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`);
@@ -2520,9 +3134,9 @@ function createWorkflowEngine(composition, core, agentEngine) {
2520
3134
  session = stored;
2521
3135
  }
2522
3136
  if (activeSessions.has(session.id)) throw new FevexRunError("RUN_CONFLICT", `Session "${session.id}" already has an active run`);
2523
- activeSessions.add(session.id);
2524
3137
  const controller = new AbortController();
2525
- const signal = request.signal ? AbortSignal.any([controller.signal, request.signal]) : controller.signal;
3138
+ const signal = runSignal(controller, request);
3139
+ const deadlineAt = deadlineAtFromMs(request.deadlineMs);
2526
3140
  const input = toJsonValue(workflow.inputSchema === void 0 ? request.input : await abortable(() => validateSchema(workflow.inputSchema, request.input, `Input for ${kind} "${name}" does not match inputSchema`), signal), `${kind === "team" ? "Team" : "Workflow"} input must be a string or JSON-serializable value`);
2527
3141
  const id = crypto.randomUUID();
2528
3142
  const run = kind === "team" ? {
@@ -2533,7 +3147,8 @@ function createWorkflowEngine(composition, core, agentEngine) {
2533
3147
  status: "running",
2534
3148
  revision: 0,
2535
3149
  createdAt: now,
2536
- updatedAt: now
3150
+ updatedAt: now,
3151
+ ...deadlineAt === void 0 ? {} : { deadlineAt }
2537
3152
  } : {
2538
3153
  kind: "workflow",
2539
3154
  id,
@@ -2542,7 +3157,8 @@ function createWorkflowEngine(composition, core, agentEngine) {
2542
3157
  status: "running",
2543
3158
  revision: 0,
2544
3159
  createdAt: now,
2545
- updatedAt: now
3160
+ updatedAt: now,
3161
+ ...deadlineAt === void 0 ? {} : { deadlineAt }
2546
3162
  };
2547
3163
  const checkpoint = kind === "team" ? {
2548
3164
  version: 2,
@@ -2569,6 +3185,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
2569
3185
  steps: {},
2570
3186
  limits: combineLimits(workflow.limits, request.limits)
2571
3187
  };
3188
+ activeSessions.add(session.id);
2572
3189
  const state = {
2573
3190
  run,
2574
3191
  session,
@@ -2610,6 +3227,14 @@ function createWorkflowEngine(composition, core, agentEngine) {
2610
3227
  activeWorkflowRuns.set(run.id, state);
2611
3228
  return state;
2612
3229
  };
3230
+ const prepareWorkflowExecution = async (name, request, kind = "workflow") => {
3231
+ const release = await admitStart(kind, name, request);
3232
+ try {
3233
+ return await prepareWorkflowExecutionBody(name, request, kind);
3234
+ } finally {
3235
+ release();
3236
+ }
3237
+ };
2613
3238
  async function* executeWorkflow(name, state) {
2614
3239
  const workflow = workflows.get(name);
2615
3240
  const events = [];
@@ -2771,7 +3396,10 @@ function createWorkflowEngine(composition, core, agentEngine) {
2771
3396
  if (!child) throw new Error(`Child run "${existing.childRunId}" does not exist`);
2772
3397
  if (child.status === "running") {
2773
3398
  if (!state.recoveryActor) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Child run "${child.id}" is still running`, state.run.id);
2774
- child = await resumeAgent(child.id, void 0, true, state.recoveryActor);
3399
+ child = await resumeAgent(child.id, void 0, true, state.recoveryActor, {
3400
+ admit: false,
3401
+ internal: true
3402
+ });
2775
3403
  }
2776
3404
  if (child.status === "completed") return completeAgentStep(stepId, existing.agentName, existing.childRunId, toJsonValue(child.output, "Child agent output must be JSON-serializable"), options);
2777
3405
  if (child.status === "paused" && child.pause) return pauseForChild(stepId, new RunPausedError(child.id, child.pause));
@@ -2782,11 +3410,16 @@ function createWorkflowEngine(composition, core, agentEngine) {
2782
3410
  const childSignal = request.signal ? AbortSignal.any([state.request.signal, request.signal]) : state.request.signal;
2783
3411
  const childState = await prepareExecution(agentName, {
2784
3412
  ...request,
3413
+ deadlineMs: minDeadline(remainingDeadlineMs(state.run.deadlineAt, state.run.id), request.deadlineMs),
2785
3414
  model: request.model ?? state.request.model,
2786
3415
  reasoning: request.reasoning ?? state.request.reasoning,
2787
3416
  context: mergeExecutionContext(state.request.context, request.context),
2788
3417
  limits: combineLimits(request.limits, remainingWorkflowLimits(state.checkpoint.limits, state.checkpoint.budget)),
2789
3418
  signal: childSignal
3419
+ }, {
3420
+ admit: false,
3421
+ parentRunId: state.run.id,
3422
+ rootRunId: state.run.rootRunId ?? state.run.id
2790
3423
  });
2791
3424
  const action = options?.metadata?.teamAction === "handoff" ? "handoff" : "delegate";
2792
3425
  const lifecycleEvents = [];
@@ -2799,11 +3432,13 @@ function createWorkflowEngine(composition, core, agentEngine) {
2799
3432
  const started = createCoordinatorEvent(state, "workflow.step.started", {
2800
3433
  stepId,
2801
3434
  kind: "agent",
2802
- agentName
3435
+ agentName,
3436
+ childRunId: childState.run.id
2803
3437
  }, "team.agent.assigned", {
2804
3438
  delegationId: stepId,
2805
3439
  agentName,
2806
3440
  action,
3441
+ childRunId: childState.run.id,
2807
3442
  ...typeof options?.metadata?.expectedOutput === "string" ? { expectedOutput: options.metadata.expectedOutput } : {},
2808
3443
  ...Array.isArray(options?.metadata?.constraints) ? { constraints: options.metadata.constraints } : {}
2809
3444
  });
@@ -3121,8 +3756,10 @@ function createWorkflowEngine(composition, core, agentEngine) {
3121
3756
  };
3122
3757
  } catch (error) {
3123
3758
  let thrown = error;
3124
- if (state.request.signal.aborted) try {
3125
- const event = await cancelWorkflowExecution(state);
3759
+ const parentDeadline = state.run.deadlineAt === void 0 ? void 0 : Date.parse(state.run.deadlineAt);
3760
+ const parentTimedOut = isTimeoutError(error) && parentDeadline !== void 0 && Date.now() >= parentDeadline - 10;
3761
+ if (state.request.signal.aborted || parentTimedOut) try {
3762
+ const event = await cancelWorkflowExecution(state, parentTimedOut ? "timeout" : void 0);
3126
3763
  if (event) {
3127
3764
  events.push(event);
3128
3765
  yield event;
@@ -3184,13 +3821,18 @@ function createWorkflowEngine(composition, core, agentEngine) {
3184
3821
  }
3185
3822
  }
3186
3823
  };
3187
- const resumeWorkflow = async (runId, resolution, recoveryActor) => {
3824
+ const resumeWorkflowBody = async (runId, resolution, recoveryActor) => {
3188
3825
  if (!isDurableRunStore(runStore)) throw new FevexRunError("DURABLE_STORE_REQUIRED", "resumeRun requires DurableRunStore", runId);
3189
3826
  const run = await runStore.getRun(runId);
3190
3827
  const checkpoint = await runStore.getCheckpoint(runId);
3191
3828
  if (!run || !checkpoint) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be ${resolution ? "resumed" : "recovered"}`, runId);
3192
3829
  if (checkpoint.version !== 2) throw new FevexRunError("CHECKPOINT_UNSUPPORTED", `Checkpoint for run "${runId}" is unsupported`, runId);
3193
3830
  if (resolution && run.status !== "paused" && run.status !== "running" || !resolution && run.status !== "running") throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" is not ${resolution ? "resumable" : "recoverable"}`, runId);
3831
+ if (run.deadlineAt !== void 0 && Date.parse(run.deadlineAt) <= Date.now()) {
3832
+ if (!await cancelStoredWorkflow(runId, "timeout")) throw new FevexRunError(resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE", `Run "${runId}" deadline has passed`, runId);
3833
+ return await runStore.getRun(runId);
3834
+ }
3835
+ const deadlineSignal = resumeDeadlineSignal(run.deadlineAt, runId, resolution ? "RUN_NOT_RESUMABLE" : "RUN_NOT_RECOVERABLE");
3194
3836
  const name = coordinatorName(run);
3195
3837
  const workflow = workflows.get(name);
3196
3838
  const team = isTeamRun(run) ? teams.get(name) : void 0;
@@ -3213,6 +3855,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3213
3855
  if (!resolution) {
3214
3856
  if (!recoveryActor?.id.trim()) throw new FevexRunError("RUN_NOT_RECOVERABLE", "Recovery actor is required", runId);
3215
3857
  const controller = new AbortController();
3858
+ const signal = deadlineSignal ? AbortSignal.any([controller.signal, deadlineSignal]) : controller.signal;
3216
3859
  const state = {
3217
3860
  run,
3218
3861
  session,
@@ -3223,7 +3866,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3223
3866
  sessionId: session.id,
3224
3867
  context: checkpoint.context,
3225
3868
  limits: checkpoint.limits,
3226
- signal: controller.signal
3869
+ signal
3227
3870
  },
3228
3871
  controller,
3229
3872
  eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
@@ -3266,6 +3909,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3266
3909
  if (validatedPayload !== void 0) eventPayload = toJsonValue(validatedPayload, "Workflow event payload must be JSON-serializable");
3267
3910
  }
3268
3911
  const controller = new AbortController();
3912
+ const signal = deadlineSignal ? AbortSignal.any([controller.signal, deadlineSignal]) : controller.signal;
3269
3913
  const state = {
3270
3914
  run,
3271
3915
  session,
@@ -3276,7 +3920,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3276
3920
  sessionId: session.id,
3277
3921
  context: checkpoint.context,
3278
3922
  limits: checkpoint.limits,
3279
- signal: controller.signal
3923
+ signal
3280
3924
  },
3281
3925
  controller,
3282
3926
  eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
@@ -3314,8 +3958,93 @@ function createWorkflowEngine(composition, core, agentEngine) {
3314
3958
  if (resolution.type === "timer" || resolution.type === "event") throw new FevexRunError("RUN_NOT_RESUMABLE", `Run "${runId}" cannot be resumed`, runId);
3315
3959
  const childRunId = workflowPause.type === "workflow_children" ? workflowPause.children.find(({ childPause }) => pauseMatchesResolution(childPause, resolution))?.childRunId : workflowPause.childRunId;
3316
3960
  if (!childRunId) throw new FevexRunError("RUN_NOT_RESUMABLE", "Resolution does not match a paused child run", runId);
3317
- const childRun = await resumeAgent(childRunId, resolution, true);
3961
+ let childRun;
3962
+ try {
3963
+ childRun = await resumeAgent(childRunId, resolution, true, void 0, {
3964
+ admit: false,
3965
+ internal: true
3966
+ });
3967
+ } catch (error) {
3968
+ const parentDeadline = run.deadlineAt === void 0 ? void 0 : Date.parse(run.deadlineAt);
3969
+ const child = await runStore.getRun(childRunId);
3970
+ const childDeadline = child?.deadlineAt === void 0 ? void 0 : Date.parse(child.deadlineAt);
3971
+ if (isTimeoutError(error) && parentDeadline !== void 0 && childDeadline !== void 0 && Math.abs(childDeadline - parentDeadline) <= 5) {
3972
+ const pausedChildIds = workflowPause.type === "workflow_children" ? workflowPause.children.map(({ childRunId }) => childRunId) : [workflowPause.childRunId];
3973
+ for (const pausedChildId of pausedChildIds) if (!await cancelStoredAgent(pausedChildId, "timeout")) throw error;
3974
+ const cancelController = new AbortController();
3975
+ const cancelState = {
3976
+ run,
3977
+ session,
3978
+ request: {
3979
+ input: checkpoint.input,
3980
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
3981
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3982
+ sessionId: session.id,
3983
+ context: checkpoint.context,
3984
+ limits: checkpoint.limits,
3985
+ signal: cancelController.signal
3986
+ },
3987
+ controller: cancelController,
3988
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3989
+ checkpoint,
3990
+ advancing: false,
3991
+ lease,
3992
+ commitQueue: Promise.resolve()
3993
+ };
3994
+ run.status = "cancelled";
3995
+ run.pause = void 0;
3996
+ run.error = "timeout";
3997
+ run.usage = checkpoint.budget?.usage;
3998
+ const cancelled = createCoordinatorEvent(cancelState, "workflow.run.cancelled", { reason: "timeout" }, "team.run.cancelled");
3999
+ await commit(cancelState, {
4000
+ checkpoint: null,
4001
+ events: [cancelled]
4002
+ });
4003
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
4004
+ return structuredClone(run);
4005
+ }
4006
+ if (isTimeoutError(error)) {
4007
+ const pausedChildIds = workflowPause.type === "workflow_children" ? workflowPause.children.map(({ childRunId }) => childRunId) : [workflowPause.childRunId];
4008
+ for (const pausedChildId of pausedChildIds) if (!await cancelStoredAgent(pausedChildId, "aborted")) throw error;
4009
+ const failController = new AbortController();
4010
+ const failState = {
4011
+ run,
4012
+ session,
4013
+ request: {
4014
+ input: checkpoint.input,
4015
+ ...checkpoint.model === void 0 ? {} : { model: checkpoint.model },
4016
+ ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
4017
+ sessionId: session.id,
4018
+ context: checkpoint.context,
4019
+ limits: checkpoint.limits,
4020
+ signal: failController.signal
4021
+ },
4022
+ controller: failController,
4023
+ eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
4024
+ checkpoint,
4025
+ advancing: false,
4026
+ lease,
4027
+ commitQueue: Promise.resolve()
4028
+ };
4029
+ run.status = "failed";
4030
+ run.pause = void 0;
4031
+ run.error = toErrorMessage(error);
4032
+ run.usage = checkpoint.budget?.usage;
4033
+ const failed = createCoordinatorEvent(failState, "workflow.run.failed", {
4034
+ error: toErrorMessage(error),
4035
+ ...runErrorPayload(error)
4036
+ }, "team.run.failed");
4037
+ await commit(failState, {
4038
+ checkpoint: null,
4039
+ events: [failed]
4040
+ });
4041
+ await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
4042
+ return structuredClone(run);
4043
+ }
4044
+ throw error;
4045
+ }
3318
4046
  const controller = new AbortController();
4047
+ const signal = deadlineSignal ? AbortSignal.any([controller.signal, deadlineSignal]) : controller.signal;
3319
4048
  const state = {
3320
4049
  run,
3321
4050
  session,
@@ -3326,7 +4055,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3326
4055
  sessionId: session.id,
3327
4056
  context: checkpoint.context,
3328
4057
  limits: checkpoint.limits,
3329
- signal: controller.signal
4058
+ signal
3330
4059
  },
3331
4060
  controller,
3332
4061
  eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
@@ -3339,7 +4068,7 @@ function createWorkflowEngine(composition, core, agentEngine) {
3339
4068
  run.status = "cancelled";
3340
4069
  run.pause = void 0;
3341
4070
  run.error = childRun.error ?? "aborted";
3342
- const reason = childRun.error === "approval_rejected" ? "approval_rejected" : "aborted";
4071
+ const reason = childRun.error === "approval_rejected" ? "approval_rejected" : childRun.error === "timeout" ? "timeout" : "aborted";
3343
4072
  const cancelled = createCoordinatorEvent(state, "workflow.run.cancelled", { reason }, "team.run.cancelled");
3344
4073
  await commit(state, {
3345
4074
  checkpoint: null,
@@ -3365,35 +4094,104 @@ function createWorkflowEngine(composition, core, agentEngine) {
3365
4094
  throw error;
3366
4095
  }
3367
4096
  };
4097
+ const resumeWorkflow = async (runId, resolution, recoveryActor, options = {}) => {
4098
+ let release;
4099
+ if (options.admit !== false) {
4100
+ const run = await runStore.getRun(runId);
4101
+ const checkpoint = isDurableRunStore(runStore) ? await runStore.getCheckpoint(runId) : void 0;
4102
+ if (run && checkpoint && (run.status === "paused" || run.status === "running")) release = await admitStart(isTeamRun(run) ? "team" : "workflow", coordinatorName(run), {
4103
+ input: "",
4104
+ ...checkpoint.context === void 0 ? {} : { context: checkpoint.context }
4105
+ });
4106
+ }
4107
+ try {
4108
+ return await resumeWorkflowBody(runId, resolution, recoveryActor);
4109
+ } finally {
4110
+ release?.();
4111
+ }
4112
+ };
3368
4113
  return {
3369
4114
  prepareWorkflowExecution,
3370
4115
  executeWorkflow,
3371
4116
  nextWorkflowEvent,
3372
4117
  drainWorkflowExecution,
3373
4118
  resumeWorkflow,
3374
- cancelWorkflowExecution
4119
+ cancelWorkflowExecution,
4120
+ cancelStoredWorkflow
3375
4121
  };
3376
4122
  }
3377
4123
  //#endregion
3378
4124
  //#region src/internal/runtime.ts
4125
+ const DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
4126
+ const SHUTDOWN_CLEANUP_MS = 5e3;
3379
4127
  function createRuntime(composition) {
3380
4128
  const { models, agents, workflows, teams, tools, contextProviders, memoryStore, runStore, credentialStore, sandbox, policies } = composition;
3381
4129
  const core = createRunCore(composition);
3382
- 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;
3383
4131
  const agentEngine = createAgentEngine(composition, core);
3384
4132
  const { prepareExecution, executeAgent, nextEvent, drainExecution, resumeAgent, cancelStoredAgent } = agentEngine;
3385
- const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution } = createWorkflowEngine(composition, core, agentEngine);
4133
+ const { prepareWorkflowExecution, executeWorkflow, nextWorkflowEvent, drainWorkflowExecution, resumeWorkflow, cancelWorkflowExecution, cancelStoredWorkflow } = createWorkflowEngine(composition, core, agentEngine);
3386
4134
  const resumeRun = async (runId, resolution) => {
4135
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3387
4136
  const run = await runStore.getRun(runId);
3388
4137
  if (run && isCoordinatorRun(run)) return resumeWorkflow(runId, resolution);
3389
4138
  return resumeAgent(runId, resolution);
3390
4139
  };
3391
4140
  const recoverRun = async (runId, actor) => {
4141
+ if (isClosed()) throw new FevexRunError("RUNTIME_CLOSED", "Runtime is not accepting new runs", runId);
3392
4142
  const run = await runStore.getRun(runId);
3393
4143
  if (!run) throw new FevexRunError("RUN_NOT_RECOVERABLE", `Run "${runId}" cannot be recovered`, runId);
3394
4144
  if (isCoordinatorRun(run)) return resumeWorkflow(runId, void 0, actor);
3395
4145
  return resumeAgent(runId, void 0, false, actor);
3396
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
+ };
3397
4195
  return {
3398
4196
  async startAgent(name, request) {
3399
4197
  const state = await prepareExecution(name, request);
@@ -3499,58 +4297,7 @@ function createRuntime(composition) {
3499
4297
  }
3500
4298
  const run = await runStore.getRun(runId);
3501
4299
  if (!run || run.status !== "paused" && run.status !== "running") return false;
3502
- if (!isDurableRunStore(runStore)) return false;
3503
- const session = await runStore.getSession(run.sessionId);
3504
- if (!session) return false;
3505
- if (isCoordinatorRun(run)) {
3506
- const checkpoint = await runStore.getCheckpoint(runId);
3507
- if (!checkpoint) return false;
3508
- const ownerId = `${runtimeOwner}:${crypto.randomUUID()}`;
3509
- const lease = {
3510
- generation: 0,
3511
- runId,
3512
- ownerId,
3513
- expiresAt: new Date(Date.now() + LEASE_MS).toISOString()
3514
- };
3515
- if (!await runStore.acquireLease(lease)) return false;
3516
- try {
3517
- const childRunIds = Object.values(checkpoint.steps).filter((record) => record.type === "agent" && record.status === "running").map(({ childRunId }) => childRunId);
3518
- for (const childRunId of childRunIds) if (!await cancelStoredAgent(childRunId)) return false;
3519
- const state = {
3520
- run,
3521
- session,
3522
- request: {
3523
- input: checkpoint.input,
3524
- ...checkpoint.reasoning === void 0 ? {} : { reasoning: checkpoint.reasoning },
3525
- sessionId: session.id,
3526
- context: checkpoint.context,
3527
- limits: checkpoint.limits,
3528
- signal: new AbortController().signal
3529
- },
3530
- controller: new AbortController(),
3531
- eventSequence: (await runStore.listEvents(runId)).at(-1)?.sequence ?? 0,
3532
- checkpoint,
3533
- advancing: false,
3534
- lease,
3535
- commitQueue: Promise.resolve()
3536
- };
3537
- run.status = "cancelled";
3538
- run.pause = void 0;
3539
- run.error = "aborted";
3540
- run.usage = checkpoint.budget?.usage;
3541
- const cancelled = createCoordinatorEvent(state, "workflow.run.cancelled", { reason: "aborted" }, "team.run.cancelled");
3542
- await commit(state, {
3543
- checkpoint: null,
3544
- events: [cancelled]
3545
- });
3546
- return true;
3547
- } catch (error) {
3548
- if (error instanceof FevexRunError && error.code === "RUN_CONFLICT" && (await runStore.getRun(runId))?.status !== "paused") return false;
3549
- throw error;
3550
- } finally {
3551
- await runStore.releaseLease(runId, ownerId, lease.generation).catch(() => {});
3552
- }
3553
- }
4300
+ if (isCoordinatorRun(run)) return cancelStoredWorkflow(runId);
3554
4301
  return cancelStoredAgent(runId);
3555
4302
  },
3556
4303
  resumeRun(runId, resolution) {
@@ -3559,6 +4306,10 @@ function createRuntime(composition) {
3559
4306
  recoverRun(runId, options) {
3560
4307
  return recoverRun(runId, options.actor);
3561
4308
  },
4309
+ async purgeRuns(options) {
4310
+ if (!isRetainableRunStore(runStore)) throw new FevexRunError("RETENTION_UNSUPPORTED", "The configured run store does not support purgeRuns");
4311
+ return runStore.purgeRuns(options);
4312
+ },
3562
4313
  async compactSession(sessionId, summary) {
3563
4314
  if (typeof summary !== "string" || !summary.trim()) throw new Error("Session summary must be a non-empty string");
3564
4315
  if (activeSessions.has(sessionId)) throw new Error(`Session "${sessionId}" has an active run`);
@@ -3572,13 +4323,13 @@ function createRuntime(composition) {
3572
4323
  await runStore.saveSession(session);
3573
4324
  return structuredClone(session);
3574
4325
  },
3575
- async flushObservability() {
3576
- while (pendingExports.size) await Promise.all([...pendingExports]);
3577
- if (exportFailures.length) {
3578
- const failures = exportFailures.splice(0);
3579
- throw new AggregateError(failures, "Observability export failed");
3580
- }
3581
- }
4326
+ flushObservability(options) {
4327
+ return traceExports.flush(options);
4328
+ },
4329
+ getObservabilityStats() {
4330
+ return traceExports.stats();
4331
+ },
4332
+ shutdown
3582
4333
  };
3583
4334
  }
3584
4335
  //#endregion
@@ -3593,4 +4344,4 @@ function createFevex(config) {
3593
4344
  return createRuntime(createComposition(config));
3594
4345
  }
3595
4346
  //#endregion
3596
- export { ChannelError, FEVEX_JSON_SCHEMA_PROFILE_VERSION, FevexConfigurationError, FevexRunError, InMemoryMemoryStore, InMemoryRunStore, IntegrationError, PROVIDER_REASONING_UNSUPPORTED, PROVIDER_SCHEMA_UNSUPPORTED, RunPausedError, SCHEMA_NOT_TRANSPORTABLE, SandboxError, compileFevexJsonSchema, createFevex, createLocalSandbox, createMemoryContextProvider, defineAgent, defineConnection, defineContextProvider, definePolicy, defineSkill, defineTeam, defineTool, defineWorkflow, deliverChannelRun, handleChannelInput, isDurableRunStore, sandboxResultToJson, toToolSpec, validateFevexJsonSchemaProfile, validateListEventsOptions };
4347
+ 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 };