@kortyx/hooks 0.26.0 → 0.28.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
@@ -23,8 +23,10 @@ __export(index_exports, {
23
23
  WorkflowCallError: () => WorkflowCallError,
24
24
  completeResponse: () => completeResponse,
25
25
  createWorkflowHooks: () => createWorkflowHooks,
26
+ defineInterruptContract: () => defineInterruptContract,
26
27
  emitWorkflowCall: () => emitWorkflowCall,
27
28
  parallel: () => parallel,
29
+ reportError: () => reportError,
28
30
  runWithHookContext: () => runWithHookContext,
29
31
  useAbortSignal: () => useAbortSignal,
30
32
  useInterrupt: () => useInterrupt,
@@ -348,6 +350,97 @@ var awaitInterruptInternal = (args) => {
348
350
  return Promise.resolve(response);
349
351
  };
350
352
 
353
+ // src/interrupt-contract.ts
354
+ var CONTROL_TOOL_PREFIX = "kortyx_request_input__";
355
+ var LEGACY_CONTRACT_NAME = "default";
356
+ var legacyWarningEmitted = false;
357
+ var legacyConfigs = /* @__PURE__ */ new WeakSet();
358
+ var defineInterruptContract = (contract) => {
359
+ if (!contract || typeof contract !== "object")
360
+ throw new Error("defineInterruptContract requires a contract object.");
361
+ for (const [field, value] of [
362
+ ["description", contract.description],
363
+ ["schemaId", contract.schemaId],
364
+ ["schemaVersion", contract.schemaVersion]
365
+ ])
366
+ if (typeof value !== "string" || value.trim().length === 0)
367
+ throw new Error(
368
+ `Interrupt contract ${field} must be a non-empty string.`
369
+ );
370
+ if (typeof contract.requestSchema?.safeParse !== "function")
371
+ throw new Error(
372
+ "Interrupt contract requestSchema must provide safeParse(value)."
373
+ );
374
+ if (typeof contract.responseSchema?.safeParse !== "function")
375
+ throw new Error(
376
+ "Interrupt contract responseSchema must provide safeParse(value)."
377
+ );
378
+ return contract;
379
+ };
380
+ var interruptControlToolName = (name) => `${CONTROL_TOOL_PREFIX}${name}`;
381
+ var isInterruptControlToolName = (name) => name.startsWith(CONTROL_TOOL_PREFIX);
382
+ var assertInterruptContractName = (name) => {
383
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
384
+ throw new Error(
385
+ `Invalid useReason interrupt contract name "${name}". Use letters, numbers, underscores, or hyphens, starting with a letter.`
386
+ );
387
+ if (interruptControlToolName(name).length > 64)
388
+ throw new Error(
389
+ `useReason interrupt contract name "${name}" is too long for a provider tool name.`
390
+ );
391
+ };
392
+ var warnLegacyUseReasonInterrupt = () => {
393
+ if (legacyWarningEmitted) return;
394
+ legacyWarningEmitted = true;
395
+ process.emitWarning(
396
+ "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.",
397
+ {
398
+ code: "KORTYX_USE_REASON_INTERRUPT_DEPRECATED",
399
+ type: "DeprecationWarning"
400
+ }
401
+ );
402
+ };
403
+ var normalizeReasonInterrupts = (args) => {
404
+ if (args.interrupt && args.interrupts)
405
+ throw new Error(
406
+ "useReason cannot receive both deprecated `interrupt` and `interrupts`. Migrate the legacy configuration to an interrupt contract."
407
+ );
408
+ if (args.interrupts) {
409
+ for (const [name, contract] of Object.entries(args.interrupts.contracts)) {
410
+ assertInterruptContractName(name);
411
+ defineInterruptContract(contract);
412
+ }
413
+ if (Object.keys(args.interrupts.contracts).length === 0)
414
+ throw new Error("useReason interrupts.contracts cannot be empty.");
415
+ if (args.interrupts.maxRequests !== void 0 && (!Number.isInteger(args.interrupts.maxRequests) || args.interrupts.maxRequests < 1))
416
+ throw new Error("useReason interrupts.maxRequests must be at least 1.");
417
+ return args.interrupts;
418
+ }
419
+ if (!args.interrupt) return void 0;
420
+ warnLegacyUseReasonInterrupt();
421
+ const normalized = {
422
+ mode: args.interrupt.mode,
423
+ maxRequests: 1,
424
+ contracts: {
425
+ [LEGACY_CONTRACT_NAME]: {
426
+ description: "Ask the user for the additional information required to complete this request.",
427
+ requestSchema: args.interrupt.requestSchema,
428
+ responseSchema: args.interrupt.responseSchema ?? {
429
+ safeParse: (value) => ({
430
+ success: true,
431
+ data: value
432
+ })
433
+ },
434
+ schemaId: args.interrupt.schemaId ?? "kortyx.legacy-interrupt",
435
+ schemaVersion: args.interrupt.schemaVersion ?? "1"
436
+ }
437
+ }
438
+ };
439
+ legacyConfigs.add(normalized);
440
+ return normalized;
441
+ };
442
+ var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
443
+
351
444
  // src/reason/use-reason.ts
352
445
  var import_core6 = require("@kortyx/core");
353
446
 
@@ -531,26 +624,53 @@ var import_errors3 = require("@kortyx/core/errors");
531
624
 
532
625
  // src/error-diagnostics.ts
533
626
  var import_errors2 = require("@kortyx/core/errors");
534
- function errorDiagnostics(error, project) {
627
+ var MAX_STACK_LENGTH = 32768;
628
+ var MAX_CAUSE_DEPTH = 4;
629
+ var projectDetails = (value, depth, seen) => {
630
+ const objectValue = value && typeof value === "object" ? value : void 0;
631
+ if (objectValue) {
632
+ if (seen.has(objectValue))
633
+ return { type: "Error", message: "Circular error cause." };
634
+ seen.add(objectValue);
635
+ }
636
+ const name = (0, import_errors2.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
637
+ const message = (0, import_errors2.errorProperty)(value, "message");
638
+ const stack = (0, import_errors2.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
639
+ try {
640
+ return value.stack;
641
+ } catch {
642
+ return void 0;
643
+ }
644
+ })() : void 0);
645
+ const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors2.errorProperty)(value, "cause") : null;
646
+ return {
647
+ type: typeof name === "string" ? name.slice(0, 256) : "Error",
648
+ message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
649
+ ...typeof stack === "string" ? { stack: stack.slice(0, MAX_STACK_LENGTH) } : {},
650
+ ...cause !== null && cause !== void 0 ? { cause: projectDetails(cause, depth + 1, seen) } : {}
651
+ };
652
+ };
653
+ var sanitizeProjectedDetails = (details, depth = 0) => ({
654
+ ...typeof details.type === "string" ? { type: details.type.slice(0, 256) } : {},
655
+ message: typeof details.message === "string" ? details.message.slice(0, 8192) : "Execution reported a fault.",
656
+ ...typeof details.stack === "string" ? { stack: details.stack.slice(0, MAX_STACK_LENGTH) } : {},
657
+ ...depth < MAX_CAUSE_DEPTH && details.cause ? { cause: sanitizeProjectedDetails(details.cause, depth + 1) } : {}
658
+ });
659
+ function exceptionDiagnostics(error, project) {
535
660
  try {
536
- const name = (0, import_errors2.errorProperty)(error, "name") ?? (error instanceof Error ? error.name : void 0);
537
- const message = (0, import_errors2.errorProperty)(error, "message");
538
- const details = project ? project(error) : {
539
- type: typeof name === "string" ? name : "Error",
540
- message: typeof error === "string" ? error : typeof message === "string" ? message : "Execution reported a fault."
541
- };
542
- if (!details) return {};
543
- const projectedMessage = details.message;
544
- const projectedType = details.type;
545
- if (typeof projectedMessage !== "string") return {};
546
- return {
547
- errorMessage: projectedMessage.slice(0, 8192),
548
- ...typeof projectedType === "string" ? { errorType: projectedType.slice(0, 256) } : {}
549
- };
661
+ const details = project ? project(error) : projectDetails(error, 0, /* @__PURE__ */ new WeakSet());
662
+ return details ? sanitizeProjectedDetails(details) : null;
550
663
  } catch {
551
- return {};
664
+ return null;
552
665
  }
553
666
  }
667
+ function errorDiagnostics(error, project) {
668
+ const details = exceptionDiagnostics(error, project);
669
+ return details ? {
670
+ errorMessage: details.message,
671
+ ...details.type ? { errorType: details.type } : {}
672
+ } : {};
673
+ }
554
674
 
555
675
  // src/workflow-telemetry.ts
556
676
  var import_node_crypto2 = require("crypto");
@@ -2334,6 +2454,7 @@ var emitReasonStructuredOutput = (args) => {
2334
2454
  // src/reason/tools.ts
2335
2455
  var import_core5 = require("@kortyx/core");
2336
2456
  var import_errors6 = require("@kortyx/core/errors");
2457
+ var import_zod4 = require("zod");
2337
2458
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2338
2459
  name: tool.name,
2339
2460
  ...tool.title ? { title: tool.title } : {},
@@ -2369,7 +2490,12 @@ var normalizeToolResult = (toolCall, value) => {
2369
2490
  };
2370
2491
  };
2371
2492
  var createInitialMessages = (args) => [
2372
- ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
2493
+ ...typeof args.system === "string" || args.interruptInstructions ? [
2494
+ {
2495
+ role: "system",
2496
+ content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
2497
+ }
2498
+ ] : [],
2373
2499
  { role: "user", content: String(args.input ?? "") }
2374
2500
  ];
2375
2501
  var runReasonToolLoop = async (args) => {
@@ -2384,19 +2510,23 @@ var runReasonToolLoop = async (args) => {
2384
2510
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
2385
2511
  const opId = checkpoint?.opId ?? args.opId;
2386
2512
  const tools2 = useReasonArgs.tools ?? [];
2513
+ const interruptConfig = normalizeReasonInterrupts({
2514
+ interrupt: useReasonArgs.interrupt,
2515
+ interrupts: useReasonArgs.interrupts
2516
+ });
2517
+ const interruptByToolName = /* @__PURE__ */ new Map();
2387
2518
  const toolByName = /* @__PURE__ */ new Map();
2388
2519
  let separateOutput = false;
2389
2520
  for (const tool of tools2) {
2521
+ if (isInterruptControlToolName(tool.name))
2522
+ throw new Error(
2523
+ `useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
2524
+ );
2390
2525
  if (toolByName.has(tool.name)) {
2391
2526
  throw new Error(`useReason received duplicate tool name "${tool.name}".`);
2392
2527
  }
2393
2528
  toolByName.set(tool.name, tool);
2394
2529
  }
2395
- if (useReasonArgs.interrupt) {
2396
- throw new Error(
2397
- "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
2398
- );
2399
- }
2400
2530
  separateOutput = Boolean(
2401
2531
  useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
2402
2532
  ...useReasonArgs.model.options,
@@ -2408,21 +2538,63 @@ var runReasonToolLoop = async (args) => {
2408
2538
  ) ?? {}
2409
2539
  }).requiresSeparateStructuredOutput
2410
2540
  );
2411
- const toolDefinitions = toToolDefinitions(tools2);
2541
+ const interruptDefinitions = [];
2542
+ for (const [name, contract] of Object.entries(
2543
+ interruptConfig?.contracts ?? {}
2544
+ )) {
2545
+ const toolName = interruptControlToolName(name);
2546
+ if (toolByName.has(toolName))
2547
+ throw new Error(
2548
+ `useReason tool name "${toolName}" is reserved for interrupt contracts.`
2549
+ );
2550
+ let inputSchema;
2551
+ try {
2552
+ inputSchema = (0, import_zod4.toJSONSchema)(contract.requestSchema);
2553
+ } catch (error) {
2554
+ throw new Error(
2555
+ `useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
2556
+ { cause: error }
2557
+ );
2558
+ }
2559
+ interruptByToolName.set(toolName, { name, contract });
2560
+ interruptDefinitions.push({
2561
+ name: toolName,
2562
+ title: `Request human input: ${name}`,
2563
+ description: [
2564
+ contract.description,
2565
+ "Call this only when human input is needed. Execution pauses until the validated response is returned as this tool's result."
2566
+ ].join(" "),
2567
+ inputSchema,
2568
+ metadata: {
2569
+ kortyxControl: "interrupt",
2570
+ contract: name,
2571
+ schemaId: contract.schemaId,
2572
+ schemaVersion: contract.schemaVersion
2573
+ }
2574
+ });
2575
+ }
2576
+ const toolDefinitions = [
2577
+ ...toToolDefinitions(tools2),
2578
+ ...interruptDefinitions
2579
+ ];
2412
2580
  let finalizing = checkpoint?.finalizing ?? false;
2413
2581
  let completed = false;
2414
2582
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
2415
2583
  const messages = checkpoint?.messages ?? createInitialMessages({
2416
2584
  system: useReasonArgs.system,
2417
- input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input
2585
+ input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input,
2586
+ ...interruptConfig ? {
2587
+ interruptInstructions: [
2588
+ "Human-input rules:",
2589
+ "- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
2590
+ "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
2591
+ 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."
2592
+ ].join("\n")
2593
+ } : {}
2418
2594
  });
2419
2595
  const steps = checkpoint?.steps ?? [];
2420
- const allToolCalls = steps.flatMap(
2421
- (step) => step.toolCalls
2422
- );
2423
- const allToolResults = steps.flatMap(
2424
- (step) => step.toolResults
2425
- );
2596
+ const allToolCalls = steps.flatMap((step) => step.toolCalls).filter((call) => !interruptByToolName.has(call.name));
2597
+ const allToolResults = steps.flatMap((step) => step.toolResults).filter((result) => !interruptByToolName.has(result.name));
2426
2598
  let finalText = "";
2427
2599
  let finalRaw;
2428
2600
  let finalOutput;
@@ -2438,6 +2610,7 @@ var runReasonToolLoop = async (args) => {
2438
2610
  );
2439
2611
  let pending = checkpoint?.pending;
2440
2612
  const approvedCalls = checkpoint?.approvedCalls ?? [];
2613
+ const interruptHistory = checkpoint?.interruptHistory ?? [];
2441
2614
  const save = () => {
2442
2615
  ctx.currentNodeState.byKey[checkpointKey] = {
2443
2616
  status: "tool_loop",
@@ -2445,6 +2618,7 @@ var runReasonToolLoop = async (args) => {
2445
2618
  messages,
2446
2619
  steps,
2447
2620
  approvedCalls,
2621
+ interruptHistory,
2448
2622
  finalizing,
2449
2623
  ...pending ? { pending } : {}
2450
2624
  };
@@ -2523,6 +2697,10 @@ var runReasonToolLoop = async (args) => {
2523
2697
  ...step.warnings ? { warnings: step.warnings } : {}
2524
2698
  });
2525
2699
  if (toolCalls.length === 0) {
2700
+ if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
2701
+ throw new Error(
2702
+ "useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
2703
+ );
2526
2704
  if (separateOutput && !finalizing) {
2527
2705
  if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
2528
2706
  try {
@@ -2567,7 +2745,9 @@ var runReasonToolLoop = async (args) => {
2567
2745
  "Provider requested a tool during schema-only finalization."
2568
2746
  );
2569
2747
  if (!reused) {
2570
- allToolCalls.push(...toolCalls);
2748
+ allToolCalls.push(
2749
+ ...toolCalls.filter((call) => !interruptByToolName.has(call.name))
2750
+ );
2571
2751
  messages.push({
2572
2752
  role: "assistant",
2573
2753
  content: step.text,
@@ -2577,6 +2757,84 @@ var runReasonToolLoop = async (args) => {
2577
2757
  }
2578
2758
  pending = step;
2579
2759
  save();
2760
+ const interruptCalls = toolCalls.filter(
2761
+ (call) => interruptByToolName.has(call.name)
2762
+ );
2763
+ if (interruptCalls.length > 0) {
2764
+ if (toolCalls.length !== 1)
2765
+ throw new Error(
2766
+ "A useReason interrupt contract call must be the only tool call in its model turn."
2767
+ );
2768
+ const interruptCall = interruptCalls[0];
2769
+ if (!interruptCall)
2770
+ throw new Error("Missing useReason interrupt control call.");
2771
+ if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
2772
+ throw new Error(
2773
+ `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
2774
+ );
2775
+ const resolved = interruptByToolName.get(interruptCall.name);
2776
+ if (!resolved)
2777
+ throw new Error(
2778
+ `Unknown useReason interrupt contract tool "${interruptCall.name}".`
2779
+ );
2780
+ if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
2781
+ pending = void 0;
2782
+ save();
2783
+ continue;
2784
+ }
2785
+ const request = parseWithSchema(
2786
+ resolved.contract.requestSchema,
2787
+ interruptCall.input,
2788
+ `useReason interrupt contract "${resolved.name}" request`
2789
+ );
2790
+ traceSpan?.addEvent?.("useReason.interrupt.requested", {
2791
+ checkpointKey,
2792
+ contract: resolved.name,
2793
+ interruptIndex: interruptHistory.length
2794
+ });
2795
+ const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
2796
+ const response = await awaitInterruptInternal({
2797
+ request: {
2798
+ kind: "custom",
2799
+ request,
2800
+ contract: resolved.name,
2801
+ ...question ? { question } : {},
2802
+ schemaId: resolved.contract.schemaId,
2803
+ schemaVersion: resolved.contract.schemaVersion
2804
+ },
2805
+ responseSchema: resolved.contract.responseSchema,
2806
+ id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
2807
+ meta: {
2808
+ __kortyxReason: {
2809
+ opId,
2810
+ stepIndex,
2811
+ interruptIndex: interruptHistory.length,
2812
+ contract: resolved.name
2813
+ }
2814
+ }
2815
+ });
2816
+ const result2 = normalizeToolResult(interruptCall, response);
2817
+ toolResults.push(result2);
2818
+ messages.push({
2819
+ role: "tool",
2820
+ content: result2.content,
2821
+ toolCallId: result2.toolCallId,
2822
+ name: result2.name
2823
+ });
2824
+ interruptHistory.push({
2825
+ contract: resolved.name,
2826
+ request,
2827
+ response
2828
+ });
2829
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
2830
+ checkpointKey,
2831
+ contract: resolved.name,
2832
+ interruptIndex: interruptHistory.length - 1
2833
+ });
2834
+ pending = void 0;
2835
+ save();
2836
+ continue;
2837
+ }
2580
2838
  for (const toolCall of toolCalls) {
2581
2839
  if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
2582
2840
  await replayToolObservations(
@@ -2786,7 +3044,11 @@ var runReasonToolLoop = async (args) => {
2786
3044
  ...finalOutput !== void 0 ? { output: finalOutput } : {},
2787
3045
  toolCalls: allToolCalls,
2788
3046
  toolResults: allToolResults,
2789
- steps
3047
+ steps,
3048
+ ...interruptHistory.length > 0 ? { interruptHistory } : {},
3049
+ ...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
3050
+ interruptResponse: interruptHistory.at(-1)?.response
3051
+ } : {}
2790
3052
  };
2791
3053
  traceSpan?.end?.({
2792
3054
  ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
@@ -2797,7 +3059,8 @@ var runReasonToolLoop = async (args) => {
2797
3059
  textLength: finalText.length,
2798
3060
  toolCallCount: allToolCalls.length,
2799
3061
  toolResultCount: allToolResults.length,
2800
- toolStepCount: steps.length
3062
+ toolStepCount: steps.length,
3063
+ interruptCount: interruptHistory.length
2801
3064
  },
2802
3065
  telemetry: {
2803
3066
  ...useReasonArgs.telemetry ?? {},
@@ -2827,7 +3090,7 @@ var resolveEffectiveReasoningIncludeThoughts = (args) => args.reasoning?.include
2827
3090
  var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
2828
3091
  var assertReasoningThoughtsCompatibility = (args) => {
2829
3092
  if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
2830
- const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3093
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
2831
3094
  if (!usesStructuredOutput) return;
2832
3095
  throw new Error(
2833
3096
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
@@ -2835,6 +3098,10 @@ var assertReasoningThoughtsCompatibility = (args) => {
2835
3098
  };
2836
3099
  async function runReason(args) {
2837
3100
  assertReasoningThoughtsCompatibility(args);
3101
+ const interruptConfig = normalizeReasonInterrupts({
3102
+ interrupt: args.interrupt,
3103
+ interrupts: args.interrupts
3104
+ });
2838
3105
  const ctx = getHookContext();
2839
3106
  const abortSignal = (0, import_core6.combineAbortSignals)(
2840
3107
  ctx.node.abortSignal,
@@ -2849,20 +3116,23 @@ async function runReason(args) {
2849
3116
  args.outputSchema,
2850
3117
  args.responseFormat ?? args.model.options?.responseFormat
2851
3118
  );
3119
+ const usesLegacyNoToolInterrupt = Boolean(
3120
+ args.interrupt && !args.interrupts && !args.tools?.length
3121
+ );
2852
3122
  args = {
2853
3123
  ...args,
2854
3124
  abortSignal,
2855
- ...!args.interrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
3125
+ ...!usesLegacyNoToolInterrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
2856
3126
  };
2857
3127
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
2858
- const opId = createRuntimeId2();
3128
+ const provisionalOpId = createRuntimeId2();
2859
3129
  const checkpointKey = resolveReasonCheckpointKey({
2860
3130
  ...id ? { id } : {},
2861
3131
  autoIndex: ctx.reasonCallIndex++
2862
3132
  });
2863
- const existingCompleted = readReasonCompletedCheckpoint(
2864
- ctx.currentNodeState.byKey[checkpointKey]
2865
- );
3133
+ const checkpointValue = ctx.currentNodeState.byKey[checkpointKey];
3134
+ const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
3135
+ const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
2866
3136
  if (existingCompleted) {
2867
3137
  await replayToolObservations(
2868
3138
  existingCompleted.result.steps?.flatMap(
@@ -2871,9 +3141,7 @@ async function runReason(args) {
2871
3141
  );
2872
3142
  return existingCompleted.result;
2873
3143
  }
2874
- const existingCheckpoint = readReasonCheckpoint(
2875
- ctx.currentNodeState.byKey[checkpointKey]
2876
- );
3144
+ const existingCheckpoint = readReasonCheckpoint(checkpointValue);
2877
3145
  const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
2878
3146
  return withSafeTraceSpan(
2879
3147
  getReasonTraceAdapter(),
@@ -2892,7 +3160,8 @@ async function runReason(args) {
2892
3160
  ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
2893
3161
  ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
2894
3162
  hasOutputSchema: Boolean(args.outputSchema),
2895
- hasInterrupt: Boolean(args.interrupt),
3163
+ hasInterrupt: Boolean(interruptConfig),
3164
+ interruptContractCount: Object.keys(interruptConfig?.contracts ?? {}).length,
2896
3165
  hasStructured: Boolean(args.structured),
2897
3166
  hasTools: Boolean(args.tools?.length)
2898
3167
  },
@@ -2902,7 +3171,7 @@ async function runReason(args) {
2902
3171
  }
2903
3172
  },
2904
3173
  async (traceSpan) => {
2905
- if (args.tools?.length) {
3174
+ if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
2906
3175
  return runReasonToolLoop({
2907
3176
  useReasonArgs: args,
2908
3177
  allowValidatedToolOutput,
@@ -3171,6 +3440,24 @@ function useReason2(args) {
3171
3440
  return useReason(args);
3172
3441
  }
3173
3442
  function useInterrupt(args) {
3443
+ if ("contract" in args) {
3444
+ const contract = defineInterruptContract(args.contract);
3445
+ return awaitInterruptInternal({
3446
+ request: {
3447
+ kind: "custom",
3448
+ request: parseWithSchema(
3449
+ contract.requestSchema,
3450
+ args.request,
3451
+ "useInterrupt contract request"
3452
+ ),
3453
+ schemaId: contract.schemaId,
3454
+ schemaVersion: contract.schemaVersion,
3455
+ ...args.id ? { id: args.id } : {},
3456
+ ...args.meta ? { meta: args.meta } : {}
3457
+ },
3458
+ responseSchema: contract.responseSchema
3459
+ });
3460
+ }
3174
3461
  return awaitInterruptInternal(args);
3175
3462
  }
3176
3463
  function useAbortSignal() {
@@ -3387,6 +3674,14 @@ function parallel(values) {
3387
3674
  return promise;
3388
3675
  }
3389
3676
 
3677
+ // src/report-error.ts
3678
+ function reportError(error, options = {}) {
3679
+ try {
3680
+ getReasonTraceAdapter()?.reportError?.(error, options);
3681
+ } catch {
3682
+ }
3683
+ }
3684
+
3390
3685
  // src/workflow.ts
3391
3686
  var import_node_crypto6 = require("crypto");
3392
3687
  var import_core8 = require("@kortyx/core");
@@ -3661,8 +3956,10 @@ async function runWorkflowCall(args, task) {
3661
3956
  WorkflowCallError,
3662
3957
  completeResponse,
3663
3958
  createWorkflowHooks,
3959
+ defineInterruptContract,
3664
3960
  emitWorkflowCall,
3665
3961
  parallel,
3962
+ reportError,
3666
3963
  runWithHookContext,
3667
3964
  useAbortSignal,
3668
3965
  useInterrupt,