@kortyx/hooks 0.34.2 → 0.35.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
@@ -245,7 +245,7 @@ var import_node = require("@kortyx/usage-telemetry/node");
245
245
  // package.json
246
246
  var package_default = {
247
247
  name: "@kortyx/hooks",
248
- version: "0.34.2",
248
+ version: "0.35.0",
249
249
  private: false,
250
250
  description: "Lifecycle hooks and extension points for Kortyx.",
251
251
  keywords: [
@@ -822,252 +822,17 @@ var emitStructuredData = (args) => {
822
822
  var import_node_async_hooks2 = require("async_hooks");
823
823
  var import_node_crypto3 = require("crypto");
824
824
  var import_core2 = require("@kortyx/core");
825
- var import_errors3 = require("@kortyx/core/errors");
825
+ var import_errors2 = require("@kortyx/core/errors");
826
826
 
827
827
  // src/error-diagnostics.ts
828
828
  var import_telemetry_contracts2 = require("@kortyx/telemetry-contracts");
829
829
 
830
830
  // src/diagnostic-capture.ts
831
- var import_errors2 = require("@kortyx/core/errors");
832
831
  var import_telemetry_contracts = require("@kortyx/telemetry-contracts");
833
- var encoder = new TextEncoder();
834
- var nativeStackGetter = Object.getOwnPropertyDescriptor(
835
- new Error(),
836
- "stack"
837
- )?.get;
838
- function nativeStack(object, getter) {
839
- if (!getter || getter !== nativeStackGetter)
840
- throw new Error("Custom stack accessor");
841
- for (const key of ["name", "message"]) {
842
- let cursor = object;
843
- while (cursor) {
844
- const descriptor = Object.getOwnPropertyDescriptor(cursor, key);
845
- if (descriptor) {
846
- if (!("value" in descriptor) || typeof descriptor.value !== "string")
847
- throw new Error("Custom error formatting");
848
- break;
849
- }
850
- cursor = Object.getPrototypeOf(cursor);
851
- }
852
- }
853
- const formatter = Object.getOwnPropertyDescriptor(Error, "prepareStackTrace");
854
- if (formatter && (!("value" in formatter) || !formatter.writable))
855
- throw new Error("Custom stack formatter");
856
- Object.defineProperty(Error, "prepareStackTrace", {
857
- ...formatter ?? { configurable: true, writable: true },
858
- value: void 0
859
- });
860
- try {
861
- return getter.call(object);
862
- } finally {
863
- if (formatter) Object.defineProperty(Error, "prepareStackTrace", formatter);
864
- else Reflect.deleteProperty(Error, "prepareStackTrace");
865
- }
866
- }
867
- var pointer = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
868
- function captureDiagnosticContent(error, project) {
869
- const omissions = [];
870
- const redactions = [];
871
- const seen = /* @__PURE__ */ new WeakMap();
872
- let nodes = 0;
873
- let remaining = import_telemetry_contracts.DIAGNOSTIC_LIMITS.bytes - 64 * 1024;
874
- const omit = (path, reason, extra = {}) => {
875
- if (omissions.length < 1e3)
876
- omissions.push({ path: path.slice(0, 4096), reason, ...extra });
877
- return { $omitted: reason };
878
- };
879
- const visit = (value, path, depth) => {
880
- if (++nodes > import_telemetry_contracts.DIAGNOSTIC_LIMITS.nodes) return omit(path, "node_limit");
881
- if (depth > import_telemetry_contracts.DIAGNOSTIC_LIMITS.depth) return omit(path, "depth_limit");
882
- if (typeof value === "string") {
883
- const clean = (0, import_telemetry_contracts.redactDiagnosticText)(value);
884
- if (clean !== value && redactions.length < 1e3)
885
- redactions.push({
886
- path: path.slice(0, 4096),
887
- reason: "credential_text"
888
- });
889
- const bytes = encoder.encode(JSON.stringify(clean)).length;
890
- if (bytes > remaining)
891
- return omit(path, "byte_limit", {
892
- originalBytes: encoder.encode(value).length
893
- });
894
- remaining -= bytes;
895
- return clean;
896
- }
897
- if (value === null || typeof value === "boolean") return value;
898
- if (typeof value === "number")
899
- return Number.isFinite(value) ? value : { $type: "number", value: String(value) };
900
- if (typeof value === "bigint")
901
- return { $type: "bigint", value: value.toString() };
902
- if (typeof value === "undefined") return { $type: "undefined" };
903
- if (typeof value === "function" || typeof value === "symbol")
904
- return omit(path, `unsupported_${typeof value}`);
905
- const object = value;
906
- const reference = seen.get(object);
907
- if (reference) return { $ref: reference };
908
- seen.set(object, path);
909
- remaining -= 32;
910
- if (remaining < 0) return omit(path, "byte_limit");
911
- try {
912
- if (value instanceof Date)
913
- return { $type: "Date", value: Date.prototype.toISOString.call(value) };
914
- if (value instanceof Map || value instanceof Set) {
915
- const isMap = value instanceof Map;
916
- const iterator = isMap ? Map.prototype.entries.call(value) : Set.prototype.values.call(value);
917
- const count = Object.getOwnPropertyDescriptor(
918
- isMap ? Map.prototype : Set.prototype,
919
- "size"
920
- )?.get?.call(value);
921
- const entries = [];
922
- for (let index = 0; index < Math.min(count, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties); index++)
923
- entries.push(iterator.next().value);
924
- if (count > entries.length)
925
- omissions.push({
926
- path,
927
- reason: "collection_limit",
928
- originalCount: count
929
- });
930
- return isMap ? {
931
- $type: "Map",
932
- entries: visit(entries, `${path}/entries`, depth + 1)
933
- } : {
934
- $type: "Set",
935
- values: visit(entries, `${path}/values`, depth + 1)
936
- };
937
- }
938
- if (Array.isArray(value)) {
939
- const count = Math.min(value.length, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties);
940
- const items = Array.from(
941
- { length: count },
942
- (_, index) => read(value, String(index), `${path}/${index}`, depth)
943
- );
944
- if (count < value.length)
945
- items.push(
946
- omit(path, "collection_limit", { originalCount: value.length })
947
- );
948
- return items;
949
- }
950
- const keys = Object.getOwnPropertyNames(object);
951
- const result = /* @__PURE__ */ Object.create(null);
952
- if (value instanceof Error) {
953
- const ownName = Object.getOwnPropertyDescriptor(object, "name");
954
- let type = ownName && "value" in ownName ? ownName.value : void 0;
955
- if (typeof type !== "string") {
956
- const name = Object.getOwnPropertyDescriptor(
957
- Object.getPrototypeOf(value),
958
- "name"
959
- );
960
- type = name && "value" in name ? name.value : "Error";
961
- }
962
- result.type = visit(
963
- typeof type === "string" ? type : "Error",
964
- `${path}/type`,
965
- depth + 1
966
- );
967
- }
968
- for (const key of keys.slice(0, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties)) {
969
- if (["__proto__", "constructor", "prototype"].includes(key)) {
970
- result[key] = omit(`${path}/${pointer(key)}`, "unsafe_key");
971
- continue;
972
- }
973
- remaining -= encoder.encode(key).length + 4;
974
- result[key] = read(object, key, `${path}/${pointer(key)}`, depth);
975
- }
976
- if (keys.length > import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties)
977
- result.$omittedProperties = omit(path, "property_limit", {
978
- originalCount: keys.length
979
- });
980
- return result;
981
- } catch {
982
- return omit(path, "unreadable_object");
983
- }
984
- };
985
- const read = (object, key, path, depth) => {
986
- if ((0, import_telemetry_contracts.isPrivateDiagnosticField)(key)) {
987
- if (redactions.length < 1e3)
988
- redactions.push({
989
- path: path.slice(0, 4096),
990
- reason: "credential_field"
991
- });
992
- return "[REDACTED]";
993
- }
994
- try {
995
- const property = Object.getOwnPropertyDescriptor(object, key);
996
- if (property && "value" in property)
997
- return visit(property.value, path, depth + 1);
998
- if (key === "stack" && object instanceof Error && property?.get && property.get === nativeStackGetter)
999
- return visit(nativeStack(object, property.get), path, depth + 1);
1000
- return omit(
1001
- path,
1002
- property ? "accessor_not_evaluated" : "unavailable_property"
1003
- );
1004
- } catch {
1005
- return omit(path, "unreadable_property");
1006
- }
1007
- };
1008
- try {
1009
- const original = project ? project(error) : error;
1010
- if (project && original === null) return null;
1011
- const captured = visit(original, "#/data", 0);
1012
- const upstreamOmissions = (0, import_errors2.errorProperty)(original, "diagnosticOmissions");
1013
- if (Array.isArray(upstreamOmissions))
1014
- for (const note of upstreamOmissions.slice(0, 1e3)) {
1015
- const path = (0, import_errors2.errorProperty)(note, "path");
1016
- const reason = (0, import_errors2.errorProperty)(note, "reason");
1017
- if (typeof path === "string" && typeof reason === "string")
1018
- omissions.push({
1019
- path: (0, import_telemetry_contracts.redactDiagnosticText)(path).slice(0, 4096),
1020
- reason: (0, import_telemetry_contracts.redactDiagnosticText)(reason).slice(0, 128)
1021
- });
1022
- }
1023
- const data = captured && typeof captured === "object" && !Array.isArray(captured) ? captured : {
1024
- type: "Error",
1025
- message: typeof captured === "string" ? captured : "Execution reported a fault.",
1026
- value: captured
1027
- };
1028
- if (typeof data.message !== "string" && !("message" in data))
1029
- data.message = "Execution reported a fault.";
1030
- if (!("type" in data))
1031
- data.type = typeof data.name === "string" ? data.name : "Error";
1032
- const content = {
1033
- schemaVersion: 1,
1034
- data,
1035
- capture: {
1036
- status: omissions.length ? "partial" : "complete",
1037
- omissions,
1038
- redactions
1039
- }
1040
- };
1041
- if (encoder.encode(JSON.stringify(content)).length > import_telemetry_contracts.DIAGNOSTIC_LIMITS.bytes)
1042
- return {
1043
- schemaVersion: 1,
1044
- data: {
1045
- type: "Error",
1046
- message: "Diagnostic exceeded the capture budget."
1047
- },
1048
- capture: {
1049
- status: "partial",
1050
- redactions: [],
1051
- omissions: [{ path: "#/data", reason: "serialized_byte_limit" }]
1052
- }
1053
- };
1054
- return content;
1055
- } catch {
1056
- return {
1057
- schemaVersion: 1,
1058
- data: { type: "Error", message: "Diagnostic capture failed." },
1059
- capture: {
1060
- status: "failed",
1061
- redactions: [],
1062
- omissions: [{ path: "#/data", reason: "projection_or_capture_failed" }]
1063
- }
1064
- };
1065
- }
1066
- }
1067
832
 
1068
833
  // src/error-diagnostics.ts
1069
834
  function exceptionDiagnostics(error, project) {
1070
- const content = captureDiagnosticContent(error, project);
835
+ const content = (0, import_telemetry_contracts.captureDiagnosticContent)(error, project);
1071
836
  return !content || content.capture.status === "failed" ? null : content.data;
1072
837
  }
1073
838
  function errorDiagnostics(error, project) {
@@ -1332,7 +1097,7 @@ async function executeObservedTool(args) {
1332
1097
  return result;
1333
1098
  } catch (error) {
1334
1099
  if (args.abortSignal?.aborted) observation.outcome = "cancelled";
1335
- else if (!(0, import_errors3.isControlFlowError)(error)) {
1100
+ else if (!(0, import_errors2.isControlFlowError)(error)) {
1336
1101
  const outcome = classification(
1337
1102
  args.tool,
1338
1103
  error,
@@ -1471,11 +1236,11 @@ var import_prompts = require("@kortyx/prompts");
1471
1236
  // src/reason-engine.ts
1472
1237
  var import_node_crypto4 = require("crypto");
1473
1238
  var import_core3 = require("@kortyx/core");
1474
- var import_errors4 = require("@kortyx/core/errors");
1239
+ var import_errors3 = require("@kortyx/core/errors");
1475
1240
  function assertModelFinish(result) {
1476
1241
  if (result.finishReason?.unified !== "content-filter") return;
1477
1242
  throw Object.assign(
1478
- new import_errors4.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
1243
+ new import_errors3.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
1479
1244
  category: "provider",
1480
1245
  retryable: false,
1481
1246
  safeMessage: "The provider refused the request."
@@ -2549,7 +2314,7 @@ function createStructuredOutputStreamer(args, id, opId) {
2549
2314
  }
2550
2315
 
2551
2316
  // src/reason/parsing.ts
2552
- var import_errors5 = require("@kortyx/core/errors");
2317
+ var import_errors4 = require("@kortyx/core/errors");
2553
2318
  var tryJsonParse = (value) => {
2554
2319
  try {
2555
2320
  return { ok: true, value: JSON.parse(value) };
@@ -2587,12 +2352,12 @@ var resolveOutputCandidate = (text) => {
2587
2352
  }
2588
2353
  return text;
2589
2354
  };
2590
- var createTruncatedStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
2355
+ var createTruncatedStructuredOutputError = (label, cause) => new import_errors4.ValidationError(
2591
2356
  "OUTPUT_TRUNCATED",
2592
2357
  `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`,
2593
2358
  cause
2594
2359
  );
2595
- var createInvalidStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
2360
+ var createInvalidStructuredOutputError = (label, cause) => new import_errors4.ValidationError(
2596
2361
  "INVALID_MODEL_JSON",
2597
2362
  `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check the output contract or outputSchema and provider structured-output settings.`,
2598
2363
  cause
@@ -2604,7 +2369,7 @@ var parseReasonOutputWithSchema = (args) => {
2604
2369
  try {
2605
2370
  return parseWithSchema(args.schema, candidate, args.label);
2606
2371
  } catch (error) {
2607
- if (!(error instanceof import_errors5.ValidationError)) throw error;
2372
+ if (!(error instanceof import_errors4.ValidationError)) throw error;
2608
2373
  const failure = args.finishReason?.unified === "length" ? createTruncatedStructuredOutputError(args.label, error) : typeof candidate === "string" && !validJson ? createInvalidStructuredOutputError(args.label, error) : error;
2609
2374
  if (failure && typeof failure === "object")
2610
2375
  Object.assign(failure, {
@@ -2624,7 +2389,7 @@ var parseInterruptFirstPassResult = (args) => {
2624
2389
  if (typeof candidate === "string" && candidate.trim().length > 0) {
2625
2390
  throw createInvalidStructuredOutputError("useReason output");
2626
2391
  }
2627
- throw new import_errors5.ValidationError(
2392
+ throw new import_errors4.ValidationError(
2628
2393
  "MODEL_OUTPUT_SCHEMA",
2629
2394
  "useReason first pass with interrupt must return a JSON object."
2630
2395
  );
@@ -2633,7 +2398,7 @@ var parseInterruptFirstPassResult = (args) => {
2633
2398
  let interruptRequired = true;
2634
2399
  if (mode === "optional") {
2635
2400
  if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
2636
- throw new import_errors5.ValidationError(
2401
+ throw new import_errors4.ValidationError(
2637
2402
  "MODEL_OUTPUT_SCHEMA",
2638
2403
  'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
2639
2404
  );
@@ -2646,7 +2411,7 @@ var parseInterruptFirstPassResult = (args) => {
2646
2411
  candidate.interrupt
2647
2412
  ].some((value) => value !== null && value !== void 0);
2648
2413
  if (hasUnexpectedRequest) {
2649
- throw new import_errors5.ValidationError(
2414
+ throw new import_errors4.ValidationError(
2650
2415
  "MODEL_OUTPUT_SCHEMA",
2651
2416
  'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
2652
2417
  );
@@ -2680,7 +2445,7 @@ var parseInterruptFirstPassResult = (args) => {
2680
2445
  "useReason output"
2681
2446
  );
2682
2447
  } catch (error) {
2683
- if (!(error instanceof import_errors5.ValidationError)) throw error;
2448
+ if (!(error instanceof import_errors4.ValidationError)) throw error;
2684
2449
  if (args.finishReason?.unified === "length") {
2685
2450
  throw createTruncatedStructuredOutputError("useReason output", error);
2686
2451
  }
@@ -2689,7 +2454,7 @@ var parseInterruptFirstPassResult = (args) => {
2689
2454
  }
2690
2455
  const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
2691
2456
  if (!args.outputSchema && !explicitDraft) {
2692
- throw new import_errors5.ValidationError(
2457
+ throw new import_errors4.ValidationError(
2693
2458
  "MODEL_OUTPUT_SCHEMA",
2694
2459
  "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
2695
2460
  );
@@ -2963,8 +2728,42 @@ var emitReasonStructuredOutput = (args) => {
2963
2728
 
2964
2729
  // src/reason/tools.ts
2965
2730
  var import_core5 = require("@kortyx/core");
2966
- var import_errors6 = require("@kortyx/core/errors");
2731
+ var import_errors5 = require("@kortyx/core/errors");
2967
2732
  var import_zod5 = require("zod");
2733
+
2734
+ // src/reason/input-feedback.ts
2735
+ var record = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
2736
+ function schemaPath(schema, path) {
2737
+ let current = schema;
2738
+ let label = "";
2739
+ for (const part of path.slice(0, 8)) {
2740
+ if (!record(current)) break;
2741
+ if (typeof part === "number" && Number.isSafeInteger(part) && part >= 0) {
2742
+ if (!record(current.items)) break;
2743
+ current = current.items;
2744
+ label += `[${part}]`;
2745
+ } else if (typeof part === "string" && record(current.properties) && Object.hasOwn(current.properties, part)) {
2746
+ current = current.properties[part];
2747
+ label += `${label ? "." : ""}${part.slice(0, 64)}`;
2748
+ } else break;
2749
+ }
2750
+ return label || "input";
2751
+ }
2752
+ function interruptInputFeedback(error, schema, name, required) {
2753
+ const issues = Array.isArray(error.issues) ? error.issues : [];
2754
+ const problems = issues.slice(0, 5).map((issue) => {
2755
+ const path = record(issue) && Array.isArray(issue.path) ? issue.path : [];
2756
+ return `- ${schemaPath(schema, path)} must satisfy its declared input schema.`;
2757
+ });
2758
+ return [
2759
+ `Invalid input for ${name}:`,
2760
+ ...problems,
2761
+ ...issues.length > 5 ? ["- Additional input fields are invalid."] : [],
2762
+ 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."
2763
+ ].join("\n");
2764
+ }
2765
+
2766
+ // src/reason/tools.ts
2968
2767
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2969
2768
  name: tool.name,
2970
2769
  ...tool.title ? { title: tool.title } : {},
@@ -3169,7 +2968,7 @@ var runReasonToolLoop = async (args) => {
3169
2968
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
3170
2969
  const missingReturnCorrections = useReasonArgs.toolExecution?.missingReturnCorrections ?? 0;
3171
2970
  if (!Number.isSafeInteger(missingReturnCorrections) || missingReturnCorrections < 0)
3172
- throw new import_errors6.KortyxError(
2971
+ throw new import_errors5.KortyxError(
3173
2972
  "INVALID_REASON_CORRECTION_POLICY",
3174
2973
  "Invalid missing return correction budget.",
3175
2974
  {
@@ -3230,7 +3029,7 @@ var runReasonToolLoop = async (args) => {
3230
3029
  let outputContractCorrections = checkpoint?.outputContractCorrections ?? 0;
3231
3030
  const correctionExhausted = () => {
3232
3031
  const message = `useReason could not complete output-contract correction within toolExecution.maxSteps (${maxSteps}). Select exactly one terminal return contract; multiple emit contracts are allowed. Increase maxSteps to allow correction and output generation.`;
3233
- return new import_errors6.KortyxError(
3032
+ return new import_errors5.KortyxError(
3234
3033
  "REASON_OUTPUT_CONTRACT_CORRECTION_EXHAUSTED",
3235
3034
  message,
3236
3035
  {
@@ -3327,7 +3126,7 @@ var runReasonToolLoop = async (args) => {
3327
3126
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, step.warnings);
3328
3127
  const toolCalls = step.toolCalls ?? [];
3329
3128
  if (missingReturnAttempts > 0 && toolCalls.some((call) => !outputByToolName.has(call.name)))
3330
- throw new import_errors6.KortyxError(
3129
+ throw new import_errors5.KortyxError(
3331
3130
  "REASON_OUTPUT_CORRECTION_TOOL_DISALLOWED",
3332
3131
  "A return correction requested a non-output tool.",
3333
3132
  {
@@ -3370,7 +3169,7 @@ var runReasonToolLoop = async (args) => {
3370
3169
  save();
3371
3170
  continue;
3372
3171
  }
3373
- throw new import_errors6.KortyxError(
3172
+ throw new import_errors5.KortyxError(
3374
3173
  "REASON_TERMINAL_OUTPUT_MISSING",
3375
3174
  "useReason outputs.return requires one terminal output contract call.",
3376
3175
  {
@@ -3504,7 +3303,7 @@ var runReasonToolLoop = async (args) => {
3504
3303
  );
3505
3304
  if (steps.length >= maxSteps) {
3506
3305
  const message = `useReason output generation requires another model pass within toolExecution.maxSteps (${maxSteps}). Increase maxSteps to allow outputs to use completed tool and interrupt results.`;
3507
- throw new import_errors6.ProviderRequestError(message, {
3306
+ throw new import_errors5.ProviderRequestError(message, {
3508
3307
  code: "REASON_OUTPUT_BUDGET_EXHAUSTED",
3509
3308
  operation: "useReason",
3510
3309
  safeMessage: message,
@@ -3691,11 +3490,35 @@ var runReasonToolLoop = async (args) => {
3691
3490
  throw new Error(
3692
3491
  `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3693
3492
  );
3694
- const request = parseWithSchema(
3695
- resolved.contract.requestSchema,
3696
- toolCall.input,
3697
- `useReason interrupt contract "${resolved.name}" request`
3698
- );
3493
+ let request;
3494
+ try {
3495
+ request = parseWithSchema(
3496
+ resolved.contract.requestSchema,
3497
+ toolCall.input,
3498
+ `useReason interrupt contract "${resolved.name}" request`
3499
+ );
3500
+ } catch (error) {
3501
+ if (!(error instanceof import_errors5.ValidationError)) throw error;
3502
+ const result3 = {
3503
+ toolCallId: toolCall.id,
3504
+ name: toolCall.name,
3505
+ isError: true,
3506
+ content: interruptInputFeedback(
3507
+ error,
3508
+ interruptDefinitions.find((tool2) => tool2.name === toolCall.name)?.inputSchema,
3509
+ toolCall.name,
3510
+ interruptConfig?.mode === "required"
3511
+ )
3512
+ };
3513
+ toolResults.push(result3);
3514
+ messages.push({ role: "tool", ...result3 });
3515
+ traceSpan?.addEvent?.("useReason.interrupt.invalid", {
3516
+ checkpointKey,
3517
+ contract: resolved.name
3518
+ });
3519
+ save();
3520
+ continue;
3521
+ }
3699
3522
  traceSpan?.addEvent?.("useReason.interrupt.requested", {
3700
3523
  checkpointKey,
3701
3524
  contract: resolved.name,
@@ -3885,12 +3708,12 @@ var runReasonToolLoop = async (args) => {
3885
3708
  }
3886
3709
  } catch (error) {
3887
3710
  (0, import_core5.throwIfExecutionAborted)(abortSignal);
3888
- if ((0, import_errors6.isControlFlowError)(error)) throw error;
3711
+ if ((0, import_errors5.isControlFlowError)(error)) throw error;
3889
3712
  const result2 = {
3890
3713
  toolCallId: toolCall.id,
3891
3714
  name: tool.name,
3892
- content: (0, import_errors6.serializeFailure)(error).message,
3893
- failure: (0, import_errors6.serializeFailure)(error),
3715
+ content: (0, import_errors5.serializeFailure)(error).message,
3716
+ failure: (0, import_errors5.serializeFailure)(error),
3894
3717
  isError: true
3895
3718
  };
3896
3719
  toolResults.push(result2);
@@ -4493,7 +4316,7 @@ function useWorkflowState(key, ...rest) {
4493
4316
 
4494
4317
  // src/parallel.ts
4495
4318
  var import_core7 = require("@kortyx/core");
4496
- var import_errors7 = require("@kortyx/core/errors");
4319
+ var import_errors6 = require("@kortyx/core/errors");
4497
4320
  var ParallelChildWaiting = class extends Error {
4498
4321
  name = "ParallelChildWaiting";
4499
4322
  };
@@ -4507,14 +4330,14 @@ var ParallelError = class extends AggregateError {
4507
4330
  );
4508
4331
  this.results = results;
4509
4332
  this.name = "ParallelError";
4510
- this.failure = (0, import_errors7.serializeFailure)({
4333
+ this.failure = (0, import_errors6.serializeFailure)({
4511
4334
  version: 1,
4512
4335
  code: this.code,
4513
4336
  category: "workflow",
4514
4337
  message: this.message,
4515
4338
  retryable: null,
4516
4339
  results: results.map(
4517
- (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors7.serializeFailure)(result.reason) }
4340
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors6.serializeFailure)(result.reason) }
4518
4341
  )
4519
4342
  });
4520
4343
  }
@@ -4522,7 +4345,7 @@ var ParallelError = class extends AggregateError {
4522
4345
  code = "PARALLEL_FAILED";
4523
4346
  failure;
4524
4347
  toJSON() {
4525
- return (0, import_errors7.serializeFailure)(this);
4348
+ return (0, import_errors6.serializeFailure)(this);
4526
4349
  }
4527
4350
  };
4528
4351
  function registerWorkflowTask(id) {
@@ -4635,10 +4458,10 @@ function parallel(values) {
4635
4458
  throw new Error("Parallel call membership changed during replay.");
4636
4459
  }
4637
4460
  for (const callId of group.record.calls) {
4638
- const record = callRecord(callId);
4639
- if (record?.status !== "interrupted") continue;
4640
- const index = record.interrupts.length - 1;
4641
- if (record.interrupts[index]?.response !== void 0) continue;
4461
+ const record2 = callRecord(callId);
4462
+ if (record2?.status !== "interrupted") continue;
4463
+ const index = record2.interrupts.length - 1;
4464
+ if (record2.interrupts[index]?.response !== void 0) continue;
4642
4465
  group.record.interrupts.push({ callId, index });
4643
4466
  await bridge(callId, index);
4644
4467
  throw new Error(
@@ -4703,11 +4526,11 @@ async function usePrompt(reference, options) {
4703
4526
  // src/workflow.ts
4704
4527
  var import_node_crypto6 = require("crypto");
4705
4528
  var import_core8 = require("@kortyx/core");
4706
- var import_errors8 = require("@kortyx/core/errors");
4707
- var WorkflowCallError = class extends import_errors8.KortyxError {
4529
+ var import_errors7 = require("@kortyx/core/errors");
4530
+ var WorkflowCallError = class extends import_errors7.KortyxError {
4708
4531
  constructor(workflow, message, cause, failure) {
4709
- const detail = (0, import_errors8.serializeFailure)(
4710
- failure ?? cause ?? new import_errors8.WorkflowContractError(message)
4532
+ const detail = (0, import_errors7.serializeFailure)(
4533
+ failure ?? cause ?? new import_errors7.WorkflowContractError(message)
4711
4534
  );
4712
4535
  super(detail.code, `Workflow '${workflow}': ${message}`, {
4713
4536
  ...detail,
@@ -4728,12 +4551,12 @@ function workflowCallFingerprint(value) {
4728
4551
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
4729
4552
  if (typeof v === "number" && Number.isFinite(v)) return v;
4730
4553
  if (typeof v !== "object" || !v || seen.has(v)) {
4731
- throw new import_errors8.WorkflowContractError(
4554
+ throw new import_errors7.WorkflowContractError(
4732
4555
  "Workflow input and output must be JSON serializable."
4733
4556
  );
4734
4557
  }
4735
4558
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
4736
- throw new import_errors8.WorkflowContractError(
4559
+ throw new import_errors7.WorkflowContractError(
4737
4560
  "Workflow input and output must contain plain JSON objects."
4738
4561
  );
4739
4562
  }
@@ -4754,7 +4577,7 @@ function createWorkflowHooks(workflows) {
4754
4577
  useWorkflow: (args) => {
4755
4578
  const workflow = workflows[args.workflow];
4756
4579
  if (!workflow || workflow.id !== args.workflow)
4757
- throw new import_errors8.WorkflowContractError(
4580
+ throw new import_errors7.WorkflowContractError(
4758
4581
  "Workflow registry keys must match definition ids."
4759
4582
  );
4760
4583
  return useWorkflow({ ...args, workflow });
@@ -4780,23 +4603,23 @@ async function runWorkflowCall(args, task) {
4780
4603
  (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
4781
4604
  const service = ctx.node.callWorkflow;
4782
4605
  if (!service)
4783
- throw new import_errors8.WorkflowContractError(
4606
+ throw new import_errors7.WorkflowContractError(
4784
4607
  "useWorkflow requires an agent workflow registry."
4785
4608
  );
4786
4609
  if (!args.id.trim())
4787
- throw new import_errors8.WorkflowContractError(
4610
+ throw new import_errors7.WorkflowContractError(
4788
4611
  "useWorkflow requires a stable, nonempty id."
4789
4612
  );
4790
4613
  if (ctx.workflowCallActive && !task.group)
4791
- throw new import_errors8.WorkflowContractError(
4614
+ throw new import_errors7.WorkflowContractError(
4792
4615
  "Concurrent workflow calls require parallel([...]); otherwise await each call."
4793
4616
  );
4794
4617
  if (ctx.workflowCallIds.has(args.id))
4795
- throw new import_errors8.WorkflowContractError(
4618
+ throw new import_errors7.WorkflowContractError(
4796
4619
  `Duplicate workflow call id '${args.id}' in one node activation.`
4797
4620
  );
4798
4621
  if (ctx.workflowCallIds.size >= 64)
4799
- throw new import_errors8.WorkflowContractError(
4622
+ throw new import_errors7.WorkflowContractError(
4800
4623
  "A node may call at most 64 child workflows per activation."
4801
4624
  );
4802
4625
  ctx.workflowCallIds.add(args.id);
@@ -4809,26 +4632,26 @@ async function runWorkflowCall(args, task) {
4809
4632
  version: definition?.version ?? null
4810
4633
  });
4811
4634
  const key = `__useWorkflow:${args.id}`;
4812
- let record = ctx.currentNodeState.byKey[key];
4813
- if (record && record.parallelKey !== task.group?.key)
4635
+ let record2 = ctx.currentNodeState.byKey[key];
4636
+ if (record2 && record2.parallelKey !== task.group?.key)
4814
4637
  throw new WorkflowCallError(
4815
4638
  workflow,
4816
4639
  "parallel call membership changed during replay"
4817
4640
  );
4818
- if (record && record.fingerprint !== fingerprint)
4641
+ if (record2 && record2.fingerprint !== fingerprint)
4819
4642
  throw new WorkflowCallError(
4820
4643
  workflow,
4821
4644
  "call input or target changed during replay"
4822
4645
  );
4823
- if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
4824
- record ??= {
4646
+ if (!record2) ctx.node.consumeExecution?.("maxChildInvocations");
4647
+ record2 ??= {
4825
4648
  fingerprint,
4826
4649
  invocationId: (0, import_node_crypto6.randomUUID)(),
4827
4650
  status: "running",
4828
4651
  interrupts: [],
4829
4652
  ...task.group ? { parallelKey: task.group.key } : {}
4830
4653
  };
4831
- ctx.currentNodeState.byKey[key] = record;
4654
+ ctx.currentNodeState.byKey[key] = record2;
4832
4655
  ctx.stateDirty = true;
4833
4656
  if (!task.group) ctx.workflowCallActive = true;
4834
4657
  const telemetry = ctx.node.workflowCallTelemetry;
@@ -4836,7 +4659,7 @@ async function runWorkflowCall(args, task) {
4836
4659
  ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto6.randomUUID)();
4837
4660
  const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
4838
4661
  const metadata = {
4839
- invocationId: record.invocationId,
4662
+ invocationId: record2.invocationId,
4840
4663
  parentInvocationId: telemetry?.correlation?.invocationId ?? null,
4841
4664
  branchId: telemetry?.correlation?.branchId ?? telemetry?.correlation?.runId ?? "unknown",
4842
4665
  callId: args.id,
@@ -4846,46 +4669,49 @@ async function runWorkflowCall(args, task) {
4846
4669
  targetWorkflowId: workflow,
4847
4670
  targetVersion: definition?.version ?? null
4848
4671
  };
4849
- record.telemetry = metadata;
4672
+ record2.telemetry = metadata;
4850
4673
  const report = (status, content = {}) => {
4851
- record.sequence = (record.sequence ?? 0) + 1;
4674
+ record2.sequence = (record2.sequence ?? 0) + 1;
4852
4675
  emitWorkflowCall(telemetry, `workflow.call.${status}`, {
4853
4676
  ...metadata,
4854
- sequence: record.sequence,
4677
+ sequence: record2.sequence,
4855
4678
  ...content
4856
4679
  });
4857
4680
  };
4858
4681
  const recordUsage = (usage2) => {
4859
4682
  if (!usage2) return;
4860
4683
  accumulateTokenUsage({
4861
- input: usage2.input - (record.usage?.input ?? 0),
4862
- output: usage2.output - (record.usage?.output ?? 0),
4863
- total: usage2.total - (record.usage?.total ?? 0)
4684
+ input: usage2.input - (record2.usage?.input ?? 0),
4685
+ output: usage2.output - (record2.usage?.output ?? 0),
4686
+ total: usage2.total - (record2.usage?.total ?? 0)
4864
4687
  });
4865
- record.usage = usage2;
4688
+ record2.usage = usage2;
4866
4689
  };
4867
- const cached = record.status === "completed";
4690
+ const cached = record2.status === "completed";
4868
4691
  try {
4869
- for (const interruption of task.group ? [] : record.interrupts) {
4692
+ for (const interruption of task.group ? [] : record2.interrupts) {
4870
4693
  interruption.response = await awaitInterruptInternal({
4871
4694
  request: interruption.request,
4872
4695
  ...interruption.request.meta ? { meta: interruption.request.meta } : {}
4873
4696
  });
4874
4697
  }
4875
- if (record.status === "failed")
4698
+ if (record2.status === "failed")
4876
4699
  throw new WorkflowCallError(
4877
4700
  workflow,
4878
- record.error ?? "child failed",
4701
+ record2.error ?? "child failed",
4879
4702
  void 0,
4880
- record.failure ?? (0, import_errors8.serializeFailure)(void 0)
4703
+ record2.failure ?? (0, import_errors7.serializeFailure)(void 0)
4881
4704
  );
4882
- if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
4705
+ if (task.group && record2.status === "interrupted" && record2.interrupts.at(-1)?.response === void 0)
4883
4706
  throw new ParallelChildWaiting();
4884
- while (record.status !== "completed") {
4707
+ while (record2.status !== "completed") {
4885
4708
  let outcome;
4886
4709
  try {
4710
+ if (Array.isArray(record2.snapshot) && record2.snapshot.length !== 1)
4711
+ throw new import_errors7.WorkflowContractError("Invalid child workflow snapshot.");
4712
+ const snapshot = Array.isArray(record2.snapshot) ? record2.snapshot[0] : record2.snapshot;
4887
4713
  report(
4888
- record.snapshot ? "resumed" : "started",
4714
+ snapshot ? "resumed" : "started",
4889
4715
  workflowCallContent(telemetry, "input", input)
4890
4716
  );
4891
4717
  outcome = await service({
@@ -4894,9 +4720,9 @@ async function runWorkflowCall(args, task) {
4894
4720
  workflow,
4895
4721
  input,
4896
4722
  definition,
4897
- invocationId: record.invocationId,
4898
- ...record.snapshot ? { snapshot: record.snapshot } : {},
4899
- ...record.interrupts.length ? { response: record.interrupts.at(-1)?.response } : {}
4723
+ invocationId: record2.invocationId,
4724
+ ...snapshot ? { snapshot } : {},
4725
+ ...record2.interrupts.length ? { response: record2.interrupts.at(-1)?.response } : {}
4900
4726
  });
4901
4727
  } catch (error) {
4902
4728
  const child = error;
@@ -4905,48 +4731,48 @@ async function runWorkflowCall(args, task) {
4905
4731
  if ((0, import_core8.isExecutionCancelled)(error)) throw error;
4906
4732
  if ((0, import_core8.isExecutionLimitReached)(error)) {
4907
4733
  if (child?.__kortyxChildSnapshot)
4908
- record.snapshot = child.__kortyxChildSnapshot;
4734
+ record2.snapshot = child.__kortyxChildSnapshot;
4909
4735
  report("suspended", {
4910
4736
  reason: "limit_reached",
4911
4737
  limit: error.limitReached
4912
4738
  });
4913
4739
  throw error;
4914
4740
  }
4915
- record.status = "failed";
4916
- record.failure = (0, import_errors8.serializeFailure)(error);
4917
- record.error = record.failure.message;
4741
+ record2.status = "failed";
4742
+ record2.failure = (0, import_errors7.serializeFailure)(error);
4743
+ record2.error = record2.failure.message;
4918
4744
  report("failed", { error: "Child workflow failed" });
4919
4745
  throw new WorkflowCallError(
4920
4746
  workflow,
4921
- record.error,
4747
+ record2.error,
4922
4748
  error,
4923
- record.failure
4749
+ record2.failure
4924
4750
  );
4925
4751
  }
4926
4752
  (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
4927
4753
  recordUsage(outcome.usage);
4928
4754
  if (outcome.status === "completed") {
4929
- record.data = outcome.data;
4930
- workflowCallFingerprint(record.data);
4931
- record.status = "completed";
4932
- record.toolObservations = outcome.toolObservations ?? [];
4933
- for (const observation of record.toolObservations)
4755
+ record2.data = outcome.data;
4756
+ workflowCallFingerprint(record2.data);
4757
+ record2.status = "completed";
4758
+ record2.toolObservations = outcome.toolObservations ?? [];
4759
+ for (const observation of record2.toolObservations)
4934
4760
  rememberToolObservation(observation);
4935
- delete record.snapshot;
4761
+ delete record2.snapshot;
4936
4762
  report(
4937
4763
  "completed",
4938
- workflowCallContent(telemetry, "output", record.data)
4764
+ workflowCallContent(telemetry, "output", record2.data)
4939
4765
  );
4940
4766
  } else {
4941
- record.status = "interrupted";
4942
- record.snapshot = outcome.snapshot;
4767
+ record2.status = "interrupted";
4768
+ record2.snapshot = outcome.snapshot;
4943
4769
  report("suspended", {
4944
4770
  leaf: outcome.request.meta?.workflowCall ?? null
4945
4771
  });
4946
4772
  const interruption = {
4947
4773
  request: outcome.request
4948
4774
  };
4949
- record.interrupts.push(interruption);
4775
+ record2.interrupts.push(interruption);
4950
4776
  if (task.group) throw new ParallelChildWaiting();
4951
4777
  interruption.response = await awaitInterruptInternal({
4952
4778
  request: interruption.request,
@@ -4955,12 +4781,12 @@ async function runWorkflowCall(args, task) {
4955
4781
  }
4956
4782
  }
4957
4783
  if (cached) {
4958
- report("reused", workflowCallContent(telemetry, "output", record.data));
4959
- if (record.restoredToolBranch !== telemetry?.correlation?.branchId)
4960
- await replayToolObservations(record.toolObservations ?? [], true);
4961
- delete record.restoredToolBranch;
4784
+ report("reused", workflowCallContent(telemetry, "output", record2.data));
4785
+ if (record2.restoredToolBranch !== telemetry?.correlation?.branchId)
4786
+ await replayToolObservations(record2.toolObservations ?? [], true);
4787
+ delete record2.restoredToolBranch;
4962
4788
  }
4963
- const data = record.data ?? {};
4789
+ const data = record2.data ?? {};
4964
4790
  return {
4965
4791
  data: JSON.parse(JSON.stringify(data))
4966
4792
  };