@kortyx/agent 0.38.0 → 0.38.1

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/CHANGELOG.md CHANGED
@@ -2,10 +2,29 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Preserve completed tool results and addressed human answers across root and child execution-budget continuations. Continue renews only the saved allowance and does not answer a later review.
6
+
5
7
  - Add bounded, idempotent queued eval execution with shared consumer capacity,
6
8
  event replay, explicit cancellation and optional PostgreSQL recovery.
7
9
 
8
10
 
11
+ ## [0.38.1](https://github.com/kortyx-io/kortyx/compare/agent-v0.38.0...agent-v0.38.1) (2026-10-14)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **runtime:** preserve interrupt state across pauses and resume ([#348](https://github.com/kortyx-io/kortyx/issues/348)) ([03b49f2](https://github.com/kortyx-io/kortyx/commit/03b49f2b8b6b44a2852d16fd4fe1b3e199316072))
17
+
18
+
19
+ ### Dependencies
20
+
21
+ * The following workspace dependencies were updated
22
+ * dependencies
23
+ * @kortyx/runtime bumped to 0.24.1
24
+ * @kortyx/utils bumped to 0.3.8
25
+ * devDependencies
26
+ * @kortyx/hooks bumped to 0.34.3
27
+
9
28
  ## [0.38.0](https://github.com/kortyx-io/kortyx/compare/agent-v0.37.1...agent-v0.38.0) (2026-10-13)
10
29
 
11
30
 
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export { ar as AgentProcessOptions, as as AgentProjectTopologyOptions, at as Cha
3
3
  import { C as ChatMessage } from './chat-message-D4EsFcr1.js';
4
4
  export { StreamChatFromRouteArgs, streamChatFromRoute } from './browser.js';
5
5
  import { GetProviderFn } from '@kortyx/providers';
6
+ import * as _kortyx_runtime from '@kortyx/runtime';
6
7
  import { PendingRequestRecord, FrameworkAdapter, WorkflowRegistry, EvalExecutionStore } from '@kortyx/runtime';
7
8
  import { StreamChunk } from '@kortyx/stream';
8
9
  import { ExecutionBudget, GraphState, WorkflowDefinition, ExecutionLimits } from '@kortyx/core';
@@ -109,6 +110,7 @@ interface CompiledGraphLike {
109
110
  execution?: {
110
111
  abortSignal?: AbortSignal | undefined;
111
112
  budget?: ExecutionBudget | undefined;
113
+ resumeDelivery?: _kortyx_runtime.ResumeDelivery | undefined;
112
114
  completeResponse?: ((options: _kortyx_hooks.CompleteResponseOptions, state: GraphState) => Promise<void>) | undefined;
113
115
  };
114
116
  config?: Record<string, unknown>;
package/dist/index.js CHANGED
@@ -1996,7 +1996,7 @@ var require_internal = __commonJS({
1996
1996
  var import_node2 = require("@kortyx/usage-telemetry/node");
1997
1997
  var package_default2 = {
1998
1998
  name: "@kortyx/hooks",
1999
- version: "0.34.2",
1999
+ version: "0.34.3",
2000
2000
  private: false,
2001
2001
  description: "Lifecycle hooks and extension points for Kortyx.",
2002
2002
  keywords: [
@@ -2632,7 +2632,7 @@ var require_dist2 = __commonJS({
2632
2632
  var import_node2 = require("@kortyx/usage-telemetry/node");
2633
2633
  var package_default2 = {
2634
2634
  name: "@kortyx/hooks",
2635
- version: "0.34.2",
2635
+ version: "0.34.3",
2636
2636
  private: false,
2637
2637
  description: "Lifecycle hooks and extension points for Kortyx.",
2638
2638
  keywords: [
@@ -5292,6 +5292,36 @@ ${tryStringify2(args.response)}`
5292
5292
  var import_core52 = require("@kortyx/core");
5293
5293
  var import_errors62 = require("@kortyx/core/errors");
5294
5294
  var import_zod52 = require("zod");
5295
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
5296
+ function schemaPath(schema, path) {
5297
+ let current = schema;
5298
+ let label = "";
5299
+ for (const part of path.slice(0, 8)) {
5300
+ if (!record(current)) break;
5301
+ if (typeof part === "number" && Number.isSafeInteger(part) && part >= 0) {
5302
+ if (!record(current.items)) break;
5303
+ current = current.items;
5304
+ label += `[${part}]`;
5305
+ } else if (typeof part === "string" && record(current.properties) && Object.hasOwn(current.properties, part)) {
5306
+ current = current.properties[part];
5307
+ label += `${label ? "." : ""}${part.slice(0, 64)}`;
5308
+ } else break;
5309
+ }
5310
+ return label || "input";
5311
+ }
5312
+ function interruptInputFeedback(error, schema, name, required) {
5313
+ const issues = Array.isArray(error.issues) ? error.issues : [];
5314
+ const problems = issues.slice(0, 5).map((issue2) => {
5315
+ const path = record(issue2) && Array.isArray(issue2.path) ? issue2.path : [];
5316
+ return `- ${schemaPath(schema, path)} must satisfy its declared input schema.`;
5317
+ });
5318
+ return [
5319
+ `Invalid input for ${name}:`,
5320
+ ...problems,
5321
+ ...issues.length > 5 ? ["- Additional input fields are invalid."] : [],
5322
+ required ? "Correct the input and call the tool again. Human input is required before finishing." : "Correct the input and call the tool again, or finish without requesting human input when none is needed."
5323
+ ].join("\n");
5324
+ }
5295
5325
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
5296
5326
  name: tool.name,
5297
5327
  ...tool.title ? { title: tool.title } : {},
@@ -6018,11 +6048,35 @@ ${tryStringify2(args.response)}`
6018
6048
  throw new Error(
6019
6049
  `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
6020
6050
  );
6021
- const request = parseWithSchema(
6022
- resolved.contract.requestSchema,
6023
- toolCall.input,
6024
- `useReason interrupt contract "${resolved.name}" request`
6025
- );
6051
+ let request;
6052
+ try {
6053
+ request = parseWithSchema(
6054
+ resolved.contract.requestSchema,
6055
+ toolCall.input,
6056
+ `useReason interrupt contract "${resolved.name}" request`
6057
+ );
6058
+ } catch (error) {
6059
+ if (!(error instanceof import_errors62.ValidationError)) throw error;
6060
+ const result3 = {
6061
+ toolCallId: toolCall.id,
6062
+ name: toolCall.name,
6063
+ isError: true,
6064
+ content: interruptInputFeedback(
6065
+ error,
6066
+ interruptDefinitions.find((tool2) => tool2.name === toolCall.name)?.inputSchema,
6067
+ toolCall.name,
6068
+ interruptConfig?.mode === "required"
6069
+ )
6070
+ };
6071
+ toolResults.push(result3);
6072
+ messages2.push({ role: "tool", ...result3 });
6073
+ traceSpan?.addEvent?.("useReason.interrupt.invalid", {
6074
+ checkpointKey,
6075
+ contract: resolved.name
6076
+ });
6077
+ save();
6078
+ continue;
6079
+ }
6026
6080
  traceSpan?.addEvent?.("useReason.interrupt.requested", {
6027
6081
  checkpointKey,
6028
6082
  contract: resolved.name,
@@ -6956,10 +7010,10 @@ ${tryStringify2(args.response)}`
6956
7010
  throw new Error("Parallel call membership changed during replay.");
6957
7011
  }
6958
7012
  for (const callId of group.record.calls) {
6959
- const record = callRecord(callId);
6960
- if (record?.status !== "interrupted") continue;
6961
- const index = record.interrupts.length - 1;
6962
- if (record.interrupts[index]?.response !== void 0) continue;
7013
+ const record2 = callRecord(callId);
7014
+ if (record2?.status !== "interrupted") continue;
7015
+ const index = record2.interrupts.length - 1;
7016
+ if (record2.interrupts[index]?.response !== void 0) continue;
6963
7017
  group.record.interrupts.push({ callId, index });
6964
7018
  await bridge(callId, index);
6965
7019
  throw new Error(
@@ -7124,26 +7178,26 @@ ${tryStringify2(args.response)}`
7124
7178
  version: definition?.version ?? null
7125
7179
  });
7126
7180
  const key = `__useWorkflow:${args.id}`;
7127
- let record = ctx.currentNodeState.byKey[key];
7128
- if (record && record.parallelKey !== task.group?.key)
7181
+ let record2 = ctx.currentNodeState.byKey[key];
7182
+ if (record2 && record2.parallelKey !== task.group?.key)
7129
7183
  throw new WorkflowCallError(
7130
7184
  workflow,
7131
7185
  "parallel call membership changed during replay"
7132
7186
  );
7133
- if (record && record.fingerprint !== fingerprint)
7187
+ if (record2 && record2.fingerprint !== fingerprint)
7134
7188
  throw new WorkflowCallError(
7135
7189
  workflow,
7136
7190
  "call input or target changed during replay"
7137
7191
  );
7138
- if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
7139
- record ??= {
7192
+ if (!record2) ctx.node.consumeExecution?.("maxChildInvocations");
7193
+ record2 ??= {
7140
7194
  fingerprint,
7141
7195
  invocationId: (0, import_node_crypto62.randomUUID)(),
7142
7196
  status: "running",
7143
7197
  interrupts: [],
7144
7198
  ...task.group ? { parallelKey: task.group.key } : {}
7145
7199
  };
7146
- ctx.currentNodeState.byKey[key] = record;
7200
+ ctx.currentNodeState.byKey[key] = record2;
7147
7201
  ctx.stateDirty = true;
7148
7202
  if (!task.group) ctx.workflowCallActive = true;
7149
7203
  const telemetry = ctx.node.workflowCallTelemetry;
@@ -7151,7 +7205,7 @@ ${tryStringify2(args.response)}`
7151
7205
  ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto62.randomUUID)();
7152
7206
  const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
7153
7207
  const metadata2 = {
7154
- invocationId: record.invocationId,
7208
+ invocationId: record2.invocationId,
7155
7209
  parentInvocationId: telemetry?.correlation?.invocationId ?? null,
7156
7210
  branchId: telemetry?.correlation?.branchId ?? telemetry?.correlation?.runId ?? "unknown",
7157
7211
  callId: args.id,
@@ -7161,46 +7215,49 @@ ${tryStringify2(args.response)}`
7161
7215
  targetWorkflowId: workflow,
7162
7216
  targetVersion: definition?.version ?? null
7163
7217
  };
7164
- record.telemetry = metadata2;
7218
+ record2.telemetry = metadata2;
7165
7219
  const report = (status2, content = {}) => {
7166
- record.sequence = (record.sequence ?? 0) + 1;
7220
+ record2.sequence = (record2.sequence ?? 0) + 1;
7167
7221
  emitWorkflowCall2(telemetry, `workflow.call.${status2}`, {
7168
7222
  ...metadata2,
7169
- sequence: record.sequence,
7223
+ sequence: record2.sequence,
7170
7224
  ...content
7171
7225
  });
7172
7226
  };
7173
7227
  const recordUsage = (usage22) => {
7174
7228
  if (!usage22) return;
7175
7229
  accumulateTokenUsage({
7176
- input: usage22.input - (record.usage?.input ?? 0),
7177
- output: usage22.output - (record.usage?.output ?? 0),
7178
- total: usage22.total - (record.usage?.total ?? 0)
7230
+ input: usage22.input - (record2.usage?.input ?? 0),
7231
+ output: usage22.output - (record2.usage?.output ?? 0),
7232
+ total: usage22.total - (record2.usage?.total ?? 0)
7179
7233
  });
7180
- record.usage = usage22;
7234
+ record2.usage = usage22;
7181
7235
  };
7182
- const cached = record.status === "completed";
7236
+ const cached = record2.status === "completed";
7183
7237
  try {
7184
- for (const interruption of task.group ? [] : record.interrupts) {
7238
+ for (const interruption of task.group ? [] : record2.interrupts) {
7185
7239
  interruption.response = await awaitInterruptInternal({
7186
7240
  request: interruption.request,
7187
7241
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
7188
7242
  });
7189
7243
  }
7190
- if (record.status === "failed")
7244
+ if (record2.status === "failed")
7191
7245
  throw new WorkflowCallError(
7192
7246
  workflow,
7193
- record.error ?? "child failed",
7247
+ record2.error ?? "child failed",
7194
7248
  void 0,
7195
- record.failure ?? (0, import_errors82.serializeFailure)(void 0)
7249
+ record2.failure ?? (0, import_errors82.serializeFailure)(void 0)
7196
7250
  );
7197
- if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
7251
+ if (task.group && record2.status === "interrupted" && record2.interrupts.at(-1)?.response === void 0)
7198
7252
  throw new ParallelChildWaiting();
7199
- while (record.status !== "completed") {
7253
+ while (record2.status !== "completed") {
7200
7254
  let outcome;
7201
7255
  try {
7256
+ if (Array.isArray(record2.snapshot) && record2.snapshot.length !== 1)
7257
+ throw new import_errors82.WorkflowContractError("Invalid child workflow snapshot.");
7258
+ const snapshot = Array.isArray(record2.snapshot) ? record2.snapshot[0] : record2.snapshot;
7202
7259
  report(
7203
- record.snapshot ? "resumed" : "started",
7260
+ snapshot ? "resumed" : "started",
7204
7261
  workflowCallContent(telemetry, "input", input)
7205
7262
  );
7206
7263
  outcome = await service({
@@ -7209,9 +7266,9 @@ ${tryStringify2(args.response)}`
7209
7266
  workflow,
7210
7267
  input,
7211
7268
  definition,
7212
- invocationId: record.invocationId,
7213
- ...record.snapshot ? { snapshot: record.snapshot } : {},
7214
- ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
7269
+ invocationId: record2.invocationId,
7270
+ ...snapshot ? { snapshot } : {},
7271
+ ...record2.interrupts.length ? { response: record2.interrupts.at(-1)?.response } : {}
7215
7272
  });
7216
7273
  } catch (error) {
7217
7274
  const child = error;
@@ -7220,48 +7277,48 @@ ${tryStringify2(args.response)}`
7220
7277
  if ((0, import_core8.isExecutionCancelled)(error)) throw error;
7221
7278
  if ((0, import_core8.isExecutionLimitReached)(error)) {
7222
7279
  if (child?.__kortyxChildSnapshot)
7223
- record.snapshot = child.__kortyxChildSnapshot;
7280
+ record2.snapshot = child.__kortyxChildSnapshot;
7224
7281
  report("suspended", {
7225
7282
  reason: "limit_reached",
7226
7283
  limit: error.limitReached
7227
7284
  });
7228
7285
  throw error;
7229
7286
  }
7230
- record.status = "failed";
7231
- record.failure = (0, import_errors82.serializeFailure)(error);
7232
- record.error = record.failure.message;
7287
+ record2.status = "failed";
7288
+ record2.failure = (0, import_errors82.serializeFailure)(error);
7289
+ record2.error = record2.failure.message;
7233
7290
  report("failed", { error: "Child workflow failed" });
7234
7291
  throw new WorkflowCallError(
7235
7292
  workflow,
7236
- record.error,
7293
+ record2.error,
7237
7294
  error,
7238
- record.failure
7295
+ record2.failure
7239
7296
  );
7240
7297
  }
7241
7298
  (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
7242
7299
  recordUsage(outcome.usage);
7243
7300
  if (outcome.status === "completed") {
7244
- record.data = outcome.data;
7245
- workflowCallFingerprint(record.data);
7246
- record.status = "completed";
7247
- record.toolObservations = outcome.toolObservations ?? [];
7248
- for (const observation of record.toolObservations)
7301
+ record2.data = outcome.data;
7302
+ workflowCallFingerprint(record2.data);
7303
+ record2.status = "completed";
7304
+ record2.toolObservations = outcome.toolObservations ?? [];
7305
+ for (const observation of record2.toolObservations)
7249
7306
  rememberToolObservation(observation);
7250
- delete record.snapshot;
7307
+ delete record2.snapshot;
7251
7308
  report(
7252
7309
  "completed",
7253
- workflowCallContent(telemetry, "output", record.data)
7310
+ workflowCallContent(telemetry, "output", record2.data)
7254
7311
  );
7255
7312
  } else {
7256
- record.status = "interrupted";
7257
- record.snapshot = outcome.snapshot;
7313
+ record2.status = "interrupted";
7314
+ record2.snapshot = outcome.snapshot;
7258
7315
  report("suspended", {
7259
7316
  leaf: outcome.request.meta?.workflowCall ?? null
7260
7317
  });
7261
7318
  const interruption = {
7262
7319
  request: outcome.request
7263
7320
  };
7264
- record.interrupts.push(interruption);
7321
+ record2.interrupts.push(interruption);
7265
7322
  if (task.group) throw new ParallelChildWaiting();
7266
7323
  interruption.response = await awaitInterruptInternal({
7267
7324
  request: interruption.request,
@@ -7270,12 +7327,12 @@ ${tryStringify2(args.response)}`
7270
7327
  }
7271
7328
  }
7272
7329
  if (cached) {
7273
- report("reused", workflowCallContent(telemetry, "output", record.data));
7274
- if (record.restoredToolBranch !== telemetry?.correlation?.branchId)
7275
- await replayToolObservations(record.toolObservations ?? [], true);
7276
- delete record.restoredToolBranch;
7330
+ report("reused", workflowCallContent(telemetry, "output", record2.data));
7331
+ if (record2.restoredToolBranch !== telemetry?.correlation?.branchId)
7332
+ await replayToolObservations(record2.toolObservations ?? [], true);
7333
+ delete record2.restoredToolBranch;
7277
7334
  }
7278
- const data = record.data ?? {};
7335
+ const data = record2.data ?? {};
7279
7336
  return {
7280
7337
  data: JSON.parse(JSON.stringify(data))
7281
7338
  };
@@ -7368,6 +7425,7 @@ var ChatLifecycleHookError = class extends import_errors.KortyxError {
7368
7425
  // src/interrupt/resume-handler.ts
7369
7426
  var import_core3 = require("@kortyx/core");
7370
7427
  var import_runtime2 = require("@kortyx/runtime");
7428
+ var import_utils = require("@kortyx/utils");
7371
7429
 
7372
7430
  // src/execution/types.ts
7373
7431
  var import_errors2 = require("@kortyx/core/errors");
@@ -7848,7 +7906,7 @@ var import_node = require("@kortyx/usage-telemetry/node");
7848
7906
  // package.json
7849
7907
  var package_default = {
7850
7908
  name: "@kortyx/agent",
7851
- version: "0.38.0",
7909
+ version: "0.38.1",
7852
7910
  private: false,
7853
7911
  description: "Agent composition utilities for Kortyx.",
7854
7912
  keywords: [
@@ -9040,7 +9098,15 @@ async function orchestrateGraphStream({
9040
9098
  if ((0, import_core2.isExecutionLimitReached)(err)) {
9041
9099
  usageLimitReached = true;
9042
9100
  try {
9043
- const snapshot = frameworkAdapter && await (0, import_runtime.captureGraphSnapshot)(frameworkAdapter.checkpointer, runId);
9101
+ const snapshot = frameworkAdapter && await (0, import_runtime.captureGraphSnapshot)(
9102
+ frameworkAdapter.checkpointer,
9103
+ runId,
9104
+ void 0,
9105
+ {
9106
+ runtimePatch,
9107
+ response: currentGraph.execution?.resumeDelivery
9108
+ }
9109
+ );
9044
9110
  (0, import_core2.throwIfExecutionAborted)(abortSignal);
9045
9111
  if (!snapshot)
9046
9112
  throw new Error(
@@ -9367,12 +9433,13 @@ async function tryPrepareResumeStream({
9367
9433
  }
9368
9434
  } : {};
9369
9435
  if (isLimitPause && pending.graphSnapshot) {
9436
+ pending.graphSnapshot = (0, import_runtime2.prepareSuspensionSnapshot)(pending.graphSnapshot);
9370
9437
  pending.graphSnapshot.checkpoint.channel_values.config = telemetryConfig;
9371
9438
  if (resumeStatePatch)
9372
- pending.graphSnapshot.checkpoint.channel_values.runtime = {
9373
- ...pending.graphSnapshot.checkpoint.channel_values.runtime,
9374
- ...resumeStatePatch
9375
- };
9439
+ pending.graphSnapshot.checkpoint.channel_values.runtime = (0, import_utils.mergeRuntimeState)(
9440
+ pending.graphSnapshot.checkpoint.channel_values.runtime,
9441
+ resumeStatePatch
9442
+ );
9376
9443
  }
9377
9444
  leaseAbort.signal.throwIfAborted();
9378
9445
  if (pending.graphSnapshot && frameworkAdapter)
@@ -9391,8 +9458,17 @@ async function tryPrepareResumeStream({
9391
9458
  resumeUpdate.runtime = resumeStatePatch;
9392
9459
  }
9393
9460
  const hasResumeUpdate = !isLimitPause && Object.keys(resumeUpdate).length > 0;
9394
- const resumeValue = isLimitPause ? void 0 : meta.hasValue && pending.schema.kind === "custom" ? meta.value : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
9461
+ const outstandingResponse = isLimitPause ? pending.graphSnapshot?.suspension?.response : void 0;
9462
+ const humanValue = isLimitPause ? outstandingResponse?.value : meta.hasValue && pending.schema.kind === "custom" ? meta.value : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
9395
9463
  const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
9464
+ const resumeDelivery = isLimitPause ? outstandingResponse : humanValue !== void 0 ? (0, import_runtime2.createResumeDelivery)(
9465
+ pending.requestId,
9466
+ pending.node,
9467
+ pending.schema,
9468
+ humanValue,
9469
+ pending.graphSnapshot
9470
+ ) : void 0;
9471
+ const resumeValue = resumeDelivery ? (0, import_runtime2.resumeDeliveryInput)(resumeDelivery) : humanValue;
9396
9472
  const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
9397
9473
  ...telemetryConfig,
9398
9474
  resume: true,
@@ -9400,6 +9476,8 @@ async function tryPrepareResumeStream({
9400
9476
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
9401
9477
  ...hasResumeUpdate ? { resumeUpdate } : {}
9402
9478
  });
9479
+ resumedGraph.execution ??= {};
9480
+ resumedGraph.execution.resumeDelivery = resumeDelivery;
9403
9481
  await store.delete(pending.token);
9404
9482
  const response = meta.hasValue ? JSON.stringify(meta.value) : responseFromSelection(meta.selected);
9405
9483
  const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};