@kortyx/hooks 0.29.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  completeResponse: () => completeResponse,
25
25
  createWorkflowHooks: () => createWorkflowHooks,
26
26
  defineInterruptContract: () => defineInterruptContract,
27
+ defineOutputContract: () => defineOutputContract,
27
28
  emitWorkflowCall: () => emitWorkflowCall,
28
29
  parallel: () => parallel,
29
30
  reportError: () => reportError,
@@ -441,6 +442,118 @@ var normalizeReasonInterrupts = (args) => {
441
442
  };
442
443
  var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
443
444
 
445
+ // src/output-contract.ts
446
+ var import_zod2 = require("zod");
447
+
448
+ // src/structured-path.ts
449
+ var parseStructuredPath = (path, source) => {
450
+ if (path.length === 0) {
451
+ throw new Error(`${source} path must be a non-empty dot-separated string.`);
452
+ }
453
+ const parts = path.split(".");
454
+ if (parts.some((part) => part.length === 0)) {
455
+ throw new Error(
456
+ `${source} path "${path}" must not contain empty segments.`
457
+ );
458
+ }
459
+ return parts;
460
+ };
461
+ var assertStructuredPath = (path, source) => {
462
+ parseStructuredPath(path, source);
463
+ };
464
+
465
+ // src/output-contract.ts
466
+ var EMIT_PREFIX = "kortyx_emit__";
467
+ var RETURN_PREFIX = "kortyx_return__";
468
+ var STREAM_EMIT_PREFIX = "kortyx_stream_emit__";
469
+ var STREAM_RETURN_PREFIX = "kortyx_stream_return__";
470
+ var legacyOutputWarningEmitted = false;
471
+ var warnLegacyReasonOutput = () => {
472
+ if (legacyOutputWarningEmitted) return;
473
+ legacyOutputWarningEmitted = true;
474
+ process.emitWarning(
475
+ "useReason({ outputSchema, structured }) and result.output are deprecated and will be removed in the next major release. Define reusable contracts with defineOutputContract() and pass them through useReason({ outputs }). See docs/internal/next-major-removals.md.",
476
+ { code: "KORTYX_USE_REASON_OUTPUT_DEPRECATED", type: "DeprecationWarning" }
477
+ );
478
+ };
479
+ var defineOutputContract = (contract) => {
480
+ if (!contract || typeof contract !== "object")
481
+ throw new Error("defineOutputContract requires a contract object.");
482
+ for (const [field, value] of [
483
+ ["description", contract.description],
484
+ ["schemaId", contract.schemaId],
485
+ ["schemaVersion", contract.schemaVersion]
486
+ ])
487
+ if (typeof value !== "string" || value.trim().length === 0)
488
+ throw new Error(`Output contract ${field} must be a non-empty string.`);
489
+ if (typeof contract.schema?.safeParse !== "function")
490
+ throw new Error("Output contract schema must provide safeParse(value).");
491
+ if (contract.stream) {
492
+ if (!contract.stream.fields || Object.keys(contract.stream.fields).length === 0)
493
+ throw new Error("Output contract stream.fields must not be empty.");
494
+ for (const [path, mode] of Object.entries(contract.stream.fields)) {
495
+ assertStructuredPath(path, "Output contract stream.fields");
496
+ if (!["set", "append", "text-delta"].includes(mode))
497
+ throw new Error(`Invalid output contract stream mode for "${path}".`);
498
+ }
499
+ }
500
+ return contract;
501
+ };
502
+ var outputControlToolName = (kind, name) => `${kind === "emit" ? EMIT_PREFIX : kind === "return" ? RETURN_PREFIX : kind === "stream-emit" ? STREAM_EMIT_PREFIX : STREAM_RETURN_PREFIX}${name}`;
503
+ var isOutputControlToolName = (name) => name.startsWith(EMIT_PREFIX) || name.startsWith(RETURN_PREFIX) || name.startsWith(STREAM_EMIT_PREFIX) || name.startsWith(STREAM_RETURN_PREFIX);
504
+ var normalizeReasonOutputs = (config, hasLegacyOutput) => {
505
+ if (!config) return void 0;
506
+ if (hasLegacyOutput)
507
+ throw new Error(
508
+ "useReason cannot combine `outputs` with deprecated `outputSchema` or `structured`."
509
+ );
510
+ const entries = [
511
+ ...Object.entries(config.emit ?? {}).map(([name, contract]) => ({
512
+ name,
513
+ contract,
514
+ kind: "emit"
515
+ })),
516
+ ...Object.entries(config.return ?? {}).map(([name, contract]) => ({
517
+ name,
518
+ contract,
519
+ kind: "return"
520
+ }))
521
+ ];
522
+ if (entries.length === 0)
523
+ throw new Error(
524
+ "useReason outputs must include at least one emit or return contract."
525
+ );
526
+ for (const { name, contract, kind } of entries) {
527
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name) || outputControlToolName(kind, name).length > 64)
528
+ throw new Error(`Invalid useReason output contract name "${name}".`);
529
+ defineOutputContract(contract);
530
+ if (contract.schema["~kortyx"]?.nativeOutput === true)
531
+ throw new Error(
532
+ `useReason output contract "${name}" uses a provider-native output schema that cannot be selected through tools. Continue using outputSchema for this provider until a non-tool contract path is available.`
533
+ );
534
+ if (contract.stream && outputControlToolName(
535
+ kind === "emit" ? "stream-emit" : "stream-return",
536
+ name
537
+ ).length > 64)
538
+ throw new Error(
539
+ `Invalid useReason output contract name "${name}" for streaming tools.`
540
+ );
541
+ try {
542
+ const json = (0, import_zod2.toJSONSchema)(contract.schema);
543
+ if (json.type !== "object")
544
+ throw new Error("The tool input schema must have an object root.");
545
+ } catch (error) {
546
+ throw new Error(
547
+ `useReason output contract "${name}" schema must support JSON Schema conversion.`,
548
+ { cause: error }
549
+ );
550
+ }
551
+ }
552
+ if (config.maxEmissions !== void 0 && (!Number.isInteger(config.maxEmissions) || config.maxEmissions < 1))
553
+ throw new Error("useReason outputs.maxEmissions must be at least 1.");
554
+ return config;
555
+ };
556
+
444
557
  // src/reason/use-reason.ts
445
558
  var import_core6 = require("@kortyx/core");
446
559
 
@@ -526,25 +639,6 @@ async function withSafeTraceSpan(adapter, args, fn) {
526
639
 
527
640
  // src/structured.ts
528
641
  var import_node_crypto = require("crypto");
529
-
530
- // src/structured-path.ts
531
- var parseStructuredPath = (path, source) => {
532
- if (path.length === 0) {
533
- throw new Error(`${source} path must be a non-empty dot-separated string.`);
534
- }
535
- const parts = path.split(".");
536
- if (parts.some((part) => part.length === 0)) {
537
- throw new Error(
538
- `${source} path "${path}" must not contain empty segments.`
539
- );
540
- }
541
- return parts;
542
- };
543
- var assertStructuredPath = (path, source) => {
544
- parseStructuredPath(path, source);
545
- };
546
-
547
- // src/structured.ts
548
642
  var createStructuredStreamId = () => {
549
643
  try {
550
644
  return (0, import_node_crypto.randomUUID)();
@@ -1356,12 +1450,12 @@ async function reasonEngine(args, meta, inputOverride) {
1356
1450
  }
1357
1451
 
1358
1452
  // src/reason/output-format.ts
1359
- var import_zod2 = require("zod");
1453
+ var import_zod3 = require("zod");
1360
1454
  function inferOutputFormat(schema, explicit) {
1361
1455
  if (explicit) return { responseFormat: explicit };
1362
1456
  if (!schema) return {};
1363
1457
  try {
1364
- const json = (0, import_zod2.toJSONSchema)(schema);
1458
+ const json = (0, import_zod3.toJSONSchema)(schema);
1365
1459
  const clean = (value) => {
1366
1460
  if (Array.isArray(value)) return value.map(clean);
1367
1461
  if (!value || typeof value !== "object") return value;
@@ -2116,7 +2210,7 @@ var createTruncatedStructuredOutputError = (label, cause) => new import_errors5.
2116
2210
  );
2117
2211
  var createInvalidStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
2118
2212
  "INVALID_MODEL_JSON",
2119
- `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check outputSchema and provider structured-output settings.`,
2213
+ `${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.`,
2120
2214
  cause
2121
2215
  );
2122
2216
  var parseReasonOutputWithSchema = (args) => {
@@ -2241,7 +2335,7 @@ var parseInterruptFirstPassResult = (args) => {
2241
2335
  };
2242
2336
 
2243
2337
  // src/reason/prompting.ts
2244
- var import_zod3 = require("zod");
2338
+ var import_zod4 = require("zod");
2245
2339
  var tryStringify2 = (value) => {
2246
2340
  if (typeof value === "string") return value;
2247
2341
  try {
@@ -2252,7 +2346,7 @@ var tryStringify2 = (value) => {
2252
2346
  };
2253
2347
  var tryGetSchemaHint = (schema) => {
2254
2348
  try {
2255
- const hint = (0, import_zod3.toJSONSchema)(schema);
2349
+ const hint = (0, import_zod4.toJSONSchema)(schema);
2256
2350
  return JSON.stringify(hint, null, 2);
2257
2351
  } catch {
2258
2352
  return void 0;
@@ -2454,7 +2548,7 @@ var emitReasonStructuredOutput = (args) => {
2454
2548
  // src/reason/tools.ts
2455
2549
  var import_core5 = require("@kortyx/core");
2456
2550
  var import_errors6 = require("@kortyx/core/errors");
2457
- var import_zod4 = require("zod");
2551
+ var import_zod5 = require("zod");
2458
2552
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2459
2553
  name: tool.name,
2460
2554
  ...tool.title ? { title: tool.title } : {},
@@ -2490,10 +2584,14 @@ var normalizeToolResult = (toolCall, value) => {
2490
2584
  };
2491
2585
  };
2492
2586
  var createInitialMessages = (args) => [
2493
- ...typeof args.system === "string" || args.interruptInstructions ? [
2587
+ ...typeof args.system === "string" || args.interruptInstructions || args.outputInstructions ? [
2494
2588
  {
2495
2589
  role: "system",
2496
- content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
2590
+ content: [
2591
+ args.system,
2592
+ args.interruptInstructions,
2593
+ args.outputInstructions
2594
+ ].filter((value) => Boolean(value)).join("\n\n")
2497
2595
  }
2498
2596
  ] : [],
2499
2597
  { role: "user", content: String(args.input ?? "") }
@@ -2515,12 +2613,13 @@ var runReasonToolLoop = async (args) => {
2515
2613
  interrupts: useReasonArgs.interrupts
2516
2614
  });
2517
2615
  const interruptByToolName = /* @__PURE__ */ new Map();
2616
+ const outputByToolName = /* @__PURE__ */ new Map();
2518
2617
  const toolByName = /* @__PURE__ */ new Map();
2519
2618
  let separateOutput = false;
2520
2619
  for (const tool of tools2) {
2521
- if (isInterruptControlToolName(tool.name))
2620
+ if (isInterruptControlToolName(tool.name) || isOutputControlToolName(tool.name))
2522
2621
  throw new Error(
2523
- `useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
2622
+ `useReason tool name "${tool.name}" uses a reserved control-tool namespace.`
2524
2623
  );
2525
2624
  if (toolByName.has(tool.name)) {
2526
2625
  throw new Error(`useReason received duplicate tool name "${tool.name}".`);
@@ -2549,7 +2648,7 @@ var runReasonToolLoop = async (args) => {
2549
2648
  );
2550
2649
  let inputSchema;
2551
2650
  try {
2552
- inputSchema = (0, import_zod4.toJSONSchema)(contract.requestSchema);
2651
+ inputSchema = (0, import_zod5.toJSONSchema)(contract.requestSchema);
2553
2652
  } catch (error) {
2554
2653
  throw new Error(
2555
2654
  `useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
@@ -2573,9 +2672,66 @@ var runReasonToolLoop = async (args) => {
2573
2672
  }
2574
2673
  });
2575
2674
  }
2675
+ const outputDefinitions = [];
2676
+ for (const kind of ["emit", "return"]) {
2677
+ for (const [name, contract] of Object.entries(
2678
+ useReasonArgs.outputs?.[kind] ?? {}
2679
+ )) {
2680
+ if (contract.stream) {
2681
+ const streamName = outputControlToolName(
2682
+ kind === "emit" ? "stream-emit" : "stream-return",
2683
+ name
2684
+ );
2685
+ outputByToolName.set(streamName, {
2686
+ name,
2687
+ kind,
2688
+ contract,
2689
+ stream: true
2690
+ });
2691
+ outputDefinitions.push({
2692
+ name: streamName,
2693
+ title: `Stream structured output: ${name}`,
2694
+ description: `${contract.description} Call this to start realtime generation of this structured value. The value is generated in a separate streamed model pass, validated, and ${kind === "emit" ? "returned as a tool result so you can continue" : "used to finish this operation"}.`,
2695
+ inputSchema: {
2696
+ type: "object",
2697
+ properties: {
2698
+ instruction: {
2699
+ type: "string",
2700
+ description: "What the streamed output should contain."
2701
+ }
2702
+ },
2703
+ required: ["instruction"],
2704
+ additionalProperties: false
2705
+ },
2706
+ metadata: {
2707
+ kortyxControl: kind === "emit" ? "output.stream.emit" : "output.stream.return",
2708
+ contract: name,
2709
+ schemaId: contract.schemaId,
2710
+ schemaVersion: contract.schemaVersion
2711
+ }
2712
+ });
2713
+ } else {
2714
+ const toolName = outputControlToolName(kind, name);
2715
+ outputByToolName.set(toolName, { name, kind, contract, stream: false });
2716
+ outputDefinitions.push({
2717
+ name: toolName,
2718
+ title: `${kind === "emit" ? "Emit" : "Return"} structured output: ${name}`,
2719
+ description: `${contract.description} ${kind === "emit" ? "Emit this structured value and continue reasoning." : "Return this structured value and finish reasoning."}`,
2720
+ inputSchema: (0, import_zod5.toJSONSchema)(contract.schema),
2721
+ metadata: {
2722
+ kortyxControl: kind === "emit" ? "output.emit" : "output.return",
2723
+ contract: name,
2724
+ schemaId: contract.schemaId,
2725
+ schemaVersion: contract.schemaVersion
2726
+ }
2727
+ });
2728
+ }
2729
+ }
2730
+ }
2576
2731
  const toolDefinitions = [
2577
2732
  ...toToolDefinitions(tools2),
2578
- ...interruptDefinitions
2733
+ ...interruptDefinitions,
2734
+ ...outputDefinitions
2579
2735
  ];
2580
2736
  let finalizing = checkpoint?.finalizing ?? false;
2581
2737
  let completed = false;
@@ -2590,11 +2746,27 @@ var runReasonToolLoop = async (args) => {
2590
2746
  "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
2591
2747
  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
2748
  ].join("\n")
2749
+ } : {},
2750
+ ...useReasonArgs.outputs ? {
2751
+ outputInstructions: [
2752
+ "Structured output rules:",
2753
+ "- The kortyx_emit__* tools publish validated structured output and return an acknowledgement. You may continue with text or more tools afterward.",
2754
+ "- The kortyx_return__* tools publish a validated result and end this reasoning operation. If return tools are available, call exactly one before finishing.",
2755
+ "- For contracts with realtime streaming, use kortyx_stream_emit__* or kortyx_stream_return__*. These start a separate streamed JSON generation; its fields become visible as tokens arrive.",
2756
+ "- Call a structured output control tool alone in its model turn."
2757
+ ].join("\n")
2593
2758
  } : {}
2594
2759
  });
2595
2760
  const steps = checkpoint?.steps ?? [];
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));
2761
+ const allToolCalls = steps.flatMap((step) => step.toolCalls).filter(
2762
+ (call) => !interruptByToolName.has(call.name) && !outputByToolName.has(call.name)
2763
+ );
2764
+ const knownToolCallIds = new Set(
2765
+ steps.flatMap((step) => step.toolCalls.map((call) => call.id))
2766
+ );
2767
+ const allToolResults = steps.flatMap((step) => step.toolResults).filter(
2768
+ (result) => !interruptByToolName.has(result.name) && !outputByToolName.has(result.name)
2769
+ );
2598
2770
  let finalText = "";
2599
2771
  let finalRaw;
2600
2772
  let finalOutput;
@@ -2609,8 +2781,11 @@ var runReasonToolLoop = async (args) => {
2609
2781
  args.initialWarnings
2610
2782
  );
2611
2783
  let pending = checkpoint?.pending;
2784
+ let pendingStepIndex = checkpoint?.pendingStepIndex;
2612
2785
  const approvedCalls = checkpoint?.approvedCalls ?? [];
2613
2786
  const interruptHistory = checkpoint?.interruptHistory ?? [];
2787
+ const emissions = checkpoint?.emissions ?? [];
2788
+ let returned = checkpoint?.returned;
2614
2789
  const save = () => {
2615
2790
  ctx.currentNodeState.byKey[checkpointKey] = {
2616
2791
  status: "tool_loop",
@@ -2619,8 +2794,11 @@ var runReasonToolLoop = async (args) => {
2619
2794
  steps,
2620
2795
  approvedCalls,
2621
2796
  interruptHistory,
2797
+ emissions,
2798
+ ...returned ? { returned } : {},
2622
2799
  finalizing,
2623
- ...pending ? { pending } : {}
2800
+ ...pending ? { pending } : {},
2801
+ ...pending && pendingStepIndex !== void 0 ? { pendingStepIndex } : {}
2624
2802
  };
2625
2803
  ctx.stateDirty = true;
2626
2804
  };
@@ -2633,7 +2811,7 @@ var runReasonToolLoop = async (args) => {
2633
2811
  });
2634
2812
  };
2635
2813
  try {
2636
- for (let stepIndex = pending ? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
2814
+ for (let stepIndex = pending ? pendingStepIndex ?? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
2637
2815
  (0, import_core5.throwIfExecutionAborted)(abortSignal);
2638
2816
  traceSpan?.addEvent?.("useReason.tool-step.start", {
2639
2817
  stepIndex,
@@ -2643,6 +2821,7 @@ var runReasonToolLoop = async (args) => {
2643
2821
  const stream = useReasonArgs.stream ?? useReasonArgs.model.options?.streaming ?? true;
2644
2822
  const emit = useReasonArgs.emit ?? true;
2645
2823
  let textStarted = false;
2824
+ const segmentId = `step-${stepIndex}`;
2646
2825
  const structuredChunk = useReasonArgs.outputSchema && (!separateOutput || finalizing) && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
2647
2826
  const step = pending ?? await reasonEngine(
2648
2827
  {
@@ -2656,15 +2835,15 @@ var runReasonToolLoop = async (args) => {
2656
2835
  if (structuredChunk) structuredChunk(delta);
2657
2836
  if (!emit || useReasonArgs.outputSchema || !delta) return;
2658
2837
  if (!textStarted) {
2659
- emitToolEvent("text-start", {});
2838
+ emitToolEvent("text-start", { segmentId });
2660
2839
  textStarted = true;
2661
2840
  }
2662
- emitToolEvent("text-delta", { delta });
2841
+ emitToolEvent("text-delta", { delta, segmentId });
2663
2842
  }
2664
2843
  },
2665
2844
  { ...id ? { id } : {}, opId }
2666
2845
  );
2667
- if (textStarted) emitToolEvent("text-end", {});
2846
+ if (textStarted) emitToolEvent("text-end", { segmentId });
2668
2847
  finalText = step.text;
2669
2848
  finalRaw = step.raw;
2670
2849
  if (!reused) {
@@ -2680,7 +2859,7 @@ var runReasonToolLoop = async (args) => {
2680
2859
  const toolCalls = step.toolCalls ?? [];
2681
2860
  const ids = /* @__PURE__ */ new Set();
2682
2861
  for (const call of toolCalls) {
2683
- if (!call.id || ids.has(call.id) || !reused && allToolCalls.some((prior) => prior.id === call.id))
2862
+ if (!call.id || ids.has(call.id) || !reused && knownToolCallIds.has(call.id))
2684
2863
  throw new Error("Model returned duplicate or empty tool call IDs.");
2685
2864
  ids.add(call.id);
2686
2865
  }
@@ -2696,7 +2875,12 @@ var runReasonToolLoop = async (args) => {
2696
2875
  ...step.providerMetadata ? { providerMetadata: step.providerMetadata } : {},
2697
2876
  ...step.warnings ? { warnings: step.warnings } : {}
2698
2877
  });
2878
+ if (!reused) for (const call of toolCalls) knownToolCallIds.add(call.id);
2699
2879
  if (toolCalls.length === 0) {
2880
+ if (Object.keys(useReasonArgs.outputs?.return ?? {}).length > 0 && !returned)
2881
+ throw new Error(
2882
+ "useReason outputs.return requires one terminal output contract call."
2883
+ );
2700
2884
  if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
2701
2885
  throw new Error(
2702
2886
  "useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
@@ -2734,6 +2918,7 @@ var runReasonToolLoop = async (args) => {
2734
2918
  });
2735
2919
  finalizing = true;
2736
2920
  pending = void 0;
2921
+ pendingStepIndex = void 0;
2737
2922
  save();
2738
2923
  continue;
2739
2924
  }
@@ -2746,7 +2931,9 @@ var runReasonToolLoop = async (args) => {
2746
2931
  );
2747
2932
  if (!reused) {
2748
2933
  allToolCalls.push(
2749
- ...toolCalls.filter((call) => !interruptByToolName.has(call.name))
2934
+ ...toolCalls.filter(
2935
+ (call) => !interruptByToolName.has(call.name) && !outputByToolName.has(call.name)
2936
+ )
2750
2937
  );
2751
2938
  messages.push({
2752
2939
  role: "assistant",
@@ -2756,7 +2943,197 @@ var runReasonToolLoop = async (args) => {
2756
2943
  });
2757
2944
  }
2758
2945
  pending = step;
2946
+ pendingStepIndex = stepIndex;
2759
2947
  save();
2948
+ const outputCalls = toolCalls.filter(
2949
+ (call) => outputByToolName.has(call.name)
2950
+ );
2951
+ if (outputCalls.length > 0) {
2952
+ if (toolCalls.length !== 1)
2953
+ throw new Error(
2954
+ "A useReason output contract call must be the only tool call in its model turn."
2955
+ );
2956
+ const call = outputCalls[0];
2957
+ if (!call) throw new Error("Missing useReason output control call.");
2958
+ const resolved = outputByToolName.get(call.name);
2959
+ if (!resolved)
2960
+ throw new Error(
2961
+ `Unknown useReason output contract tool "${call.name}".`
2962
+ );
2963
+ if (toolResults.some((result3) => result3.toolCallId === call.id)) {
2964
+ pending = void 0;
2965
+ pendingStepIndex = void 0;
2966
+ save();
2967
+ continue;
2968
+ }
2969
+ if (resolved.kind === "emit" && emissions.length >= (useReasonArgs.outputs?.maxEmissions ?? 16))
2970
+ throw new Error(
2971
+ `useReason reached outputs.maxEmissions (${useReasonArgs.outputs?.maxEmissions ?? 16}).`
2972
+ );
2973
+ let data;
2974
+ if (resolved.stream) {
2975
+ if (!stream)
2976
+ throw new Error(
2977
+ `useReason streaming output contract "${resolved.name}" requires stream: true.`
2978
+ );
2979
+ if (stepIndex + 1 >= maxSteps)
2980
+ throw new Error(
2981
+ `useReason output streaming requires another model pass within maxSteps (${maxSteps}).`
2982
+ );
2983
+ const instruction = call.input && typeof call.input === "object" && !Array.isArray(call.input) ? call.input.instruction : void 0;
2984
+ if (typeof instruction !== "string" || instruction.trim().length === 0)
2985
+ throw new Error(
2986
+ `useReason streaming output contract "${resolved.name}" requires an instruction.`
2987
+ );
2988
+ const streamId = `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissions.length : "return"}`;
2989
+ const structured = {
2990
+ dataType: resolved.contract.schemaId,
2991
+ schemaId: resolved.contract.schemaId,
2992
+ schemaVersion: resolved.contract.schemaVersion,
2993
+ fields: resolved.contract.stream?.fields
2994
+ };
2995
+ const inferred = inferOutputFormat(
2996
+ resolved.contract.schema,
2997
+ void 0
2998
+ );
2999
+ try {
3000
+ const outputStep = await reasonEngine(
3001
+ {
3002
+ ...useReasonArgs,
3003
+ tools: [],
3004
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3005
+ messages: [
3006
+ ...messages,
3007
+ {
3008
+ role: "tool",
3009
+ toolCallId: call.id,
3010
+ name: call.name,
3011
+ content: "Generating the requested structured output."
3012
+ },
3013
+ {
3014
+ role: "user",
3015
+ content: withOutputGuardrails(
3016
+ instruction,
3017
+ resolved.contract.schema
3018
+ )
3019
+ }
3020
+ ],
3021
+ emit: false,
3022
+ stream,
3023
+ ...emit && stream ? {
3024
+ onTextChunk: createStructuredOutputStreamer(
3025
+ { structured },
3026
+ id,
3027
+ streamId
3028
+ )
3029
+ } : {}
3030
+ },
3031
+ { ...id ? { id } : {}, opId }
3032
+ );
3033
+ if (outputStep.toolCalls?.length)
3034
+ throw new Error(
3035
+ `Provider requested a tool during streamed output contract "${resolved.name}".`
3036
+ );
3037
+ aggregatedUsage = mergeUsage(aggregatedUsage, outputStep.usage);
3038
+ accumulateTokenUsage(outputStep.usage);
3039
+ aggregatedWarnings = mergeWarnings(
3040
+ aggregatedWarnings,
3041
+ mergeWarnings(inferred.warnings, outputStep.warnings)
3042
+ );
3043
+ aggregatedProviderMetadata = mergeProviderMetadata(
3044
+ aggregatedProviderMetadata,
3045
+ outputStep.providerMetadata
3046
+ );
3047
+ finalFinishReason = outputStep.finishReason;
3048
+ finalRaw = outputStep.raw;
3049
+ data = parseReasonOutputWithSchema({
3050
+ text: outputStep.text,
3051
+ schema: resolved.contract.schema,
3052
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3053
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3054
+ label: `useReason output contract "${resolved.name}"`
3055
+ });
3056
+ steps.push({
3057
+ stepIndex: stepIndex + 1,
3058
+ kind: "output",
3059
+ text: outputStep.text,
3060
+ toolCalls: [],
3061
+ toolResults: [],
3062
+ ...outputStep.usage ? { usage: outputStep.usage } : {},
3063
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3064
+ ...outputStep.providerMetadata ? { providerMetadata: outputStep.providerMetadata } : {},
3065
+ ...outputStep.warnings ? { warnings: outputStep.warnings } : {}
3066
+ });
3067
+ stepIndex += 1;
3068
+ } catch (error) {
3069
+ traceSpan?.addEvent?.("useReason.output.invalidated", {
3070
+ contract: resolved.name,
3071
+ kind: resolved.kind,
3072
+ schemaId: resolved.contract.schemaId,
3073
+ schemaVersion: resolved.contract.schemaVersion,
3074
+ streamId
3075
+ });
3076
+ if (emit) {
3077
+ try {
3078
+ ctx.node.emit("structured_data_invalidated", {
3079
+ node: ctx.node.graph.node,
3080
+ streamId
3081
+ });
3082
+ } catch {
3083
+ }
3084
+ }
3085
+ throw error;
3086
+ }
3087
+ } else {
3088
+ data = parseWithSchema(
3089
+ resolved.contract.schema,
3090
+ call.input,
3091
+ `useReason output contract "${resolved.name}"`
3092
+ );
3093
+ }
3094
+ const entry = { contract: resolved.name, data };
3095
+ const emissionIndex = emissions.length;
3096
+ if (resolved.kind === "emit")
3097
+ emissions.push(entry);
3098
+ else returned = entry;
3099
+ if (emit)
3100
+ emitStructuredData({
3101
+ kind: "final",
3102
+ data,
3103
+ dataType: resolved.contract.schemaId,
3104
+ schemaId: resolved.contract.schemaId,
3105
+ schemaVersion: resolved.contract.schemaVersion,
3106
+ streamId: `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissionIndex : "return"}`,
3107
+ ...id ? { id } : {}
3108
+ });
3109
+ traceSpan?.addEvent?.("useReason.output.emitted", {
3110
+ contract: resolved.name,
3111
+ kind: resolved.kind,
3112
+ schemaId: resolved.contract.schemaId,
3113
+ schemaVersion: resolved.contract.schemaVersion,
3114
+ emissionIndex,
3115
+ data
3116
+ });
3117
+ if (resolved.kind === "return") {
3118
+ pending = void 0;
3119
+ pendingStepIndex = void 0;
3120
+ completed = true;
3121
+ save();
3122
+ break;
3123
+ }
3124
+ const result2 = normalizeToolResult(call, { emitted: true, data });
3125
+ toolResults.push(result2);
3126
+ messages.push({
3127
+ role: "tool",
3128
+ content: result2.content,
3129
+ toolCallId: result2.toolCallId,
3130
+ name: result2.name
3131
+ });
3132
+ pending = void 0;
3133
+ pendingStepIndex = void 0;
3134
+ save();
3135
+ continue;
3136
+ }
2760
3137
  const interruptCalls = toolCalls.filter(
2761
3138
  (call) => interruptByToolName.has(call.name)
2762
3139
  );
@@ -2779,6 +3156,7 @@ var runReasonToolLoop = async (args) => {
2779
3156
  );
2780
3157
  if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
2781
3158
  pending = void 0;
3159
+ pendingStepIndex = void 0;
2782
3160
  save();
2783
3161
  continue;
2784
3162
  }
@@ -2832,6 +3210,7 @@ var runReasonToolLoop = async (args) => {
2832
3210
  interruptIndex: interruptHistory.length - 1
2833
3211
  });
2834
3212
  pending = void 0;
3213
+ pendingStepIndex = void 0;
2835
3214
  save();
2836
3215
  continue;
2837
3216
  }
@@ -3007,6 +3386,7 @@ var runReasonToolLoop = async (args) => {
3007
3386
  save();
3008
3387
  }
3009
3388
  pending = void 0;
3389
+ pendingStepIndex = void 0;
3010
3390
  save();
3011
3391
  }
3012
3392
  if (!completed) {
@@ -3035,7 +3415,7 @@ var runReasonToolLoop = async (args) => {
3035
3415
  const result = {
3036
3416
  ...id ? { id } : {},
3037
3417
  opId,
3038
- text: finalText,
3418
+ text: useReasonArgs.outputs ? steps.filter((step) => step.kind !== "output").map((step) => step.text).filter(Boolean).join("\n") : finalText,
3039
3419
  ...finalRaw !== void 0 ? { raw: finalRaw } : {},
3040
3420
  ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3041
3421
  ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
@@ -3045,6 +3425,8 @@ var runReasonToolLoop = async (args) => {
3045
3425
  toolCalls: allToolCalls,
3046
3426
  toolResults: allToolResults,
3047
3427
  steps,
3428
+ ...emissions.length ? { emissions } : {},
3429
+ ...returned ? { returned } : {},
3048
3430
  ...interruptHistory.length > 0 ? { interruptHistory } : {},
3049
3431
  ...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
3050
3432
  interruptResponse: interruptHistory.at(-1)?.response
@@ -3060,11 +3442,13 @@ var runReasonToolLoop = async (args) => {
3060
3442
  toolCallCount: allToolCalls.length,
3061
3443
  toolResultCount: allToolResults.length,
3062
3444
  toolStepCount: steps.length,
3063
- interruptCount: interruptHistory.length
3445
+ interruptCount: interruptHistory.length,
3446
+ emissionCount: emissions.length,
3447
+ ...returned ? { returnContract: returned.contract } : {}
3064
3448
  },
3065
3449
  telemetry: {
3066
3450
  ...useReasonArgs.telemetry ?? {},
3067
- output: useReasonArgs.telemetry?.output ?? finalOutput ?? finalText
3451
+ output: useReasonArgs.telemetry?.output ?? returned?.data ?? finalOutput ?? finalText
3068
3452
  }
3069
3453
  });
3070
3454
  ctx.currentNodeState.byKey[checkpointKey] = { status: "completed", result };
@@ -3091,7 +3475,7 @@ var resolveEffectiveReasoningIncludeThoughts = (args) => args.reasoning?.include
3091
3475
  var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
3092
3476
  var assertReasoningThoughtsCompatibility = (args) => {
3093
3477
  if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
3094
- const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3478
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || Boolean(args.outputs) || resolveEffectiveResponseFormatType(args) === "json";
3095
3479
  if (!usesStructuredOutput) return;
3096
3480
  throw new Error(
3097
3481
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
@@ -3099,6 +3483,15 @@ var assertReasoningThoughtsCompatibility = (args) => {
3099
3483
  };
3100
3484
  async function runReason(args) {
3101
3485
  assertReasoningThoughtsCompatibility(args);
3486
+ normalizeReasonOutputs(
3487
+ args.outputs,
3488
+ Boolean(args.outputSchema || args.structured)
3489
+ );
3490
+ if (args.outputSchema || args.structured) warnLegacyReasonOutput();
3491
+ if (args.outputs && resolveEffectiveResponseFormatType(args) === "json")
3492
+ throw new Error(
3493
+ "useReason output contracts use tool calls and cannot be combined with JSON responseFormat."
3494
+ );
3102
3495
  const interruptConfig = normalizeReasonInterrupts({
3103
3496
  interrupt: args.interrupt,
3104
3497
  interrupts: args.interrupts
@@ -3172,7 +3565,7 @@ async function runReason(args) {
3172
3565
  }
3173
3566
  },
3174
3567
  async (traceSpan) => {
3175
- if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
3568
+ if ((args.tools?.length || args.interrupts || args.outputs) && !existingCheckpoint) {
3176
3569
  return runReasonToolLoop({
3177
3570
  useReasonArgs: args,
3178
3571
  allowValidatedToolOutput,
@@ -3435,6 +3828,53 @@ async function useReason(args) {
3435
3828
 
3436
3829
  // src/hooks.ts
3437
3830
  function useStructuredData(args) {
3831
+ if ("contract" in args) {
3832
+ const contract = defineOutputContract(args.contract);
3833
+ const base = {
3834
+ dataType: contract.schemaId,
3835
+ schemaId: contract.schemaId,
3836
+ schemaVersion: contract.schemaVersion,
3837
+ ...args.id ? { id: args.id } : {},
3838
+ ...args.streamId ? { streamId: args.streamId } : {}
3839
+ };
3840
+ if (args.kind === "set" || args.kind === "append" || args.kind === "text-delta") {
3841
+ if (contract.stream?.fields[args.path] !== args.kind)
3842
+ throw new Error(
3843
+ `Output contract does not allow ${args.kind} at "${args.path}".`
3844
+ );
3845
+ if (args.kind === "set")
3846
+ emitStructuredData({
3847
+ ...base,
3848
+ kind: "set",
3849
+ path: args.path,
3850
+ value: args.value,
3851
+ ...args.valueSchema ? { valueSchema: args.valueSchema } : {}
3852
+ });
3853
+ else if (args.kind === "append")
3854
+ emitStructuredData({
3855
+ ...base,
3856
+ kind: "append",
3857
+ path: args.path,
3858
+ items: args.items,
3859
+ ...args.itemSchema ? { itemSchema: args.itemSchema } : {}
3860
+ });
3861
+ else
3862
+ emitStructuredData({
3863
+ ...base,
3864
+ kind: "text-delta",
3865
+ path: args.path,
3866
+ delta: args.delta
3867
+ });
3868
+ return;
3869
+ }
3870
+ emitStructuredData({
3871
+ ...base,
3872
+ kind: "final",
3873
+ data: args.data,
3874
+ dataSchema: contract.schema
3875
+ });
3876
+ return;
3877
+ }
3438
3878
  emitStructuredData(args);
3439
3879
  }
3440
3880
  function useReason2(args) {
@@ -3958,6 +4398,7 @@ async function runWorkflowCall(args, task) {
3958
4398
  completeResponse,
3959
4399
  createWorkflowHooks,
3960
4400
  defineInterruptContract,
4401
+ defineOutputContract,
3961
4402
  emitWorkflowCall,
3962
4403
  parallel,
3963
4404
  reportError,