@kortyx/agent 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ var require_dist = __commonJS({
55
55
  createWorkflowHooks: () => createWorkflowHooks,
56
56
  emitWorkflowCall: () => emitWorkflowCall2,
57
57
  runWithHookContext: () => runWithHookContext,
58
+ useAbortSignal: () => useAbortSignal,
58
59
  useInterrupt: () => useInterrupt,
59
60
  useNodeState: () => useNodeState,
60
61
  useReason: () => useReason2,
@@ -217,6 +218,7 @@ var require_dist = __commonJS({
217
218
  }
218
219
  return ctx;
219
220
  }
221
+ var import_core5 = require("@kortyx/core");
220
222
  var resolveReasonCheckpointKey = (args) => typeof args.id === "string" && args.id.length > 0 ? `__useReason:${args.id}` : `__useReason:auto:${args.autoIndex}`;
221
223
  var readReasonCheckpoint = (raw) => {
222
224
  if (!raw || typeof raw !== "object") return void 0;
@@ -267,6 +269,7 @@ var require_dist = __commonJS({
267
269
  };
268
270
  var awaitInterruptInternal = (args) => {
269
271
  const ctx = getHookContext();
272
+ (0, import_core5.throwIfExecutionAborted)(ctx.node.abortSignal);
270
273
  const request = parseWithSchema(
271
274
  args.requestSchema,
272
275
  args.request,
@@ -299,6 +302,7 @@ var require_dist = __commonJS({
299
302
  );
300
303
  return Promise.resolve(response);
301
304
  };
305
+ var import_core52 = require("@kortyx/core");
302
306
  var import_node_crypto4 = require("crypto");
303
307
  var parseStructuredPath = (path, source) => {
304
308
  if (path.length === 0) {
@@ -386,7 +390,9 @@ var require_dist = __commonJS({
386
390
  var shouldEmitStructured = (cfg) => Boolean(cfg);
387
391
  var shouldStreamStructured = (cfg) => Boolean(cfg) && (cfg?.stream ?? true);
388
392
  var import_node_crypto32 = require("crypto");
393
+ var import_core32 = require("@kortyx/core");
389
394
  var import_node_crypto22 = require("crypto");
395
+ var import_core22 = require("@kortyx/core");
390
396
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
391
397
  var createRuntimeId = () => {
392
398
  try {
@@ -413,6 +419,7 @@ var require_dist = __commonJS({
413
419
  const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
414
420
  const providerOptions = args.providerOptions ?? args.model.options?.providerOptions;
415
421
  const tools = args.tools ?? args.model.options?.tools;
422
+ (0, import_core22.throwIfExecutionAborted)(abortSignal);
416
423
  const model = args.model.provider.getModel(args.model.modelId, {
417
424
  ...temperature !== void 0 ? { temperature } : {},
418
425
  streaming: stream,
@@ -505,6 +512,7 @@ var require_dist = __commonJS({
505
512
  }
506
513
  const response2 = await model.stream(messages);
507
514
  for await (const chunk of response2) {
515
+ (0, import_core22.throwIfExecutionAborted)(abortSignal);
508
516
  switch (chunk.type) {
509
517
  case "text-delta": {
510
518
  if (chunk.delta.length === 0) break;
@@ -583,10 +591,12 @@ var require_dist = __commonJS({
583
591
  ...providerMetadata !== void 0 ? { providerMetadata } : {},
584
592
  ...warnings !== void 0 ? { warnings } : {}
585
593
  };
594
+ (0, import_core22.throwIfExecutionAborted)(abortSignal);
586
595
  endTrace(traceSpan, result2);
587
596
  return result2;
588
597
  }
589
598
  const response = await model.invoke(messages);
599
+ (0, import_core22.throwIfExecutionAborted)(abortSignal);
590
600
  if (emit) {
591
601
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
592
602
  ...commonMeta
@@ -613,8 +623,9 @@ var require_dist = __commonJS({
613
623
  endTrace(traceSpan, result);
614
624
  return result;
615
625
  } catch (error) {
616
- failTrace(traceSpan, error);
617
- throw error;
626
+ const failure = abortSignal?.aborted ? (0, import_core22.createExecutionCancelledError)() : error;
627
+ failTrace(traceSpan, failure);
628
+ throw failure;
618
629
  }
619
630
  }
620
631
  var createRuntimeId2 = () => {
@@ -626,6 +637,13 @@ var require_dist = __commonJS({
626
637
  };
627
638
  async function reasonEngine(args, meta, inputOverride) {
628
639
  const ctx = getHookContext();
640
+ const abortSignal = (0, import_core32.combineAbortSignals)(
641
+ ctx.node.abortSignal,
642
+ args.abortSignal,
643
+ args.model.options?.abortSignal
644
+ );
645
+ (0, import_core32.throwIfExecutionAborted)(abortSignal);
646
+ ctx.node.consumeExecution?.("maxModelPasses");
629
647
  return runReasonEngine({
630
648
  model: args.model,
631
649
  input: inputOverride ?? args.input,
@@ -633,7 +651,7 @@ var require_dist = __commonJS({
633
651
  temperature: args.temperature,
634
652
  maxOutputTokens: args.maxOutputTokens,
635
653
  stopSequences: args.stopSequences,
636
- abortSignal: args.abortSignal,
654
+ abortSignal,
637
655
  reasoning: args.reasoning,
638
656
  responseFormat: args.responseFormat,
639
657
  providerOptions: args.providerOptions,
@@ -815,7 +833,7 @@ var require_dist = __commonJS({
815
833
  ...output !== void 0 ? { output } : {}
816
834
  };
817
835
  };
818
- var import_zod3 = require("zod");
836
+ var import_zod5 = require("zod");
819
837
  var tryStringify2 = (value) => {
820
838
  if (typeof value === "string") return value;
821
839
  try {
@@ -826,7 +844,7 @@ var require_dist = __commonJS({
826
844
  };
827
845
  var tryGetSchemaHint = (schema) => {
828
846
  try {
829
- const hint = (0, import_zod3.toJSONSchema)(schema);
847
+ const hint = (0, import_zod5.toJSONSchema)(schema);
830
848
  return JSON.stringify(hint, null, 2);
831
849
  } catch {
832
850
  return void 0;
@@ -1574,6 +1592,7 @@ ${tryStringify2(args.response)}`
1574
1592
  ] : [];
1575
1593
  });
1576
1594
  };
1595
+ var import_core42 = require("@kortyx/core");
1577
1596
  var toErrorMessage2 = (error) => error instanceof Error ? error.message : String(error);
1578
1597
  var toToolDefinitions = (tools) => tools.map((tool) => ({
1579
1598
  name: tool.name,
@@ -1627,6 +1646,11 @@ ${tryStringify2(args.response)}`
1627
1646
  var runReasonToolLoop = async (args) => {
1628
1647
  const { useReasonArgs, id, opId, traceSpan } = args;
1629
1648
  const ctx = getHookContext();
1649
+ const abortSignal = (0, import_core42.combineAbortSignals)(
1650
+ ctx.node.abortSignal,
1651
+ useReasonArgs.abortSignal,
1652
+ useReasonArgs.model.options?.abortSignal
1653
+ );
1630
1654
  const tools = useReasonArgs.tools ?? [];
1631
1655
  const toolByName = /* @__PURE__ */ new Map();
1632
1656
  let validationCompleted = false;
@@ -1676,6 +1700,7 @@ ${tryStringify2(args.response)}`
1676
1700
  };
1677
1701
  try {
1678
1702
  for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
1703
+ (0, import_core42.throwIfExecutionAborted)(abortSignal);
1679
1704
  traceSpan?.addEvent?.("useReason.tool-step.start", {
1680
1705
  stepIndex,
1681
1706
  toolCount: toolDefinitions.length
@@ -1693,6 +1718,7 @@ ${tryStringify2(args.response)}`
1693
1718
  finalText = step.text;
1694
1719
  finalRaw = step.raw;
1695
1720
  aggregatedUsage = mergeUsage(aggregatedUsage, step.usage);
1721
+ accumulateTokenUsage(step.usage);
1696
1722
  finalFinishReason = step.finishReason;
1697
1723
  aggregatedProviderMetadata = mergeProviderMetadata(
1698
1724
  aggregatedProviderMetadata,
@@ -1726,6 +1752,7 @@ ${tryStringify2(args.response)}`
1726
1752
  toolCalls
1727
1753
  });
1728
1754
  for (const toolCall of toolCalls) {
1755
+ (0, import_core42.throwIfExecutionAborted)(abortSignal);
1729
1756
  const tool = toolByName.get(toolCall.name);
1730
1757
  if (!tool) {
1731
1758
  throw new Error(
@@ -1793,10 +1820,13 @@ ${tryStringify2(args.response)}`
1793
1820
  }
1794
1821
  }
1795
1822
  try {
1823
+ (0, import_core42.throwIfExecutionAborted)(abortSignal);
1824
+ ctx.node.consumeExecution?.("maxToolCalls");
1796
1825
  const rawResult = await tool.execute(toolCall.input, {
1797
1826
  toolCallId: toolCall.id,
1798
- ...useReasonArgs.abortSignal ? { abortSignal: useReasonArgs.abortSignal } : {}
1827
+ ...abortSignal ? { abortSignal } : {}
1799
1828
  });
1829
+ (0, import_core42.throwIfExecutionAborted)(abortSignal);
1800
1830
  const result2 = normalizeToolResult(toolCall, rawResult);
1801
1831
  toolResults.push(result2);
1802
1832
  allToolResults.push(result2);
@@ -1816,6 +1846,9 @@ ${tryStringify2(args.response)}`
1816
1846
  isError: Boolean(result2.isError)
1817
1847
  });
1818
1848
  } catch (error) {
1849
+ (0, import_core42.throwIfExecutionAborted)(abortSignal);
1850
+ if ((0, import_core42.isExecutionLimitReached)(error) || (0, import_core42.isExecutionCancelled)(error))
1851
+ throw error;
1819
1852
  const result2 = {
1820
1853
  toolCallId: toolCall.id,
1821
1854
  name: tool.name,
@@ -1937,6 +1970,13 @@ ${tryStringify2(args.response)}`
1937
1970
  async function useReason(args) {
1938
1971
  assertReasoningThoughtsCompatibility(args);
1939
1972
  const ctx = getHookContext();
1973
+ const abortSignal = (0, import_core52.combineAbortSignals)(
1974
+ ctx.node.abortSignal,
1975
+ args.abortSignal,
1976
+ args.model.options?.abortSignal
1977
+ );
1978
+ (0, import_core52.throwIfExecutionAborted)(abortSignal);
1979
+ args = { ...args, abortSignal };
1940
1980
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
1941
1981
  const opId = createRuntimeId2();
1942
1982
  const checkpointKey = resolveReasonCheckpointKey({
@@ -2304,6 +2344,9 @@ ${tryStringify2(args.response)}`
2304
2344
  function useInterrupt(args) {
2305
2345
  return awaitInterruptInternal(args);
2306
2346
  }
2347
+ function useAbortSignal() {
2348
+ return getHookContext().node.abortSignal;
2349
+ }
2307
2350
  function useRuntimeContext() {
2308
2351
  const ctx = getHookContext();
2309
2352
  const config = ctx.state.config;
@@ -2346,6 +2389,7 @@ ${tryStringify2(args.response)}`
2346
2389
  return [getValue(), setValue];
2347
2390
  }
2348
2391
  var import_node_crypto5 = require("crypto");
2392
+ var import_core6 = require("@kortyx/core");
2349
2393
  var import_node_crypto42 = require("crypto");
2350
2394
  function emitWorkflowCall2(telemetry, type, payload) {
2351
2395
  const base = telemetry?.correlation;
@@ -2428,6 +2472,7 @@ ${tryStringify2(args.response)}`
2428
2472
  }
2429
2473
  async function useWorkflow(args) {
2430
2474
  const ctx = getHookContext();
2475
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2431
2476
  const service = ctx.node.callWorkflow;
2432
2477
  if (!service)
2433
2478
  throw new Error("useWorkflow requires an agent workflow registry.");
@@ -2461,6 +2506,7 @@ ${tryStringify2(args.response)}`
2461
2506
  workflow,
2462
2507
  "call input or target changed during replay"
2463
2508
  );
2509
+ if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
2464
2510
  record ??= {
2465
2511
  fingerprint,
2466
2512
  invocationId: (0, import_node_crypto5.randomUUID)(),
@@ -2494,6 +2540,15 @@ ${tryStringify2(args.response)}`
2494
2540
  ...content
2495
2541
  });
2496
2542
  };
2543
+ const recordUsage = (usage) => {
2544
+ if (!usage) return;
2545
+ accumulateTokenUsage({
2546
+ input: usage.input - (record.usage?.input ?? 0),
2547
+ output: usage.output - (record.usage?.output ?? 0),
2548
+ total: usage.total - (record.usage?.total ?? 0)
2549
+ });
2550
+ record.usage = usage;
2551
+ };
2497
2552
  const cached = record.status === "completed";
2498
2553
  try {
2499
2554
  for (const interruption of record.interrupts) {
@@ -2522,14 +2577,28 @@ ${tryStringify2(args.response)}`
2522
2577
  ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2523
2578
  });
2524
2579
  } catch (error) {
2580
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2581
+ if ((0, import_core6.isExecutionCancelled)(error)) throw error;
2582
+ if ((0, import_core6.isExecutionLimitReached)(error)) {
2583
+ const child = error;
2584
+ if (child.__kortyxChildSnapshot)
2585
+ record.snapshot = child.__kortyxChildSnapshot;
2586
+ recordUsage(child.__kortyxChildUsage);
2587
+ report("suspended", {
2588
+ reason: "limit_reached",
2589
+ limit: error.limitReached
2590
+ });
2591
+ throw error;
2592
+ }
2525
2593
  record.status = "failed";
2526
2594
  record.error = error instanceof Error ? error.message : String(error);
2527
2595
  report("failed", { error: "Child workflow failed" });
2528
2596
  throw new WorkflowCallError(workflow, record.error);
2529
2597
  }
2598
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2599
+ recordUsage(outcome.usage);
2530
2600
  if (outcome.status === "completed") {
2531
2601
  record.data = outcome.data;
2532
- accumulateTokenUsage(outcome.usage);
2533
2602
  workflowCallFingerprint(record.data);
2534
2603
  record.status = "completed";
2535
2604
  delete record.snapshot;
@@ -2569,6 +2638,7 @@ ${tryStringify2(args.response)}`
2569
2638
  // src/index.ts
2570
2639
  var index_exports = {};
2571
2640
  __export(index_exports, {
2641
+ ExecutionRequestError: () => ExecutionRequestError,
2572
2642
  createAgent: () => createAgent,
2573
2643
  createChatRouteHandler: () => createChatRouteHandler,
2574
2644
  createCheckpointRouteHandler: () => createCheckpointRouteHandler,
@@ -2649,6 +2719,7 @@ function parseCheckpointRequestBody(value) {
2649
2719
  async function handleChatRequestBody(args) {
2650
2720
  const { agent, body } = args;
2651
2721
  const stream = await agent.streamChat(body.messages, {
2722
+ ...args.abortSignal ? { abortSignal: args.abortSignal } : {},
2652
2723
  sessionId: body.sessionId,
2653
2724
  workflowId: body.workflowId,
2654
2725
  context: body.context
@@ -2668,7 +2739,11 @@ function createChatRouteHandler(args) {
2668
2739
  return async function POST(request) {
2669
2740
  try {
2670
2741
  const body = parseChatRequestBody(await request.json());
2671
- return await handleChatRequestBody({ agent, body });
2742
+ return await handleChatRequestBody({
2743
+ agent,
2744
+ body,
2745
+ abortSignal: request.signal
2746
+ });
2672
2747
  } catch (error) {
2673
2748
  return new Response(
2674
2749
  JSON.stringify({
@@ -2734,10 +2809,168 @@ async function* streamChatFromRoute(args) {
2734
2809
 
2735
2810
  // src/chat/create-agent.ts
2736
2811
  var import_node_path = require("path");
2812
+ var import_core4 = require("@kortyx/core");
2737
2813
  var import_providers = require("@kortyx/providers");
2738
- var import_runtime4 = require("@kortyx/runtime");
2814
+ var import_runtime5 = require("@kortyx/runtime");
2815
+ var import_zod4 = require("zod");
2816
+
2817
+ // src/execution/execute.ts
2818
+ var import_core3 = require("@kortyx/core");
2819
+ var import_runtime3 = require("@kortyx/runtime");
2820
+ var import_zod3 = require("zod");
2821
+
2822
+ // src/interrupt/resume-handler.ts
2823
+ var import_core2 = require("@kortyx/core");
2824
+ var import_runtime2 = require("@kortyx/runtime");
2825
+
2826
+ // src/orchestrator.ts
2827
+ var import_node_stream = require("stream");
2828
+ var import_core = require("@kortyx/core");
2829
+ var import_runtime = require("@kortyx/runtime");
2830
+ var import_langgraph = require("@langchain/langgraph");
2831
+
2832
+ // src/execution/contracts.ts
2739
2833
  var import_zod2 = require("zod");
2740
2834
 
2835
+ // src/execution/types.ts
2836
+ var ExecutionRequestError = class extends Error {
2837
+ constructor(code, message) {
2838
+ super(message);
2839
+ this.code = code;
2840
+ }
2841
+ code;
2842
+ name = "ExecutionRequestError";
2843
+ };
2844
+
2845
+ // src/execution/contracts.ts
2846
+ async function resolveExecutionWorkflow(select, reference) {
2847
+ const id = typeof reference === "string" ? reference : reference.id;
2848
+ let workflow;
2849
+ try {
2850
+ workflow = await select(id);
2851
+ } catch {
2852
+ throw new ExecutionRequestError(
2853
+ "UNKNOWN_WORKFLOW",
2854
+ `Workflow '${id}' is not registered.`
2855
+ );
2856
+ }
2857
+ if (workflow.id !== id)
2858
+ throw new ExecutionRequestError(
2859
+ "UNKNOWN_WORKFLOW",
2860
+ `Workflow '${id}' is not registered.`
2861
+ );
2862
+ if (!workflow.inputSchema || !workflow.outputSchema)
2863
+ throw new ExecutionRequestError(
2864
+ "MISSING_CONTRACT",
2865
+ `Workflow '${id}' requires inputSchema and outputSchema.`
2866
+ );
2867
+ if (typeof reference !== "string" && (reference.version !== workflow.version || reference.inputSchema !== workflow.inputSchema || reference.outputSchema !== workflow.outputSchema))
2868
+ throw new ExecutionRequestError(
2869
+ "CONTRACT_MISMATCH",
2870
+ "The typed workflow contract does not match the registered definition."
2871
+ );
2872
+ return workflow;
2873
+ }
2874
+ function parseExecutionInput(workflow, input) {
2875
+ try {
2876
+ import_zod2.z.json().parse(input);
2877
+ const parsed = workflow.inputSchema ? workflow.inputSchema.parse(input) : input;
2878
+ import_zod2.z.json().parse(parsed);
2879
+ return parsed;
2880
+ } catch (error) {
2881
+ throw new ExecutionRequestError(
2882
+ "INVALID_INPUT",
2883
+ error instanceof Error ? error.message : String(error)
2884
+ );
2885
+ }
2886
+ }
2887
+ async function validateExecutionOutput(state, select) {
2888
+ if (!state.config?.executionContract) return state;
2889
+ const terminal = await select(String(state.currentWorkflow));
2890
+ const contract = state.config.executionContract;
2891
+ const root = await select(contract.id);
2892
+ if (root.id !== contract.id || root.version !== contract.version)
2893
+ throw new Error("The root workflow changed since execution started.");
2894
+ let data = state.data ?? {};
2895
+ try {
2896
+ for (const schema of /* @__PURE__ */ new Set([terminal.outputSchema, root.outputSchema])) {
2897
+ if (schema) data = schema.parse(data);
2898
+ }
2899
+ if (terminal.outputSchema || root.outputSchema) import_zod2.z.json().parse(data);
2900
+ } catch (error) {
2901
+ const failure = new Error(
2902
+ `Workflow output validation failed: ${error instanceof Error ? error.message : String(error)}`
2903
+ );
2904
+ Object.assign(failure, { code: "INVALID_OUTPUT" });
2905
+ throw failure;
2906
+ }
2907
+ return { ...state, data };
2908
+ }
2909
+
2910
+ // src/stream/transform-graph-stream-for-ui.ts
2911
+ var isGraphStateOutput = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && "currentWorkflow" in value;
2912
+ async function* transformGraphStreamForUI(stream, options = {}) {
2913
+ const { debug = false, emitStatus = debug } = options;
2914
+ const startedNodes = /* @__PURE__ */ new Set();
2915
+ const endedNodes = /* @__PURE__ */ new Set();
2916
+ let latestGraphState = null;
2917
+ for await (const event of stream) {
2918
+ const { event: type, name, data } = event ?? {};
2919
+ if (debug) console.log(`[debug:event]`, JSON.stringify(event, null, 2));
2920
+ switch (type) {
2921
+ case "on_chain_start":
2922
+ if (name && !name.startsWith("ChannelWrite")) {
2923
+ if (name === "__start__" || name === "__end__") break;
2924
+ if (startedNodes.has(name)) break;
2925
+ startedNodes.add(name);
2926
+ if (debug) console.log(`[debug:start] node=${name}`);
2927
+ if (emitStatus) {
2928
+ yield { type: "status", message: `Processing node: ${name}` };
2929
+ }
2930
+ }
2931
+ break;
2932
+ case "on_chain_end": {
2933
+ const nodeName = name;
2934
+ const output = data?.output;
2935
+ if (debug)
2936
+ console.log(
2937
+ `[debug:on_chain_end:${nodeName}] output=`,
2938
+ JSON.stringify(output, null, 2)
2939
+ );
2940
+ if (!output || nodeName?.startsWith("ChannelWrite")) break;
2941
+ if (isGraphStateOutput(output)) latestGraphState = output;
2942
+ if (nodeName !== "__start__" && nodeName !== "__end__") {
2943
+ if (!endedNodes.has(nodeName)) {
2944
+ if (emitStatus) {
2945
+ yield {
2946
+ type: "status",
2947
+ message: `\u2705 Completed node: ${nodeName}`
2948
+ };
2949
+ }
2950
+ endedNodes.add(nodeName);
2951
+ }
2952
+ }
2953
+ break;
2954
+ }
2955
+ case "on_graph_end": {
2956
+ if (debug)
2957
+ console.log(`[debug:on_graph_end]`, JSON.stringify(data, null, 2));
2958
+ const output = data?.output;
2959
+ yield {
2960
+ type: "done",
2961
+ data: isGraphStateOutput(output) ? output : latestGraphState
2962
+ };
2963
+ break;
2964
+ }
2965
+ default:
2966
+ if (debug) {
2967
+ console.warn(`[debug:unknown_event]`, type);
2968
+ }
2969
+ break;
2970
+ }
2971
+ }
2972
+ }
2973
+
2741
2974
  // src/telemetry/events.ts
2742
2975
  var import_node_crypto = require("crypto");
2743
2976
  var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -3008,134 +3241,6 @@ var prepareWorkflowTelemetry = (args) => {
3008
3241
  };
3009
3242
  };
3010
3243
 
3011
- // src/telemetry/workflow-call-branch.ts
3012
- var import_node_crypto3 = require("crypto");
3013
- var import_hooks = __toESM(require_dist());
3014
- function restoreWorkflowCallBranch(requests, telemetry, sourceRunId) {
3015
- const branchId = (0, import_node_crypto3.randomUUID)();
3016
- const emitted = /* @__PURE__ */ new Set();
3017
- for (const request of requests) {
3018
- if (request.state)
3019
- request.state.config = {
3020
- ...request.state.config,
3021
- executionBranchId: branchId
3022
- };
3023
- const seen = /* @__PURE__ */ new Set();
3024
- const visit = (value) => {
3025
- if (!value || typeof value !== "object" || seen.has(value)) return;
3026
- seen.add(value);
3027
- const record = value;
3028
- if (typeof record.invocationId === "string" && typeof record.fingerprint === "string" && Array.isArray(record.interrupts)) {
3029
- const previous = record.telemetry;
3030
- if (previous) {
3031
- record.sequence = 0;
3032
- record.telemetry = { ...previous, branchId };
3033
- if (!emitted.has(record.invocationId)) {
3034
- emitted.add(record.invocationId);
3035
- (0, import_hooks.emitWorkflowCall)(
3036
- telemetry ? {
3037
- ...telemetry,
3038
- correlation: {
3039
- runId: request.runId,
3040
- workflowId: request.workflow,
3041
- ...request.sessionId ? { sessionId: request.sessionId } : {}
3042
- }
3043
- } : void 0,
3044
- "workflow.call.restored",
3045
- {
3046
- ...previous,
3047
- branchId,
3048
- sourceBranchId: previous.branchId,
3049
- sourceRunId,
3050
- sequence: 0,
3051
- status: record.status
3052
- }
3053
- );
3054
- }
3055
- }
3056
- }
3057
- for (const child of Object.values(record)) visit(child);
3058
- };
3059
- visit(request);
3060
- }
3061
- return branchId;
3062
- }
3063
-
3064
- // src/chat/process-chat.ts
3065
- var import_runtime3 = require("@kortyx/runtime");
3066
-
3067
- // src/interrupt/resume-handler.ts
3068
- var import_runtime2 = require("@kortyx/runtime");
3069
-
3070
- // src/orchestrator.ts
3071
- var import_node_stream = require("stream");
3072
- var import_runtime = require("@kortyx/runtime");
3073
- var import_langgraph = require("@langchain/langgraph");
3074
-
3075
- // src/stream/transform-graph-stream-for-ui.ts
3076
- var isGraphStateOutput = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && "currentWorkflow" in value;
3077
- async function* transformGraphStreamForUI(stream, options = {}) {
3078
- const { debug = false, emitStatus = debug } = options;
3079
- const startedNodes = /* @__PURE__ */ new Set();
3080
- const endedNodes = /* @__PURE__ */ new Set();
3081
- let latestGraphState = null;
3082
- for await (const event of stream) {
3083
- const { event: type, name, data } = event ?? {};
3084
- if (debug) console.log(`[debug:event]`, JSON.stringify(event, null, 2));
3085
- switch (type) {
3086
- case "on_chain_start":
3087
- if (name && !name.startsWith("ChannelWrite")) {
3088
- if (name === "__start__" || name === "__end__") break;
3089
- if (startedNodes.has(name)) break;
3090
- startedNodes.add(name);
3091
- if (debug) console.log(`[debug:start] node=${name}`);
3092
- if (emitStatus) {
3093
- yield { type: "status", message: `Processing node: ${name}` };
3094
- }
3095
- }
3096
- break;
3097
- case "on_chain_end": {
3098
- const nodeName = name;
3099
- const output = data?.output;
3100
- if (debug)
3101
- console.log(
3102
- `[debug:on_chain_end:${nodeName}] output=`,
3103
- JSON.stringify(output, null, 2)
3104
- );
3105
- if (!output || nodeName?.startsWith("ChannelWrite")) break;
3106
- if (isGraphStateOutput(output)) latestGraphState = output;
3107
- if (nodeName !== "__start__" && nodeName !== "__end__") {
3108
- if (!endedNodes.has(nodeName)) {
3109
- if (emitStatus) {
3110
- yield {
3111
- type: "status",
3112
- message: `\u2705 Completed node: ${nodeName}`
3113
- };
3114
- }
3115
- endedNodes.add(nodeName);
3116
- }
3117
- }
3118
- break;
3119
- }
3120
- case "on_graph_end": {
3121
- if (debug)
3122
- console.log(`[debug:on_graph_end]`, JSON.stringify(data, null, 2));
3123
- const output = data?.output;
3124
- yield {
3125
- type: "done",
3126
- data: isGraphStateOutput(output) ? output : latestGraphState
3127
- };
3128
- break;
3129
- }
3130
- default:
3131
- if (debug) {
3132
- console.warn(`[debug:unknown_event]`, type);
3133
- }
3134
- break;
3135
- }
3136
- }
3137
- }
3138
-
3139
3244
  // src/orchestrator.ts
3140
3245
  var isRecord3 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3141
3246
  var isGraphState = (value) => isRecord3(value) && typeof value.currentWorkflow === "string";
@@ -3157,15 +3262,30 @@ async function orchestrateGraphStream({
3157
3262
  graph,
3158
3263
  state,
3159
3264
  config: initialConfig,
3265
+ abortSignal: requestSignal,
3266
+ emitOutput = true,
3267
+ onOutcome,
3160
3268
  selectWorkflow,
3161
3269
  knownWorkflowIds,
3162
3270
  frameworkAdapter
3163
3271
  }) {
3164
3272
  const out = new import_node_stream.PassThrough({ objectMode: true });
3273
+ const write = (chunk) => {
3274
+ if (emitOutput && !out.destroyed) out.write(chunk);
3275
+ };
3276
+ let outcomeState = state;
3277
+ let outcomeError;
3278
+ let sessionCheckpointId;
3165
3279
  let config = initialConfig;
3280
+ const budget = config.executionBudget;
3166
3281
  let currentGraph = graph;
3167
3282
  let currentState = state;
3168
3283
  let finished = false;
3284
+ const controller = new AbortController();
3285
+ const abortSignal = (0, import_core.combineAbortSignals)(requestSignal, controller.signal);
3286
+ out.on("close", () => {
3287
+ if (!finished) controller.abort();
3288
+ });
3169
3289
  let structuredSeq = 0;
3170
3290
  const debugEnabled = Boolean(config?.features?.tracing);
3171
3291
  const traceAdapter = getTraceAdapter(config);
@@ -3206,7 +3326,7 @@ async function orchestrateGraphStream({
3206
3326
  },
3207
3327
  tags: Array.isArray(telemetryConfig.tags) ? telemetryConfig.tags : void 0,
3208
3328
  captureContent: telemetryConfig.captureContent,
3209
- ...typeof state.input === "string" ? { input: state.input } : {}
3329
+ input: typeof state.input === "string" ? state.input : JSON.stringify(state.input)
3210
3330
  }
3211
3331
  };
3212
3332
  const namespacesUsed = /* @__PURE__ */ new Set();
@@ -3217,7 +3337,7 @@ async function orchestrateGraphStream({
3217
3337
  try {
3218
3338
  const sid = config?.session?.id;
3219
3339
  if (sid && typeof sid === "string") {
3220
- out.write({ type: "session", sessionId: sid });
3340
+ write({ type: "session", sessionId: sid });
3221
3341
  }
3222
3342
  } catch {
3223
3343
  }
@@ -3241,6 +3361,7 @@ async function orchestrateGraphStream({
3241
3361
  }
3242
3362
  lastOutputSegmentId = textStreamId;
3243
3363
  accumulatedOutput += delta;
3364
+ if (!emitOutput) accumulatedOutput = accumulatedOutput.slice(0, 65536);
3244
3365
  };
3245
3366
  const nextStructuredStreamId = () => `${runId}:structured:${structuredSeq++}`;
3246
3367
  const runTraceEndArgs = () => accumulatedOutput.length > 0 ? { telemetry: { output: accumulatedOutput } } : void 0;
@@ -3248,8 +3369,10 @@ async function orchestrateGraphStream({
3248
3369
  let wroteHumanInput = false;
3249
3370
  const pendingStore = frameworkAdapter?.pendingRequests;
3250
3371
  const sealPendingSnapshots = async () => {
3372
+ if (budget) outcomeState.config.executionBudget = budget;
3251
3373
  if (typeof frameworkAdapter?.checkpointer?.getTuple !== "function") return;
3252
3374
  for (const [token, request] of activePendingRequests) {
3375
+ if (budget) request.state.config.executionBudget = budget;
3253
3376
  const graphSnapshot = await (0, import_runtime.captureGraphSnapshot)(
3254
3377
  frameworkAdapter.checkpointer,
3255
3378
  runId,
@@ -3258,7 +3381,10 @@ async function orchestrateGraphStream({
3258
3381
  if (graphSnapshot) {
3259
3382
  const sealed = { ...request, graphSnapshot };
3260
3383
  activePendingRequests.set(token, sealed);
3261
- await pendingStore?.update(token, { graphSnapshot });
3384
+ await pendingStore?.update(token, {
3385
+ graphSnapshot,
3386
+ state: request.state
3387
+ });
3262
3388
  }
3263
3389
  }
3264
3390
  };
@@ -3309,6 +3435,7 @@ async function orchestrateGraphStream({
3309
3435
  activePendingRequests.set(record.token, record);
3310
3436
  if (pendingStore) {
3311
3437
  const savePromise = pendingStore.save(record).catch((error) => {
3438
+ outcomeError = error;
3312
3439
  console.error("[orchestrator] failed to save pending request", error);
3313
3440
  });
3314
3441
  pendingRequestWrites.push(savePromise);
@@ -3318,7 +3445,7 @@ async function orchestrateGraphStream({
3318
3445
  ([key]) => !key.startsWith("__kortyx")
3319
3446
  )
3320
3447
  ) : void 0;
3321
- out.write({
3448
+ write({
3322
3449
  type: "interrupt",
3323
3450
  requestId: record.requestId,
3324
3451
  resumeToken: record.token,
@@ -3380,8 +3507,9 @@ async function orchestrateGraphStream({
3380
3507
  const msg = String(
3381
3508
  payload?.message ?? "Unexpected error"
3382
3509
  );
3383
- out.write({ type: "error", message: msg });
3384
- out.write({ type: "done" });
3510
+ outcomeError = new Error(msg);
3511
+ write({ type: "error", message: msg });
3512
+ write({ type: "done" });
3385
3513
  finished = true;
3386
3514
  out.end();
3387
3515
  return;
@@ -3394,14 +3522,14 @@ async function orchestrateGraphStream({
3394
3522
  if (msg && msg === lastStatusMsg && now - lastStatusAt < 250) return;
3395
3523
  lastStatusMsg = msg;
3396
3524
  lastStatusAt = now;
3397
- out.write({ type: "status", message: msg });
3525
+ write({ type: "status", message: msg });
3398
3526
  return;
3399
3527
  }
3400
3528
  if (event === "text-start") {
3401
3529
  const node = payload?.node;
3402
3530
  if (!node) return;
3403
3531
  touchedNodes.add(node);
3404
- out.write({
3532
+ write({
3405
3533
  type: "text-start",
3406
3534
  node,
3407
3535
  ...typeof payload.id === "string" ? { id: payload.id } : {},
@@ -3416,7 +3544,7 @@ async function orchestrateGraphStream({
3416
3544
  if (!node || !delta) return;
3417
3545
  touchedNodes.add(node);
3418
3546
  appendAccumulatedOutput(payload, node, delta);
3419
- out.write({
3547
+ write({
3420
3548
  type: "text-delta",
3421
3549
  delta,
3422
3550
  node,
@@ -3430,7 +3558,7 @@ async function orchestrateGraphStream({
3430
3558
  const node = payload?.node;
3431
3559
  if (!node) return;
3432
3560
  touchedNodes.add(node);
3433
- out.write({
3561
+ write({
3434
3562
  type: "text-end",
3435
3563
  node,
3436
3564
  ...typeof payload.id === "string" ? { id: payload.id } : {},
@@ -3444,7 +3572,7 @@ async function orchestrateGraphStream({
3444
3572
  if (typeof payloadObj.tool !== "string" || typeof payloadObj.toolCallId !== "string") {
3445
3573
  return;
3446
3574
  }
3447
- out.write({
3575
+ write({
3448
3576
  type: "tool-call-start",
3449
3577
  tool: payloadObj.tool,
3450
3578
  toolCallId: payloadObj.toolCallId,
@@ -3461,7 +3589,7 @@ async function orchestrateGraphStream({
3461
3589
  if (typeof payloadObj.tool !== "string" || typeof payloadObj.toolCallId !== "string") {
3462
3590
  return;
3463
3591
  }
3464
- out.write({
3592
+ write({
3465
3593
  type: "tool-call-result",
3466
3594
  tool: payloadObj.tool,
3467
3595
  toolCallId: payloadObj.toolCallId,
@@ -3480,7 +3608,7 @@ async function orchestrateGraphStream({
3480
3608
  if (typeof payloadObj.tool !== "string" || typeof payloadObj.toolCallId !== "string") {
3481
3609
  return;
3482
3610
  }
3483
- out.write({
3611
+ write({
3484
3612
  type: "tool-call-error",
3485
3613
  tool: payloadObj.tool,
3486
3614
  toolCallId: payloadObj.toolCallId,
@@ -3496,7 +3624,7 @@ async function orchestrateGraphStream({
3496
3624
  const node = payload?.node;
3497
3625
  const text = String(payload?.content ?? "");
3498
3626
  if (node) touchedNodes.add(node);
3499
- out.write({ type: "message", node, content: text });
3627
+ write({ type: "message", node, content: text });
3500
3628
  return;
3501
3629
  }
3502
3630
  if (event === "structured_data") {
@@ -3505,7 +3633,7 @@ async function orchestrateGraphStream({
3505
3633
  const streamId = typeof payloadObj.streamId === "string" && payloadObj.streamId ? payloadObj.streamId : nextStructuredStreamId();
3506
3634
  structuredStreamIds.add(streamId);
3507
3635
  if (payloadObj.node) touchedNodes.add(payloadObj.node);
3508
- out.write({
3636
+ write({
3509
3637
  type: "structured-data",
3510
3638
  node: payloadObj.node,
3511
3639
  streamId,
@@ -3531,7 +3659,7 @@ async function orchestrateGraphStream({
3531
3659
  return;
3532
3660
  }
3533
3661
  if (event === "transition") {
3534
- out.write({
3662
+ write({
3535
3663
  type: "transition",
3536
3664
  transitionTo: payload?.transitionTo,
3537
3665
  payload: payload?.payload ?? {}
@@ -3559,10 +3687,14 @@ async function orchestrateGraphStream({
3559
3687
  };
3560
3688
  const runLoop = async (runTraceSpan) => {
3561
3689
  while (true) {
3690
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
3562
3691
  let workflowFinalState = null;
3563
3692
  let workflowFinalGraphCheckpointId;
3564
3693
  let sawWorkflowDone = false;
3565
3694
  let workflowDoneData;
3695
+ currentGraph.execution ??= {};
3696
+ currentGraph.execution.abortSignal = abortSignal;
3697
+ currentGraph.execution.budget = budget;
3566
3698
  currentGraph.config = currentGraph.config || {};
3567
3699
  currentGraph.config.emit = forwardEmit;
3568
3700
  currentGraph.config.executionRunId = runId;
@@ -3595,7 +3727,7 @@ async function orchestrateGraphStream({
3595
3727
  };
3596
3728
  };
3597
3729
  if (debugEnabled) {
3598
- out.write({
3730
+ write({
3599
3731
  type: "status",
3600
3732
  message: `\u{1F9F5} thread_id=${threadId} run_id=${runId} workflow=${currentState.currentWorkflow}`
3601
3733
  });
@@ -3614,16 +3746,32 @@ async function orchestrateGraphStream({
3614
3746
  }
3615
3747
  });
3616
3748
  if (debugEnabled) {
3617
- out.write({
3749
+ write({
3618
3750
  type: "status",
3619
3751
  message: `\u25B6\uFE0F streamEvents invoke: resume=${Boolean(currentGraph.config?.resume)} thread_id=${threadId} run_id=${runId} ns=${String(currentState.currentWorkflow || "default")}`
3620
3752
  });
3621
3753
  }
3622
- const uiStream = transformGraphStreamForUI(runtimeStream, {
3754
+ const observedStream = (async function* () {
3755
+ for await (const event of runtimeStream) {
3756
+ const raw = event;
3757
+ if ((raw.event === "on_chain_end" || raw.event === "on_graph_end") && !raw.name?.startsWith("ChannelWrite") && isGraphState(raw.data?.output)) {
3758
+ outcomeState = raw.data.output;
3759
+ }
3760
+ yield event;
3761
+ }
3762
+ })();
3763
+ const uiStream = transformGraphStreamForUI(observedStream, {
3623
3764
  debug: debugEnabled,
3624
3765
  emitStatus: debugEnabled
3625
3766
  });
3626
- for await (const chunk of uiStream) {
3767
+ if (!emitOutput) {
3768
+ for await (const _event of observedStream) {
3769
+ if (finished) break;
3770
+ }
3771
+ sawWorkflowDone = true;
3772
+ workflowDoneData = outcomeState;
3773
+ }
3774
+ for await (const chunk of emitOutput ? uiStream : []) {
3627
3775
  if (finished) break;
3628
3776
  if ("node" in chunk && typeof chunk.node === "string") {
3629
3777
  touchedNodes.add(chunk.node);
@@ -3633,14 +3781,18 @@ async function orchestrateGraphStream({
3633
3781
  workflowDoneData = chunk.data;
3634
3782
  continue;
3635
3783
  }
3636
- out.write(chunk);
3784
+ write(chunk);
3785
+ }
3786
+ if (finished) {
3787
+ runTraceSpan.fail?.(outcomeError);
3788
+ return;
3637
3789
  }
3638
- if (finished) return;
3639
3790
  if (sawWorkflowDone) {
3640
3791
  const graphSnapshot = await readLatestGraphSnapshot();
3641
3792
  workflowFinalGraphCheckpointId = graphSnapshot?.checkpointId;
3642
3793
  workflowFinalState = isGraphState(workflowDoneData) ? workflowDoneData : graphSnapshot?.state ?? currentState;
3643
3794
  }
3795
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
3644
3796
  const transitionTo = pending.to;
3645
3797
  const transitionPayload = pending.payload;
3646
3798
  const sourceNodeId = pending.sourceNodeId;
@@ -3677,6 +3829,10 @@ async function orchestrateGraphStream({
3677
3829
  } : {}
3678
3830
  }
3679
3831
  });
3832
+ delete config.resume;
3833
+ delete config.resumeValue;
3834
+ delete config.resumeUpdate;
3835
+ delete config.resumeCheckpointId;
3680
3836
  const nextGraph = await (0, import_runtime.createExecutionGraph)(nextWorkflow, {
3681
3837
  ...config,
3682
3838
  emit: forwardEmit
@@ -3687,12 +3843,13 @@ async function orchestrateGraphStream({
3687
3843
  ...transitionPayload
3688
3844
  };
3689
3845
  const rawInputFromPayload = transitionPayload?.rawInput;
3690
- const newInput = typeof rawInputFromPayload === "string" ? rawInputFromPayload : currentState.input;
3846
+ const rawNextInput = typeof rawInputFromPayload === "string" ? rawInputFromPayload : currentState.input;
3691
3847
  currentState = {
3692
3848
  ...currentState,
3849
+ runtime: workflowFinalState?.runtime ?? currentState.runtime,
3693
3850
  currentWorkflow: transitionTo,
3694
3851
  config,
3695
- input: newInput,
3852
+ input: nextWorkflow.inputSchema ? parseExecutionInput(nextWorkflow, rawNextInput) : rawNextInput,
3696
3853
  data: mergedData,
3697
3854
  ui: {}
3698
3855
  // reset UI layer on new graph
@@ -3700,14 +3857,18 @@ async function orchestrateGraphStream({
3700
3857
  currentGraph = nextGraph;
3701
3858
  continue;
3702
3859
  } catch (err) {
3860
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
3861
+ if ((0, import_core.isExecutionCancelled)(err)) throw err;
3862
+ outcomeError = err;
3863
+ runTraceSpan.fail?.(err);
3703
3864
  runTraceSpan.addEvent?.("kortyx.transition.error", {
3704
3865
  transitionTo
3705
3866
  });
3706
- out.write({
3867
+ write({
3707
3868
  type: "error",
3708
3869
  message: `Transition failed to '${transitionTo}': ${err instanceof Error ? err.message : String(err)}`
3709
3870
  });
3710
- out.write({ type: "done" });
3871
+ write({ type: "done" });
3711
3872
  out.end();
3712
3873
  return;
3713
3874
  }
@@ -3737,6 +3898,10 @@ async function orchestrateGraphStream({
3737
3898
  }
3738
3899
  const shouldKeepFrameworkState = Boolean(pendingRecordToken) || Boolean(workflowFinalState?.awaitingHumanInput);
3739
3900
  if (!shouldKeepFrameworkState) {
3901
+ workflowFinalState = await validateExecutionOutput(
3902
+ workflowFinalState,
3903
+ selectWorkflow
3904
+ );
3740
3905
  try {
3741
3906
  if (frameworkAdapter?.cleanupRun) {
3742
3907
  await frameworkAdapter.cleanupRun(
@@ -3753,6 +3918,12 @@ async function orchestrateGraphStream({
3753
3918
  console.error("[orchestrator] framework cleanup failed", e);
3754
3919
  }
3755
3920
  }
3921
+ outcomeState = workflowFinalState;
3922
+ if (!emitOutput && !shouldKeepFrameworkState)
3923
+ accumulatedOutput = JSON.stringify(workflowFinalState.data).slice(
3924
+ 0,
3925
+ 65536
3926
+ );
3756
3927
  finished = true;
3757
3928
  runTraceSpan.setAttributes?.({
3758
3929
  "kortyx.run.awaiting_human_input": shouldKeepFrameworkState,
@@ -3775,7 +3946,8 @@ async function orchestrateGraphStream({
3775
3946
  pendingRequests: Array.from(activePendingRequests.values())
3776
3947
  }
3777
3948
  );
3778
- out.write({
3949
+ sessionCheckpointId = checkpoint.id;
3950
+ write({
3779
3951
  type: "checkpoint",
3780
3952
  id: checkpoint.id,
3781
3953
  sessionId: checkpoint.sessionId,
@@ -3793,15 +3965,23 @@ async function orchestrateGraphStream({
3793
3965
  }
3794
3966
  });
3795
3967
  } catch (error) {
3968
+ outcomeError = error;
3796
3969
  console.error("[orchestrator] session checkpoint failed", error);
3797
3970
  }
3798
3971
  }
3799
- out.write({ type: "done", data: workflowFinalState });
3972
+ write({ type: "done", data: workflowFinalState });
3800
3973
  out.end();
3801
3974
  return;
3802
3975
  }
3803
3976
  const naturalGraphSnapshot = await readLatestGraphSnapshot();
3804
- const naturalFinalState = naturalGraphSnapshot?.state ?? currentState;
3977
+ let naturalFinalState = naturalGraphSnapshot?.state ?? currentState;
3978
+ if (!pendingRecordToken && !naturalFinalState.awaitingHumanInput) {
3979
+ naturalFinalState = await validateExecutionOutput(
3980
+ naturalFinalState,
3981
+ selectWorkflow
3982
+ );
3983
+ }
3984
+ outcomeState = naturalFinalState;
3805
3985
  if (pendingRequestWrites.length > 0) {
3806
3986
  await Promise.all(pendingRequestWrites);
3807
3987
  }
@@ -3835,7 +4015,8 @@ async function orchestrateGraphStream({
3835
4015
  structuredStreamIds: Array.from(structuredStreamIds),
3836
4016
  pendingRequests: Array.from(activePendingRequests.values())
3837
4017
  });
3838
- out.write({
4018
+ sessionCheckpointId = checkpoint.id;
4019
+ write({
3839
4020
  type: "checkpoint",
3840
4021
  id: checkpoint.id,
3841
4022
  sessionId: checkpoint.sessionId,
@@ -3853,14 +4034,16 @@ async function orchestrateGraphStream({
3853
4034
  }
3854
4035
  });
3855
4036
  } catch (error) {
4037
+ outcomeError = error;
3856
4038
  console.error("[orchestrator] session checkpoint failed", error);
3857
4039
  }
3858
4040
  }
3859
4041
  runTraceSpan.setAttributes?.({
3860
4042
  "kortyx.run.final_workflow": String(naturalFinalState.currentWorkflow)
3861
4043
  });
4044
+ finished = true;
3862
4045
  runTraceSpan.end?.(runTraceEndArgs());
3863
- out.write({ type: "done", data: naturalFinalState });
4046
+ write({ type: "done", data: naturalFinalState });
3864
4047
  out.end();
3865
4048
  return;
3866
4049
  }
@@ -3868,7 +4051,7 @@ async function orchestrateGraphStream({
3868
4051
  const emitTraceChunk = () => {
3869
4052
  const traceContext = traceAdapter?.getActiveContext?.();
3870
4053
  if (!traceContext) return;
3871
- out.write({
4054
+ write({
3872
4055
  type: "trace",
3873
4056
  traceId: traceContext.traceId,
3874
4057
  spanId: traceContext.spanId,
@@ -3884,16 +4067,165 @@ async function orchestrateGraphStream({
3884
4067
  emitTraceChunk();
3885
4068
  return runLoop(runTraceSpan);
3886
4069
  }) : runLoop(fallbackRunSpan ?? {});
3887
- runPromise.catch((err) => {
4070
+ const finishCancelled = async () => {
4071
+ emitTelemetryEvent({
4072
+ config,
4073
+ type: "run.cancelled",
4074
+ correlation: { runId, workflowId: outcomeState.currentWorkflow },
4075
+ payload: { reason: "execution_aborted" },
4076
+ flush: true
4077
+ });
4078
+ await Promise.allSettled(pendingRequestWrites);
4079
+ await Promise.allSettled(
4080
+ [...activePendingRequests.keys()].map(
4081
+ (token) => pendingStore?.delete(token)
4082
+ )
4083
+ );
4084
+ activePendingRequests.clear();
4085
+ try {
4086
+ await frameworkAdapter?.cleanupRun?.(runId, [...namespacesUsed]);
4087
+ } catch (cleanupError) {
4088
+ console.error("[cancel:cleanupRun]", cleanupError);
4089
+ }
4090
+ outcomeError = (0, import_core.createExecutionCancelledError)();
4091
+ fallbackRunSpan?.setAttributes?.({ "kortyx.run.cancelled": true });
4092
+ fallbackRunSpan?.end?.();
4093
+ write({ type: "cancelled", runId, reason: "Execution cancelled." });
4094
+ write({ type: "done" });
4095
+ finished = true;
4096
+ out.end();
4097
+ };
4098
+ runPromise.catch(async (err) => {
4099
+ if (abortSignal?.aborted || (0, import_core.isExecutionCancelled)(err)) {
4100
+ await finishCancelled();
4101
+ return;
4102
+ }
4103
+ if ((0, import_core.isExecutionLimitReached)(err)) {
4104
+ try {
4105
+ const snapshot = frameworkAdapter && await (0, import_runtime.captureGraphSnapshot)(frameworkAdapter.checkpointer, runId);
4106
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
4107
+ if (!snapshot)
4108
+ throw new Error(
4109
+ "Execution limit reached without a resumable checkpoint."
4110
+ );
4111
+ const savedState = snapshot.checkpoint.channel_values;
4112
+ const pausedState = {
4113
+ ...savedState,
4114
+ config: {
4115
+ ...savedState.config,
4116
+ ...config,
4117
+ executionBudget: budget
4118
+ },
4119
+ awaitingHumanInput: true
4120
+ };
4121
+ const patch = err.__kortyxHookStatePatch;
4122
+ currentState = pausedState;
4123
+ outcomeState = pausedState;
4124
+ const limitReached = err.limitReached;
4125
+ write({ type: "limit-reached", runId, ...limitReached });
4126
+ emitTelemetryEvent({
4127
+ config,
4128
+ type: "run.limit_reached",
4129
+ payload: limitReached
4130
+ });
4131
+ await persistAndEmitInterrupt({
4132
+ node: String(
4133
+ err.nodeId ?? savedState.lastNode
4134
+ ),
4135
+ workflow: String(savedState.currentWorkflow),
4136
+ input: {
4137
+ kind: "choice",
4138
+ question: "Limit reached \u2014 Continue?",
4139
+ options: [{ id: "continue", label: "Continue" }],
4140
+ meta: {
4141
+ __kortyxExecutionLimit: true,
4142
+ executionLimit: limitReached,
4143
+ ...patch ? { __kortyxResumeStatePatch: patch } : {}
4144
+ }
4145
+ }
4146
+ });
4147
+ await Promise.all(pendingRequestWrites);
4148
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
4149
+ if (outcomeError) throw outcomeError;
4150
+ const request = activePendingRequests.get(pendingRecordToken);
4151
+ const sealed = {
4152
+ ...request,
4153
+ state: pausedState,
4154
+ graphCheckpointId: snapshot.checkpoint.id,
4155
+ graphSnapshot: snapshot
4156
+ };
4157
+ activePendingRequests.set(request.token, sealed);
4158
+ await pendingStore?.update(request.token, sealed);
4159
+ const checkpoint = await frameworkAdapter.sessionCheckpoints.append({
4160
+ sessionId,
4161
+ runId,
4162
+ graphCheckpointId: snapshot.checkpoint.id,
4163
+ workflow: String(pausedState.currentWorkflow),
4164
+ state: pausedState,
4165
+ nodes: Array.from(touchedNodes),
4166
+ structuredStreamIds: Array.from(structuredStreamIds),
4167
+ pendingRequests: [sealed],
4168
+ label: "Execution limit reached"
4169
+ });
4170
+ (0, import_core.throwIfExecutionAborted)(abortSignal);
4171
+ sessionCheckpointId = checkpoint.id;
4172
+ write({
4173
+ type: "checkpoint",
4174
+ id: checkpoint.id,
4175
+ sessionId: checkpoint.sessionId,
4176
+ turnIndex: checkpoint.turnIndex,
4177
+ label: checkpoint.label
4178
+ });
4179
+ emitTelemetryEvent({
4180
+ config,
4181
+ type: "session.checkpointed",
4182
+ payload: {
4183
+ checkpointId: checkpoint.id,
4184
+ turnIndex: checkpoint.turnIndex,
4185
+ nodes: checkpoint.nodes
4186
+ }
4187
+ });
4188
+ fallbackRunSpan?.setAttributes?.({
4189
+ "kortyx.run.awaiting_human_input": true,
4190
+ "kortyx.run.limit_reached": limitReached.limit
4191
+ });
4192
+ fallbackRunSpan?.end?.();
4193
+ finished = true;
4194
+ write({ type: "done" });
4195
+ out.end();
4196
+ return;
4197
+ } catch (pauseError) {
4198
+ if (abortSignal?.aborted || (0, import_core.isExecutionCancelled)(pauseError)) {
4199
+ await finishCancelled();
4200
+ return;
4201
+ }
4202
+ await Promise.allSettled(
4203
+ [...activePendingRequests.keys()].map(
4204
+ (token) => pendingStore?.delete(token)
4205
+ )
4206
+ );
4207
+ activePendingRequests.clear();
4208
+ err = pauseError;
4209
+ }
4210
+ }
4211
+ outcomeError = err;
3888
4212
  fallbackRunSpan?.fail?.(err);
3889
4213
  emitResumeFailure(err);
3890
4214
  console.error("[error:orchestrateGraphStream]", err);
3891
- out.write({
4215
+ write({
3892
4216
  type: "error",
3893
4217
  message: err instanceof Error ? err.message : String(err)
3894
4218
  });
3895
- out.write({ type: "done" });
4219
+ write({ type: "done" });
4220
+ finished = true;
3896
4221
  out.end();
4222
+ }).finally(() => {
4223
+ onOutcome?.({
4224
+ state: outcomeState,
4225
+ pending: pendingRecordToken ? activePendingRequests.get(pendingRecordToken) : void 0,
4226
+ checkpointId: sessionCheckpointId,
4227
+ ...outcomeError ? { error: outcomeError } : {}
4228
+ });
3897
4229
  });
3898
4230
  return out;
3899
4231
  }
@@ -3924,7 +4256,13 @@ function parseResumeMeta(msg) {
3924
4256
  return { token, requestId, selected, cancel };
3925
4257
  }
3926
4258
  async function tryPrepareResumeStream({
4259
+ limits,
4260
+ abortSignal,
3927
4261
  lastMessage,
4262
+ meta: suppliedMeta,
4263
+ emitOutput,
4264
+ onOutcome,
4265
+ validatePending,
3928
4266
  sessionId,
3929
4267
  config,
3930
4268
  selectWorkflow,
@@ -3933,7 +4271,7 @@ async function tryPrepareResumeStream({
3933
4271
  applyResumeSelection,
3934
4272
  frameworkAdapter
3935
4273
  }) {
3936
- const meta = parseResumeMeta(lastMessage);
4274
+ const meta = suppliedMeta ?? parseResumeMeta(lastMessage);
3937
4275
  if (!meta) return null;
3938
4276
  const store = frameworkAdapter?.pendingRequests;
3939
4277
  if (!store) return null;
@@ -3945,6 +4283,24 @@ async function tryPrepareResumeStream({
3945
4283
  }
3946
4284
  if (pending.sessionId && pending.sessionId !== sessionId)
3947
4285
  throw new Error("Interrupt belongs to another session.");
4286
+ await validatePending?.(pending);
4287
+ const isLimitPause = Boolean(pending.schema.meta?.__kortyxExecutionLimit);
4288
+ const savedBudget = pending.state?.config?.executionBudget;
4289
+ const resumedBudget = savedBudget && isLimitPause && !meta.cancel ? (0, import_core2.restartExecutionBudget)(savedBudget, limits) : savedBudget;
4290
+ if (abortSignal?.aborted) {
4291
+ onOutcome?.({
4292
+ state: pending.state,
4293
+ error: (0, import_core2.createExecutionCancelledError)()
4294
+ });
4295
+ return (async function* () {
4296
+ yield {
4297
+ type: "cancelled",
4298
+ runId: pending.runId,
4299
+ reason: "Execution cancelled."
4300
+ };
4301
+ yield { type: "done" };
4302
+ })();
4303
+ }
3948
4304
  if (store.take && !await store.take(meta.token))
3949
4305
  throw new Error("Interrupt has already been resumed or cancelled.");
3950
4306
  if (meta.cancel) {
@@ -3964,125 +4320,412 @@ async function tryPrepareResumeStream({
3964
4320
  },
3965
4321
  flush: true
3966
4322
  });
4323
+ onOutcome?.({ state: pending.state, cancelled: true });
3967
4324
  return (async function* () {
3968
4325
  yield { type: "done" };
3969
4326
  })();
3970
4327
  }
3971
- console.log(
3972
- `[resume] token=${meta.token} requestId=${meta.requestId} selected=${JSON.stringify(
3973
- meta.selected
3974
- )} sessionId=${sessionId}`
3975
- );
3976
- const resumeData = applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
3977
- const resumeDataPatch = isRecord4(resumeData) ? resumeData : {};
3978
- const pendingMeta = isRecord4(pending.schema?.meta) ? pending.schema.meta : {};
3979
- const resumeStatePatch = isRecord4(pendingMeta.__kortyxResumeStatePatch) ? pendingMeta.__kortyxResumeStatePatch : void 0;
3980
- const pendingData = isRecord4(pending.state?.data) ? pending.state?.data : {};
3981
- const workflowId = typeof pending.workflow === "string" && pending.workflow.trim() ? pending.workflow : typeof defaultWorkflowId === "string" && defaultWorkflowId.trim() ? defaultWorkflowId : "job-search";
3982
- const resumedState = {
3983
- // For static breakpoints, resume with null input (set in orchestrator),
3984
- // and stash the user selection into data so the next node can read it.
3985
- input: "",
3986
- lastNode: "__start__",
3987
- currentWorkflow: workflowId,
3988
- config,
3989
- runtime: {},
3990
- conversationHistory: [],
3991
- awaitingHumanInput: false,
3992
- data: {
3993
- ...pendingData,
3994
- ...resumeDataPatch
3995
- }
3996
- };
3997
- const wf = await selectWorkflow(resumedState.currentWorkflow);
3998
- const telemetryConfig = prepareWorkflowTelemetry({
3999
- config: {
4000
- ...config,
4001
- ...pending.state?.config?.executionBranchId ? { executionBranchId: pending.state.config.executionBranchId } : {}
4002
- },
4003
- workflow: wf,
4004
- runId: pending.runId,
4005
- sessionId,
4006
- knownWorkflowIds
4007
- });
4008
- resumedState.config = telemetryConfig;
4009
- const resumeUpdate = pending.graphSnapshot ? {
4010
- config: {
4011
- ...telemetryConfig,
4012
- context: pending.state?.config?.context ?? telemetryConfig.context
4013
- }
4014
- } : {};
4015
- if (pending.graphSnapshot && frameworkAdapter)
4016
- await (0, import_runtime2.restoreGraphSnapshot)(
4017
- frameworkAdapter.checkpointer,
4018
- pending.graphSnapshot,
4019
- pending.runId
4020
- );
4021
- if (Object.keys(resumeDataPatch).length > 0) {
4022
- resumeUpdate.data = {
4023
- ...pendingData,
4024
- ...resumeDataPatch
4328
+ try {
4329
+ const resumeData = isLimitPause ? {} : applyResumeSelection ? applyResumeSelection({ pending, selected: meta.selected }) : meta.selected?.length ? { coordinates: String(meta.selected[0]) } : {};
4330
+ const resumeDataPatch = isRecord4(resumeData) ? resumeData : {};
4331
+ const pendingMeta = isRecord4(pending.schema?.meta) ? pending.schema.meta : {};
4332
+ const resumeStatePatch = isRecord4(pendingMeta.__kortyxResumeStatePatch) ? pendingMeta.__kortyxResumeStatePatch : void 0;
4333
+ const pendingData = isRecord4(pending.state?.data) ? pending.state?.data : {};
4334
+ const workflowId = typeof pending.workflow === "string" && pending.workflow.trim() ? pending.workflow : typeof defaultWorkflowId === "string" && defaultWorkflowId.trim() ? defaultWorkflowId : "job-search";
4335
+ const resumedState = {
4336
+ // For static breakpoints, resume with null input (set in orchestrator),
4337
+ // and stash the user selection into data so the next node can read it.
4338
+ input: pending.state ? pending.state.input : "",
4339
+ lastNode: "__start__",
4340
+ currentWorkflow: workflowId,
4341
+ config,
4342
+ runtime: {},
4343
+ conversationHistory: [],
4344
+ awaitingHumanInput: false,
4345
+ data: {
4346
+ ...pendingData,
4347
+ ...resumeDataPatch
4348
+ }
4025
4349
  };
4026
- }
4027
- if (resumeStatePatch) {
4028
- resumeUpdate.runtime = resumeStatePatch;
4029
- }
4030
- const hasResumeUpdate = Object.keys(resumeUpdate).length > 0;
4031
- const resumeValue = meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
4032
- const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
4033
- const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
4034
- ...telemetryConfig,
4035
- resume: true,
4036
- ...resumeValue !== void 0 ? { resumeValue } : {},
4037
- ...resumeCheckpointId ? { resumeCheckpointId } : {},
4038
- ...hasResumeUpdate ? { resumeUpdate } : {}
4039
- });
4040
- await store.delete(pending.token);
4041
- const response = responseFromSelection(meta.selected);
4042
- const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};
4043
- const responseCaptured = Boolean(
4044
- response && shouldCaptureTelemetryContent(telemetry.captureContent, "input")
4045
- );
4046
- emitTelemetryEvent({
4047
- config: telemetryConfig,
4048
- type: "interrupt.resolved",
4049
- correlation: {
4350
+ const wf = await selectWorkflow(resumedState.currentWorkflow);
4351
+ const telemetryConfig = prepareWorkflowTelemetry({
4352
+ config: {
4353
+ ...config,
4354
+ ...resumedBudget ? { executionBudget: resumedBudget } : {},
4355
+ ...pending.state?.config?.context ? { context: pending.state.config.context } : {},
4356
+ ...pending.state?.config?.executionContract ? { executionContract: pending.state.config.executionContract } : {},
4357
+ ...pending.state?.config?.executionBranchId ? { executionBranchId: pending.state.config.executionBranchId } : {}
4358
+ },
4359
+ workflow: wf,
4050
4360
  runId: pending.runId,
4051
4361
  sessionId,
4052
- workflowId,
4053
- nodeId: pending.node
4054
- },
4055
- payload: {
4056
- interruptId: pending.requestId,
4057
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
4058
- resumeOutcome: "resumed",
4059
- responseCaptured,
4060
- ...responseCaptured && response ? { response } : {}
4061
- },
4062
- flush: true
4063
- });
4064
- const args = {
4065
- sessionId,
4066
- runId: pending.runId,
4067
- graph: resumedGraph,
4068
- state: resumedState,
4069
- config: {
4362
+ knownWorkflowIds
4363
+ });
4364
+ resumedState.config = telemetryConfig;
4365
+ const resumeUpdate = pending.graphSnapshot ? {
4366
+ config: {
4367
+ ...telemetryConfig,
4368
+ context: pending.state?.config?.context ?? telemetryConfig.context
4369
+ }
4370
+ } : {};
4371
+ if (isLimitPause && pending.graphSnapshot) {
4372
+ pending.graphSnapshot.checkpoint.channel_values.config = telemetryConfig;
4373
+ if (resumeStatePatch)
4374
+ pending.graphSnapshot.checkpoint.channel_values.runtime = {
4375
+ ...pending.graphSnapshot.checkpoint.channel_values.runtime,
4376
+ ...resumeStatePatch
4377
+ };
4378
+ }
4379
+ if (pending.graphSnapshot && frameworkAdapter)
4380
+ await (0, import_runtime2.restoreGraphSnapshot)(
4381
+ frameworkAdapter.checkpointer,
4382
+ pending.graphSnapshot,
4383
+ pending.runId
4384
+ );
4385
+ if (Object.keys(resumeDataPatch).length > 0) {
4386
+ resumeUpdate.data = {
4387
+ ...pendingData,
4388
+ ...resumeDataPatch
4389
+ };
4390
+ }
4391
+ if (resumeStatePatch) {
4392
+ resumeUpdate.runtime = resumeStatePatch;
4393
+ }
4394
+ const hasResumeUpdate = !isLimitPause && Object.keys(resumeUpdate).length > 0;
4395
+ const resumeValue = isLimitPause ? void 0 : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
4396
+ const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
4397
+ const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
4070
4398
  ...telemetryConfig,
4071
4399
  resume: true,
4072
- telemetryInterruptId: pending.requestId,
4073
- telemetryInterruptNodeId: pending.node,
4074
4400
  ...resumeValue !== void 0 ? { resumeValue } : {},
4075
4401
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
4076
4402
  ...hasResumeUpdate ? { resumeUpdate } : {}
4403
+ });
4404
+ await store.delete(pending.token);
4405
+ const response = responseFromSelection(meta.selected);
4406
+ const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};
4407
+ const responseCaptured = Boolean(
4408
+ response && shouldCaptureTelemetryContent(telemetry.captureContent, "input")
4409
+ );
4410
+ emitTelemetryEvent({
4411
+ config: telemetryConfig,
4412
+ type: "interrupt.resolved",
4413
+ correlation: {
4414
+ runId: pending.runId,
4415
+ sessionId,
4416
+ workflowId,
4417
+ nodeId: pending.node
4418
+ },
4419
+ payload: {
4420
+ interruptId: pending.requestId,
4421
+ resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
4422
+ resumeOutcome: "resumed",
4423
+ responseCaptured,
4424
+ ...responseCaptured && response ? { response } : {}
4425
+ },
4426
+ flush: true
4427
+ });
4428
+ const args = {
4429
+ abortSignal,
4430
+ emitOutput,
4431
+ onOutcome,
4432
+ sessionId,
4433
+ runId: pending.runId,
4434
+ graph: resumedGraph,
4435
+ state: resumedState,
4436
+ config: {
4437
+ ...telemetryConfig,
4438
+ resume: true,
4439
+ telemetryInterruptId: pending.requestId,
4440
+ telemetryInterruptNodeId: pending.node,
4441
+ ...resumeValue !== void 0 ? { resumeValue } : {},
4442
+ ...resumeCheckpointId ? { resumeCheckpointId } : {},
4443
+ ...hasResumeUpdate ? { resumeUpdate } : {}
4444
+ },
4445
+ selectWorkflow,
4446
+ knownWorkflowIds,
4447
+ frameworkAdapter
4448
+ };
4449
+ const stream = await orchestrateGraphStream(args);
4450
+ return stream;
4451
+ } catch (error) {
4452
+ await store.save(pending);
4453
+ throw error;
4454
+ }
4455
+ }
4456
+
4457
+ // src/execution/execute.ts
4458
+ var handleSchema = import_zod3.z.object({
4459
+ token: import_zod3.z.string().min(1),
4460
+ requestId: import_zod3.z.string().min(1),
4461
+ sessionId: import_zod3.z.string().min(1),
4462
+ runId: import_zod3.z.string().min(1)
4463
+ }).strict();
4464
+ var responseSchema = import_zod3.z.discriminatedUnion("type", [
4465
+ import_zod3.z.object({ type: import_zod3.z.literal("text"), text: import_zod3.z.string().min(1) }).strict(),
4466
+ import_zod3.z.object({
4467
+ type: import_zod3.z.literal("select"),
4468
+ ids: import_zod3.z.array(import_zod3.z.string().min(1)).min(1)
4469
+ }).strict(),
4470
+ import_zod3.z.object({ type: import_zod3.z.literal("cancel") }).strict()
4471
+ ]);
4472
+ function resultFromOutcome(outcome, runId, sessionId) {
4473
+ const usage = outcome.pending?.schema.meta?.__kortyxResumeStatePatch?.tokenUsage ?? outcome.state.runtime?.tokenUsage;
4474
+ const info = {
4475
+ runId,
4476
+ sessionId,
4477
+ ...outcome.checkpointId ? { checkpointId: outcome.checkpointId } : {},
4478
+ ...usage ? { usage } : {}
4479
+ };
4480
+ if ((0, import_core3.isExecutionCancelled)(outcome.error))
4481
+ return { ...info, status: "cancelled", reason: "Execution cancelled." };
4482
+ if (outcome.error) {
4483
+ const error = outcome.error;
4484
+ return {
4485
+ ...info,
4486
+ status: "failed",
4487
+ error: {
4488
+ code: typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : "EXECUTION_FAILED",
4489
+ message: error instanceof Error ? error.message : String(error)
4490
+ }
4491
+ };
4492
+ }
4493
+ if (outcome.cancelled)
4494
+ return { ...info, status: "cancelled", reason: "cancelled_by_client" };
4495
+ if (outcome.pending) {
4496
+ const pending = outcome.pending;
4497
+ const meta = Object.fromEntries(
4498
+ Object.entries(pending.schema.meta ?? {}).filter(
4499
+ ([key]) => !key.startsWith("__kortyx")
4500
+ )
4501
+ );
4502
+ return {
4503
+ ...info,
4504
+ status: "suspended",
4505
+ ...pending.schema.meta?.__kortyxExecutionLimit ? {
4506
+ reason: "limit_reached",
4507
+ limit: pending.schema.meta.executionLimit
4508
+ } : {},
4509
+ interrupt: {
4510
+ workflow: pending.workflow,
4511
+ node: pending.node,
4512
+ input: {
4513
+ ...pending.schema,
4514
+ meta,
4515
+ options: pending.options.map(({ id, label, description }) => ({
4516
+ id,
4517
+ label,
4518
+ description
4519
+ }))
4520
+ }
4521
+ },
4522
+ resume: {
4523
+ token: pending.token,
4524
+ requestId: pending.requestId,
4525
+ sessionId,
4526
+ runId
4527
+ }
4528
+ };
4529
+ }
4530
+ return { ...info, status: "completed", data: outcome.state.data ?? {} };
4531
+ }
4532
+ function runtimeConfig(services, sessionId, context) {
4533
+ return {
4534
+ session: { id: sessionId },
4535
+ ...context ? { context } : {},
4536
+ getProvider: services.getProvider,
4537
+ checkpointer: services.frameworkAdapter.checkpointer,
4538
+ selectWorkflow: (id) => services.registry.select(id),
4539
+ ...services.telemetry ? { telemetry: services.telemetry } : {}
4540
+ };
4541
+ }
4542
+ async function executeWorkflow(services, args) {
4543
+ const select = (id) => services.registry.select(id);
4544
+ const workflow = await resolveExecutionWorkflow(select, args.workflow);
4545
+ const input = parseExecutionInput(workflow, args.input);
4546
+ if (args.sessionId !== void 0 && !args.sessionId.trim())
4547
+ throw new ExecutionRequestError(
4548
+ "INVALID_SESSION",
4549
+ "sessionId must not be empty."
4550
+ );
4551
+ const sessionId = args.sessionId ?? (0, import_runtime3.makeRequestId)("session");
4552
+ const runId = (0, import_runtime3.makeRequestId)("run");
4553
+ const config = prepareWorkflowTelemetry({
4554
+ config: {
4555
+ ...runtimeConfig(services, sessionId, args.context),
4556
+ ...services.limits || args.limits ? {
4557
+ executionBudget: (0, import_core3.createExecutionBudget)(
4558
+ services.limits,
4559
+ args.limits
4560
+ )
4561
+ } : {},
4562
+ executionContract: { id: workflow.id, version: workflow.version }
4077
4563
  },
4078
- selectWorkflow,
4079
- knownWorkflowIds,
4080
- frameworkAdapter
4564
+ workflow,
4565
+ runId,
4566
+ sessionId,
4567
+ knownWorkflowIds: services.knownWorkflowIds
4568
+ });
4569
+ const state = await (0, import_runtime3.buildInitialGraphState)({
4570
+ input,
4571
+ runtime: {},
4572
+ config,
4573
+ defaultWorkflowId: workflow.id
4574
+ });
4575
+ return new Promise((resolve2) => {
4576
+ const onOutcome = (outcome) => resolve2(resultFromOutcome(outcome, runId, sessionId));
4577
+ void (async () => {
4578
+ (0, import_core3.throwIfExecutionAborted)(args.abortSignal);
4579
+ const graph = await (0, import_runtime3.createExecutionGraph)(workflow, config);
4580
+ await orchestrateGraphStream({
4581
+ graph,
4582
+ state,
4583
+ config,
4584
+ runId,
4585
+ sessionId,
4586
+ selectWorkflow: select,
4587
+ frameworkAdapter: services.frameworkAdapter,
4588
+ knownWorkflowIds: services.knownWorkflowIds,
4589
+ abortSignal: args.abortSignal,
4590
+ emitOutput: false,
4591
+ onOutcome
4592
+ });
4593
+ })().catch((error) => onOutcome({ state, error }));
4594
+ });
4595
+ }
4596
+ async function resumeWorkflow(services, args) {
4597
+ const parsed = import_zod3.z.object({ resume: handleSchema, response: responseSchema }).safeParse(args);
4598
+ if (!parsed.success)
4599
+ throw new ExecutionRequestError("INVALID_RESUME", parsed.error.message);
4600
+ const { resume: handle, response } = parsed.data;
4601
+ const select = (id) => services.registry.select(id);
4602
+ const workflow = await resolveExecutionWorkflow(select, args.workflow);
4603
+ const validatePending = async (pending) => {
4604
+ const contract = pending.state?.config?.executionContract ?? {
4605
+ id: pending.workflow
4606
+ };
4607
+ if (pending.runId !== handle.runId || contract.id !== workflow.id || contract.version && contract.version !== workflow.version)
4608
+ throw new ExecutionRequestError(
4609
+ "RESUME_MISMATCH",
4610
+ "Resume handle does not match the root execution and workflow version."
4611
+ );
4612
+ if (response.type === "cancel") return;
4613
+ if (response.type === "text") {
4614
+ if (pending.schema.kind !== "text")
4615
+ throw new ExecutionRequestError(
4616
+ "INVALID_RESPONSE",
4617
+ "This interrupt requires a selection."
4618
+ );
4619
+ } else {
4620
+ const multi = pending.schema.kind === "multi-choice" || pending.schema.multiple;
4621
+ if (pending.schema.kind === "text" || !multi && response.ids.length !== 1 || new Set(response.ids).size !== response.ids.length)
4622
+ throw new ExecutionRequestError(
4623
+ "INVALID_RESPONSE",
4624
+ "Invalid selection for this interrupt."
4625
+ );
4626
+ if (pending.options.length && response.ids.some(
4627
+ (id) => !pending.options.some((option) => option.id === id)
4628
+ ))
4629
+ throw new ExecutionRequestError(
4630
+ "INVALID_RESPONSE",
4631
+ "Selection contains an unknown option."
4632
+ );
4633
+ }
4081
4634
  };
4082
- const stream = await orchestrateGraphStream(args);
4083
- return stream;
4635
+ return new Promise((resolve2, reject) => {
4636
+ void tryPrepareResumeStream({
4637
+ limits: args.limits,
4638
+ abortSignal: args.abortSignal,
4639
+ meta: {
4640
+ token: handle.token,
4641
+ requestId: handle.requestId,
4642
+ selected: response.type === "text" ? [response.text] : response.type === "select" ? response.ids : [],
4643
+ cancel: response.type === "cancel"
4644
+ },
4645
+ sessionId: handle.sessionId,
4646
+ config: runtimeConfig(services, handle.sessionId),
4647
+ selectWorkflow: select,
4648
+ frameworkAdapter: services.frameworkAdapter,
4649
+ knownWorkflowIds: services.knownWorkflowIds,
4650
+ validatePending,
4651
+ applyResumeSelection: () => ({}),
4652
+ emitOutput: false,
4653
+ onOutcome: (outcome) => resolve2(resultFromOutcome(outcome, handle.runId, handle.sessionId))
4654
+ }).then((stream) => {
4655
+ if (!stream)
4656
+ reject(
4657
+ new ExecutionRequestError(
4658
+ "INVALID_RESUME",
4659
+ "No waiting execution found."
4660
+ )
4661
+ );
4662
+ }).catch(
4663
+ (error) => reject(
4664
+ error instanceof ExecutionRequestError ? error : new ExecutionRequestError(
4665
+ "INVALID_RESUME",
4666
+ error instanceof Error ? error.message : String(error)
4667
+ )
4668
+ )
4669
+ );
4670
+ });
4084
4671
  }
4085
4672
 
4673
+ // src/telemetry/workflow-call-branch.ts
4674
+ var import_node_crypto3 = require("crypto");
4675
+ var import_hooks = __toESM(require_dist());
4676
+ function restoreWorkflowCallBranch(requests, telemetry, sourceRunId) {
4677
+ const branchId = (0, import_node_crypto3.randomUUID)();
4678
+ const emitted = /* @__PURE__ */ new Set();
4679
+ for (const request of requests) {
4680
+ if (request.state)
4681
+ request.state.config = {
4682
+ ...request.state.config,
4683
+ executionBranchId: branchId
4684
+ };
4685
+ const seen = /* @__PURE__ */ new Set();
4686
+ const visit = (value) => {
4687
+ if (!value || typeof value !== "object" || seen.has(value)) return;
4688
+ seen.add(value);
4689
+ const record = value;
4690
+ if (typeof record.invocationId === "string" && typeof record.fingerprint === "string" && Array.isArray(record.interrupts)) {
4691
+ const previous = record.telemetry;
4692
+ if (previous) {
4693
+ record.sequence = 0;
4694
+ record.telemetry = { ...previous, branchId };
4695
+ if (!emitted.has(record.invocationId)) {
4696
+ emitted.add(record.invocationId);
4697
+ (0, import_hooks.emitWorkflowCall)(
4698
+ telemetry ? {
4699
+ ...telemetry,
4700
+ correlation: {
4701
+ runId: request.runId,
4702
+ workflowId: request.workflow,
4703
+ ...request.sessionId ? { sessionId: request.sessionId } : {}
4704
+ }
4705
+ } : void 0,
4706
+ "workflow.call.restored",
4707
+ {
4708
+ ...previous,
4709
+ branchId,
4710
+ sourceBranchId: previous.branchId,
4711
+ sourceRunId,
4712
+ sequence: 0,
4713
+ status: record.status
4714
+ }
4715
+ );
4716
+ }
4717
+ }
4718
+ }
4719
+ for (const child of Object.values(record)) visit(child);
4720
+ };
4721
+ visit(request);
4722
+ }
4723
+ return branchId;
4724
+ }
4725
+
4726
+ // src/chat/process-chat.ts
4727
+ var import_runtime4 = require("@kortyx/runtime");
4728
+
4086
4729
  // src/utils/extract-latest-message.ts
4087
4730
  function extractLatestUserMessage(messages) {
4088
4731
  if (!messages || messages.length === 0) return "";
@@ -4098,6 +4741,7 @@ function extractLatestUserMessage(messages) {
4098
4741
  // src/chat/process-chat.ts
4099
4742
  async function streamChat({
4100
4743
  messages,
4744
+ abortSignal,
4101
4745
  options,
4102
4746
  sessionId,
4103
4747
  defaultWorkflowId,
@@ -4110,7 +4754,7 @@ async function streamChat({
4110
4754
  applyResumeSelection
4111
4755
  }) {
4112
4756
  const config = await loadRuntimeConfig(options);
4113
- let runtimeConfig = {
4757
+ let runtimeConfig2 = {
4114
4758
  ...config,
4115
4759
  getProvider,
4116
4760
  ...frameworkAdapter ? { checkpointer: frameworkAdapter.checkpointer } : {}
@@ -4121,7 +4765,7 @@ async function streamChat({
4121
4765
  "streamChat requires selectWorkflow or workflowRegistry to resolve workflows."
4122
4766
  );
4123
4767
  }
4124
- runtimeConfig.selectWorkflow = workflowSelector;
4768
+ runtimeConfig2.selectWorkflow = workflowSelector;
4125
4769
  const fallbackSessionId = options?.sessionId;
4126
4770
  const resolvedSessionId = sessionId || fallbackSessionId || "anonymous-session";
4127
4771
  const last = messages[messages.length - 1];
@@ -4143,16 +4787,11 @@ async function streamChat({
4143
4787
  if (requestedWorkflowId.trim() === "") delete runtime.requestedWorkflow;
4144
4788
  else runtime.requestedWorkflow = requestedWorkflowId;
4145
4789
  }
4146
- const baseState = await (0, import_runtime3.buildInitialGraphState)({
4147
- input,
4148
- config: runtimeConfig,
4149
- runtime,
4150
- ...defaultWorkflowId ? { defaultWorkflowId } : {}
4151
- });
4152
4790
  const resumeStream = await tryPrepareResumeStream({
4791
+ abortSignal,
4153
4792
  lastMessage: last,
4154
4793
  sessionId: resolvedSessionId,
4155
- config: runtimeConfig,
4794
+ config: runtimeConfig2,
4156
4795
  selectWorkflow: workflowSelector,
4157
4796
  knownWorkflowIds,
4158
4797
  ...frameworkAdapter ? { frameworkAdapter } : {},
@@ -4160,7 +4799,13 @@ async function streamChat({
4160
4799
  ...applyResumeSelection ? { applyResumeSelection } : {}
4161
4800
  });
4162
4801
  if (resumeStream) return resumeStream;
4163
- const runId = (0, import_runtime3.makeRequestId)("run");
4802
+ const baseState = await (0, import_runtime4.buildInitialGraphState)({
4803
+ input,
4804
+ config: runtimeConfig2,
4805
+ runtime,
4806
+ ...defaultWorkflowId ? { defaultWorkflowId } : {}
4807
+ });
4808
+ const runId = (0, import_runtime4.makeRequestId)("run");
4164
4809
  let headCheckpoint = !isResumeRequest && frameworkAdapter?.sessionCheckpoints ? await frameworkAdapter.sessionCheckpoints.getHead(resolvedSessionId) : null;
4165
4810
  if (!isResumeRequest && !headCheckpoint && frameworkAdapter?.sessionCheckpoints) {
4166
4811
  headCheckpoint = await frameworkAdapter.sessionCheckpoints.append({
@@ -4180,18 +4825,26 @@ async function streamChat({
4180
4825
  }
4181
4826
  const currentWorkflow = requestedWorkflowId ? baseState.currentWorkflow : headCheckpoint?.state.currentWorkflow ?? baseState.currentWorkflow;
4182
4827
  const selectedWorkflow = await workflowSelector(currentWorkflow);
4183
- runtimeConfig = prepareWorkflowTelemetry({
4184
- config: runtimeConfig,
4828
+ runtimeConfig2 = prepareWorkflowTelemetry({
4829
+ config: runtimeConfig2,
4185
4830
  workflow: selectedWorkflow,
4186
4831
  runId,
4187
4832
  sessionId: resolvedSessionId,
4188
4833
  knownWorkflowIds
4189
4834
  });
4190
- const graph = await (0, import_runtime3.createExecutionGraph)(selectedWorkflow, runtimeConfig);
4835
+ runtimeConfig2 = {
4836
+ ...runtimeConfig2,
4837
+ executionContract: {
4838
+ id: selectedWorkflow.id,
4839
+ version: selectedWorkflow.version
4840
+ }
4841
+ };
4842
+ const parsedInput = selectedWorkflow.inputSchema ? parseExecutionInput(selectedWorkflow, input) : input;
4843
+ const graph = await (0, import_runtime4.createExecutionGraph)(selectedWorkflow, runtimeConfig2);
4191
4844
  const initialState = headCheckpoint ? {
4192
4845
  ...headCheckpoint.state,
4193
- input,
4194
- config: runtimeConfig,
4846
+ input: parsedInput,
4847
+ config: runtimeConfig2,
4195
4848
  currentWorkflow,
4196
4849
  awaitingHumanInput: false,
4197
4850
  runtime: {
@@ -4199,13 +4852,19 @@ async function streamChat({
4199
4852
  ...runtime.requestedWorkflow ? { requestedWorkflow: runtime.requestedWorkflow } : {},
4200
4853
  ...runtime.priorMessages ? { priorMessages: runtime.priorMessages } : {}
4201
4854
  }
4202
- } : { ...baseState, config: runtimeConfig, currentWorkflow };
4855
+ } : {
4856
+ ...baseState,
4857
+ input: parsedInput,
4858
+ config: runtimeConfig2,
4859
+ currentWorkflow
4860
+ };
4203
4861
  const orchestratedStream = await orchestrateGraphStream({
4862
+ abortSignal,
4204
4863
  sessionId: resolvedSessionId,
4205
4864
  runId,
4206
4865
  graph,
4207
4866
  state: initialState,
4208
- config: runtimeConfig,
4867
+ config: runtimeConfig2,
4209
4868
  selectWorkflow: workflowSelector,
4210
4869
  knownWorkflowIds,
4211
4870
  ...frameworkAdapter ? { frameworkAdapter } : {}
@@ -4214,19 +4873,22 @@ async function streamChat({
4214
4873
  }
4215
4874
 
4216
4875
  // src/chat/create-agent.ts
4217
- var agentProcessOptionsSchema = import_zod2.z.object({
4218
- sessionId: import_zod2.z.string().optional(),
4219
- workflowId: import_zod2.z.string().optional(),
4220
- context: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional()
4876
+ var agentProcessOptionsSchema = import_zod4.z.object({
4877
+ limits: import_core4.ExecutionLimitsSchema.optional(),
4878
+ abortSignal: import_zod4.z.custom((value) => value instanceof AbortSignal).optional(),
4879
+ sessionId: import_zod4.z.string().optional(),
4880
+ workflowId: import_zod4.z.string().optional(),
4881
+ context: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.unknown()).optional()
4221
4882
  }).strict();
4222
- var createAgentArgsBaseSchema = import_zod2.z.object({
4223
- getProvider: import_zod2.z.unknown().optional(),
4224
- workflows: import_zod2.z.array(import_zod2.z.unknown()).optional(),
4225
- workflowsDir: import_zod2.z.string().optional(),
4226
- workflowRegistry: import_zod2.z.unknown().optional(),
4227
- defaultWorkflowId: import_zod2.z.string().optional(),
4228
- frameworkAdapter: import_zod2.z.unknown().optional(),
4229
- telemetry: import_zod2.z.unknown().optional()
4883
+ var createAgentArgsBaseSchema = import_zod4.z.object({
4884
+ limits: import_core4.ExecutionLimitsSchema.optional(),
4885
+ getProvider: import_zod4.z.unknown().optional(),
4886
+ workflows: import_zod4.z.array(import_zod4.z.unknown()).optional(),
4887
+ workflowsDir: import_zod4.z.string().optional(),
4888
+ workflowRegistry: import_zod4.z.unknown().optional(),
4889
+ defaultWorkflowId: import_zod4.z.string().optional(),
4890
+ frameworkAdapter: import_zod4.z.unknown().optional(),
4891
+ telemetry: import_zod4.z.unknown().optional()
4230
4892
  }).strict();
4231
4893
  var createAgentArgsSchema = createAgentArgsBaseSchema.superRefine(
4232
4894
  (value, ctx) => {
@@ -4290,6 +4952,7 @@ var clearPendingGraphWrites = async (frameworkAdapter, request) => {
4290
4952
  function createAgent(args) {
4291
4953
  const parsedArgs = parseCreateAgentArgs(args);
4292
4954
  const {
4955
+ limits,
4293
4956
  getProvider,
4294
4957
  workflows,
4295
4958
  workflowsDir,
@@ -4299,24 +4962,24 @@ function createAgent(args) {
4299
4962
  telemetry
4300
4963
  } = parsedArgs;
4301
4964
  const resolvedDefaultWorkflowId = defaultWorkflowId;
4302
- const resolvedFrameworkAdapter = frameworkAdapter ?? (0, import_runtime4.createFrameworkAdapterFromEnv)();
4965
+ const resolvedFrameworkAdapter = frameworkAdapter ?? (0, import_runtime5.createFrameworkAdapterFromEnv)();
4303
4966
  const resolvedGetProvider = getProvider ?? import_providers.getProvider;
4304
4967
  const resolvedCwd = process.cwd();
4305
4968
  const registryPromise = (async () => {
4306
4969
  if (workflowRegistry) return workflowRegistry;
4307
4970
  if (workflows) {
4308
- return (0, import_runtime4.createInMemoryWorkflowRegistry)(workflows, {
4971
+ return (0, import_runtime5.createInMemoryWorkflowRegistry)(workflows, {
4309
4972
  fallbackId: resolvedDefaultWorkflowId ?? "general-chat"
4310
4973
  });
4311
4974
  }
4312
4975
  if (workflowsDir) {
4313
- return (0, import_runtime4.createFileWorkflowRegistry)({
4976
+ return (0, import_runtime5.createFileWorkflowRegistry)({
4314
4977
  workflowsDir,
4315
4978
  fallbackId: resolvedDefaultWorkflowId ?? "general-chat"
4316
4979
  });
4317
4980
  }
4318
4981
  const resolvedWorkflowsDir = (0, import_node_path.resolve)(resolvedCwd, "src", "workflows");
4319
- return (0, import_runtime4.createFileWorkflowRegistry)({
4982
+ return (0, import_runtime5.createFileWorkflowRegistry)({
4320
4983
  workflowsDir: resolvedWorkflowsDir,
4321
4984
  fallbackId: resolvedDefaultWorkflowId ?? "general-chat"
4322
4985
  });
@@ -4354,12 +5017,19 @@ function createAgent(args) {
4354
5017
  return streamChat({
4355
5018
  ...resolvedDefaultWorkflowId ? { defaultWorkflowId: resolvedDefaultWorkflowId } : {},
4356
5019
  messages,
5020
+ abortSignal: parsedOptions?.abortSignal,
4357
5021
  options: parsedOptions,
4358
5022
  workflowRegistry: registry,
4359
5023
  ...knownWorkflowIds ? { knownWorkflowIds } : {},
4360
5024
  frameworkAdapter: resolvedFrameworkAdapter,
4361
5025
  getProvider: resolvedGetProvider,
4362
5026
  loadRuntimeConfig: (runtimeOptions) => ({
5027
+ ...limits || runtimeOptions?.limits ? {
5028
+ executionBudget: (0, import_core4.createExecutionBudget)(
5029
+ limits,
5030
+ runtimeOptions?.limits
5031
+ )
5032
+ } : {},
4363
5033
  ...runtimeOptions?.sessionId ? {
4364
5034
  session: {
4365
5035
  id: runtimeOptions.sessionId
@@ -4460,7 +5130,25 @@ function createAgent(args) {
4460
5130
  checkpoint: { ...result.checkpoint, activePendingRequests }
4461
5131
  };
4462
5132
  };
5133
+ const services = async () => {
5134
+ const registry = await registryPromise;
5135
+ if (!registry) throw new Error("Workflow registry is unavailable.");
5136
+ return {
5137
+ registry,
5138
+ limits,
5139
+ frameworkAdapter: resolvedFrameworkAdapter,
5140
+ getProvider: resolvedGetProvider,
5141
+ telemetry,
5142
+ knownWorkflowIds: (await registeredWorkflowsPromise)?.map(
5143
+ (workflow) => workflow.id
5144
+ )
5145
+ };
5146
+ };
5147
+ const execute = (async (options) => executeWorkflow(await services(), options));
5148
+ const resume = (async (options) => resumeWorkflow(await services(), options));
4463
5149
  return {
5150
+ execute,
5151
+ resume,
4464
5152
  projectTopology,
4465
5153
  streamChat: streamChat2,
4466
5154
  listCheckpoints,
@@ -4471,6 +5159,7 @@ function createAgent(args) {
4471
5159
  }
4472
5160
  // Annotate the CommonJS export names for ESM import in node:
4473
5161
  0 && (module.exports = {
5162
+ ExecutionRequestError,
4474
5163
  createAgent,
4475
5164
  createChatRouteHandler,
4476
5165
  createCheckpointRouteHandler,