@kortyx/agent 0.18.0 → 0.19.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
@@ -52,6 +52,7 @@ var require_dist = __commonJS({
52
52
  var index_exports2 = {};
53
53
  __export2(index_exports2, {
54
54
  WorkflowCallError: () => WorkflowCallError,
55
+ completeResponse: () => completeResponse,
55
56
  createWorkflowHooks: () => createWorkflowHooks,
56
57
  emitWorkflowCall: () => emitWorkflowCall2,
57
58
  runWithHookContext: () => runWithHookContext,
@@ -66,6 +67,7 @@ var require_dist = __commonJS({
66
67
  workflowCallFingerprint: () => workflowCallFingerprint
67
68
  });
68
69
  module2.exports = __toCommonJS2(index_exports2);
70
+ var import_zod6 = require("zod");
69
71
  var import_node_async_hooks = require("async_hooks");
70
72
  var storage = new import_node_async_hooks.AsyncLocalStorage();
71
73
  var isRecord5 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -218,7 +220,29 @@ var require_dist = __commonJS({
218
220
  }
219
221
  return ctx;
220
222
  }
221
- var import_core5 = require("@kortyx/core");
223
+ function snapshotHookState() {
224
+ const ctx = getHookContext();
225
+ return {
226
+ ...ctx.state,
227
+ lastNode: ctx.node.graph.node,
228
+ runtime: { ...ctx.state.runtime, ...buildRuntimeStateUpdates(ctx) }
229
+ };
230
+ }
231
+ var optionsSchema = import_zod6.z.object({
232
+ message: import_zod6.z.string().optional(),
233
+ data: import_zod6.z.json().optional()
234
+ }).strict();
235
+ async function completeResponse(options = {}) {
236
+ const ctx = getHookContext();
237
+ const parsed = optionsSchema.parse(options);
238
+ if (!ctx.node.completeResponse) {
239
+ throw new Error(
240
+ "completeResponse requires an agent-managed root workflow."
241
+ );
242
+ }
243
+ await ctx.node.completeResponse(parsed, snapshotHookState());
244
+ }
245
+ var import_core6 = require("@kortyx/core");
222
246
  var resolveReasonCheckpointKey = (args) => typeof args.id === "string" && args.id.length > 0 ? `__useReason:${args.id}` : `__useReason:auto:${args.autoIndex}`;
223
247
  var readReasonCheckpoint = (raw) => {
224
248
  if (!raw || typeof raw !== "object") return void 0;
@@ -269,7 +293,7 @@ var require_dist = __commonJS({
269
293
  };
270
294
  var awaitInterruptInternal = (args) => {
271
295
  const ctx = getHookContext();
272
- (0, import_core5.throwIfExecutionAborted)(ctx.node.abortSignal);
296
+ (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
273
297
  const request = parseWithSchema(
274
298
  args.requestSchema,
275
299
  args.request,
@@ -833,7 +857,7 @@ var require_dist = __commonJS({
833
857
  ...output !== void 0 ? { output } : {}
834
858
  };
835
859
  };
836
- var import_zod5 = require("zod");
860
+ var import_zod22 = require("zod");
837
861
  var tryStringify2 = (value) => {
838
862
  if (typeof value === "string") return value;
839
863
  try {
@@ -844,7 +868,7 @@ var require_dist = __commonJS({
844
868
  };
845
869
  var tryGetSchemaHint = (schema) => {
846
870
  try {
847
- const hint = (0, import_zod5.toJSONSchema)(schema);
871
+ const hint = (0, import_zod22.toJSONSchema)(schema);
848
872
  return JSON.stringify(hint, null, 2);
849
873
  } catch {
850
874
  return void 0;
@@ -1223,11 +1247,11 @@ ${tryStringify2(args.response)}`
1223
1247
  args.path,
1224
1248
  "useReason structured field"
1225
1249
  );
1226
- const matches = [];
1250
+ const matches2 = [];
1227
1251
  const visitValue = (start, currentParts) => {
1228
1252
  const valueStart = skipWhitespace(args.text, start);
1229
1253
  if (pathMatchesPattern(currentParts, targetParts)) {
1230
- matches.push({
1254
+ matches2.push({
1231
1255
  path: currentParts.join("."),
1232
1256
  start: valueStart
1233
1257
  });
@@ -1259,7 +1283,7 @@ ${tryStringify2(args.response)}`
1259
1283
  const valueStart = skipWhitespace(args.text, i + 1);
1260
1284
  const childParts = [...currentParts, key.value];
1261
1285
  if (pathMatchesPattern(childParts, targetParts)) {
1262
- matches.push({
1286
+ matches2.push({
1263
1287
  path: childParts.join("."),
1264
1288
  start: valueStart
1265
1289
  });
@@ -1286,7 +1310,7 @@ ${tryStringify2(args.response)}`
1286
1310
  if (args.text[i] === "]") return;
1287
1311
  const childParts = [...currentParts, String(index)];
1288
1312
  if (pathMatchesPattern(childParts, targetParts)) {
1289
- matches.push({
1313
+ matches2.push({
1290
1314
  path: childParts.join("."),
1291
1315
  start: i
1292
1316
  });
@@ -1308,7 +1332,7 @@ ${tryStringify2(args.response)}`
1308
1332
  }
1309
1333
  };
1310
1334
  visitValue(0, []);
1311
- return matches;
1335
+ return matches2;
1312
1336
  };
1313
1337
  var extractCompletedValueAt = (args) => {
1314
1338
  const start = skipWhitespace(args.text, args.start);
@@ -2389,7 +2413,7 @@ ${tryStringify2(args.response)}`
2389
2413
  return [getValue(), setValue];
2390
2414
  }
2391
2415
  var import_node_crypto5 = require("crypto");
2392
- var import_core6 = require("@kortyx/core");
2416
+ var import_core62 = require("@kortyx/core");
2393
2417
  var import_node_crypto42 = require("crypto");
2394
2418
  function emitWorkflowCall2(telemetry, type, payload) {
2395
2419
  const base = telemetry?.correlation;
@@ -2472,7 +2496,7 @@ ${tryStringify2(args.response)}`
2472
2496
  }
2473
2497
  async function useWorkflow(args) {
2474
2498
  const ctx = getHookContext();
2475
- (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2499
+ (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2476
2500
  const service = ctx.node.callWorkflow;
2477
2501
  if (!service)
2478
2502
  throw new Error("useWorkflow requires an agent workflow registry.");
@@ -2577,9 +2601,9 @@ ${tryStringify2(args.response)}`
2577
2601
  ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
2578
2602
  });
2579
2603
  } 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)) {
2604
+ (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2605
+ if ((0, import_core62.isExecutionCancelled)(error)) throw error;
2606
+ if ((0, import_core62.isExecutionLimitReached)(error)) {
2583
2607
  const child = error;
2584
2608
  if (child.__kortyxChildSnapshot)
2585
2609
  record.snapshot = child.__kortyxChildSnapshot;
@@ -2595,7 +2619,7 @@ ${tryStringify2(args.response)}`
2595
2619
  report("failed", { error: "Child workflow failed" });
2596
2620
  throw new WorkflowCallError(workflow, record.error);
2597
2621
  }
2598
- (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
2622
+ (0, import_core62.throwIfExecutionAborted)(ctx.node.abortSignal);
2599
2623
  recordUsage(outcome.usage);
2600
2624
  if (outcome.status === "completed") {
2601
2625
  record.data = outcome.data;
@@ -2719,6 +2743,7 @@ function parseCheckpointRequestBody(value) {
2719
2743
  async function handleChatRequestBody(args) {
2720
2744
  const { agent, body } = args;
2721
2745
  const stream = await agent.streamChat(body.messages, {
2746
+ ...args.onExecution ? { onExecution: args.onExecution } : {},
2722
2747
  ...args.abortSignal ? { abortSignal: args.abortSignal } : {},
2723
2748
  sessionId: body.sessionId,
2724
2749
  workflowId: body.workflowId,
@@ -2742,7 +2767,8 @@ function createChatRouteHandler(args) {
2742
2767
  return await handleChatRequestBody({
2743
2768
  agent,
2744
2769
  body,
2745
- abortSignal: request.signal
2770
+ abortSignal: request.signal,
2771
+ onExecution: args.onExecution
2746
2772
  });
2747
2773
  } catch (error) {
2748
2774
  return new Response(
@@ -2809,23 +2835,23 @@ async function* streamChatFromRoute(args) {
2809
2835
 
2810
2836
  // src/chat/create-agent.ts
2811
2837
  var import_node_path = require("path");
2812
- var import_core4 = require("@kortyx/core");
2838
+ var import_core5 = require("@kortyx/core");
2813
2839
  var import_providers = require("@kortyx/providers");
2814
2840
  var import_runtime5 = require("@kortyx/runtime");
2815
- var import_zod4 = require("zod");
2841
+ var import_zod5 = require("zod");
2816
2842
 
2817
2843
  // src/execution/execute.ts
2818
- var import_core3 = require("@kortyx/core");
2844
+ var import_core4 = require("@kortyx/core");
2819
2845
  var import_runtime3 = require("@kortyx/runtime");
2820
2846
  var import_zod3 = require("zod");
2821
2847
 
2822
2848
  // src/interrupt/resume-handler.ts
2823
- var import_core2 = require("@kortyx/core");
2849
+ var import_core3 = require("@kortyx/core");
2824
2850
  var import_runtime2 = require("@kortyx/runtime");
2825
2851
 
2826
2852
  // src/orchestrator.ts
2827
2853
  var import_node_stream = require("stream");
2828
- var import_core = require("@kortyx/core");
2854
+ var import_core2 = require("@kortyx/core");
2829
2855
  var import_runtime = require("@kortyx/runtime");
2830
2856
  var import_langgraph = require("@langchain/langgraph");
2831
2857
 
@@ -2907,6 +2933,47 @@ async function validateExecutionOutput(state, select) {
2907
2933
  return { ...state, data };
2908
2934
  }
2909
2935
 
2936
+ // src/stream/response-lifecycle.ts
2937
+ var import_core = require("@kortyx/core");
2938
+ function createResponseLifecycle(args) {
2939
+ const controller = new AbortController();
2940
+ let closed = args.restored;
2941
+ let completing;
2942
+ const abort = () => controller.abort();
2943
+ const detachRequest = () => args.requestSignal?.removeEventListener("abort", abort);
2944
+ if (!closed) {
2945
+ args.requestSignal?.addEventListener("abort", abort, { once: true });
2946
+ if (args.requestSignal?.aborted) abort();
2947
+ }
2948
+ args.executionSignal?.addEventListener("abort", abort, { once: true });
2949
+ if (args.executionSignal?.aborted) abort();
2950
+ return {
2951
+ get closed() {
2952
+ return closed;
2953
+ },
2954
+ signal: controller.signal,
2955
+ disconnect() {
2956
+ if (!closed) abort();
2957
+ },
2958
+ dispose() {
2959
+ detachRequest();
2960
+ args.executionSignal?.removeEventListener("abort", abort);
2961
+ },
2962
+ complete(options, state) {
2963
+ if (!args.enabled || closed) return Promise.resolve();
2964
+ completing ??= (async () => {
2965
+ (0, import_core.throwIfExecutionAborted)(controller.signal);
2966
+ await args.finalize(options, state);
2967
+ (0, import_core.throwIfExecutionAborted)(controller.signal);
2968
+ closed = true;
2969
+ detachRequest();
2970
+ args.onClosed();
2971
+ })();
2972
+ return completing;
2973
+ }
2974
+ };
2975
+ }
2976
+
2910
2977
  // src/stream/transform-graph-stream-for-ui.ts
2911
2978
  var isGraphStateOutput = (value) => value !== null && typeof value === "object" && !Array.isArray(value) && "currentWorkflow" in value;
2912
2979
  async function* transformGraphStreamForUI(stream, options = {}) {
@@ -3263,6 +3330,8 @@ async function orchestrateGraphStream({
3263
3330
  state,
3264
3331
  config: initialConfig,
3265
3332
  abortSignal: requestSignal,
3333
+ executionSignal,
3334
+ onExecution,
3266
3335
  emitOutput = true,
3267
3336
  onOutcome,
3268
3337
  selectWorkflow,
@@ -3271,20 +3340,88 @@ async function orchestrateGraphStream({
3271
3340
  }) {
3272
3341
  const out = new import_node_stream.PassThrough({ objectMode: true });
3273
3342
  const write = (chunk) => {
3274
- if (emitOutput && !out.destroyed) out.write(chunk);
3343
+ if (emitOutput && !response.closed && !out.destroyed) out.write(chunk);
3275
3344
  };
3276
3345
  let outcomeState = state;
3277
3346
  let outcomeError;
3278
- let sessionCheckpointId;
3347
+ let sessionCheckpointId = initialConfig.responseCheckpointId;
3279
3348
  let config = initialConfig;
3280
3349
  const budget = config.executionBudget;
3281
3350
  let currentGraph = graph;
3282
3351
  let currentState = state;
3283
3352
  let finished = false;
3284
- const controller = new AbortController();
3285
- const abortSignal = (0, import_core.combineAbortSignals)(requestSignal, controller.signal);
3353
+ const response = createResponseLifecycle({
3354
+ enabled: emitOutput,
3355
+ restored: initialConfig.responseCompleted === true,
3356
+ requestSignal,
3357
+ executionSignal,
3358
+ onClosed: () => {
3359
+ out.write({ type: "done" });
3360
+ out.end();
3361
+ },
3362
+ finalize: async (options, nodeState) => {
3363
+ const snapshot = JSON.parse(JSON.stringify(nodeState));
3364
+ delete snapshot.config.responseCompleted;
3365
+ delete snapshot.config.responseCheckpointId;
3366
+ if (options.message !== void 0) {
3367
+ forwardEmit("message", {
3368
+ node: nodeState.lastNode,
3369
+ content: options.message
3370
+ });
3371
+ }
3372
+ if (options.data !== void 0) {
3373
+ forwardEmit("structured_data", {
3374
+ node: nodeState.lastNode,
3375
+ data: options.data
3376
+ });
3377
+ }
3378
+ if (sessionId && frameworkAdapter) {
3379
+ const checkpoint = await frameworkAdapter.sessionCheckpoints.append({
3380
+ sessionId,
3381
+ runId,
3382
+ workflow: String(snapshot.currentWorkflow),
3383
+ state: snapshot,
3384
+ nodes: [...touchedNodes],
3385
+ structuredStreamIds: [...structuredStreamIds],
3386
+ pendingRequests: []
3387
+ });
3388
+ sessionCheckpointId = checkpoint.id;
3389
+ write({
3390
+ type: "checkpoint",
3391
+ id: checkpoint.id,
3392
+ sessionId,
3393
+ turnIndex: checkpoint.turnIndex
3394
+ });
3395
+ emitTelemetryEvent({
3396
+ config,
3397
+ type: "session.checkpointed",
3398
+ payload: {
3399
+ checkpointId: checkpoint.id,
3400
+ turnIndex: checkpoint.turnIndex,
3401
+ nodes: checkpoint.nodes
3402
+ }
3403
+ });
3404
+ }
3405
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
3406
+ config.responseCompleted = true;
3407
+ config.responseCheckpointId = sessionCheckpointId;
3408
+ currentState.config.responseCompleted = true;
3409
+ nodeState.config.responseCompleted = true;
3410
+ currentGraph.config.responseCompleted = true;
3411
+ emitTelemetryEvent({
3412
+ config,
3413
+ type: "response.completed",
3414
+ payload: {
3415
+ nodeId: nodeState.lastNode,
3416
+ checkpointId: sessionCheckpointId
3417
+ },
3418
+ flush: true
3419
+ });
3420
+ }
3421
+ });
3422
+ const abortSignal = response.signal;
3286
3423
  out.on("close", () => {
3287
- if (!finished) controller.abort();
3424
+ if (!finished) response.disconnect();
3288
3425
  });
3289
3426
  let structuredSeq = 0;
3290
3427
  const debugEnabled = Boolean(config?.features?.tracing);
@@ -3370,19 +3507,22 @@ async function orchestrateGraphStream({
3370
3507
  const pendingStore = frameworkAdapter?.pendingRequests;
3371
3508
  const sealPendingSnapshots = async () => {
3372
3509
  if (budget) outcomeState.config.executionBudget = budget;
3510
+ if (response.closed) outcomeState.config.responseCompleted = true;
3373
3511
  if (typeof frameworkAdapter?.checkpointer?.getTuple !== "function") return;
3374
3512
  for (const [token, request] of activePendingRequests) {
3375
3513
  if (budget) request.state.config.executionBudget = budget;
3514
+ if (response.closed) request.state.config.responseCompleted = true;
3376
3515
  const graphSnapshot = await (0, import_runtime.captureGraphSnapshot)(
3377
3516
  frameworkAdapter.checkpointer,
3378
3517
  runId,
3379
3518
  request.graphCheckpointId
3380
3519
  );
3381
3520
  if (graphSnapshot) {
3382
- const sealed = { ...request, graphSnapshot };
3521
+ const sealed = { ...request, graphSnapshot, ready: true };
3383
3522
  activePendingRequests.set(token, sealed);
3384
3523
  await pendingStore?.update(token, {
3385
3524
  graphSnapshot,
3525
+ ready: true,
3386
3526
  state: request.state
3387
3527
  });
3388
3528
  }
@@ -3400,6 +3540,8 @@ async function orchestrateGraphStream({
3400
3540
  const isText = kind === "text";
3401
3541
  const record = {
3402
3542
  token,
3543
+ ready: false,
3544
+ responseCompleted: response.closed,
3403
3545
  requestId,
3404
3546
  sessionId,
3405
3547
  runId,
@@ -3478,6 +3620,7 @@ async function orchestrateGraphStream({
3478
3620
  workflowCallPath: record.schema.meta?.workflowCallPath ?? null,
3479
3621
  branchId: config.executionBranchId ?? runId,
3480
3622
  interruptId: record.requestId,
3623
+ responseCompleted: response.closed,
3481
3624
  requestId: record.requestId,
3482
3625
  kind: record.schema.kind,
3483
3626
  interactionMode: record.schema.kind === "text" ? "freeform" : record.options.length > 0 ? "static-options" : record.schema.schemaId ? "dynamic-picker" : "unknown",
@@ -3687,7 +3830,7 @@ async function orchestrateGraphStream({
3687
3830
  };
3688
3831
  const runLoop = async (runTraceSpan) => {
3689
3832
  while (true) {
3690
- (0, import_core.throwIfExecutionAborted)(abortSignal);
3833
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
3691
3834
  let workflowFinalState = null;
3692
3835
  let workflowFinalGraphCheckpointId;
3693
3836
  let sawWorkflowDone = false;
@@ -3695,6 +3838,7 @@ async function orchestrateGraphStream({
3695
3838
  currentGraph.execution ??= {};
3696
3839
  currentGraph.execution.abortSignal = abortSignal;
3697
3840
  currentGraph.execution.budget = budget;
3841
+ currentGraph.execution.completeResponse = response.complete;
3698
3842
  currentGraph.config = currentGraph.config || {};
3699
3843
  currentGraph.config.emit = forwardEmit;
3700
3844
  currentGraph.config.executionRunId = runId;
@@ -3792,7 +3936,7 @@ async function orchestrateGraphStream({
3792
3936
  workflowFinalGraphCheckpointId = graphSnapshot?.checkpointId;
3793
3937
  workflowFinalState = isGraphState(workflowDoneData) ? workflowDoneData : graphSnapshot?.state ?? currentState;
3794
3938
  }
3795
- (0, import_core.throwIfExecutionAborted)(abortSignal);
3939
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
3796
3940
  const transitionTo = pending.to;
3797
3941
  const transitionPayload = pending.payload;
3798
3942
  const sourceNodeId = pending.sourceNodeId;
@@ -3857,8 +4001,8 @@ async function orchestrateGraphStream({
3857
4001
  currentGraph = nextGraph;
3858
4002
  continue;
3859
4003
  } catch (err) {
3860
- (0, import_core.throwIfExecutionAborted)(abortSignal);
3861
- if ((0, import_core.isExecutionCancelled)(err)) throw err;
4004
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
4005
+ if ((0, import_core2.isExecutionCancelled)(err)) throw err;
3862
4006
  outcomeError = err;
3863
4007
  runTraceSpan.fail?.(err);
3864
4008
  runTraceSpan.addEvent?.("kortyx.transition.error", {
@@ -3932,7 +4076,7 @@ async function orchestrateGraphStream({
3932
4076
  runTraceSpan.end?.(runTraceEndArgs());
3933
4077
  const resolvedSessionId2 = config?.session?.id || sessionId || "";
3934
4078
  await sealPendingSnapshots();
3935
- if (resolvedSessionId2 && frameworkAdapter?.sessionCheckpoints) {
4079
+ if (!response.closed && resolvedSessionId2 && frameworkAdapter?.sessionCheckpoints) {
3936
4080
  try {
3937
4081
  const checkpoint = await frameworkAdapter.sessionCheckpoints.append(
3938
4082
  {
@@ -4003,7 +4147,7 @@ async function orchestrateGraphStream({
4003
4147
  }
4004
4148
  const resolvedSessionId = config?.session?.id || sessionId || "";
4005
4149
  await sealPendingSnapshots();
4006
- if (resolvedSessionId && frameworkAdapter?.sessionCheckpoints) {
4150
+ if (!response.closed && resolvedSessionId && frameworkAdapter?.sessionCheckpoints) {
4007
4151
  try {
4008
4152
  const checkpoint = await frameworkAdapter.sessionCheckpoints.append({
4009
4153
  sessionId: resolvedSessionId,
@@ -4087,7 +4231,7 @@ async function orchestrateGraphStream({
4087
4231
  } catch (cleanupError) {
4088
4232
  console.error("[cancel:cleanupRun]", cleanupError);
4089
4233
  }
4090
- outcomeError = (0, import_core.createExecutionCancelledError)();
4234
+ outcomeError = (0, import_core2.createExecutionCancelledError)();
4091
4235
  fallbackRunSpan?.setAttributes?.({ "kortyx.run.cancelled": true });
4092
4236
  fallbackRunSpan?.end?.();
4093
4237
  write({ type: "cancelled", runId, reason: "Execution cancelled." });
@@ -4095,15 +4239,15 @@ async function orchestrateGraphStream({
4095
4239
  finished = true;
4096
4240
  out.end();
4097
4241
  };
4098
- runPromise.catch(async (err) => {
4099
- if (abortSignal?.aborted || (0, import_core.isExecutionCancelled)(err)) {
4242
+ const completion = runPromise.catch(async (err) => {
4243
+ if (abortSignal?.aborted || (0, import_core2.isExecutionCancelled)(err)) {
4100
4244
  await finishCancelled();
4101
4245
  return;
4102
4246
  }
4103
- if ((0, import_core.isExecutionLimitReached)(err)) {
4247
+ if ((0, import_core2.isExecutionLimitReached)(err)) {
4104
4248
  try {
4105
4249
  const snapshot = frameworkAdapter && await (0, import_runtime.captureGraphSnapshot)(frameworkAdapter.checkpointer, runId);
4106
- (0, import_core.throwIfExecutionAborted)(abortSignal);
4250
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
4107
4251
  if (!snapshot)
4108
4252
  throw new Error(
4109
4253
  "Execution limit reached without a resumable checkpoint."
@@ -4145,46 +4289,50 @@ async function orchestrateGraphStream({
4145
4289
  }
4146
4290
  });
4147
4291
  await Promise.all(pendingRequestWrites);
4148
- (0, import_core.throwIfExecutionAborted)(abortSignal);
4292
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
4149
4293
  if (outcomeError) throw outcomeError;
4150
4294
  const request = activePendingRequests.get(pendingRecordToken);
4151
4295
  const sealed = {
4152
4296
  ...request,
4153
4297
  state: pausedState,
4154
4298
  graphCheckpointId: snapshot.checkpoint.id,
4155
- graphSnapshot: snapshot
4299
+ graphSnapshot: snapshot,
4300
+ ready: true
4156
4301
  };
4157
4302
  activePendingRequests.set(request.token, sealed);
4158
4303
  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,
4304
+ if (response.closed) pausedState.config.responseCompleted = true;
4305
+ if (!response.closed) {
4306
+ const checkpoint = await frameworkAdapter.sessionCheckpoints.append({
4307
+ sessionId,
4308
+ runId,
4309
+ graphCheckpointId: snapshot.checkpoint.id,
4310
+ workflow: String(pausedState.currentWorkflow),
4311
+ state: pausedState,
4312
+ nodes: Array.from(touchedNodes),
4313
+ structuredStreamIds: Array.from(structuredStreamIds),
4314
+ pendingRequests: [sealed],
4315
+ label: "Execution limit reached"
4316
+ });
4317
+ (0, import_core2.throwIfExecutionAborted)(abortSignal);
4318
+ sessionCheckpointId = checkpoint.id;
4319
+ write({
4320
+ type: "checkpoint",
4321
+ id: checkpoint.id,
4322
+ sessionId: checkpoint.sessionId,
4184
4323
  turnIndex: checkpoint.turnIndex,
4185
- nodes: checkpoint.nodes
4186
- }
4187
- });
4324
+ label: checkpoint.label
4325
+ });
4326
+ emitTelemetryEvent({
4327
+ config,
4328
+ type: "session.checkpointed",
4329
+ payload: {
4330
+ checkpointId: checkpoint.id,
4331
+ turnIndex: checkpoint.turnIndex,
4332
+ nodes: checkpoint.nodes
4333
+ }
4334
+ });
4335
+ }
4188
4336
  fallbackRunSpan?.setAttributes?.({
4189
4337
  "kortyx.run.awaiting_human_input": true,
4190
4338
  "kortyx.run.limit_reached": limitReached.limit
@@ -4195,7 +4343,7 @@ async function orchestrateGraphStream({
4195
4343
  out.end();
4196
4344
  return;
4197
4345
  } catch (pauseError) {
4198
- if (abortSignal?.aborted || (0, import_core.isExecutionCancelled)(pauseError)) {
4346
+ if (abortSignal?.aborted || (0, import_core2.isExecutionCancelled)(pauseError)) {
4199
4347
  await finishCancelled();
4200
4348
  return;
4201
4349
  }
@@ -4220,6 +4368,7 @@ async function orchestrateGraphStream({
4220
4368
  finished = true;
4221
4369
  out.end();
4222
4370
  }).finally(() => {
4371
+ response.dispose();
4223
4372
  onOutcome?.({
4224
4373
  state: outcomeState,
4225
4374
  pending: pendingRecordToken ? activePendingRequests.get(pendingRecordToken) : void 0,
@@ -4227,6 +4376,8 @@ async function orchestrateGraphStream({
4227
4376
  ...outcomeError ? { error: outcomeError } : {}
4228
4377
  });
4229
4378
  });
4379
+ onExecution?.(completion);
4380
+ if (response.closed) out.end();
4230
4381
  return out;
4231
4382
  }
4232
4383
 
@@ -4258,6 +4409,8 @@ function parseResumeMeta(msg) {
4258
4409
  async function tryPrepareResumeStream({
4259
4410
  limits,
4260
4411
  abortSignal,
4412
+ executionSignal,
4413
+ onExecution,
4261
4414
  lastMessage,
4262
4415
  meta: suppliedMeta,
4263
4416
  emitOutput,
@@ -4286,11 +4439,11 @@ async function tryPrepareResumeStream({
4286
4439
  await validatePending?.(pending);
4287
4440
  const isLimitPause = Boolean(pending.schema.meta?.__kortyxExecutionLimit);
4288
4441
  const savedBudget = pending.state?.config?.executionBudget;
4289
- const resumedBudget = savedBudget && isLimitPause && !meta.cancel ? (0, import_core2.restartExecutionBudget)(savedBudget, limits) : savedBudget;
4442
+ const resumedBudget = savedBudget && isLimitPause && !meta.cancel ? (0, import_core3.restartExecutionBudget)(savedBudget, limits) : savedBudget;
4290
4443
  if (abortSignal?.aborted) {
4291
4444
  onOutcome?.({
4292
4445
  state: pending.state,
4293
- error: (0, import_core2.createExecutionCancelledError)()
4446
+ error: (0, import_core3.createExecutionCancelledError)()
4294
4447
  });
4295
4448
  return (async function* () {
4296
4449
  yield {
@@ -4351,6 +4504,10 @@ async function tryPrepareResumeStream({
4351
4504
  const telemetryConfig = prepareWorkflowTelemetry({
4352
4505
  config: {
4353
4506
  ...config,
4507
+ ...pending.responseCompleted ? {
4508
+ responseCompleted: true,
4509
+ responseCheckpointId: pending.state?.config?.responseCheckpointId
4510
+ } : {},
4354
4511
  ...resumedBudget ? { executionBudget: resumedBudget } : {},
4355
4512
  ...pending.state?.config?.context ? { context: pending.state.config.context } : {},
4356
4513
  ...pending.state?.config?.executionContract ? { executionContract: pending.state.config.executionContract } : {},
@@ -4427,6 +4584,8 @@ async function tryPrepareResumeStream({
4427
4584
  });
4428
4585
  const args = {
4429
4586
  abortSignal,
4587
+ executionSignal: pending.responseCompleted && emitOutput === false ? abortSignal : executionSignal,
4588
+ onExecution,
4430
4589
  emitOutput,
4431
4590
  onOutcome,
4432
4591
  sessionId,
@@ -4477,7 +4636,7 @@ function resultFromOutcome(outcome, runId, sessionId) {
4477
4636
  ...outcome.checkpointId ? { checkpointId: outcome.checkpointId } : {},
4478
4637
  ...usage ? { usage } : {}
4479
4638
  };
4480
- if ((0, import_core3.isExecutionCancelled)(outcome.error))
4639
+ if ((0, import_core4.isExecutionCancelled)(outcome.error))
4481
4640
  return { ...info, status: "cancelled", reason: "Execution cancelled." };
4482
4641
  if (outcome.error) {
4483
4642
  const error = outcome.error;
@@ -4554,7 +4713,7 @@ async function executeWorkflow(services, args) {
4554
4713
  config: {
4555
4714
  ...runtimeConfig(services, sessionId, args.context),
4556
4715
  ...services.limits || args.limits ? {
4557
- executionBudget: (0, import_core3.createExecutionBudget)(
4716
+ executionBudget: (0, import_core4.createExecutionBudget)(
4558
4717
  services.limits,
4559
4718
  args.limits
4560
4719
  )
@@ -4575,7 +4734,7 @@ async function executeWorkflow(services, args) {
4575
4734
  return new Promise((resolve2) => {
4576
4735
  const onOutcome = (outcome) => resolve2(resultFromOutcome(outcome, runId, sessionId));
4577
4736
  void (async () => {
4578
- (0, import_core3.throwIfExecutionAborted)(args.abortSignal);
4737
+ (0, import_core4.throwIfExecutionAborted)(args.abortSignal);
4579
4738
  const graph = await (0, import_runtime3.createExecutionGraph)(workflow, config);
4580
4739
  await orchestrateGraphStream({
4581
4740
  graph,
@@ -4670,6 +4829,98 @@ async function resumeWorkflow(services, args) {
4670
4829
  });
4671
4830
  }
4672
4831
 
4832
+ // src/interrupt/discovery.ts
4833
+ var import_zod4 = require("zod");
4834
+ var scopeSchema = import_zod4.z.object({
4835
+ sessionId: import_zod4.z.string().min(1).optional(),
4836
+ runId: import_zod4.z.string().min(1).optional(),
4837
+ context: import_zod4.z.record(
4838
+ import_zod4.z.string(),
4839
+ import_zod4.z.union([import_zod4.z.string(), import_zod4.z.number(), import_zod4.z.boolean(), import_zod4.z.null()])
4840
+ ).optional()
4841
+ }).strict().refine(
4842
+ (scope) => Boolean(
4843
+ scope.sessionId || scope.runId || Object.keys(scope.context ?? {}).length
4844
+ ),
4845
+ "Interrupt discovery requires an authorized session, run, or context scope."
4846
+ );
4847
+ var matches = (record, scope) => record.ready !== false && Date.now() < record.createdAt + record.ttlMs && (!scope.sessionId || record.sessionId === scope.sessionId) && (!scope.runId || record.runId === scope.runId) && Object.entries(scope.context ?? {}).every(
4848
+ ([key, value]) => record.state?.config.context?.[key] === value
4849
+ );
4850
+ var summary = (record) => {
4851
+ const { meta: _meta, ...input } = record.schema;
4852
+ const contract = record.state?.config.executionContract;
4853
+ return {
4854
+ id: record.requestId,
4855
+ status: "pending",
4856
+ runId: record.runId,
4857
+ sessionId: record.sessionId ?? "",
4858
+ workflow: contract?.id ?? record.workflow,
4859
+ node: record.node,
4860
+ createdAt: new Date(record.createdAt).toISOString(),
4861
+ expiresAt: new Date(record.createdAt + record.ttlMs).toISOString(),
4862
+ afterResponseCompleted: record.responseCompleted === true,
4863
+ input: {
4864
+ ...input,
4865
+ options: record.options.map(({ id, label, description }) => ({
4866
+ id,
4867
+ label,
4868
+ ...description !== void 0 ? { description } : {}
4869
+ }))
4870
+ }
4871
+ };
4872
+ };
4873
+ function createInterruptDiscovery(store) {
4874
+ const records = () => {
4875
+ if (!store.list)
4876
+ throw new Error(
4877
+ "This persistence adapter does not support interrupt discovery; implement pendingRequests.list."
4878
+ );
4879
+ return store.list();
4880
+ };
4881
+ return {
4882
+ async listInterrupts(options) {
4883
+ const {
4884
+ status: _status,
4885
+ afterResponseCompleted,
4886
+ ...scope
4887
+ } = import_zod4.z.object({
4888
+ sessionId: import_zod4.z.string().optional(),
4889
+ runId: import_zod4.z.string().optional(),
4890
+ context: import_zod4.z.record(
4891
+ import_zod4.z.string(),
4892
+ import_zod4.z.union([import_zod4.z.string(), import_zod4.z.number(), import_zod4.z.boolean(), import_zod4.z.null()])
4893
+ ).optional(),
4894
+ status: import_zod4.z.literal("pending").optional(),
4895
+ afterResponseCompleted: import_zod4.z.boolean().optional()
4896
+ }).strict().parse(options);
4897
+ scopeSchema.parse(scope);
4898
+ return (await records()).filter(
4899
+ (record) => matches(record, scope) && (afterResponseCompleted === void 0 || record.responseCompleted === true === afterResponseCompleted)
4900
+ ).sort(
4901
+ (a, b) => a.createdAt - b.createdAt || a.requestId.localeCompare(b.requestId)
4902
+ ).map(summary);
4903
+ },
4904
+ async getInterrupt(id, scope) {
4905
+ import_zod4.z.string().min(1).parse(id);
4906
+ scopeSchema.parse(scope);
4907
+ const record = (await records()).find(
4908
+ (record2) => record2.requestId === id && matches(record2, scope)
4909
+ );
4910
+ if (!record) return null;
4911
+ return {
4912
+ ...summary(record),
4913
+ resume: {
4914
+ token: record.token,
4915
+ requestId: record.requestId,
4916
+ sessionId: record.sessionId ?? "",
4917
+ runId: record.runId
4918
+ }
4919
+ };
4920
+ }
4921
+ };
4922
+ }
4923
+
4673
4924
  // src/telemetry/workflow-call-branch.ts
4674
4925
  var import_node_crypto3 = require("crypto");
4675
4926
  var import_hooks = __toESM(require_dist());
@@ -4742,6 +4993,8 @@ function extractLatestUserMessage(messages) {
4742
4993
  async function streamChat({
4743
4994
  messages,
4744
4995
  abortSignal,
4996
+ executionSignal,
4997
+ onExecution,
4745
4998
  options,
4746
4999
  sessionId,
4747
5000
  defaultWorkflowId,
@@ -4789,6 +5042,8 @@ async function streamChat({
4789
5042
  }
4790
5043
  const resumeStream = await tryPrepareResumeStream({
4791
5044
  abortSignal,
5045
+ executionSignal,
5046
+ onExecution,
4792
5047
  lastMessage: last,
4793
5048
  sessionId: resolvedSessionId,
4794
5049
  config: runtimeConfig2,
@@ -4860,6 +5115,8 @@ async function streamChat({
4860
5115
  };
4861
5116
  const orchestratedStream = await orchestrateGraphStream({
4862
5117
  abortSignal,
5118
+ executionSignal,
5119
+ onExecution,
4863
5120
  sessionId: resolvedSessionId,
4864
5121
  runId,
4865
5122
  graph,
@@ -4873,22 +5130,26 @@ async function streamChat({
4873
5130
  }
4874
5131
 
4875
5132
  // src/chat/create-agent.ts
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()
5133
+ var agentProcessOptionsSchema = import_zod5.z.object({
5134
+ executionSignal: import_zod5.z.custom((value) => value instanceof AbortSignal).optional(),
5135
+ onExecution: import_zod5.z.custom(
5136
+ (value) => typeof value === "function"
5137
+ ).optional(),
5138
+ limits: import_core5.ExecutionLimitsSchema.optional(),
5139
+ abortSignal: import_zod5.z.custom((value) => value instanceof AbortSignal).optional(),
5140
+ sessionId: import_zod5.z.string().optional(),
5141
+ workflowId: import_zod5.z.string().optional(),
5142
+ context: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.unknown()).optional()
4882
5143
  }).strict();
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()
5144
+ var createAgentArgsBaseSchema = import_zod5.z.object({
5145
+ limits: import_core5.ExecutionLimitsSchema.optional(),
5146
+ getProvider: import_zod5.z.unknown().optional(),
5147
+ workflows: import_zod5.z.array(import_zod5.z.unknown()).optional(),
5148
+ workflowsDir: import_zod5.z.string().optional(),
5149
+ workflowRegistry: import_zod5.z.unknown().optional(),
5150
+ defaultWorkflowId: import_zod5.z.string().optional(),
5151
+ frameworkAdapter: import_zod5.z.unknown().optional(),
5152
+ telemetry: import_zod5.z.unknown().optional()
4892
5153
  }).strict();
4893
5154
  var createAgentArgsSchema = createAgentArgsBaseSchema.superRefine(
4894
5155
  (value, ctx) => {
@@ -5018,6 +5279,8 @@ function createAgent(args) {
5018
5279
  ...resolvedDefaultWorkflowId ? { defaultWorkflowId: resolvedDefaultWorkflowId } : {},
5019
5280
  messages,
5020
5281
  abortSignal: parsedOptions?.abortSignal,
5282
+ executionSignal: parsedOptions?.executionSignal,
5283
+ onExecution: parsedOptions?.onExecution,
5021
5284
  options: parsedOptions,
5022
5285
  workflowRegistry: registry,
5023
5286
  ...knownWorkflowIds ? { knownWorkflowIds } : {},
@@ -5025,7 +5288,7 @@ function createAgent(args) {
5025
5288
  getProvider: resolvedGetProvider,
5026
5289
  loadRuntimeConfig: (runtimeOptions) => ({
5027
5290
  ...limits || runtimeOptions?.limits ? {
5028
- executionBudget: (0, import_core4.createExecutionBudget)(
5291
+ executionBudget: (0, import_core5.createExecutionBudget)(
5029
5292
  limits,
5030
5293
  runtimeOptions?.limits
5031
5294
  )
@@ -5147,6 +5410,7 @@ function createAgent(args) {
5147
5410
  const execute = (async (options) => executeWorkflow(await services(), options));
5148
5411
  const resume = (async (options) => resumeWorkflow(await services(), options));
5149
5412
  return {
5413
+ ...createInterruptDiscovery(resolvedFrameworkAdapter.pendingRequests),
5150
5414
  execute,
5151
5415
  resume,
5152
5416
  projectTopology,