@kortyx/agent 0.25.0 → 0.26.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_internal = __commonJS({
52
52
  var internal_exports = {};
53
53
  __export2(internal_exports, {
54
54
  errorDiagnostics: () => errorDiagnostics,
55
+ exceptionDiagnostics: () => exceptionDiagnostics,
55
56
  isModelTraceSpan: () => isModelTraceSpan,
56
57
  runReasonEngine: () => runReasonEngine,
57
58
  safeSpan: () => safeSpan,
@@ -61,26 +62,53 @@ var require_internal = __commonJS({
61
62
  });
62
63
  module2.exports = __toCommonJS2(internal_exports);
63
64
  var import_errors6 = require("@kortyx/core/errors");
64
- function errorDiagnostics(error, project) {
65
+ var MAX_STACK_LENGTH = 32768;
66
+ var MAX_CAUSE_DEPTH = 4;
67
+ var projectDetails = (value, depth, seen) => {
68
+ const objectValue = value && typeof value === "object" ? value : void 0;
69
+ if (objectValue) {
70
+ if (seen.has(objectValue))
71
+ return { type: "Error", message: "Circular error cause." };
72
+ seen.add(objectValue);
73
+ }
74
+ const name = (0, import_errors6.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
75
+ const message = (0, import_errors6.errorProperty)(value, "message");
76
+ const stack = (0, import_errors6.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
77
+ try {
78
+ return value.stack;
79
+ } catch {
80
+ return void 0;
81
+ }
82
+ })() : void 0);
83
+ const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors6.errorProperty)(value, "cause") : null;
84
+ return {
85
+ type: typeof name === "string" ? name.slice(0, 256) : "Error",
86
+ message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
87
+ ...typeof stack === "string" ? { stack: stack.slice(0, MAX_STACK_LENGTH) } : {},
88
+ ...cause !== null && cause !== void 0 ? { cause: projectDetails(cause, depth + 1, seen) } : {}
89
+ };
90
+ };
91
+ var sanitizeProjectedDetails = (details, depth = 0) => ({
92
+ ...typeof details.type === "string" ? { type: details.type.slice(0, 256) } : {},
93
+ message: typeof details.message === "string" ? details.message.slice(0, 8192) : "Execution reported a fault.",
94
+ ...typeof details.stack === "string" ? { stack: details.stack.slice(0, MAX_STACK_LENGTH) } : {},
95
+ ...depth < MAX_CAUSE_DEPTH && details.cause ? { cause: sanitizeProjectedDetails(details.cause, depth + 1) } : {}
96
+ });
97
+ function exceptionDiagnostics(error, project) {
65
98
  try {
66
- const name = (0, import_errors6.errorProperty)(error, "name") ?? (error instanceof Error ? error.name : void 0);
67
- const message = (0, import_errors6.errorProperty)(error, "message");
68
- const details = project ? project(error) : {
69
- type: typeof name === "string" ? name : "Error",
70
- message: typeof error === "string" ? error : typeof message === "string" ? message : "Execution reported a fault."
71
- };
72
- if (!details) return {};
73
- const projectedMessage = details.message;
74
- const projectedType = details.type;
75
- if (typeof projectedMessage !== "string") return {};
76
- return {
77
- errorMessage: projectedMessage.slice(0, 8192),
78
- ...typeof projectedType === "string" ? { errorType: projectedType.slice(0, 256) } : {}
79
- };
99
+ const details = project ? project(error) : projectDetails(error, 0, /* @__PURE__ */ new WeakSet());
100
+ return details ? sanitizeProjectedDetails(details) : null;
80
101
  } catch {
81
- return {};
102
+ return null;
82
103
  }
83
104
  }
105
+ function errorDiagnostics(error, project) {
106
+ const details = exceptionDiagnostics(error, project);
107
+ return details ? {
108
+ errorMessage: details.message,
109
+ ...details.type ? { errorType: details.type } : {}
110
+ } : {};
111
+ }
84
112
  function isModelTraceSpan(name) {
85
113
  return name === "runReasonEngine" || name === "useReason";
86
114
  }
@@ -497,8 +525,10 @@ var require_dist = __commonJS({
497
525
  WorkflowCallError: () => WorkflowCallError,
498
526
  completeResponse: () => completeResponse,
499
527
  createWorkflowHooks: () => createWorkflowHooks,
528
+ defineInterruptContract: () => defineInterruptContract,
500
529
  emitWorkflowCall: () => emitWorkflowCall2,
501
530
  parallel: () => parallel,
531
+ reportError: () => reportError,
502
532
  runWithHookContext: () => runWithHookContext,
503
533
  useAbortSignal: () => useAbortSignal,
504
534
  useInterrupt: () => useInterrupt,
@@ -807,6 +837,95 @@ var require_dist = __commonJS({
807
837
  );
808
838
  return Promise.resolve(response);
809
839
  };
840
+ var CONTROL_TOOL_PREFIX = "kortyx_request_input__";
841
+ var LEGACY_CONTRACT_NAME = "default";
842
+ var legacyWarningEmitted = false;
843
+ var legacyConfigs = /* @__PURE__ */ new WeakSet();
844
+ var defineInterruptContract = (contract) => {
845
+ if (!contract || typeof contract !== "object")
846
+ throw new Error("defineInterruptContract requires a contract object.");
847
+ for (const [field, value] of [
848
+ ["description", contract.description],
849
+ ["schemaId", contract.schemaId],
850
+ ["schemaVersion", contract.schemaVersion]
851
+ ])
852
+ if (typeof value !== "string" || value.trim().length === 0)
853
+ throw new Error(
854
+ `Interrupt contract ${field} must be a non-empty string.`
855
+ );
856
+ if (typeof contract.requestSchema?.safeParse !== "function")
857
+ throw new Error(
858
+ "Interrupt contract requestSchema must provide safeParse(value)."
859
+ );
860
+ if (typeof contract.responseSchema?.safeParse !== "function")
861
+ throw new Error(
862
+ "Interrupt contract responseSchema must provide safeParse(value)."
863
+ );
864
+ return contract;
865
+ };
866
+ var interruptControlToolName = (name) => `${CONTROL_TOOL_PREFIX}${name}`;
867
+ var isInterruptControlToolName = (name) => name.startsWith(CONTROL_TOOL_PREFIX);
868
+ var assertInterruptContractName = (name) => {
869
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
870
+ throw new Error(
871
+ `Invalid useReason interrupt contract name "${name}". Use letters, numbers, underscores, or hyphens, starting with a letter.`
872
+ );
873
+ if (interruptControlToolName(name).length > 64)
874
+ throw new Error(
875
+ `useReason interrupt contract name "${name}" is too long for a provider tool name.`
876
+ );
877
+ };
878
+ var warnLegacyUseReasonInterrupt = () => {
879
+ if (legacyWarningEmitted) return;
880
+ legacyWarningEmitted = true;
881
+ process.emitWarning(
882
+ "useReason({ interrupt }) is deprecated and will be removed in the next major release. Define an interrupt contract with defineInterruptContract() and pass it through useReason({ interrupts: { contracts } }). See docs/internal/next-major-removals.md.",
883
+ {
884
+ code: "KORTYX_USE_REASON_INTERRUPT_DEPRECATED",
885
+ type: "DeprecationWarning"
886
+ }
887
+ );
888
+ };
889
+ var normalizeReasonInterrupts = (args) => {
890
+ if (args.interrupt && args.interrupts)
891
+ throw new Error(
892
+ "useReason cannot receive both deprecated `interrupt` and `interrupts`. Migrate the legacy configuration to an interrupt contract."
893
+ );
894
+ if (args.interrupts) {
895
+ for (const [name, contract] of Object.entries(args.interrupts.contracts)) {
896
+ assertInterruptContractName(name);
897
+ defineInterruptContract(contract);
898
+ }
899
+ if (Object.keys(args.interrupts.contracts).length === 0)
900
+ throw new Error("useReason interrupts.contracts cannot be empty.");
901
+ if (args.interrupts.maxRequests !== void 0 && (!Number.isInteger(args.interrupts.maxRequests) || args.interrupts.maxRequests < 1))
902
+ throw new Error("useReason interrupts.maxRequests must be at least 1.");
903
+ return args.interrupts;
904
+ }
905
+ if (!args.interrupt) return void 0;
906
+ warnLegacyUseReasonInterrupt();
907
+ const normalized = {
908
+ mode: args.interrupt.mode,
909
+ maxRequests: 1,
910
+ contracts: {
911
+ [LEGACY_CONTRACT_NAME]: {
912
+ description: "Ask the user for the additional information required to complete this request.",
913
+ requestSchema: args.interrupt.requestSchema,
914
+ responseSchema: args.interrupt.responseSchema ?? {
915
+ safeParse: (value) => ({
916
+ success: true,
917
+ data: value
918
+ })
919
+ },
920
+ schemaId: args.interrupt.schemaId ?? "kortyx.legacy-interrupt",
921
+ schemaVersion: args.interrupt.schemaVersion ?? "1"
922
+ }
923
+ }
924
+ };
925
+ legacyConfigs.add(normalized);
926
+ return normalized;
927
+ };
928
+ var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
810
929
  var import_core62 = require("@kortyx/core");
811
930
  function safeSpan(span) {
812
931
  const guard = (fn) => (...args) => {
@@ -977,26 +1096,53 @@ var require_dist = __commonJS({
977
1096
  var import_core22 = require("@kortyx/core");
978
1097
  var import_errors32 = require("@kortyx/core/errors");
979
1098
  var import_errors22 = require("@kortyx/core/errors");
980
- function errorDiagnostics(error, project) {
1099
+ var MAX_STACK_LENGTH = 32768;
1100
+ var MAX_CAUSE_DEPTH = 4;
1101
+ var projectDetails = (value, depth, seen) => {
1102
+ const objectValue = value && typeof value === "object" ? value : void 0;
1103
+ if (objectValue) {
1104
+ if (seen.has(objectValue))
1105
+ return { type: "Error", message: "Circular error cause." };
1106
+ seen.add(objectValue);
1107
+ }
1108
+ const name = (0, import_errors22.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
1109
+ const message = (0, import_errors22.errorProperty)(value, "message");
1110
+ const stack = (0, import_errors22.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
1111
+ try {
1112
+ return value.stack;
1113
+ } catch {
1114
+ return void 0;
1115
+ }
1116
+ })() : void 0);
1117
+ const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors22.errorProperty)(value, "cause") : null;
1118
+ return {
1119
+ type: typeof name === "string" ? name.slice(0, 256) : "Error",
1120
+ message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
1121
+ ...typeof stack === "string" ? { stack: stack.slice(0, MAX_STACK_LENGTH) } : {},
1122
+ ...cause !== null && cause !== void 0 ? { cause: projectDetails(cause, depth + 1, seen) } : {}
1123
+ };
1124
+ };
1125
+ var sanitizeProjectedDetails = (details, depth = 0) => ({
1126
+ ...typeof details.type === "string" ? { type: details.type.slice(0, 256) } : {},
1127
+ message: typeof details.message === "string" ? details.message.slice(0, 8192) : "Execution reported a fault.",
1128
+ ...typeof details.stack === "string" ? { stack: details.stack.slice(0, MAX_STACK_LENGTH) } : {},
1129
+ ...depth < MAX_CAUSE_DEPTH && details.cause ? { cause: sanitizeProjectedDetails(details.cause, depth + 1) } : {}
1130
+ });
1131
+ function exceptionDiagnostics(error, project) {
981
1132
  try {
982
- const name = (0, import_errors22.errorProperty)(error, "name") ?? (error instanceof Error ? error.name : void 0);
983
- const message = (0, import_errors22.errorProperty)(error, "message");
984
- const details = project ? project(error) : {
985
- type: typeof name === "string" ? name : "Error",
986
- message: typeof error === "string" ? error : typeof message === "string" ? message : "Execution reported a fault."
987
- };
988
- if (!details) return {};
989
- const projectedMessage = details.message;
990
- const projectedType = details.type;
991
- if (typeof projectedMessage !== "string") return {};
992
- return {
993
- errorMessage: projectedMessage.slice(0, 8192),
994
- ...typeof projectedType === "string" ? { errorType: projectedType.slice(0, 256) } : {}
995
- };
1133
+ const details = project ? project(error) : projectDetails(error, 0, /* @__PURE__ */ new WeakSet());
1134
+ return details ? sanitizeProjectedDetails(details) : null;
996
1135
  } catch {
997
- return {};
1136
+ return null;
998
1137
  }
999
1138
  }
1139
+ function errorDiagnostics(error, project) {
1140
+ const details = exceptionDiagnostics(error, project);
1141
+ return details ? {
1142
+ errorMessage: details.message,
1143
+ ...details.type ? { errorType: details.type } : {}
1144
+ } : {};
1145
+ }
1000
1146
  var import_node_crypto22 = require("crypto");
1001
1147
  function emitWorkflowCall2(telemetry, type, payload) {
1002
1148
  const base = telemetry?.correlation;
@@ -2756,6 +2902,7 @@ ${tryStringify2(args.response)}`
2756
2902
  };
2757
2903
  var import_core52 = require("@kortyx/core");
2758
2904
  var import_errors62 = require("@kortyx/core/errors");
2905
+ var import_zod42 = require("zod");
2759
2906
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2760
2907
  name: tool.name,
2761
2908
  ...tool.title ? { title: tool.title } : {},
@@ -2791,7 +2938,12 @@ ${tryStringify2(args.response)}`
2791
2938
  };
2792
2939
  };
2793
2940
  var createInitialMessages = (args) => [
2794
- ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
2941
+ ...typeof args.system === "string" || args.interruptInstructions ? [
2942
+ {
2943
+ role: "system",
2944
+ content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
2945
+ }
2946
+ ] : [],
2795
2947
  { role: "user", content: String(args.input ?? "") }
2796
2948
  ];
2797
2949
  var runReasonToolLoop = async (args) => {
@@ -2806,19 +2958,23 @@ ${tryStringify2(args.response)}`
2806
2958
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
2807
2959
  const opId = checkpoint?.opId ?? args.opId;
2808
2960
  const tools2 = useReasonArgs.tools ?? [];
2961
+ const interruptConfig = normalizeReasonInterrupts({
2962
+ interrupt: useReasonArgs.interrupt,
2963
+ interrupts: useReasonArgs.interrupts
2964
+ });
2965
+ const interruptByToolName = /* @__PURE__ */ new Map();
2809
2966
  const toolByName = /* @__PURE__ */ new Map();
2810
2967
  let separateOutput = false;
2811
2968
  for (const tool of tools2) {
2969
+ if (isInterruptControlToolName(tool.name))
2970
+ throw new Error(
2971
+ `useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
2972
+ );
2812
2973
  if (toolByName.has(tool.name)) {
2813
2974
  throw new Error(`useReason received duplicate tool name "${tool.name}".`);
2814
2975
  }
2815
2976
  toolByName.set(tool.name, tool);
2816
2977
  }
2817
- if (useReasonArgs.interrupt) {
2818
- throw new Error(
2819
- "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
2820
- );
2821
- }
2822
2978
  separateOutput = Boolean(
2823
2979
  useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
2824
2980
  ...useReasonArgs.model.options,
@@ -2830,21 +2986,63 @@ ${tryStringify2(args.response)}`
2830
2986
  ) ?? {}
2831
2987
  }).requiresSeparateStructuredOutput
2832
2988
  );
2833
- const toolDefinitions = toToolDefinitions(tools2);
2989
+ const interruptDefinitions = [];
2990
+ for (const [name, contract] of Object.entries(
2991
+ interruptConfig?.contracts ?? {}
2992
+ )) {
2993
+ const toolName = interruptControlToolName(name);
2994
+ if (toolByName.has(toolName))
2995
+ throw new Error(
2996
+ `useReason tool name "${toolName}" is reserved for interrupt contracts.`
2997
+ );
2998
+ let inputSchema;
2999
+ try {
3000
+ inputSchema = (0, import_zod42.toJSONSchema)(contract.requestSchema);
3001
+ } catch (error) {
3002
+ throw new Error(
3003
+ `useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
3004
+ { cause: error }
3005
+ );
3006
+ }
3007
+ interruptByToolName.set(toolName, { name, contract });
3008
+ interruptDefinitions.push({
3009
+ name: toolName,
3010
+ title: `Request human input: ${name}`,
3011
+ description: [
3012
+ contract.description,
3013
+ "Call this only when human input is needed. Execution pauses until the validated response is returned as this tool's result."
3014
+ ].join(" "),
3015
+ inputSchema,
3016
+ metadata: {
3017
+ kortyxControl: "interrupt",
3018
+ contract: name,
3019
+ schemaId: contract.schemaId,
3020
+ schemaVersion: contract.schemaVersion
3021
+ }
3022
+ });
3023
+ }
3024
+ const toolDefinitions = [
3025
+ ...toToolDefinitions(tools2),
3026
+ ...interruptDefinitions
3027
+ ];
2834
3028
  let finalizing = checkpoint?.finalizing ?? false;
2835
3029
  let completed = false;
2836
3030
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
2837
3031
  const messages = checkpoint?.messages ?? createInitialMessages({
2838
3032
  system: useReasonArgs.system,
2839
- input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input
3033
+ input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input,
3034
+ ...interruptConfig ? {
3035
+ interruptInstructions: [
3036
+ "Human-input rules:",
3037
+ "- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
3038
+ "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
3039
+ interruptConfig.mode === "required" ? "- You must call at least one human-input tool before returning the final answer." : "- Human input is optional; call a human-input tool only when the request cannot be completed safely without it."
3040
+ ].join("\n")
3041
+ } : {}
2840
3042
  });
2841
3043
  const steps = checkpoint?.steps ?? [];
2842
- const allToolCalls = steps.flatMap(
2843
- (step) => step.toolCalls
2844
- );
2845
- const allToolResults = steps.flatMap(
2846
- (step) => step.toolResults
2847
- );
3044
+ const allToolCalls = steps.flatMap((step) => step.toolCalls).filter((call) => !interruptByToolName.has(call.name));
3045
+ const allToolResults = steps.flatMap((step) => step.toolResults).filter((result) => !interruptByToolName.has(result.name));
2848
3046
  let finalText = "";
2849
3047
  let finalRaw;
2850
3048
  let finalOutput;
@@ -2860,6 +3058,7 @@ ${tryStringify2(args.response)}`
2860
3058
  );
2861
3059
  let pending = checkpoint?.pending;
2862
3060
  const approvedCalls = checkpoint?.approvedCalls ?? [];
3061
+ const interruptHistory = checkpoint?.interruptHistory ?? [];
2863
3062
  const save = () => {
2864
3063
  ctx.currentNodeState.byKey[checkpointKey] = {
2865
3064
  status: "tool_loop",
@@ -2867,6 +3066,7 @@ ${tryStringify2(args.response)}`
2867
3066
  messages,
2868
3067
  steps,
2869
3068
  approvedCalls,
3069
+ interruptHistory,
2870
3070
  finalizing,
2871
3071
  ...pending ? { pending } : {}
2872
3072
  };
@@ -2945,6 +3145,10 @@ ${tryStringify2(args.response)}`
2945
3145
  ...step.warnings ? { warnings: step.warnings } : {}
2946
3146
  });
2947
3147
  if (toolCalls.length === 0) {
3148
+ if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
3149
+ throw new Error(
3150
+ "useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
3151
+ );
2948
3152
  if (separateOutput && !finalizing) {
2949
3153
  if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
2950
3154
  try {
@@ -2989,7 +3193,9 @@ ${tryStringify2(args.response)}`
2989
3193
  "Provider requested a tool during schema-only finalization."
2990
3194
  );
2991
3195
  if (!reused) {
2992
- allToolCalls.push(...toolCalls);
3196
+ allToolCalls.push(
3197
+ ...toolCalls.filter((call) => !interruptByToolName.has(call.name))
3198
+ );
2993
3199
  messages.push({
2994
3200
  role: "assistant",
2995
3201
  content: step.text,
@@ -2999,6 +3205,84 @@ ${tryStringify2(args.response)}`
2999
3205
  }
3000
3206
  pending = step;
3001
3207
  save();
3208
+ const interruptCalls = toolCalls.filter(
3209
+ (call) => interruptByToolName.has(call.name)
3210
+ );
3211
+ if (interruptCalls.length > 0) {
3212
+ if (toolCalls.length !== 1)
3213
+ throw new Error(
3214
+ "A useReason interrupt contract call must be the only tool call in its model turn."
3215
+ );
3216
+ const interruptCall = interruptCalls[0];
3217
+ if (!interruptCall)
3218
+ throw new Error("Missing useReason interrupt control call.");
3219
+ if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
3220
+ throw new Error(
3221
+ `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3222
+ );
3223
+ const resolved = interruptByToolName.get(interruptCall.name);
3224
+ if (!resolved)
3225
+ throw new Error(
3226
+ `Unknown useReason interrupt contract tool "${interruptCall.name}".`
3227
+ );
3228
+ if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
3229
+ pending = void 0;
3230
+ save();
3231
+ continue;
3232
+ }
3233
+ const request = parseWithSchema(
3234
+ resolved.contract.requestSchema,
3235
+ interruptCall.input,
3236
+ `useReason interrupt contract "${resolved.name}" request`
3237
+ );
3238
+ traceSpan?.addEvent?.("useReason.interrupt.requested", {
3239
+ checkpointKey,
3240
+ contract: resolved.name,
3241
+ interruptIndex: interruptHistory.length
3242
+ });
3243
+ const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
3244
+ const response = await awaitInterruptInternal({
3245
+ request: {
3246
+ kind: "custom",
3247
+ request,
3248
+ contract: resolved.name,
3249
+ ...question ? { question } : {},
3250
+ schemaId: resolved.contract.schemaId,
3251
+ schemaVersion: resolved.contract.schemaVersion
3252
+ },
3253
+ responseSchema: resolved.contract.responseSchema,
3254
+ id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
3255
+ meta: {
3256
+ __kortyxReason: {
3257
+ opId,
3258
+ stepIndex,
3259
+ interruptIndex: interruptHistory.length,
3260
+ contract: resolved.name
3261
+ }
3262
+ }
3263
+ });
3264
+ const result2 = normalizeToolResult(interruptCall, response);
3265
+ toolResults.push(result2);
3266
+ messages.push({
3267
+ role: "tool",
3268
+ content: result2.content,
3269
+ toolCallId: result2.toolCallId,
3270
+ name: result2.name
3271
+ });
3272
+ interruptHistory.push({
3273
+ contract: resolved.name,
3274
+ request,
3275
+ response
3276
+ });
3277
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3278
+ checkpointKey,
3279
+ contract: resolved.name,
3280
+ interruptIndex: interruptHistory.length - 1
3281
+ });
3282
+ pending = void 0;
3283
+ save();
3284
+ continue;
3285
+ }
3002
3286
  for (const toolCall of toolCalls) {
3003
3287
  if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
3004
3288
  await replayToolObservations(
@@ -3208,7 +3492,11 @@ ${tryStringify2(args.response)}`
3208
3492
  ...finalOutput !== void 0 ? { output: finalOutput } : {},
3209
3493
  toolCalls: allToolCalls,
3210
3494
  toolResults: allToolResults,
3211
- steps
3495
+ steps,
3496
+ ...interruptHistory.length > 0 ? { interruptHistory } : {},
3497
+ ...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
3498
+ interruptResponse: interruptHistory.at(-1)?.response
3499
+ } : {}
3212
3500
  };
3213
3501
  traceSpan?.end?.({
3214
3502
  ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
@@ -3219,7 +3507,8 @@ ${tryStringify2(args.response)}`
3219
3507
  textLength: finalText.length,
3220
3508
  toolCallCount: allToolCalls.length,
3221
3509
  toolResultCount: allToolResults.length,
3222
- toolStepCount: steps.length
3510
+ toolStepCount: steps.length,
3511
+ interruptCount: interruptHistory.length
3223
3512
  },
3224
3513
  telemetry: {
3225
3514
  ...useReasonArgs.telemetry ?? {},
@@ -3247,7 +3536,7 @@ ${tryStringify2(args.response)}`
3247
3536
  var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
3248
3537
  var assertReasoningThoughtsCompatibility = (args) => {
3249
3538
  if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
3250
- const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3539
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3251
3540
  if (!usesStructuredOutput) return;
3252
3541
  throw new Error(
3253
3542
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
@@ -3255,6 +3544,10 @@ ${tryStringify2(args.response)}`
3255
3544
  };
3256
3545
  async function runReason(args) {
3257
3546
  assertReasoningThoughtsCompatibility(args);
3547
+ const interruptConfig = normalizeReasonInterrupts({
3548
+ interrupt: args.interrupt,
3549
+ interrupts: args.interrupts
3550
+ });
3258
3551
  const ctx = getHookContext();
3259
3552
  const abortSignal = (0, import_core62.combineAbortSignals)(
3260
3553
  ctx.node.abortSignal,
@@ -3269,20 +3562,23 @@ ${tryStringify2(args.response)}`
3269
3562
  args.outputSchema,
3270
3563
  args.responseFormat ?? args.model.options?.responseFormat
3271
3564
  );
3565
+ const usesLegacyNoToolInterrupt = Boolean(
3566
+ args.interrupt && !args.interrupts && !args.tools?.length
3567
+ );
3272
3568
  args = {
3273
3569
  ...args,
3274
3570
  abortSignal,
3275
- ...!args.interrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
3571
+ ...!usesLegacyNoToolInterrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
3276
3572
  };
3277
3573
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
3278
- const opId = createRuntimeId2();
3574
+ const provisionalOpId = createRuntimeId2();
3279
3575
  const checkpointKey = resolveReasonCheckpointKey({
3280
3576
  ...id ? { id } : {},
3281
3577
  autoIndex: ctx.reasonCallIndex++
3282
3578
  });
3283
- const existingCompleted = readReasonCompletedCheckpoint(
3284
- ctx.currentNodeState.byKey[checkpointKey]
3285
- );
3579
+ const checkpointValue = ctx.currentNodeState.byKey[checkpointKey];
3580
+ const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
3581
+ const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
3286
3582
  if (existingCompleted) {
3287
3583
  await replayToolObservations(
3288
3584
  existingCompleted.result.steps?.flatMap(
@@ -3291,9 +3587,7 @@ ${tryStringify2(args.response)}`
3291
3587
  );
3292
3588
  return existingCompleted.result;
3293
3589
  }
3294
- const existingCheckpoint = readReasonCheckpoint(
3295
- ctx.currentNodeState.byKey[checkpointKey]
3296
- );
3590
+ const existingCheckpoint = readReasonCheckpoint(checkpointValue);
3297
3591
  const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
3298
3592
  return withSafeTraceSpan2(
3299
3593
  getReasonTraceAdapter(),
@@ -3312,7 +3606,8 @@ ${tryStringify2(args.response)}`
3312
3606
  ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
3313
3607
  ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
3314
3608
  hasOutputSchema: Boolean(args.outputSchema),
3315
- hasInterrupt: Boolean(args.interrupt),
3609
+ hasInterrupt: Boolean(interruptConfig),
3610
+ interruptContractCount: Object.keys(interruptConfig?.contracts ?? {}).length,
3316
3611
  hasStructured: Boolean(args.structured),
3317
3612
  hasTools: Boolean(args.tools?.length)
3318
3613
  },
@@ -3322,7 +3617,7 @@ ${tryStringify2(args.response)}`
3322
3617
  }
3323
3618
  },
3324
3619
  async (traceSpan) => {
3325
- if (args.tools?.length) {
3620
+ if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
3326
3621
  return runReasonToolLoop({
3327
3622
  useReasonArgs: args,
3328
3623
  allowValidatedToolOutput,
@@ -3589,6 +3884,24 @@ ${tryStringify2(args.response)}`
3589
3884
  return useReason(args);
3590
3885
  }
3591
3886
  function useInterrupt(args) {
3887
+ if ("contract" in args) {
3888
+ const contract = defineInterruptContract(args.contract);
3889
+ return awaitInterruptInternal({
3890
+ request: {
3891
+ kind: "custom",
3892
+ request: parseWithSchema(
3893
+ contract.requestSchema,
3894
+ args.request,
3895
+ "useInterrupt contract request"
3896
+ ),
3897
+ schemaId: contract.schemaId,
3898
+ schemaVersion: contract.schemaVersion,
3899
+ ...args.id ? { id: args.id } : {},
3900
+ ...args.meta ? { meta: args.meta } : {}
3901
+ },
3902
+ responseSchema: contract.responseSchema
3903
+ });
3904
+ }
3592
3905
  return awaitInterruptInternal(args);
3593
3906
  }
3594
3907
  function useAbortSignal() {
@@ -3802,6 +4115,12 @@ ${tryStringify2(args.response)}`
3802
4115
  });
3803
4116
  return promise;
3804
4117
  }
4118
+ function reportError(error, options = {}) {
4119
+ try {
4120
+ getReasonTraceAdapter()?.reportError?.(error, options);
4121
+ } catch {
4122
+ }
4123
+ }
3805
4124
  var import_node_crypto6 = require("crypto");
3806
4125
  var import_core8 = require("@kortyx/core");
3807
4126
  var import_errors8 = require("@kortyx/core/errors");
@@ -4888,7 +5207,10 @@ async function orchestrateGraphStream({
4888
5207
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
4889
5208
  resumeOutcome: "failed",
4890
5209
  resumeError: (0, import_errors4.serializeFailure)(resumeError).message,
4891
- failure: (0, import_errors4.serializeFailure)(resumeError)
5210
+ failure: (0, import_errors4.serializeFailure)(resumeError),
5211
+ ...typeof config.telemetryInterruptContract === "string" ? { contract: config.telemetryInterruptContract } : {},
5212
+ ...typeof config.telemetryInterruptSchemaId === "string" ? { schemaId: config.telemetryInterruptSchemaId } : {},
5213
+ ...typeof config.telemetryInterruptSchemaVersion === "string" ? { schemaVersion: config.telemetryInterruptSchemaVersion } : {}
4892
5214
  },
4893
5215
  flush: true
4894
5216
  });
@@ -4994,6 +5316,7 @@ async function orchestrateGraphStream({
4994
5316
  const optionsList = Array.isArray(input.options) ? input.options : [];
4995
5317
  const kind = input.kind || (input.multiple ? "multi-choice" : "choice");
4996
5318
  const isText = kind === "text";
5319
+ const isCustom = kind === "custom";
4997
5320
  const record = {
4998
5321
  token,
4999
5322
  ready: false,
@@ -5004,7 +5327,17 @@ async function orchestrateGraphStream({
5004
5327
  workflow: payload.workflow || currentState.currentWorkflow,
5005
5328
  node: payload.node || "",
5006
5329
  state: { ...currentState, awaitingHumanInput: true },
5007
- schema: isText ? {
5330
+ schema: isCustom ? {
5331
+ kind: "custom",
5332
+ multiple: false,
5333
+ ...input.question ? { question: input.question } : {},
5334
+ ...typeof input.id === "string" && input.id.length > 0 ? { id: input.id } : {},
5335
+ ...typeof input.contract === "string" && input.contract.length > 0 ? { contract: input.contract } : {},
5336
+ ...Object.hasOwn(input, "request") ? { request: input.request } : {},
5337
+ ...typeof input.schemaId === "string" && input.schemaId.length > 0 ? { schemaId: input.schemaId } : {},
5338
+ ...typeof input.schemaVersion === "string" && input.schemaVersion.length > 0 ? { schemaVersion: input.schemaVersion } : {},
5339
+ ...input.meta && typeof input.meta === "object" ? { meta: input.meta } : {}
5340
+ } : isText ? {
5008
5341
  kind,
5009
5342
  multiple: Boolean(input.multiple),
5010
5343
  ...input.question ? { question: input.question } : {},
@@ -5059,6 +5392,8 @@ async function orchestrateGraphStream({
5059
5392
  kind: record.schema.kind,
5060
5393
  multiple: record.schema.multiple,
5061
5394
  question: record.schema.question,
5395
+ ...record.schema.contract ? { contract: record.schema.contract } : {},
5396
+ ...Object.hasOwn(record.schema, "request") ? { request: record.schema.request } : {},
5062
5397
  ...typeof record.schema.id === "string" && record.schema.id.length > 0 ? { id: record.schema.id } : {},
5063
5398
  ...typeof record.schema.schemaId === "string" && record.schema.schemaId.length > 0 ? { schemaId: record.schema.schemaId } : {},
5064
5399
  ...typeof record.schema.schemaVersion === "string" && record.schema.schemaVersion.length > 0 ? { schemaVersion: record.schema.schemaVersion } : {},
@@ -5070,6 +5405,11 @@ async function orchestrateGraphStream({
5070
5405
  }))
5071
5406
  }
5072
5407
  });
5408
+ const captureRequest = shouldCaptureTelemetryContent(
5409
+ telemetryConfig.captureContent,
5410
+ "output"
5411
+ );
5412
+ const structuredRequest = isRecord3(record.schema.request) ? record.schema.request : void 0;
5073
5413
  emitTelemetryEvent({
5074
5414
  config,
5075
5415
  type: "interrupt.created",
@@ -5085,11 +5425,11 @@ async function orchestrateGraphStream({
5085
5425
  interactionMode: record.schema.kind === "text" ? "freeform" : record.options.length > 0 ? "static-options" : record.schema.schemaId ? "dynamic-picker" : "unknown",
5086
5426
  ...record.schema.schemaId ? { schemaId: record.schema.schemaId } : {},
5087
5427
  ...record.schema.schemaVersion ? { schemaVersion: record.schema.schemaVersion } : {},
5088
- ...typeof record.schema.question === "string" && shouldCaptureTelemetryContent(telemetryConfig.captureContent, "output") ? { question: record.schema.question } : {},
5089
- ...shouldCaptureTelemetryContent(
5090
- telemetryConfig.captureContent,
5091
- "output"
5092
- ) ? {
5428
+ ...record.schema.contract ? { contract: record.schema.contract } : {},
5429
+ requestCaptured: Boolean(captureRequest && structuredRequest),
5430
+ ...captureRequest && structuredRequest ? { request: structuredRequest } : {},
5431
+ ...typeof record.schema.question === "string" && captureRequest ? { question: record.schema.question } : {},
5432
+ ...captureRequest ? {
5093
5433
  options: record.options.map((option) => ({
5094
5434
  id: option.id,
5095
5435
  label: option.label,
@@ -5898,6 +6238,7 @@ function parseResumeMeta(msg) {
5898
6238
  const token = typeof raw.token === "string" ? raw.token : "";
5899
6239
  const requestId = typeof raw.requestId === "string" ? raw.requestId : "";
5900
6240
  const cancel = raw.cancel === true;
6241
+ const hasValue = Object.hasOwn(raw, "value");
5901
6242
  let selected = [];
5902
6243
  const rawSelected = raw.selected;
5903
6244
  if (typeof rawSelected === "string") selected = [rawSelected];
@@ -5907,7 +6248,13 @@ function parseResumeMeta(msg) {
5907
6248
  else if (Array.isArray(raw.choices))
5908
6249
  selected = raw.choices.map((c) => isRecord4(c) ? c.id : void 0).filter((id) => typeof id === "string");
5909
6250
  if (!token || !requestId) return null;
5910
- return { token, requestId, selected, cancel };
6251
+ return {
6252
+ token,
6253
+ requestId,
6254
+ selected,
6255
+ cancel,
6256
+ ...hasValue ? { value: raw.value, hasValue: true } : {}
6257
+ };
5911
6258
  }
5912
6259
  async function tryPrepareResumeStream({
5913
6260
  limits,
@@ -5994,7 +6341,8 @@ async function tryPrepareResumeStream({
5994
6341
  },
5995
6342
  payload: {
5996
6343
  interruptId: pending.requestId,
5997
- reason: "cancelled_by_client"
6344
+ reason: "cancelled_by_client",
6345
+ ...pending.schema.contract ? { contract: pending.schema.contract } : {}
5998
6346
  },
5999
6347
  flush: true
6000
6348
  });
@@ -6074,7 +6422,7 @@ async function tryPrepareResumeStream({
6074
6422
  resumeUpdate.runtime = resumeStatePatch;
6075
6423
  }
6076
6424
  const hasResumeUpdate = !isLimitPause && Object.keys(resumeUpdate).length > 0;
6077
- const resumeValue = isLimitPause ? void 0 : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
6425
+ 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;
6078
6426
  const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
6079
6427
  const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
6080
6428
  ...telemetryConfig,
@@ -6084,7 +6432,7 @@ async function tryPrepareResumeStream({
6084
6432
  ...hasResumeUpdate ? { resumeUpdate } : {}
6085
6433
  });
6086
6434
  await store.delete(pending.token);
6087
- const response = responseFromSelection(meta.selected);
6435
+ const response = meta.hasValue ? JSON.stringify(meta.value) : responseFromSelection(meta.selected);
6088
6436
  const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};
6089
6437
  const responseCaptured = Boolean(
6090
6438
  response && shouldCaptureTelemetryContent(telemetry.captureContent, "input")
@@ -6103,6 +6451,10 @@ async function tryPrepareResumeStream({
6103
6451
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
6104
6452
  resumeOutcome: "resumed",
6105
6453
  responseCaptured,
6454
+ ...pending.schema.contract ? { contract: pending.schema.contract } : {},
6455
+ ...pending.schema.schemaId ? { schemaId: pending.schema.schemaId } : {},
6456
+ ...pending.schema.schemaVersion ? { schemaVersion: pending.schema.schemaVersion } : {},
6457
+ ...responseCaptured && meta.hasValue ? { responseValue: meta.value } : {},
6106
6458
  ...responseCaptured && response ? { response } : {}
6107
6459
  },
6108
6460
  flush: true
@@ -6124,6 +6476,11 @@ async function tryPrepareResumeStream({
6124
6476
  resume: true,
6125
6477
  telemetryInterruptId: pending.requestId,
6126
6478
  telemetryInterruptNodeId: pending.node,
6479
+ ...pending.schema.contract ? { telemetryInterruptContract: pending.schema.contract } : {},
6480
+ ...pending.schema.schemaId ? { telemetryInterruptSchemaId: pending.schema.schemaId } : {},
6481
+ ...pending.schema.schemaVersion ? {
6482
+ telemetryInterruptSchemaVersion: pending.schema.schemaVersion
6483
+ } : {},
6127
6484
  ...resumeValue !== void 0 ? { resumeValue } : {},
6128
6485
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
6129
6486
  ...hasResumeUpdate ? { resumeUpdate } : {}
@@ -6157,6 +6514,7 @@ var responseSchema = import_zod3.z.discriminatedUnion("type", [
6157
6514
  type: import_zod3.z.literal("select"),
6158
6515
  ids: import_zod3.z.array(import_zod3.z.string().min(1)).min(1)
6159
6516
  }).strict(),
6517
+ import_zod3.z.object({ type: import_zod3.z.literal("value"), value: import_zod3.z.unknown() }).strict(),
6160
6518
  import_zod3.z.object({ type: import_zod3.z.literal("cancel") }).strict()
6161
6519
  ]);
6162
6520
  function resultFromOutcome(outcome, runId, sessionId) {
@@ -6297,7 +6655,13 @@ async function resumeWorkflow(services, args) {
6297
6655
  "Resume handle does not match the root execution and workflow version."
6298
6656
  );
6299
6657
  if (response.type === "cancel") return;
6300
- if (response.type === "text") {
6658
+ if (response.type === "value") {
6659
+ if (pending.schema.kind !== "custom")
6660
+ throw new ExecutionRequestError(
6661
+ "INVALID_RESPONSE",
6662
+ "Structured values are only valid for custom interrupts."
6663
+ );
6664
+ } else if (response.type === "text") {
6301
6665
  if (pending.schema.kind !== "text")
6302
6666
  throw new ExecutionRequestError(
6303
6667
  "INVALID_RESPONSE",
@@ -6327,6 +6691,7 @@ async function resumeWorkflow(services, args) {
6327
6691
  token: handle.token,
6328
6692
  requestId: handle.requestId,
6329
6693
  selected: response.type === "text" ? [response.text] : response.type === "select" ? response.ids : [],
6694
+ ...response.type === "value" ? { value: response.value, hasValue: true } : {},
6330
6695
  cancel: response.type === "cancel"
6331
6696
  },
6332
6697
  sessionId: handle.sessionId,