@kortyx/hooks 0.22.0 → 0.23.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
@@ -129,7 +129,7 @@ var toTokenUsageDelta = (usage) => {
129
129
  if (!usage) return void 0;
130
130
  const input = usage.input ?? 0;
131
131
  const output = usage.output ?? 0;
132
- const total = usage.total ?? input + output + (usage.reasoning ?? 0);
132
+ const total = usage.total ?? input + output + (usage.outputIncludesReasoning ? 0 : usage.reasoning ?? 0);
133
133
  if (input === 0 && output === 0 && total === 0) {
134
134
  return void 0;
135
135
  }
@@ -426,8 +426,18 @@ var emitNodeEvent = (emitEvent, nodeId, event, payload) => {
426
426
  ...typeof nodeId === "string" && nodeId.length > 0 ? { node: nodeId } : {}
427
427
  });
428
428
  };
429
+ var resolveProviderOptions = (defaults, overrides) => {
430
+ const providerOptions = defaults || overrides ? { ...defaults, ...overrides } : void 0;
431
+ for (const [key, value] of Object.entries(overrides ?? {})) {
432
+ const previous = defaults?.[key];
433
+ if (previous && value && typeof previous === "object" && typeof value === "object" && !Array.isArray(previous) && !Array.isArray(value)) {
434
+ if (providerOptions) providerOptions[key] = { ...previous, ...value };
435
+ }
436
+ }
437
+ return providerOptions;
438
+ };
429
439
  async function runReasonEngine(args) {
430
- const stream = args.stream ?? args.model.options?.streaming ?? true;
440
+ let stream = args.stream ?? args.model.options?.streaming ?? true;
431
441
  const emit = args.emit ?? true;
432
442
  const temperature = args.temperature ?? args.model.options?.temperature ?? args.defaultTemperature;
433
443
  const maxOutputTokens = args.maxOutputTokens ?? args.model.options?.maxOutputTokens;
@@ -435,7 +445,10 @@ async function runReasonEngine(args) {
435
445
  const abortSignal = args.abortSignal ?? args.model.options?.abortSignal;
436
446
  const reasoning = args.reasoning ?? args.model.options?.reasoning;
437
447
  const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
438
- const providerOptions = args.providerOptions ?? args.model.options?.providerOptions;
448
+ const providerOptions = resolveProviderOptions(
449
+ args.model.options?.providerOptions,
450
+ args.providerOptions
451
+ );
439
452
  const tools = args.tools ?? args.model.options?.tools;
440
453
  (0, import_core2.throwIfExecutionAborted)(abortSignal);
441
454
  const model = args.model.provider.getModel(args.model.modelId, {
@@ -449,6 +462,7 @@ async function runReasonEngine(args) {
449
462
  ...tools !== void 0 ? { tools } : {},
450
463
  ...providerOptions !== void 0 ? { providerOptions } : {}
451
464
  });
465
+ if (tools?.length && !model.supportsToolStreaming) stream = false;
452
466
  const messages = args.messages !== void 0 ? args.messages : [
453
467
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
454
468
  { role: "user", content: String(args.input ?? "") }
@@ -476,7 +490,9 @@ async function runReasonEngine(args) {
476
490
  },
477
491
  telemetry: {
478
492
  ...args.telemetry ?? {},
479
- input: args.telemetry?.input ?? messages
493
+ input: args.telemetry?.input ?? messages.map(
494
+ ({ continuation: _continuation, raw: _raw, ...message }) => message
495
+ )
480
496
  }
481
497
  });
482
498
  const modelRequestStartedAt = Date.now();
@@ -507,7 +523,11 @@ async function runReasonEngine(args) {
507
523
  });
508
524
  };
509
525
  const failTrace = (span, error) => {
526
+ const detail = error;
510
527
  span?.fail?.(error, {
528
+ ...detail?.usage ? { usage: detail.usage } : {},
529
+ ...detail?.finishReason ? { finishReason: detail.finishReason } : {},
530
+ ...detail?.providerMetadata ? { providerMetadata: detail.providerMetadata } : {},
511
531
  attributes: {
512
532
  providerId: args.model.provider.id,
513
533
  modelId: args.model.modelId
@@ -516,6 +536,7 @@ async function runReasonEngine(args) {
516
536
  };
517
537
  try {
518
538
  if (stream) {
539
+ let continuation;
519
540
  let final = "";
520
541
  let raw;
521
542
  let usage;
@@ -554,6 +575,7 @@ async function runReasonEngine(args) {
554
575
  break;
555
576
  }
556
577
  case "finish": {
578
+ continuation = chunk.continuation;
557
579
  if (chunk.raw !== void 0) {
558
580
  raw = chunk.raw;
559
581
  }
@@ -602,6 +624,7 @@ async function runReasonEngine(args) {
602
624
  }
603
625
  const result2 = {
604
626
  text: final,
627
+ ...continuation ? { continuation } : {},
605
628
  ...raw !== void 0 ? { raw } : {},
606
629
  ...usage !== void 0 ? { usage } : {},
607
630
  ...finishReason !== void 0 ? { finishReason } : {},
@@ -615,6 +638,7 @@ async function runReasonEngine(args) {
615
638
  }
616
639
  const response = await model.invoke(messages);
617
640
  (0, import_core2.throwIfExecutionAborted)(abortSignal);
641
+ if (response.content) args.onTextChunk?.(response.content);
618
642
  if (emit) {
619
643
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
620
644
  ...commonMeta
@@ -631,6 +655,7 @@ async function runReasonEngine(args) {
631
655
  }
632
656
  const result = {
633
657
  text: response.content,
658
+ ...response.continuation ? { continuation: response.continuation } : {},
634
659
  ...response.raw !== void 0 ? { raw: response.raw } : {},
635
660
  ...response.usage !== void 0 ? { usage: response.usage } : {},
636
661
  ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
@@ -664,480 +689,188 @@ async function reasonEngine(args, meta, inputOverride) {
664
689
  );
665
690
  (0, import_core3.throwIfExecutionAborted)(abortSignal);
666
691
  ctx.node.consumeExecution?.("maxModelPasses");
667
- return runReasonEngine({
668
- model: args.model,
669
- input: inputOverride ?? args.input,
670
- system: args.system,
671
- temperature: args.temperature,
672
- maxOutputTokens: args.maxOutputTokens,
673
- stopSequences: args.stopSequences,
674
- abortSignal,
675
- reasoning: args.reasoning,
676
- responseFormat: args.responseFormat,
677
- providerOptions: args.providerOptions,
678
- tools: args.tools,
679
- messages: args.messages,
680
- defaultTemperature: ctx.node.config?.model?.temperature,
681
- stream: args.stream,
682
- emit: args.emit,
683
- onTextChunk: args.onTextChunk,
684
- telemetry: args.telemetry,
685
- nodeId: ctx.node.graph.node,
686
- emitEvent: ctx.node.emit,
687
- reasonTrace: ctx.reasonTrace,
688
- id: meta.id,
689
- opId: meta.opId
690
- });
692
+ try {
693
+ return await runReasonEngine({
694
+ model: args.model,
695
+ input: inputOverride ?? args.input,
696
+ system: args.system,
697
+ temperature: args.temperature,
698
+ maxOutputTokens: args.maxOutputTokens,
699
+ stopSequences: args.stopSequences,
700
+ abortSignal,
701
+ reasoning: args.reasoning,
702
+ responseFormat: args.responseFormat,
703
+ providerOptions: args.providerOptions,
704
+ tools: args.tools,
705
+ messages: args.messages,
706
+ defaultTemperature: ctx.node.config?.model?.temperature,
707
+ stream: args.stream,
708
+ emit: args.emit,
709
+ onTextChunk: args.onTextChunk,
710
+ telemetry: args.telemetry,
711
+ nodeId: ctx.node.graph.node,
712
+ emitEvent: ctx.node.emit,
713
+ reasonTrace: ctx.reasonTrace,
714
+ id: meta.id,
715
+ opId: meta.opId
716
+ });
717
+ } catch (error) {
718
+ const usage = error?.usage;
719
+ if (usage) accumulateTokenUsage(usage);
720
+ throw error;
721
+ }
691
722
  }
692
723
 
693
- // src/reason/parsing.ts
694
- var tryJsonParse = (value) => {
724
+ // src/reason/output-format.ts
725
+ var import_zod2 = require("zod");
726
+ function inferOutputFormat(schema, explicit) {
727
+ if (explicit) return { responseFormat: explicit };
728
+ if (!schema) return {};
695
729
  try {
696
- return { ok: true, value: JSON.parse(value) };
697
- } catch (error) {
730
+ const json = (0, import_zod2.toJSONSchema)(schema);
731
+ const clean = (value) => {
732
+ if (Array.isArray(value)) return value.map(clean);
733
+ if (!value || typeof value !== "object") return value;
734
+ const object = value;
735
+ if ([
736
+ "allOf",
737
+ "not",
738
+ "dependentRequired",
739
+ "dependentSchemas",
740
+ "if",
741
+ "then",
742
+ "else",
743
+ "prefixItems"
744
+ ].some((key) => key in object))
745
+ throw new Error(
746
+ "Schema composition requires an explicit provider schema"
747
+ );
748
+ if (object.type === "object") {
749
+ if (object.additionalProperties !== false)
750
+ throw new Error(
751
+ "Dynamic properties require an explicit provider schema"
752
+ );
753
+ const properties = object.properties;
754
+ const required = object.required;
755
+ if (properties && Object.keys(properties).some((key) => !required?.includes(key))) {
756
+ throw new Error(
757
+ "Optional properties require an explicit provider schema"
758
+ );
759
+ }
760
+ }
761
+ return Object.fromEntries(
762
+ Object.entries(object).filter(([key]) => key !== "$schema" && key !== "default").map(([key, child]) => {
763
+ if ((key === "properties" || key === "$defs" || key === "definitions") && child && typeof child === "object")
764
+ return [
765
+ key,
766
+ Object.fromEntries(
767
+ Object.entries(child).map(([name, definition]) => [
768
+ name,
769
+ clean(definition)
770
+ ])
771
+ )
772
+ ];
773
+ return [
774
+ key,
775
+ key === "enum" || key === "const" ? child : clean(child)
776
+ ];
777
+ })
778
+ );
779
+ };
780
+ if (json.type !== "object")
781
+ throw new Error("The provider schema must have an object root");
782
+ return { responseFormat: { type: "json", schema: clean(json) } };
783
+ } catch {
698
784
  return {
699
- ok: false,
700
- error: error instanceof Error ? error.message : String(error)
785
+ responseFormat: { type: "json" },
786
+ warnings: [
787
+ {
788
+ type: "compatibility",
789
+ feature: "outputSchema",
790
+ details: "This validator cannot be inferred as a strict provider JSON schema. Kortyx still validates the final output locally. Supply responseFormat.schema to enforce a wire schema."
791
+ }
792
+ ]
701
793
  };
702
794
  }
703
- };
704
- var extractJsonCodeBlock = (value) => {
705
- const match = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
706
- if (!match || typeof match[1] !== "string") return null;
707
- const inner = match[1].trim();
708
- return inner.length > 0 ? inner : null;
709
- };
710
- var tryStringify = (value) => {
711
- if (typeof value === "string") return value;
712
- try {
713
- return JSON.stringify(value, null, 2);
714
- } catch {
715
- return String(value);
795
+ }
796
+
797
+ // src/reason/structured-stream.ts
798
+ var resolveAppendFieldPaths = (cfg) => {
799
+ const fields = cfg?.fields;
800
+ if (!fields) return [];
801
+ const appendPaths = Object.entries(fields).filter(([, mode]) => mode === "append").map(([path]) => path);
802
+ for (const path of appendPaths) {
803
+ assertStructuredPath(path, "useReason structured append");
716
804
  }
805
+ return appendPaths;
717
806
  };
718
- var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
719
- var resolveOutputCandidate = (text) => {
720
- const trimmed = text.trim();
721
- if (!trimmed) return text;
722
- const direct = tryJsonParse(trimmed);
723
- if (direct.ok) return direct.value;
724
- const block = extractJsonCodeBlock(trimmed);
725
- if (block) {
726
- const parsedBlock = tryJsonParse(block);
727
- if (parsedBlock.ok) return parsedBlock.value;
807
+ var resolveSetFieldPaths = (cfg) => {
808
+ const fields = cfg?.fields;
809
+ if (!fields) return [];
810
+ const setPaths = Object.entries(fields).filter(([, mode]) => mode === "set").map(([path]) => path);
811
+ for (const path of setPaths) {
812
+ assertStructuredPath(path, "useReason structured set");
728
813
  }
729
- return text;
730
- };
731
- var looksLikeStructuredJsonAttempt = (value) => {
732
- const trimmed = value.trim();
733
- if (!trimmed) return false;
734
- return trimmed.startsWith("{") || trimmed.startsWith("[") || /^```(?:json)?/i.test(trimmed);
735
- };
736
- var isLikelyTruncatedStructuredOutput = (text) => {
737
- const trimmed = text.trim();
738
- if (!looksLikeStructuredJsonAttempt(trimmed)) return false;
739
- const direct = tryJsonParse(trimmed);
740
- if (!direct.ok && direct.error) return true;
741
- const block = extractJsonCodeBlock(trimmed);
742
- if (!block) return false;
743
- const parsedBlock = tryJsonParse(block);
744
- return !parsedBlock.ok && Boolean(parsedBlock.error);
814
+ return setPaths;
745
815
  };
746
- var createTruncatedStructuredOutputError = (label) => new Error(
747
- `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`
748
- );
749
- var createInvalidStructuredOutputError = (label) => new Error(
750
- `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. This often happens when output is truncated or the provider does not follow JSON mode. Increase maxOutputTokens or simplify the requested output.`
751
- );
752
- var parseReasonOutputWithSchema = (args) => {
753
- const candidate = resolveOutputCandidate(args.text);
754
- try {
755
- return parseWithSchema(args.schema, candidate, args.label);
756
- } catch (error) {
757
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
758
- throw createTruncatedStructuredOutputError(args.label);
759
- }
760
- if (typeof candidate === "string" && candidate.trim().length > 0) {
761
- throw createInvalidStructuredOutputError(args.label);
762
- }
763
- throw error;
816
+ var resolveTextDeltaFieldPaths = (cfg) => {
817
+ const fields = cfg?.fields;
818
+ if (!fields) return [];
819
+ const textPaths = Object.entries(fields).filter(([, mode]) => mode === "text-delta").map(([path]) => path);
820
+ for (const path of textPaths) {
821
+ assertStructuredPath(path, "useReason structured text-delta");
764
822
  }
823
+ return textPaths;
765
824
  };
766
- var parseInterruptFirstPassResult = (args) => {
767
- const candidate = resolveOutputCandidate(args.text);
768
- if (!isRecord2(candidate)) {
769
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
770
- throw createTruncatedStructuredOutputError("useReason output");
825
+ var extractCompletedStringValueAt = (args) => {
826
+ let value = "";
827
+ for (let i = args.start; i < args.text.length; i += 1) {
828
+ const ch = args.text[i];
829
+ if (ch === void 0) break;
830
+ if (ch === '"') {
831
+ return {
832
+ complete: true,
833
+ value
834
+ };
771
835
  }
772
- if (typeof candidate === "string" && candidate.trim().length > 0) {
773
- throw createInvalidStructuredOutputError("useReason output");
836
+ if (ch !== "\\") {
837
+ value += ch;
838
+ continue;
774
839
  }
775
- throw new Error(
776
- "useReason first pass with interrupt must return a JSON object."
777
- );
778
- }
779
- const mode = args.mode ?? "required";
780
- let interruptRequired = true;
781
- if (mode === "optional") {
782
- if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
783
- throw new Error(
784
- 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
785
- );
840
+ const next = args.text[i + 1];
841
+ if (!next) {
842
+ return {
843
+ complete: false
844
+ };
786
845
  }
787
- interruptRequired = candidate.decision === "interrupt";
788
- if (!interruptRequired) {
789
- const hasUnexpectedRequest = [
790
- candidate.interruptRequest,
791
- candidate.request,
792
- candidate.interrupt
793
- ].some((value) => value !== null && value !== void 0);
794
- if (hasUnexpectedRequest) {
795
- throw new Error(
796
- 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
797
- );
798
- }
846
+ if (next === '"' || next === "\\" || next === "/" || next === "b" || next === "f" || next === "n" || next === "r" || next === "t") {
847
+ value += next === "b" ? "\b" : next === "f" ? "\f" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : next;
848
+ i += 1;
849
+ continue;
799
850
  }
800
- }
801
- let request;
802
- if (interruptRequired) {
803
- const requestCandidates = [
804
- candidate.interruptRequest,
805
- candidate.request,
806
- candidate.interrupt,
807
- mode === "required" ? candidate : void 0
808
- ];
809
- const requestPayload = requestCandidates.find(
810
- (value) => value !== null && value !== void 0
811
- );
812
- request = parseWithSchema(
813
- args.requestSchema,
814
- requestPayload,
815
- "useReason interrupt.request"
816
- );
817
- }
818
- let output;
819
- if (args.outputSchema) {
820
- const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
821
- try {
822
- output = parseWithSchema(
823
- args.outputSchema,
824
- outputPayload,
825
- "useReason output"
826
- );
827
- } catch (error) {
828
- if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
829
- throw createTruncatedStructuredOutputError("useReason output");
830
- }
831
- if (typeof outputPayload === "string" && outputPayload.trim().length > 0) {
832
- throw createInvalidStructuredOutputError("useReason output");
851
+ if (next === "u") {
852
+ const hex = args.text.slice(i + 2, i + 6);
853
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
854
+ return {
855
+ complete: false
856
+ };
833
857
  }
834
- throw error;
858
+ value += String.fromCharCode(Number.parseInt(hex, 16));
859
+ i += 5;
860
+ continue;
835
861
  }
836
- }
837
- const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
838
- if (!args.outputSchema && !explicitDraft) {
839
- throw new Error(
840
- "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
841
- );
842
- }
843
- const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
844
- if (!interruptRequired) {
845
862
  return {
846
- draftText,
847
- interruptRequired: false,
848
- ...output !== void 0 ? { output } : {}
863
+ complete: false
849
864
  };
850
865
  }
851
866
  return {
852
- draftText,
853
- interruptRequired: true,
854
- request,
855
- ...output !== void 0 ? { output } : {}
867
+ complete: false
856
868
  };
857
869
  };
858
-
859
- // src/reason/prompting.ts
860
- var import_zod2 = require("zod");
861
- var tryStringify2 = (value) => {
862
- if (typeof value === "string") return value;
863
- try {
864
- return JSON.stringify(value, null, 2);
865
- } catch {
866
- return String(value);
867
- }
868
- };
869
- var tryGetSchemaHint = (schema) => {
870
- try {
871
- const hint = (0, import_zod2.toJSONSchema)(schema);
872
- return JSON.stringify(hint, null, 2);
873
- } catch {
874
- return void 0;
875
- }
876
- };
877
- var withSchemaHint = (input, label, schema) => {
878
- const hint = tryGetSchemaHint(schema);
879
- if (!hint) return input;
880
- return `${input}
881
-
882
- ${label}:
883
- ${hint}`;
884
- };
885
- var defaultInterruptFirstPassInput = (args) => {
886
- const mode = args.mode ?? "required";
887
- const base = mode === "optional" ? args.outputSchema ? [
888
- args.input,
889
- "Output rules:",
890
- "- Return JSON only. No markdown fences.",
891
- "- Return an object with keys: decision, output, interruptRequest, draftText.",
892
- '- decision must be either "continue" or "interrupt".',
893
- '- Use decision "continue" when you can produce the final output without asking the user.',
894
- '- Use decision "interrupt" only when user input is needed before finalizing.',
895
- "- output must match the expected output JSON schema.",
896
- '- When decision is "interrupt", interruptRequest must match the expected interrupt request JSON schema and must be a non-null object.',
897
- '- When decision is "continue", interruptRequest must be null.',
898
- "- draftText should be a concise plain-language summary of output."
899
- ].join("\n\n") : [
900
- args.input,
901
- "Output rules:",
902
- "- Return JSON only. No markdown fences.",
903
- "- Return an object with keys: decision, draftText, interruptRequest.",
904
- '- decision must be either "continue" or "interrupt".',
905
- '- Use decision "continue" when you can produce the final answer without asking the user.',
906
- '- Use decision "interrupt" only when user input is needed before finalizing.',
907
- "- draftText should be the current answer text.",
908
- '- When decision is "interrupt", interruptRequest must match the expected interrupt request JSON schema and must be a non-null object.',
909
- '- When decision is "continue", interruptRequest must be null.'
910
- ].join("\n\n") : args.outputSchema ? [
911
- args.input,
912
- "Output rules:",
913
- "- Return JSON only. No markdown fences.",
914
- "- Return an object with keys: output, interruptRequest, draftText.",
915
- "- output must match the expected output JSON schema.",
916
- "- interruptRequest must match the expected interrupt request JSON schema.",
917
- "- interruptRequest must be a non-null object.",
918
- "- draftText should be a concise plain-language summary of output.",
919
- "- If uncertain, still return a valid interruptRequest with safe default options."
920
- ].join("\n\n") : [
921
- args.input,
922
- "Output rules:",
923
- "- Return JSON only. No markdown fences.",
924
- "- Return an object with keys: draftText, interruptRequest.",
925
- "- draftText should be the current draft answer text.",
926
- "- interruptRequest must match the expected interrupt request JSON schema.",
927
- "- interruptRequest must be a non-null object.",
928
- "- If uncertain, still return a valid interruptRequest with safe default options."
929
- ].join("\n\n");
930
- const withInterruptSchema = withSchemaHint(
931
- base,
932
- "Expected interrupt request JSON schema (for `interruptRequest`)",
933
- args.requestSchema
934
- );
935
- if (!args.outputSchema) return withInterruptSchema;
936
- const withOutputSchema = withSchemaHint(
937
- withInterruptSchema,
938
- "Expected output JSON schema (for `output`)",
939
- args.outputSchema
940
- );
941
- return [
942
- withOutputSchema,
943
- "JSON template:",
944
- mode === "optional" ? '{ "decision": "continue", "output": { ... }, "interruptRequest": null, "draftText": "..." }' : '{ "output": { ... }, "interruptRequest": { ... }, "draftText": "..." }'
945
- ].join("\n\n");
946
- };
947
- var defaultContinuationInput = (args) => [
948
- "You are continuing a paused reasoning task after user input.",
949
- "Update the previous result using the user response and return the final answer only.",
950
- "If the previous output was JSON, return JSON in the same schema.",
951
- "",
952
- `Original request:
953
- ${args.input}`,
954
- `Draft answer text:
955
- ${args.draftText}`,
956
- `Draft output object:
957
- ${tryStringify2(args.draftOutput ?? args.draftText)}`,
958
- `Interrupt request shown to user:
959
- ${tryStringify2(args.request)}`,
960
- `User response:
961
- ${tryStringify2(args.response)}`
962
- ].join("\n\n");
963
- var withOutputGuardrails = (input, schema) => {
964
- const base = [
965
- input,
966
- "Output rules:",
967
- "- Return only JSON, no markdown fences.",
968
- "- Ensure the response conforms to the expected output schema.",
969
- "- Do not include explanatory text outside the JSON object."
970
- ].join("\n\n");
971
- return withSchemaHint(base, "Expected output JSON schema", schema);
972
- };
973
- var withStructuredStreamHints = (input, args) => {
974
- const rules = [];
975
- for (const path of args.setFieldPaths ?? []) {
976
- rules.push(
977
- `- For the field path pattern \`${path}\`, write one stable final value and do not rewrite it after it is complete.`
978
- );
979
- }
980
- for (const path of args.appendFieldPaths ?? []) {
981
- rules.push(
982
- `- For the array field path pattern \`${path}\`, write items in final order and finish each item before starting the next.`
983
- );
984
- rules.push(
985
- `- Once an item in \`${path}\` is complete, do not rewrite or reorder earlier items.`
986
- );
987
- }
988
- for (const path of args.textDeltaFieldPaths ?? []) {
989
- rules.push(
990
- `- For the string field path pattern \`${path}\`, write the value progressively by appending text to the end.`
991
- );
992
- rules.push(
993
- `- Do not rewrite or replace the earlier prefix of \`${path}\` once emitted.`
994
- );
995
- }
996
- if (rules.length === 0) return input;
997
- return [
998
- input,
999
- "Streaming rules:",
1000
- ...rules,
1001
- "- Keep the overall response as valid JSON."
1002
- ].join("\n\n");
1003
- };
1004
-
1005
- // src/reason/result.ts
1006
- var mergeUsage = (left, right) => {
1007
- if (!left) return right;
1008
- if (!right) return left;
1009
- const sum = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
1010
- const raw = left.raw || right.raw ? {
1011
- ...left.raw ?? {},
1012
- ...right.raw ?? {}
1013
- } : void 0;
1014
- const input = sum(left.input, right.input);
1015
- const output = sum(left.output, right.output);
1016
- const total = sum(left.total, right.total);
1017
- const reasoning = sum(left.reasoning, right.reasoning);
1018
- const cacheRead = sum(left.cacheRead, right.cacheRead);
1019
- const cacheWrite = sum(left.cacheWrite, right.cacheWrite);
1020
- const merged = {};
1021
- if (input !== void 0) merged.input = input;
1022
- if (output !== void 0) merged.output = output;
1023
- if (total !== void 0) merged.total = total;
1024
- if (reasoning !== void 0) merged.reasoning = reasoning;
1025
- if (cacheRead !== void 0) merged.cacheRead = cacheRead;
1026
- if (cacheWrite !== void 0) merged.cacheWrite = cacheWrite;
1027
- if (raw) merged.raw = raw;
1028
- return merged;
1029
- };
1030
- var mergeWarnings = (left, right) => {
1031
- if (!left?.length) return right;
1032
- if (!right?.length) return left;
1033
- const seen = /* @__PURE__ */ new Set();
1034
- const merged = [];
1035
- for (const warning of [...left, ...right]) {
1036
- const key = JSON.stringify(warning);
1037
- if (seen.has(key)) continue;
1038
- seen.add(key);
1039
- merged.push(warning);
1040
- }
1041
- return merged;
1042
- };
1043
- var mergeProviderMetadata = (left, right) => {
1044
- if (!left) return right;
1045
- if (!right) return left;
1046
- return {
1047
- ...left,
1048
- ...right
1049
- };
1050
- };
1051
- var emitReasonStructuredOutput = (args) => {
1052
- if (!args.emit || !shouldEmitStructured(args.structured)) return;
1053
- emitStructuredData({
1054
- dataType: args.structured?.dataType ?? "reason-output",
1055
- data: args.output,
1056
- kind: "final",
1057
- ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
1058
- ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
1059
- ...args.id ? { id: args.id } : {},
1060
- streamId: args.opId
1061
- });
1062
- };
1063
-
1064
- // src/reason/structured-stream.ts
1065
- var resolveAppendFieldPaths = (cfg) => {
1066
- const fields = cfg?.fields;
1067
- if (!fields) return [];
1068
- const appendPaths = Object.entries(fields).filter(([, mode]) => mode === "append").map(([path]) => path);
1069
- for (const path of appendPaths) {
1070
- assertStructuredPath(path, "useReason structured append");
1071
- }
1072
- return appendPaths;
1073
- };
1074
- var resolveSetFieldPaths = (cfg) => {
1075
- const fields = cfg?.fields;
1076
- if (!fields) return [];
1077
- const setPaths = Object.entries(fields).filter(([, mode]) => mode === "set").map(([path]) => path);
1078
- for (const path of setPaths) {
1079
- assertStructuredPath(path, "useReason structured set");
1080
- }
1081
- return setPaths;
1082
- };
1083
- var resolveTextDeltaFieldPaths = (cfg) => {
1084
- const fields = cfg?.fields;
1085
- if (!fields) return [];
1086
- const textPaths = Object.entries(fields).filter(([, mode]) => mode === "text-delta").map(([path]) => path);
1087
- for (const path of textPaths) {
1088
- assertStructuredPath(path, "useReason structured text-delta");
1089
- }
1090
- return textPaths;
1091
- };
1092
- var extractCompletedStringValueAt = (args) => {
1093
- let value = "";
1094
- for (let i = args.start; i < args.text.length; i += 1) {
1095
- const ch = args.text[i];
1096
- if (ch === void 0) break;
1097
- if (ch === '"') {
1098
- return {
1099
- complete: true,
1100
- value
1101
- };
1102
- }
1103
- if (ch !== "\\") {
1104
- value += ch;
1105
- continue;
1106
- }
1107
- const next = args.text[i + 1];
1108
- if (!next) {
1109
- return {
1110
- complete: false
1111
- };
1112
- }
1113
- if (next === '"' || next === "\\" || next === "/" || next === "b" || next === "f" || next === "n" || next === "r" || next === "t") {
1114
- value += next === "b" ? "\b" : next === "f" ? "\f" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : next;
1115
- i += 1;
1116
- continue;
1117
- }
1118
- if (next === "u") {
1119
- const hex = args.text.slice(i + 2, i + 6);
1120
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
1121
- return {
1122
- complete: false
1123
- };
1124
- }
1125
- value += String.fromCharCode(Number.parseInt(hex, 16));
1126
- i += 5;
1127
- continue;
1128
- }
1129
- return {
1130
- complete: false
1131
- };
1132
- }
1133
- return {
1134
- complete: false
1135
- };
1136
- };
1137
- var skipWhitespace = (text, start) => {
1138
- let i = start;
1139
- while (i < text.length && /\s/.test(text[i] ?? "")) i += 1;
1140
- return i;
870
+ var skipWhitespace = (text, start) => {
871
+ let i = start;
872
+ while (i < text.length && /\s/.test(text[i] ?? "")) i += 1;
873
+ return i;
1141
874
  };
1142
875
  var pathSegmentMatches = (patternPart, actualPart) => patternPart === "*" || patternPart === actualPart;
1143
876
  var pathMatchesPattern = (actualParts, patternParts) => actualParts.length === patternParts.length && actualParts.every(
@@ -1521,103 +1254,560 @@ var extractCompletedArrayItems = (args) => {
1521
1254
  }
1522
1255
  return items;
1523
1256
  };
1524
- var extractCompletedArrayItemGroups = (args) => {
1525
- return locateValueStarts(args).flatMap((match) => {
1526
- const items = extractCompletedArrayItems({
1527
- text: args.text,
1528
- path: match.path
1529
- });
1530
- return items.length > 0 ? [
1531
- {
1532
- path: match.path,
1533
- items
1534
- }
1535
- ] : [];
1536
- });
1257
+ var extractCompletedArrayItemGroups = (args) => {
1258
+ return locateValueStarts(args).flatMap((match) => {
1259
+ const items = extractCompletedArrayItems({
1260
+ text: args.text,
1261
+ path: match.path
1262
+ });
1263
+ return items.length > 0 ? [
1264
+ {
1265
+ path: match.path,
1266
+ items
1267
+ }
1268
+ ] : [];
1269
+ });
1270
+ };
1271
+ var extractStreamingStringValue = (args) => {
1272
+ const located = locateValueStart(args);
1273
+ if (!located.found || located.start === void 0) {
1274
+ return {
1275
+ found: located.found,
1276
+ complete: false,
1277
+ value: ""
1278
+ };
1279
+ }
1280
+ const stringStart = skipWhitespace(args.text, located.start);
1281
+ if (args.text[stringStart] !== '"') {
1282
+ return {
1283
+ found: true,
1284
+ complete: false,
1285
+ value: ""
1286
+ };
1287
+ }
1288
+ let value = "";
1289
+ for (let i = stringStart + 1; i < args.text.length; i += 1) {
1290
+ const ch = args.text[i];
1291
+ if (ch === void 0) break;
1292
+ if (ch === '"') {
1293
+ return {
1294
+ found: true,
1295
+ complete: true,
1296
+ value
1297
+ };
1298
+ }
1299
+ if (ch !== "\\") {
1300
+ value += ch;
1301
+ continue;
1302
+ }
1303
+ const next = args.text[i + 1];
1304
+ if (!next) {
1305
+ return {
1306
+ found: true,
1307
+ complete: false,
1308
+ value
1309
+ };
1310
+ }
1311
+ if (next === '"' || next === "\\" || next === "/" || next === "b" || next === "f" || next === "n" || next === "r" || next === "t") {
1312
+ value += next === "b" ? "\b" : next === "f" ? "\f" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : next;
1313
+ i += 1;
1314
+ continue;
1315
+ }
1316
+ if (next === "u") {
1317
+ const hex = args.text.slice(i + 2, i + 6);
1318
+ if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
1319
+ return {
1320
+ found: true,
1321
+ complete: false,
1322
+ value
1323
+ };
1324
+ }
1325
+ value += String.fromCharCode(Number.parseInt(hex, 16));
1326
+ i += 5;
1327
+ continue;
1328
+ }
1329
+ return {
1330
+ found: true,
1331
+ complete: false,
1332
+ value
1333
+ };
1334
+ }
1335
+ return {
1336
+ found: true,
1337
+ complete: false,
1338
+ value
1339
+ };
1340
+ };
1341
+ var extractStreamingStringValues = (args) => {
1342
+ return locateValueStarts(args).flatMap((match) => {
1343
+ const current = extractStreamingStringValue({
1344
+ text: args.text,
1345
+ path: match.path
1346
+ });
1347
+ return current.found ? [
1348
+ {
1349
+ path: match.path,
1350
+ complete: current.complete,
1351
+ value: current.value
1352
+ }
1353
+ ] : [];
1354
+ });
1355
+ };
1356
+
1357
+ // src/reason/output-stream.ts
1358
+ function createStructuredOutputStreamer(args, id, opId) {
1359
+ let firstText = "";
1360
+ const setFieldPaths = resolveSetFieldPaths(args.structured);
1361
+ const appendFieldPaths = resolveAppendFieldPaths(args.structured);
1362
+ const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
1363
+ const emittedSetValues = /* @__PURE__ */ new Map();
1364
+ const emittedAppendCounts = /* @__PURE__ */ new Map();
1365
+ const emittedTextValues = /* @__PURE__ */ new Map();
1366
+ return (delta) => {
1367
+ firstText += delta;
1368
+ for (const fieldPath of setFieldPaths) {
1369
+ const values = extractCompletedFieldValues({
1370
+ text: firstText,
1371
+ path: fieldPath
1372
+ });
1373
+ for (const { path, value } of values) {
1374
+ const nextSerialized = JSON.stringify(value);
1375
+ if (emittedSetValues.get(path) === nextSerialized) continue;
1376
+ emittedSetValues.set(path, nextSerialized);
1377
+ emitStructuredData({
1378
+ kind: "set",
1379
+ path,
1380
+ value,
1381
+ dataType: args.structured?.dataType ?? "reason-output",
1382
+ ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
1383
+ ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
1384
+ ...id ? { id } : {},
1385
+ streamId: opId
1386
+ });
1387
+ }
1388
+ }
1389
+ for (const fieldPath of appendFieldPaths) {
1390
+ const groups = extractCompletedArrayItemGroups({
1391
+ text: firstText,
1392
+ path: fieldPath
1393
+ });
1394
+ for (const { path, items } of groups) {
1395
+ const emittedAppendCount = emittedAppendCounts.get(path) ?? 0;
1396
+ if (items.length <= emittedAppendCount) continue;
1397
+ const nextItems = items.slice(emittedAppendCount);
1398
+ emittedAppendCounts.set(path, items.length);
1399
+ emitStructuredData({
1400
+ kind: "append",
1401
+ path,
1402
+ items: nextItems,
1403
+ dataType: args.structured?.dataType ?? "reason-output",
1404
+ ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
1405
+ ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
1406
+ ...id ? { id } : {},
1407
+ streamId: opId
1408
+ });
1409
+ }
1410
+ }
1411
+ for (const fieldPath of textDeltaFieldPaths) {
1412
+ const values = extractStreamingStringValues({
1413
+ text: firstText,
1414
+ path: fieldPath
1415
+ });
1416
+ for (const { path, value } of values) {
1417
+ const emittedTextValue = emittedTextValues.get(path) ?? "";
1418
+ if (value.length <= emittedTextValue.length || !value.startsWith(emittedTextValue)) {
1419
+ continue;
1420
+ }
1421
+ const nextDelta = value.slice(emittedTextValue.length);
1422
+ emittedTextValues.set(path, value);
1423
+ if (nextDelta.length === 0) continue;
1424
+ emitStructuredData({
1425
+ kind: "text-delta",
1426
+ path,
1427
+ delta: nextDelta,
1428
+ dataType: args.structured?.dataType ?? "reason-output",
1429
+ ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
1430
+ ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
1431
+ ...id ? { id } : {},
1432
+ streamId: opId
1433
+ });
1434
+ }
1435
+ }
1436
+ };
1437
+ }
1438
+
1439
+ // src/reason/parsing.ts
1440
+ var tryJsonParse = (value) => {
1441
+ try {
1442
+ return { ok: true, value: JSON.parse(value) };
1443
+ } catch (error) {
1444
+ return {
1445
+ ok: false,
1446
+ error: error instanceof Error ? error.message : String(error)
1447
+ };
1448
+ }
1449
+ };
1450
+ var extractJsonCodeBlock = (value) => {
1451
+ const match = value.match(/```(?:json)?\s*([\s\S]*?)```/i);
1452
+ if (!match || typeof match[1] !== "string") return null;
1453
+ const inner = match[1].trim();
1454
+ return inner.length > 0 ? inner : null;
1455
+ };
1456
+ var tryStringify = (value) => {
1457
+ if (typeof value === "string") return value;
1458
+ try {
1459
+ return JSON.stringify(value, null, 2);
1460
+ } catch {
1461
+ return String(value);
1462
+ }
1463
+ };
1464
+ var isRecord2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
1465
+ var resolveOutputCandidate = (text) => {
1466
+ const trimmed = text.trim();
1467
+ if (!trimmed) return text;
1468
+ const direct = tryJsonParse(trimmed);
1469
+ if (direct.ok) return direct.value;
1470
+ const block = extractJsonCodeBlock(trimmed);
1471
+ if (block) {
1472
+ const parsedBlock = tryJsonParse(block);
1473
+ if (parsedBlock.ok) return parsedBlock.value;
1474
+ }
1475
+ return text;
1476
+ };
1477
+ var looksLikeStructuredJsonAttempt = (value) => {
1478
+ const trimmed = value.trim();
1479
+ if (!trimmed) return false;
1480
+ return trimmed.startsWith("{") || trimmed.startsWith("[") || /^```(?:json)?/i.test(trimmed);
1481
+ };
1482
+ var isLikelyTruncatedStructuredOutput = (text) => {
1483
+ const trimmed = text.trim();
1484
+ if (!looksLikeStructuredJsonAttempt(trimmed)) return false;
1485
+ const direct = tryJsonParse(trimmed);
1486
+ if (!direct.ok && direct.error) return true;
1487
+ const block = extractJsonCodeBlock(trimmed);
1488
+ if (!block) return false;
1489
+ const parsedBlock = tryJsonParse(block);
1490
+ return !parsedBlock.ok && Boolean(parsedBlock.error);
1491
+ };
1492
+ var createTruncatedStructuredOutputError = (label) => new Error(
1493
+ `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`
1494
+ );
1495
+ var createInvalidStructuredOutputError = (label) => new Error(
1496
+ `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. This often happens when output is truncated or the provider does not follow JSON mode. Increase maxOutputTokens or simplify the requested output.`
1497
+ );
1498
+ var parseReasonOutputWithSchema = (args) => {
1499
+ const candidate = resolveOutputCandidate(args.text);
1500
+ try {
1501
+ return parseWithSchema(args.schema, candidate, args.label);
1502
+ } catch (error) {
1503
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1504
+ throw createTruncatedStructuredOutputError(args.label);
1505
+ }
1506
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
1507
+ throw createInvalidStructuredOutputError(args.label);
1508
+ }
1509
+ throw error;
1510
+ }
1511
+ };
1512
+ var parseInterruptFirstPassResult = (args) => {
1513
+ const candidate = resolveOutputCandidate(args.text);
1514
+ if (!isRecord2(candidate)) {
1515
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1516
+ throw createTruncatedStructuredOutputError("useReason output");
1517
+ }
1518
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
1519
+ throw createInvalidStructuredOutputError("useReason output");
1520
+ }
1521
+ throw new Error(
1522
+ "useReason first pass with interrupt must return a JSON object."
1523
+ );
1524
+ }
1525
+ const mode = args.mode ?? "required";
1526
+ let interruptRequired = true;
1527
+ if (mode === "optional") {
1528
+ if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1529
+ throw new Error(
1530
+ 'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1531
+ );
1532
+ }
1533
+ interruptRequired = candidate.decision === "interrupt";
1534
+ if (!interruptRequired) {
1535
+ const hasUnexpectedRequest = [
1536
+ candidate.interruptRequest,
1537
+ candidate.request,
1538
+ candidate.interrupt
1539
+ ].some((value) => value !== null && value !== void 0);
1540
+ if (hasUnexpectedRequest) {
1541
+ throw new Error(
1542
+ 'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1543
+ );
1544
+ }
1545
+ }
1546
+ }
1547
+ let request;
1548
+ if (interruptRequired) {
1549
+ const requestCandidates = [
1550
+ candidate.interruptRequest,
1551
+ candidate.request,
1552
+ candidate.interrupt,
1553
+ mode === "required" ? candidate : void 0
1554
+ ];
1555
+ const requestPayload = requestCandidates.find(
1556
+ (value) => value !== null && value !== void 0
1557
+ );
1558
+ request = parseWithSchema(
1559
+ args.requestSchema,
1560
+ requestPayload,
1561
+ "useReason interrupt.request"
1562
+ );
1563
+ }
1564
+ let output;
1565
+ if (args.outputSchema) {
1566
+ const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
1567
+ try {
1568
+ output = parseWithSchema(
1569
+ args.outputSchema,
1570
+ outputPayload,
1571
+ "useReason output"
1572
+ );
1573
+ } catch (error) {
1574
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
1575
+ throw createTruncatedStructuredOutputError("useReason output");
1576
+ }
1577
+ if (typeof outputPayload === "string" && outputPayload.trim().length > 0) {
1578
+ throw createInvalidStructuredOutputError("useReason output");
1579
+ }
1580
+ throw error;
1581
+ }
1582
+ }
1583
+ const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1584
+ if (!args.outputSchema && !explicitDraft) {
1585
+ throw new Error(
1586
+ "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1587
+ );
1588
+ }
1589
+ const draftText = explicitDraft ?? (output !== void 0 ? tryStringify(output) : String(args.text ?? ""));
1590
+ if (!interruptRequired) {
1591
+ return {
1592
+ draftText,
1593
+ interruptRequired: false,
1594
+ ...output !== void 0 ? { output } : {}
1595
+ };
1596
+ }
1597
+ return {
1598
+ draftText,
1599
+ interruptRequired: true,
1600
+ request,
1601
+ ...output !== void 0 ? { output } : {}
1602
+ };
1603
+ };
1604
+
1605
+ // src/reason/prompting.ts
1606
+ var import_zod3 = require("zod");
1607
+ var tryStringify2 = (value) => {
1608
+ if (typeof value === "string") return value;
1609
+ try {
1610
+ return JSON.stringify(value, null, 2);
1611
+ } catch {
1612
+ return String(value);
1613
+ }
1614
+ };
1615
+ var tryGetSchemaHint = (schema) => {
1616
+ try {
1617
+ const hint = (0, import_zod3.toJSONSchema)(schema);
1618
+ return JSON.stringify(hint, null, 2);
1619
+ } catch {
1620
+ return void 0;
1621
+ }
1622
+ };
1623
+ var withSchemaHint = (input, label, schema) => {
1624
+ const hint = tryGetSchemaHint(schema);
1625
+ if (!hint) return input;
1626
+ return `${input}
1627
+
1628
+ ${label}:
1629
+ ${hint}`;
1630
+ };
1631
+ var defaultInterruptFirstPassInput = (args) => {
1632
+ const mode = args.mode ?? "required";
1633
+ const base = mode === "optional" ? args.outputSchema ? [
1634
+ args.input,
1635
+ "Output rules:",
1636
+ "- Return JSON only. No markdown fences.",
1637
+ "- Return an object with keys: decision, output, interruptRequest, draftText.",
1638
+ '- decision must be either "continue" or "interrupt".',
1639
+ '- Use decision "continue" when you can produce the final output without asking the user.',
1640
+ '- Use decision "interrupt" only when user input is needed before finalizing.',
1641
+ "- output must match the expected output JSON schema.",
1642
+ '- When decision is "interrupt", interruptRequest must match the expected interrupt request JSON schema and must be a non-null object.',
1643
+ '- When decision is "continue", interruptRequest must be null.',
1644
+ "- draftText should be a concise plain-language summary of output."
1645
+ ].join("\n\n") : [
1646
+ args.input,
1647
+ "Output rules:",
1648
+ "- Return JSON only. No markdown fences.",
1649
+ "- Return an object with keys: decision, draftText, interruptRequest.",
1650
+ '- decision must be either "continue" or "interrupt".',
1651
+ '- Use decision "continue" when you can produce the final answer without asking the user.',
1652
+ '- Use decision "interrupt" only when user input is needed before finalizing.',
1653
+ "- draftText should be the current answer text.",
1654
+ '- When decision is "interrupt", interruptRequest must match the expected interrupt request JSON schema and must be a non-null object.',
1655
+ '- When decision is "continue", interruptRequest must be null.'
1656
+ ].join("\n\n") : args.outputSchema ? [
1657
+ args.input,
1658
+ "Output rules:",
1659
+ "- Return JSON only. No markdown fences.",
1660
+ "- Return an object with keys: output, interruptRequest, draftText.",
1661
+ "- output must match the expected output JSON schema.",
1662
+ "- interruptRequest must match the expected interrupt request JSON schema.",
1663
+ "- interruptRequest must be a non-null object.",
1664
+ "- draftText should be a concise plain-language summary of output.",
1665
+ "- If uncertain, still return a valid interruptRequest with safe default options."
1666
+ ].join("\n\n") : [
1667
+ args.input,
1668
+ "Output rules:",
1669
+ "- Return JSON only. No markdown fences.",
1670
+ "- Return an object with keys: draftText, interruptRequest.",
1671
+ "- draftText should be the current draft answer text.",
1672
+ "- interruptRequest must match the expected interrupt request JSON schema.",
1673
+ "- interruptRequest must be a non-null object.",
1674
+ "- If uncertain, still return a valid interruptRequest with safe default options."
1675
+ ].join("\n\n");
1676
+ const withInterruptSchema = withSchemaHint(
1677
+ base,
1678
+ "Expected interrupt request JSON schema (for `interruptRequest`)",
1679
+ args.requestSchema
1680
+ );
1681
+ if (!args.outputSchema) return withInterruptSchema;
1682
+ const withOutputSchema = withSchemaHint(
1683
+ withInterruptSchema,
1684
+ "Expected output JSON schema (for `output`)",
1685
+ args.outputSchema
1686
+ );
1687
+ return [
1688
+ withOutputSchema,
1689
+ "JSON template:",
1690
+ mode === "optional" ? '{ "decision": "continue", "output": { ... }, "interruptRequest": null, "draftText": "..." }' : '{ "output": { ... }, "interruptRequest": { ... }, "draftText": "..." }'
1691
+ ].join("\n\n");
1692
+ };
1693
+ var defaultContinuationInput = (args) => [
1694
+ "You are continuing a paused reasoning task after user input.",
1695
+ "Update the previous result using the user response and return the final answer only.",
1696
+ "If the previous output was JSON, return JSON in the same schema.",
1697
+ "",
1698
+ `Original request:
1699
+ ${args.input}`,
1700
+ `Draft answer text:
1701
+ ${args.draftText}`,
1702
+ `Draft output object:
1703
+ ${tryStringify2(args.draftOutput ?? args.draftText)}`,
1704
+ `Interrupt request shown to user:
1705
+ ${tryStringify2(args.request)}`,
1706
+ `User response:
1707
+ ${tryStringify2(args.response)}`
1708
+ ].join("\n\n");
1709
+ var withOutputGuardrails = (input, schema) => {
1710
+ const base = [
1711
+ input,
1712
+ "Output rules:",
1713
+ "- Return only JSON, no markdown fences.",
1714
+ "- Ensure the response conforms to the expected output schema.",
1715
+ "- Do not include explanatory text outside the JSON object."
1716
+ ].join("\n\n");
1717
+ return withSchemaHint(base, "Expected output JSON schema", schema);
1537
1718
  };
1538
- var extractStreamingStringValue = (args) => {
1539
- const located = locateValueStart(args);
1540
- if (!located.found || located.start === void 0) {
1541
- return {
1542
- found: located.found,
1543
- complete: false,
1544
- value: ""
1545
- };
1719
+ var withStructuredStreamHints = (input, args) => {
1720
+ const rules = [];
1721
+ for (const path of args.setFieldPaths ?? []) {
1722
+ rules.push(
1723
+ `- For the field path pattern \`${path}\`, write one stable final value and do not rewrite it after it is complete.`
1724
+ );
1546
1725
  }
1547
- const stringStart = skipWhitespace(args.text, located.start);
1548
- if (args.text[stringStart] !== '"') {
1549
- return {
1550
- found: true,
1551
- complete: false,
1552
- value: ""
1553
- };
1726
+ for (const path of args.appendFieldPaths ?? []) {
1727
+ rules.push(
1728
+ `- For the array field path pattern \`${path}\`, write items in final order and finish each item before starting the next.`
1729
+ );
1730
+ rules.push(
1731
+ `- Once an item in \`${path}\` is complete, do not rewrite or reorder earlier items.`
1732
+ );
1554
1733
  }
1555
- let value = "";
1556
- for (let i = stringStart + 1; i < args.text.length; i += 1) {
1557
- const ch = args.text[i];
1558
- if (ch === void 0) break;
1559
- if (ch === '"') {
1560
- return {
1561
- found: true,
1562
- complete: true,
1563
- value
1564
- };
1565
- }
1566
- if (ch !== "\\") {
1567
- value += ch;
1568
- continue;
1569
- }
1570
- const next = args.text[i + 1];
1571
- if (!next) {
1572
- return {
1573
- found: true,
1574
- complete: false,
1575
- value
1576
- };
1577
- }
1578
- if (next === '"' || next === "\\" || next === "/" || next === "b" || next === "f" || next === "n" || next === "r" || next === "t") {
1579
- value += next === "b" ? "\b" : next === "f" ? "\f" : next === "n" ? "\n" : next === "r" ? "\r" : next === "t" ? " " : next;
1580
- i += 1;
1581
- continue;
1582
- }
1583
- if (next === "u") {
1584
- const hex = args.text.slice(i + 2, i + 6);
1585
- if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
1586
- return {
1587
- found: true,
1588
- complete: false,
1589
- value
1590
- };
1591
- }
1592
- value += String.fromCharCode(Number.parseInt(hex, 16));
1593
- i += 5;
1594
- continue;
1595
- }
1596
- return {
1597
- found: true,
1598
- complete: false,
1599
- value
1600
- };
1734
+ for (const path of args.textDeltaFieldPaths ?? []) {
1735
+ rules.push(
1736
+ `- For the string field path pattern \`${path}\`, write the value progressively by appending text to the end.`
1737
+ );
1738
+ rules.push(
1739
+ `- Do not rewrite or replace the earlier prefix of \`${path}\` once emitted.`
1740
+ );
1741
+ }
1742
+ if (rules.length === 0) return input;
1743
+ return [
1744
+ input,
1745
+ "Streaming rules:",
1746
+ ...rules,
1747
+ "- Keep the overall response as valid JSON."
1748
+ ].join("\n\n");
1749
+ };
1750
+
1751
+ // src/reason/result.ts
1752
+ var mergeUsage = (left, right) => {
1753
+ if (!left) return right;
1754
+ if (!right) return left;
1755
+ const sum = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
1756
+ const raw = left.raw || right.raw ? {
1757
+ ...left.raw ?? {},
1758
+ ...right.raw ?? {}
1759
+ } : void 0;
1760
+ const input = sum(left.input, right.input);
1761
+ const output = sum(left.output, right.output);
1762
+ const total = sum(left.total, right.total);
1763
+ const reasoning = sum(left.reasoning, right.reasoning);
1764
+ const cacheRead = sum(left.cacheRead, right.cacheRead);
1765
+ const cacheWrite = sum(left.cacheWrite, right.cacheWrite);
1766
+ const merged = {
1767
+ ...right.outputIncludesReasoning !== void 0 ? { outputIncludesReasoning: right.outputIncludesReasoning } : {},
1768
+ ...right.inputIncludesCacheWrite !== void 0 ? { inputIncludesCacheWrite: right.inputIncludesCacheWrite } : {},
1769
+ ...right.inputIncludesCacheRead !== void 0 ? { inputIncludesCacheRead: right.inputIncludesCacheRead } : {}
1770
+ };
1771
+ if (input !== void 0) merged.input = input;
1772
+ if (output !== void 0) merged.output = output;
1773
+ if (total !== void 0) merged.total = total;
1774
+ if (reasoning !== void 0) merged.reasoning = reasoning;
1775
+ if (cacheRead !== void 0) merged.cacheRead = cacheRead;
1776
+ if (cacheWrite !== void 0) merged.cacheWrite = cacheWrite;
1777
+ if (raw) merged.raw = raw;
1778
+ return merged;
1779
+ };
1780
+ var mergeWarnings = (left, right) => {
1781
+ if (!left?.length) return right;
1782
+ if (!right?.length) return left;
1783
+ const seen = /* @__PURE__ */ new Set();
1784
+ const merged = [];
1785
+ for (const warning of [...left, ...right]) {
1786
+ const key = JSON.stringify(warning);
1787
+ if (seen.has(key)) continue;
1788
+ seen.add(key);
1789
+ merged.push(warning);
1601
1790
  }
1791
+ return merged;
1792
+ };
1793
+ var mergeProviderMetadata = (left, right) => {
1794
+ if (!left) return right;
1795
+ if (!right) return left;
1602
1796
  return {
1603
- found: true,
1604
- complete: false,
1605
- value
1797
+ ...left,
1798
+ ...right
1606
1799
  };
1607
1800
  };
1608
- var extractStreamingStringValues = (args) => {
1609
- return locateValueStarts(args).flatMap((match) => {
1610
- const current = extractStreamingStringValue({
1611
- text: args.text,
1612
- path: match.path
1613
- });
1614
- return current.found ? [
1615
- {
1616
- path: match.path,
1617
- complete: current.complete,
1618
- value: current.value
1619
- }
1620
- ] : [];
1801
+ var emitReasonStructuredOutput = (args) => {
1802
+ if (!args.emit || !shouldEmitStructured(args.structured)) return;
1803
+ emitStructuredData({
1804
+ dataType: args.structured?.dataType ?? "reason-output",
1805
+ data: args.output,
1806
+ kind: "final",
1807
+ ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
1808
+ ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
1809
+ ...args.id ? { id: args.id } : {},
1810
+ streamId: args.opId
1621
1811
  });
1622
1812
  };
1623
1813
 
@@ -1674,16 +1864,20 @@ var createInitialMessages = (args) => [
1674
1864
  { role: "user", content: String(args.input ?? "") }
1675
1865
  ];
1676
1866
  var runReasonToolLoop = async (args) => {
1677
- const { useReasonArgs, id, opId, traceSpan } = args;
1867
+ const { useReasonArgs, id, traceSpan, checkpointKey } = args;
1678
1868
  const ctx = getHookContext();
1679
1869
  const abortSignal = (0, import_core4.combineAbortSignals)(
1680
1870
  ctx.node.abortSignal,
1681
1871
  useReasonArgs.abortSignal,
1682
1872
  useReasonArgs.model.options?.abortSignal
1683
1873
  );
1874
+ const saved = ctx.currentNodeState.byKey[checkpointKey];
1875
+ const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
1876
+ const opId = checkpoint?.opId ?? args.opId;
1684
1877
  const tools = useReasonArgs.tools ?? [];
1685
1878
  const toolByName = /* @__PURE__ */ new Map();
1686
1879
  let validationCompleted = false;
1880
+ let separateOutput = false;
1687
1881
  try {
1688
1882
  for (const tool of tools) {
1689
1883
  if (toolByName.has(tool.name)) {
@@ -1698,6 +1892,17 @@ var runReasonToolLoop = async (args) => {
1698
1892
  "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
1699
1893
  );
1700
1894
  }
1895
+ separateOutput = Boolean(
1896
+ useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
1897
+ ...useReasonArgs.model.options,
1898
+ ...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
1899
+ ...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
1900
+ providerOptions: resolveProviderOptions(
1901
+ useReasonArgs.model.options?.providerOptions,
1902
+ useReasonArgs.providerOptions
1903
+ ) ?? {}
1904
+ }).requiresSeparateStructuredOutput
1905
+ );
1701
1906
  validationCompleted = true;
1702
1907
  } finally {
1703
1908
  if (!validationCompleted) {
@@ -1705,21 +1910,47 @@ var runReasonToolLoop = async (args) => {
1705
1910
  }
1706
1911
  }
1707
1912
  const toolDefinitions = toToolDefinitions(tools);
1913
+ let finalizing = checkpoint?.finalizing ?? false;
1914
+ let completed = false;
1708
1915
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
1709
- const messages = createInitialMessages({
1916
+ const messages = checkpoint?.messages ?? createInitialMessages({
1710
1917
  system: useReasonArgs.system,
1711
- input: useReasonArgs.input
1918
+ input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input
1712
1919
  });
1713
- const steps = [];
1714
- const allToolCalls = [];
1715
- const allToolResults = [];
1920
+ const steps = checkpoint?.steps ?? [];
1921
+ const allToolCalls = steps.flatMap(
1922
+ (step) => step.toolCalls
1923
+ );
1924
+ const allToolResults = steps.flatMap(
1925
+ (step) => step.toolResults
1926
+ );
1716
1927
  let finalText = "";
1717
1928
  let finalRaw;
1718
1929
  let finalOutput;
1719
- let aggregatedUsage;
1930
+ let aggregatedUsage = steps.reduce(
1931
+ (usage, step) => mergeUsage(usage, step.usage),
1932
+ void 0
1933
+ );
1720
1934
  let finalFinishReason;
1721
1935
  let aggregatedProviderMetadata;
1722
- let aggregatedWarnings;
1936
+ let aggregatedWarnings = steps.reduce(
1937
+ (warnings, step) => mergeWarnings(warnings, step.warnings),
1938
+ args.initialWarnings
1939
+ );
1940
+ let pending = checkpoint?.pending;
1941
+ const approvedCalls = checkpoint?.approvedCalls ?? [];
1942
+ const save = () => {
1943
+ ctx.currentNodeState.byKey[checkpointKey] = {
1944
+ status: "tool_loop",
1945
+ opId,
1946
+ messages,
1947
+ steps,
1948
+ approvedCalls,
1949
+ finalizing,
1950
+ ...pending ? { pending } : {}
1951
+ };
1952
+ ctx.stateDirty = true;
1953
+ };
1723
1954
  const emitToolEvent = (event, payload) => {
1724
1955
  ctx.node.emit(event, {
1725
1956
  ...payload,
@@ -1729,26 +1960,44 @@ var runReasonToolLoop = async (args) => {
1729
1960
  });
1730
1961
  };
1731
1962
  try {
1732
- for (let stepIndex = 0; stepIndex < maxSteps; stepIndex += 1) {
1963
+ for (let stepIndex = pending ? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
1733
1964
  (0, import_core4.throwIfExecutionAborted)(abortSignal);
1734
1965
  traceSpan?.addEvent?.("useReason.tool-step.start", {
1735
1966
  stepIndex,
1736
1967
  toolCount: toolDefinitions.length
1737
1968
  });
1738
- const step = await reasonEngine(
1969
+ const reused = Boolean(pending);
1970
+ const stream = useReasonArgs.stream ?? useReasonArgs.model.options?.streaming ?? true;
1971
+ const emit = useReasonArgs.emit ?? true;
1972
+ let textStarted = false;
1973
+ const structuredChunk = useReasonArgs.outputSchema && (!separateOutput || finalizing) && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
1974
+ const step = pending ?? await reasonEngine(
1739
1975
  {
1740
1976
  ...useReasonArgs,
1741
- tools: toolDefinitions,
1977
+ tools: finalizing ? [] : toolDefinitions,
1978
+ ...separateOutput && !finalizing ? { responseFormat: { type: "text" } } : {},
1742
1979
  messages,
1743
1980
  emit: false,
1744
- stream: false
1981
+ stream,
1982
+ onTextChunk: (delta) => {
1983
+ if (structuredChunk) structuredChunk(delta);
1984
+ if (!emit || useReasonArgs.outputSchema || !delta) return;
1985
+ if (!textStarted) {
1986
+ emitToolEvent("text-start", {});
1987
+ textStarted = true;
1988
+ }
1989
+ emitToolEvent("text-delta", { delta });
1990
+ }
1745
1991
  },
1746
1992
  { ...id ? { id } : {}, opId }
1747
1993
  );
1994
+ if (textStarted) emitToolEvent("text-end", {});
1748
1995
  finalText = step.text;
1749
1996
  finalRaw = step.raw;
1750
- aggregatedUsage = mergeUsage(aggregatedUsage, step.usage);
1751
- accumulateTokenUsage(step.usage);
1997
+ if (!reused) {
1998
+ aggregatedUsage = mergeUsage(aggregatedUsage, step.usage);
1999
+ accumulateTokenUsage(step.usage);
2000
+ }
1752
2001
  finalFinishReason = step.finishReason;
1753
2002
  aggregatedProviderMetadata = mergeProviderMetadata(
1754
2003
  aggregatedProviderMetadata,
@@ -1756,32 +2005,75 @@ var runReasonToolLoop = async (args) => {
1756
2005
  );
1757
2006
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, step.warnings);
1758
2007
  const toolCalls = step.toolCalls ?? [];
1759
- const toolResults = [];
1760
- steps.push({
1761
- stepIndex,
1762
- text: step.text,
1763
- toolCalls,
1764
- toolResults,
1765
- ...step.usage ? { usage: step.usage } : {},
1766
- ...step.finishReason ? { finishReason: step.finishReason } : {},
1767
- ...step.providerMetadata ? { providerMetadata: step.providerMetadata } : {},
1768
- ...step.warnings ? { warnings: step.warnings } : {}
1769
- });
2008
+ const toolResults = reused ? steps[stepIndex]?.toolResults ?? [] : [];
2009
+ if (!reused)
2010
+ steps.push({
2011
+ stepIndex,
2012
+ text: step.text,
2013
+ toolCalls,
2014
+ toolResults,
2015
+ ...step.usage ? { usage: step.usage } : {},
2016
+ ...step.finishReason ? { finishReason: step.finishReason } : {},
2017
+ ...step.providerMetadata ? { providerMetadata: step.providerMetadata } : {},
2018
+ ...step.warnings ? { warnings: step.warnings } : {}
2019
+ });
1770
2020
  if (toolCalls.length === 0) {
1771
- if ((useReasonArgs.emit ?? true) && step.text.length > 0) {
1772
- emitToolEvent("text-start", {});
1773
- emitToolEvent("text-delta", { delta: step.text });
1774
- emitToolEvent("text-end", {});
2021
+ if (separateOutput && !finalizing) {
2022
+ if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
2023
+ try {
2024
+ finalOutput = parseReasonOutputWithSchema({
2025
+ text: step.text,
2026
+ schema: useReasonArgs.outputSchema,
2027
+ ...step.finishReason ? { finishReason: step.finishReason } : {},
2028
+ label: "useReason output"
2029
+ });
2030
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, [
2031
+ {
2032
+ type: "compatibility",
2033
+ feature: "responseFormat",
2034
+ details: "Reused locally validated tool-phase output without an extra native-schema request. Set responseFormat.schema explicitly to require provider schema enforcement."
2035
+ }
2036
+ ]);
2037
+ completed = true;
2038
+ break;
2039
+ } catch {
2040
+ }
2041
+ }
2042
+ messages.push({
2043
+ role: "assistant",
2044
+ content: step.text,
2045
+ ...step.continuation ? { continuation: step.continuation } : {}
2046
+ });
2047
+ messages.push({
2048
+ role: "user",
2049
+ content: "Return the final answer using the requested output schema and the tool results above."
2050
+ });
2051
+ finalizing = true;
2052
+ pending = void 0;
2053
+ save();
2054
+ continue;
1775
2055
  }
2056
+ completed = true;
1776
2057
  break;
1777
2058
  }
1778
- allToolCalls.push(...toolCalls);
1779
- messages.push({
1780
- role: "assistant",
1781
- content: step.text,
1782
- toolCalls
1783
- });
2059
+ if (finalizing)
2060
+ throw new Error(
2061
+ "Provider requested a tool during schema-only finalization."
2062
+ );
2063
+ if (!reused) {
2064
+ allToolCalls.push(...toolCalls);
2065
+ messages.push({
2066
+ role: "assistant",
2067
+ content: step.text,
2068
+ toolCalls,
2069
+ ...step.continuation ? { continuation: step.continuation } : {}
2070
+ });
2071
+ }
2072
+ pending = step;
2073
+ save();
1784
2074
  for (const toolCall of toolCalls) {
2075
+ if (toolResults.some((result2) => result2.toolCallId === toolCall.id))
2076
+ continue;
1785
2077
  (0, import_core4.throwIfExecutionAborted)(abortSignal);
1786
2078
  const tool = toolByName.get(toolCall.name);
1787
2079
  if (!tool) {
@@ -1811,7 +2103,7 @@ var runReasonToolLoop = async (args) => {
1811
2103
  input: toolCall.input
1812
2104
  });
1813
2105
  }
1814
- if (shouldApproveTool) {
2106
+ if (shouldApproveTool && !approvedCalls.includes(toolCall.id)) {
1815
2107
  const approval = await awaitInterruptInternal({
1816
2108
  id: `tool:${toolCall.id}`,
1817
2109
  request: {
@@ -1846,8 +2138,18 @@ var runReasonToolLoop = async (args) => {
1846
2138
  isError: true
1847
2139
  });
1848
2140
  }
2141
+ messages.push({
2142
+ role: "tool",
2143
+ content: denied.content,
2144
+ toolCallId: denied.toolCallId,
2145
+ name: denied.name,
2146
+ isError: true
2147
+ });
2148
+ save();
1849
2149
  continue;
1850
2150
  }
2151
+ approvedCalls.push(toolCall.id);
2152
+ save();
1851
2153
  }
1852
2154
  try {
1853
2155
  (0, import_core4.throwIfExecutionAborted)(abortSignal);
@@ -1901,32 +2203,26 @@ var runReasonToolLoop = async (args) => {
1901
2203
  message: result2.content
1902
2204
  });
1903
2205
  }
2206
+ const completed2 = toolResults.at(-1);
2207
+ if (completed2)
2208
+ messages.push({
2209
+ role: "tool",
2210
+ content: completed2.content,
2211
+ toolCallId: completed2.toolCallId,
2212
+ name: completed2.name,
2213
+ ...completed2.isError !== void 0 ? { isError: completed2.isError } : {}
2214
+ });
2215
+ save();
1904
2216
  }
1905
- const currentStep = steps[steps.length - 1];
1906
- if (currentStep) {
1907
- steps[steps.length - 1] = {
1908
- ...currentStep,
1909
- toolResults
1910
- };
1911
- }
1912
- for (const result2 of toolResults) {
1913
- messages.push({
1914
- role: "tool",
1915
- content: result2.content,
1916
- toolCallId: result2.toolCallId,
1917
- name: result2.name,
1918
- ...result2.structuredContent !== void 0 ? { structuredContent: result2.structuredContent } : {},
1919
- ...result2.isError !== void 0 ? { isError: result2.isError } : {},
1920
- ...result2.raw !== void 0 ? { raw: result2.raw } : {}
1921
- });
1922
- }
2217
+ pending = void 0;
2218
+ save();
1923
2219
  }
1924
- if (steps.length >= maxSteps && steps.at(-1)?.toolCalls.length) {
2220
+ if (!completed) {
1925
2221
  throw new Error(
1926
2222
  `useReason tool loop reached maxSteps (${maxSteps}) before producing a final response.`
1927
2223
  );
1928
2224
  }
1929
- if (useReasonArgs.outputSchema) {
2225
+ if (useReasonArgs.outputSchema && finalOutput === void 0) {
1930
2226
  finalOutput = parseReasonOutputWithSchema({
1931
2227
  text: finalText,
1932
2228
  schema: useReasonArgs.outputSchema,
@@ -1973,6 +2269,8 @@ var runReasonToolLoop = async (args) => {
1973
2269
  output: useReasonArgs.telemetry?.output ?? finalOutput ?? finalText
1974
2270
  }
1975
2271
  });
2272
+ ctx.currentNodeState.byKey[checkpointKey] = { status: "completed", result };
2273
+ ctx.stateDirty = true;
1976
2274
  return result;
1977
2275
  } catch (error) {
1978
2276
  traceSpan?.fail?.(error, {
@@ -2008,7 +2306,18 @@ async function useReason(args) {
2008
2306
  args.model.options?.abortSignal
2009
2307
  );
2010
2308
  (0, import_core5.throwIfExecutionAborted)(abortSignal);
2011
- args = { ...args, abortSignal };
2309
+ const allowValidatedToolOutput = Boolean(
2310
+ args.outputSchema && !(args.responseFormat ?? args.model.options?.responseFormat)
2311
+ );
2312
+ const inferred = inferOutputFormat(
2313
+ args.outputSchema,
2314
+ args.responseFormat ?? args.model.options?.responseFormat
2315
+ );
2316
+ args = {
2317
+ ...args,
2318
+ abortSignal,
2319
+ ...!args.interrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
2320
+ };
2012
2321
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
2013
2322
  const opId = createRuntimeId2();
2014
2323
  const checkpointKey = resolveReasonCheckpointKey({
@@ -2052,6 +2361,9 @@ async function useReason(args) {
2052
2361
  if (args.tools?.length) {
2053
2362
  return runReasonToolLoop({
2054
2363
  useReasonArgs: args,
2364
+ allowValidatedToolOutput,
2365
+ checkpointKey,
2366
+ initialWarnings: inferred.warnings,
2055
2367
  ...id ? { id } : {},
2056
2368
  opId,
2057
2369
  ...traceSpan ? { traceSpan } : {}
@@ -2075,10 +2387,7 @@ async function useReason(args) {
2075
2387
  let aggregatedUsage;
2076
2388
  let finalFinishReason;
2077
2389
  let aggregatedProviderMetadata;
2078
- let aggregatedWarnings;
2079
- const emittedSetValues = /* @__PURE__ */ new Map();
2080
- const emittedAppendCounts = /* @__PURE__ */ new Map();
2081
- const emittedTextValues = /* @__PURE__ */ new Map();
2390
+ let aggregatedWarnings = inferred.warnings;
2082
2391
  if (existingCheckpoint) {
2083
2392
  firstText = existingCheckpoint.firstText;
2084
2393
  firstRaw = existingCheckpoint.firstRaw;
@@ -2100,78 +2409,7 @@ async function useReason(args) {
2100
2409
  emit: suppressTextStream ? false : args.emit,
2101
2410
  stream: useStructuredIncrementalStreaming ? true : suppressTextStream ? false : args.stream,
2102
2411
  ...useStructuredIncrementalStreaming ? {
2103
- onTextChunk: (delta) => {
2104
- firstText += delta;
2105
- finalText = firstText;
2106
- for (const fieldPath of setFieldPaths) {
2107
- const values = extractCompletedFieldValues({
2108
- text: firstText,
2109
- path: fieldPath
2110
- });
2111
- for (const { path, value } of values) {
2112
- const nextSerialized = JSON.stringify(value);
2113
- if (emittedSetValues.get(path) === nextSerialized) continue;
2114
- emittedSetValues.set(path, nextSerialized);
2115
- emitStructuredData({
2116
- kind: "set",
2117
- path,
2118
- value,
2119
- dataType: args.structured?.dataType ?? "reason-output",
2120
- ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
2121
- ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
2122
- ...id ? { id } : {},
2123
- streamId: opId
2124
- });
2125
- }
2126
- }
2127
- for (const fieldPath of appendFieldPaths) {
2128
- const groups = extractCompletedArrayItemGroups({
2129
- text: firstText,
2130
- path: fieldPath
2131
- });
2132
- for (const { path, items } of groups) {
2133
- const emittedAppendCount = emittedAppendCounts.get(path) ?? 0;
2134
- if (items.length <= emittedAppendCount) continue;
2135
- const nextItems = items.slice(emittedAppendCount);
2136
- emittedAppendCounts.set(path, items.length);
2137
- emitStructuredData({
2138
- kind: "append",
2139
- path,
2140
- items: nextItems,
2141
- dataType: args.structured?.dataType ?? "reason-output",
2142
- ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
2143
- ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
2144
- ...id ? { id } : {},
2145
- streamId: opId
2146
- });
2147
- }
2148
- }
2149
- for (const fieldPath of textDeltaFieldPaths) {
2150
- const values = extractStreamingStringValues({
2151
- text: firstText,
2152
- path: fieldPath
2153
- });
2154
- for (const { path, value } of values) {
2155
- const emittedTextValue = emittedTextValues.get(path) ?? "";
2156
- if (value.length <= emittedTextValue.length || !value.startsWith(emittedTextValue)) {
2157
- continue;
2158
- }
2159
- const nextDelta = value.slice(emittedTextValue.length);
2160
- emittedTextValues.set(path, value);
2161
- if (nextDelta.length === 0) continue;
2162
- emitStructuredData({
2163
- kind: "text-delta",
2164
- path,
2165
- delta: nextDelta,
2166
- dataType: args.structured?.dataType ?? "reason-output",
2167
- ...args.structured?.schemaId ? { schemaId: args.structured.schemaId } : {},
2168
- ...args.structured?.schemaVersion ? { schemaVersion: args.structured.schemaVersion } : {},
2169
- ...id ? { id } : {},
2170
- streamId: opId
2171
- });
2172
- }
2173
- }
2174
- }
2412
+ onTextChunk: createStructuredOutputStreamer(args, id, opId)
2175
2413
  } : {}
2176
2414
  },
2177
2415
  reasonMeta,
@@ -2291,6 +2529,7 @@ async function useReason(args) {
2291
2529
  const second = await reasonEngine(
2292
2530
  {
2293
2531
  ...args,
2532
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
2294
2533
  emit: suppressTextStream ? false : args.emit,
2295
2534
  stream: suppressTextStream ? false : args.stream
2296
2535
  },