@kortyx/hooks 0.30.2 → 0.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -286,7 +286,8 @@ var readReasonCompletedCheckpoint = (raw) => {
286
286
  if (!value.result || typeof value.result !== "object") return void 0;
287
287
  return {
288
288
  status: "completed",
289
- result: value.result
289
+ result: value.result,
290
+ ...Array.isArray(value.resolvedInterrupts) ? { resolvedInterrupts: value.resolvedInterrupts } : {}
290
291
  };
291
292
  };
292
293
  var resolveHookStatePatch = (args) => ({
@@ -348,6 +349,10 @@ var awaitInterruptInternal = (args) => {
348
349
  raw,
349
350
  "useInterrupt response"
350
351
  );
352
+ if (args.onResolved) {
353
+ const { __kortyxResumeStatePatch: _patch, ...meta } = enrichedRequest.meta ?? {};
354
+ args.onResolved({ ...enrichedRequest, meta });
355
+ }
351
356
  return Promise.resolve(response);
352
357
  };
353
358
 
@@ -717,52 +722,257 @@ var import_core2 = require("@kortyx/core");
717
722
  var import_errors3 = require("@kortyx/core/errors");
718
723
 
719
724
  // src/error-diagnostics.ts
725
+ var import_telemetry_contracts2 = require("@kortyx/telemetry-contracts");
726
+
727
+ // src/diagnostic-capture.ts
720
728
  var import_errors2 = require("@kortyx/core/errors");
721
- var MAX_STACK_LENGTH = 32768;
722
- var MAX_CAUSE_DEPTH = 4;
723
- var projectDetails = (value, depth, seen) => {
724
- const objectValue = value && typeof value === "object" ? value : void 0;
725
- if (objectValue) {
726
- if (seen.has(objectValue))
727
- return { type: "Error", message: "Circular error cause." };
728
- seen.add(objectValue);
729
+ var import_telemetry_contracts = require("@kortyx/telemetry-contracts");
730
+ var encoder = new TextEncoder();
731
+ var nativeStackGetter = Object.getOwnPropertyDescriptor(
732
+ new Error(),
733
+ "stack"
734
+ )?.get;
735
+ function nativeStack(object, getter) {
736
+ if (!getter || getter !== nativeStackGetter)
737
+ throw new Error("Custom stack accessor");
738
+ for (const key of ["name", "message"]) {
739
+ let cursor = object;
740
+ while (cursor) {
741
+ const descriptor = Object.getOwnPropertyDescriptor(cursor, key);
742
+ if (descriptor) {
743
+ if (!("value" in descriptor) || typeof descriptor.value !== "string")
744
+ throw new Error("Custom error formatting");
745
+ break;
746
+ }
747
+ cursor = Object.getPrototypeOf(cursor);
748
+ }
729
749
  }
730
- const name = (0, import_errors2.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
731
- const message = (0, import_errors2.errorProperty)(value, "message");
732
- const stack = (0, import_errors2.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
750
+ const formatter = Object.getOwnPropertyDescriptor(Error, "prepareStackTrace");
751
+ if (formatter && (!("value" in formatter) || !formatter.writable))
752
+ throw new Error("Custom stack formatter");
753
+ Object.defineProperty(Error, "prepareStackTrace", {
754
+ ...formatter ?? { configurable: true, writable: true },
755
+ value: void 0
756
+ });
757
+ try {
758
+ return getter.call(object);
759
+ } finally {
760
+ if (formatter) Object.defineProperty(Error, "prepareStackTrace", formatter);
761
+ else Reflect.deleteProperty(Error, "prepareStackTrace");
762
+ }
763
+ }
764
+ var pointer = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1");
765
+ function captureDiagnosticContent(error, project) {
766
+ const omissions = [];
767
+ const redactions = [];
768
+ const seen = /* @__PURE__ */ new WeakMap();
769
+ let nodes = 0;
770
+ let remaining = import_telemetry_contracts.DIAGNOSTIC_LIMITS.bytes - 64 * 1024;
771
+ const omit = (path, reason, extra = {}) => {
772
+ if (omissions.length < 1e3)
773
+ omissions.push({ path: path.slice(0, 4096), reason, ...extra });
774
+ return { $omitted: reason };
775
+ };
776
+ const visit = (value, path, depth) => {
777
+ if (++nodes > import_telemetry_contracts.DIAGNOSTIC_LIMITS.nodes) return omit(path, "node_limit");
778
+ if (depth > import_telemetry_contracts.DIAGNOSTIC_LIMITS.depth) return omit(path, "depth_limit");
779
+ if (typeof value === "string") {
780
+ const clean = (0, import_telemetry_contracts.redactDiagnosticText)(value);
781
+ if (clean !== value && redactions.length < 1e3)
782
+ redactions.push({
783
+ path: path.slice(0, 4096),
784
+ reason: "credential_text"
785
+ });
786
+ const bytes = encoder.encode(JSON.stringify(clean)).length;
787
+ if (bytes > remaining)
788
+ return omit(path, "byte_limit", {
789
+ originalBytes: encoder.encode(value).length
790
+ });
791
+ remaining -= bytes;
792
+ return clean;
793
+ }
794
+ if (value === null || typeof value === "boolean") return value;
795
+ if (typeof value === "number")
796
+ return Number.isFinite(value) ? value : { $type: "number", value: String(value) };
797
+ if (typeof value === "bigint")
798
+ return { $type: "bigint", value: value.toString() };
799
+ if (typeof value === "undefined") return { $type: "undefined" };
800
+ if (typeof value === "function" || typeof value === "symbol")
801
+ return omit(path, `unsupported_${typeof value}`);
802
+ const object = value;
803
+ const reference = seen.get(object);
804
+ if (reference) return { $ref: reference };
805
+ seen.set(object, path);
806
+ remaining -= 32;
807
+ if (remaining < 0) return omit(path, "byte_limit");
733
808
  try {
734
- return value.stack;
809
+ if (value instanceof Date)
810
+ return { $type: "Date", value: Date.prototype.toISOString.call(value) };
811
+ if (value instanceof Map || value instanceof Set) {
812
+ const isMap = value instanceof Map;
813
+ const iterator = isMap ? Map.prototype.entries.call(value) : Set.prototype.values.call(value);
814
+ const count = Object.getOwnPropertyDescriptor(
815
+ isMap ? Map.prototype : Set.prototype,
816
+ "size"
817
+ )?.get?.call(value);
818
+ const entries = [];
819
+ for (let index = 0; index < Math.min(count, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties); index++)
820
+ entries.push(iterator.next().value);
821
+ if (count > entries.length)
822
+ omissions.push({
823
+ path,
824
+ reason: "collection_limit",
825
+ originalCount: count
826
+ });
827
+ return isMap ? {
828
+ $type: "Map",
829
+ entries: visit(entries, `${path}/entries`, depth + 1)
830
+ } : {
831
+ $type: "Set",
832
+ values: visit(entries, `${path}/values`, depth + 1)
833
+ };
834
+ }
835
+ if (Array.isArray(value)) {
836
+ const count = Math.min(value.length, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties);
837
+ const items = Array.from(
838
+ { length: count },
839
+ (_, index) => read(value, String(index), `${path}/${index}`, depth)
840
+ );
841
+ if (count < value.length)
842
+ items.push(
843
+ omit(path, "collection_limit", { originalCount: value.length })
844
+ );
845
+ return items;
846
+ }
847
+ const keys = Object.getOwnPropertyNames(object);
848
+ const result = /* @__PURE__ */ Object.create(null);
849
+ if (value instanceof Error) {
850
+ const ownName = Object.getOwnPropertyDescriptor(object, "name");
851
+ let type = ownName && "value" in ownName ? ownName.value : void 0;
852
+ if (typeof type !== "string") {
853
+ const name = Object.getOwnPropertyDescriptor(
854
+ Object.getPrototypeOf(value),
855
+ "name"
856
+ );
857
+ type = name && "value" in name ? name.value : "Error";
858
+ }
859
+ result.type = visit(
860
+ typeof type === "string" ? type : "Error",
861
+ `${path}/type`,
862
+ depth + 1
863
+ );
864
+ }
865
+ for (const key of keys.slice(0, import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties)) {
866
+ if (["__proto__", "constructor", "prototype"].includes(key)) {
867
+ result[key] = omit(`${path}/${pointer(key)}`, "unsafe_key");
868
+ continue;
869
+ }
870
+ remaining -= encoder.encode(key).length + 4;
871
+ result[key] = read(object, key, `${path}/${pointer(key)}`, depth);
872
+ }
873
+ if (keys.length > import_telemetry_contracts.DIAGNOSTIC_LIMITS.properties)
874
+ result.$omittedProperties = omit(path, "property_limit", {
875
+ originalCount: keys.length
876
+ });
877
+ return result;
735
878
  } catch {
736
- return void 0;
879
+ return omit(path, "unreadable_object");
880
+ }
881
+ };
882
+ const read = (object, key, path, depth) => {
883
+ if ((0, import_telemetry_contracts.isPrivateDiagnosticField)(key)) {
884
+ if (redactions.length < 1e3)
885
+ redactions.push({
886
+ path: path.slice(0, 4096),
887
+ reason: "credential_field"
888
+ });
889
+ return "[REDACTED]";
890
+ }
891
+ try {
892
+ const property = Object.getOwnPropertyDescriptor(object, key);
893
+ if (property && "value" in property)
894
+ return visit(property.value, path, depth + 1);
895
+ if (key === "stack" && object instanceof Error && property?.get && property.get === nativeStackGetter)
896
+ return visit(nativeStack(object, property.get), path, depth + 1);
897
+ return omit(
898
+ path,
899
+ property ? "accessor_not_evaluated" : "unavailable_property"
900
+ );
901
+ } catch {
902
+ return omit(path, "unreadable_property");
737
903
  }
738
- })() : void 0);
739
- const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors2.errorProperty)(value, "cause") : null;
740
- return {
741
- type: typeof name === "string" ? name.slice(0, 256) : "Error",
742
- message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
743
- ...typeof stack === "string" ? { stack: stack.slice(0, MAX_STACK_LENGTH) } : {},
744
- ...cause !== null && cause !== void 0 ? { cause: projectDetails(cause, depth + 1, seen) } : {}
745
904
  };
746
- };
747
- var sanitizeProjectedDetails = (details, depth = 0) => ({
748
- ...typeof details.type === "string" ? { type: details.type.slice(0, 256) } : {},
749
- message: typeof details.message === "string" ? details.message.slice(0, 8192) : "Execution reported a fault.",
750
- ...typeof details.stack === "string" ? { stack: details.stack.slice(0, MAX_STACK_LENGTH) } : {},
751
- ...depth < MAX_CAUSE_DEPTH && details.cause ? { cause: sanitizeProjectedDetails(details.cause, depth + 1) } : {}
752
- });
753
- function exceptionDiagnostics(error, project) {
754
905
  try {
755
- const details = project ? project(error) : projectDetails(error, 0, /* @__PURE__ */ new WeakSet());
756
- return details ? sanitizeProjectedDetails(details) : null;
906
+ const original = project ? project(error) : error;
907
+ if (project && original === null) return null;
908
+ const captured = visit(original, "#/data", 0);
909
+ const upstreamOmissions = (0, import_errors2.errorProperty)(original, "diagnosticOmissions");
910
+ if (Array.isArray(upstreamOmissions))
911
+ for (const note of upstreamOmissions.slice(0, 1e3)) {
912
+ const path = (0, import_errors2.errorProperty)(note, "path");
913
+ const reason = (0, import_errors2.errorProperty)(note, "reason");
914
+ if (typeof path === "string" && typeof reason === "string")
915
+ omissions.push({
916
+ path: (0, import_telemetry_contracts.redactDiagnosticText)(path).slice(0, 4096),
917
+ reason: (0, import_telemetry_contracts.redactDiagnosticText)(reason).slice(0, 128)
918
+ });
919
+ }
920
+ const data = captured && typeof captured === "object" && !Array.isArray(captured) ? captured : {
921
+ type: "Error",
922
+ message: typeof captured === "string" ? captured : "Execution reported a fault.",
923
+ value: captured
924
+ };
925
+ if (typeof data.message !== "string" && !("message" in data))
926
+ data.message = "Execution reported a fault.";
927
+ if (!("type" in data))
928
+ data.type = typeof data.name === "string" ? data.name : "Error";
929
+ const content = {
930
+ schemaVersion: 1,
931
+ data,
932
+ capture: {
933
+ status: omissions.length ? "partial" : "complete",
934
+ omissions,
935
+ redactions
936
+ }
937
+ };
938
+ if (encoder.encode(JSON.stringify(content)).length > import_telemetry_contracts.DIAGNOSTIC_LIMITS.bytes)
939
+ return {
940
+ schemaVersion: 1,
941
+ data: {
942
+ type: "Error",
943
+ message: "Diagnostic exceeded the capture budget."
944
+ },
945
+ capture: {
946
+ status: "partial",
947
+ redactions: [],
948
+ omissions: [{ path: "#/data", reason: "serialized_byte_limit" }]
949
+ }
950
+ };
951
+ return content;
757
952
  } catch {
758
- return null;
953
+ return {
954
+ schemaVersion: 1,
955
+ data: { type: "Error", message: "Diagnostic capture failed." },
956
+ capture: {
957
+ status: "failed",
958
+ redactions: [],
959
+ omissions: [{ path: "#/data", reason: "projection_or_capture_failed" }]
960
+ }
961
+ };
759
962
  }
760
963
  }
964
+
965
+ // src/error-diagnostics.ts
966
+ function exceptionDiagnostics(error, project) {
967
+ const content = captureDiagnosticContent(error, project);
968
+ return !content || content.capture.status === "failed" ? null : content.data;
969
+ }
761
970
  function errorDiagnostics(error, project) {
762
- const details = exceptionDiagnostics(error, project);
971
+ const captured = exceptionDiagnostics(error, project);
972
+ const details = captured ? (0, import_telemetry_contracts2.summarizeDiagnostic)(captured) : null;
763
973
  return details ? {
764
- errorMessage: details.message,
765
- ...details.type ? { errorType: details.type } : {}
974
+ errorMessage: details.message.slice(0, 8192),
975
+ ...details.type ? { errorType: details.type.slice(0, 256) } : {}
766
976
  } : {};
767
977
  }
768
978
 
@@ -854,10 +1064,40 @@ async function closeOwnedTools(owned) {
854
1064
  function captureToolError(observation, error, policy, returned = false) {
855
1065
  try {
856
1066
  const reported = returned && error && typeof error === "object" && "isError" in error && error.isError === true && "content" in error && typeof error.content === "string" ? { name: "ToolError", message: error.content } : error;
1067
+ let projected = false;
1068
+ let projection;
1069
+ let projectionFailure;
1070
+ let projectionFailed = false;
1071
+ const project = policy?.error ? (original) => {
1072
+ if (!projected) {
1073
+ projected = true;
1074
+ try {
1075
+ projection = policy.error?.(original);
1076
+ } catch (error2) {
1077
+ projectionFailure = error2;
1078
+ projectionFailed = true;
1079
+ }
1080
+ }
1081
+ if (projectionFailed) throw projectionFailure;
1082
+ return projection ?? null;
1083
+ } : void 0;
857
1084
  Object.assign(
858
1085
  observation,
859
- errorDiagnostics(policy?.error ? error : reported, policy?.error)
1086
+ errorDiagnostics(policy?.error ? error : reported, project)
1087
+ );
1088
+ const diagnosticId = getHookContext().reasonTrace?.reportError?.(
1089
+ policy?.error ? error : reported,
1090
+ {
1091
+ mechanism: "tool",
1092
+ ...project ? { error: project } : {},
1093
+ correlation: {
1094
+ toolCallId: observation.toolCallId,
1095
+ attemptId: observation.attemptId
1096
+ }
1097
+ }
860
1098
  );
1099
+ if (typeof diagnosticId === "string")
1100
+ observation.diagnosticId = diagnosticId;
861
1101
  } catch {
862
1102
  }
863
1103
  }
@@ -2743,7 +2983,7 @@ var runReasonToolLoop = async (args) => {
2743
2983
  interruptInstructions: [
2744
2984
  "Human-input rules:",
2745
2985
  "- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
2746
- "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
2986
+ "- You may combine human-input tools with domain tools and output controls. Tools, emitted outputs, and human-input requests run in their requested order. Terminal output publishes last using the completed results.",
2747
2987
  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."
2748
2988
  ].join("\n")
2749
2989
  } : {},
@@ -2753,7 +2993,7 @@ var runReasonToolLoop = async (args) => {
2753
2993
  "- The kortyx_emit__* tools publish validated structured output and return an acknowledgement. You may continue with text or more tools afterward.",
2754
2994
  "- The kortyx_return__* tools publish a validated result and end this reasoning operation. If return tools are available, call exactly one before finishing.",
2755
2995
  "- 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."
2996
+ "- You may combine multiple emit controls, domain tools, and human-input requests in one turn. Tools, emitted outputs, and human-input requests run in their requested order; the terminal return runs last. Outputs after domain tools or human input are regenerated using their completed results. Request at most one terminal return per turn."
2757
2997
  ].join("\n")
2758
2998
  } : {}
2759
2999
  });
@@ -2782,6 +3022,19 @@ var runReasonToolLoop = async (args) => {
2782
3022
  );
2783
3023
  let pending = checkpoint?.pending;
2784
3024
  let pendingStepIndex = checkpoint?.pendingStepIndex;
3025
+ let outputContractCorrections = checkpoint?.outputContractCorrections ?? 0;
3026
+ const correctionExhausted = () => {
3027
+ const message = `useReason could not complete output-contract correction within toolExecution.maxSteps (${maxSteps}). Select exactly one terminal return contract; multiple emit contracts are allowed. Increase maxSteps to allow correction and output generation.`;
3028
+ return new import_errors6.ProviderRequestError(message, {
3029
+ code: "REASON_OUTPUT_CONTRACT_CORRECTION_EXHAUSTED",
3030
+ operation: "useReason",
3031
+ safeMessage: message,
3032
+ retryable: false,
3033
+ details: { maxSteps, outputContractCorrections }
3034
+ });
3035
+ };
3036
+ const resolvedInterrupts = checkpoint?.resolvedInterrupts ?? [];
3037
+ for (const request of resolvedInterrupts) ctx.node.replayInterrupt?.(request);
2785
3038
  const approvedCalls = checkpoint?.approvedCalls ?? [];
2786
3039
  const interruptHistory = checkpoint?.interruptHistory ?? [];
2787
3040
  const emissions = checkpoint?.emissions ?? [];
@@ -2793,8 +3046,10 @@ var runReasonToolLoop = async (args) => {
2793
3046
  messages,
2794
3047
  steps,
2795
3048
  approvedCalls,
3049
+ resolvedInterrupts,
2796
3050
  interruptHistory,
2797
3051
  emissions,
3052
+ outputContractCorrections,
2798
3053
  ...returned ? { returned } : {},
2799
3054
  finalizing,
2800
3055
  ...pending ? { pending } : {},
@@ -2802,6 +3057,10 @@ var runReasonToolLoop = async (args) => {
2802
3057
  };
2803
3058
  ctx.stateDirty = true;
2804
3059
  };
3060
+ const rememberInterrupt = (request) => {
3061
+ resolvedInterrupts.push(request);
3062
+ save();
3063
+ };
2805
3064
  const emitToolEvent = (event, payload) => {
2806
3065
  ctx.node.emit(event, {
2807
3066
  ...payload,
@@ -2811,7 +3070,7 @@ var runReasonToolLoop = async (args) => {
2811
3070
  });
2812
3071
  };
2813
3072
  try {
2814
- for (let stepIndex = pending ? pendingStepIndex ?? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
3073
+ for (let stepIndex = pending ? pendingStepIndex ?? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex = steps.length) {
2815
3074
  (0, import_core5.throwIfExecutionAborted)(abortSignal);
2816
3075
  traceSpan?.addEvent?.("useReason.tool-step.start", {
2817
3076
  stepIndex,
@@ -2948,273 +3207,301 @@ var runReasonToolLoop = async (args) => {
2948
3207
  const outputCalls = toolCalls.filter(
2949
3208
  (call) => outputByToolName.has(call.name)
2950
3209
  );
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;
3210
+ const returnCalls = outputCalls.filter(
3211
+ (call) => outputByToolName.get(call.name)?.kind === "return"
3212
+ );
3213
+ if (returnCalls.length > 1) {
3214
+ let corrected = false;
3215
+ for (const call of returnCalls) {
3216
+ if (toolResults.some((result3) => result3.toolCallId === call.id))
3217
+ continue;
3218
+ const result2 = {
3219
+ toolCallId: call.id,
3220
+ name: call.name,
3221
+ isError: true,
3222
+ content: "MULTIPLE_TERMINAL_OUTPUTS: No terminal output was published. Select exactly one return contract using the tool and human-input results. Do not repeat completed calls. Multiple emit contracts are allowed."
3223
+ };
3224
+ toolResults.push(result2);
3225
+ messages.push({ role: "tool", ...result2 });
3226
+ corrected = true;
2968
3227
  }
2969
- if (resolved.kind === "emit" && emissions.length >= (useReasonArgs.outputs?.maxEmissions ?? 16))
2970
- throw new Error(
2971
- `useReason reached outputs.maxEmissions (${useReasonArgs.outputs?.maxEmissions ?? 16}).`
3228
+ if (corrected) outputContractCorrections += 1;
3229
+ save();
3230
+ }
3231
+ const orderedCalls = [
3232
+ ...toolCalls.filter(
3233
+ (call) => outputByToolName.get(call.name)?.kind !== "return"
3234
+ ),
3235
+ ...returnCalls
3236
+ ];
3237
+ for (const toolCall of orderedCalls) {
3238
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
3239
+ if (outputByToolName.has(toolCall.name)) {
3240
+ const call = toolCall;
3241
+ const hasEvidenceCalls = toolResults.some(
3242
+ (result3) => !outputByToolName.has(result3.name)
2972
3243
  );
2973
- let data;
2974
- if (resolved.stream) {
2975
- if (!stream)
3244
+ const resolved = outputByToolName.get(call.name);
3245
+ if (!resolved)
2976
3246
  throw new Error(
2977
- `useReason streaming output contract "${resolved.name}" requires stream: true.`
3247
+ `Unknown useReason output contract tool "${call.name}".`
2978
3248
  );
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)
3249
+ if (toolResults.some((result3) => result3.toolCallId === call.id)) {
3250
+ if (resolved.kind === "return" && returned) completed = true;
3251
+ continue;
3252
+ }
3253
+ if (resolved.kind === "emit" && emissions.length >= (useReasonArgs.outputs?.maxEmissions ?? 16))
2985
3254
  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 }
3255
+ `useReason reached outputs.maxEmissions (${useReasonArgs.outputs?.maxEmissions ?? 16}).`
3032
3256
  );
3033
- if (outputStep.toolCalls?.length)
3257
+ let data;
3258
+ if (resolved.stream || hasEvidenceCalls) {
3259
+ if (resolved.stream && !stream)
3034
3260
  throw new Error(
3035
- `Provider requested a tool during streamed output contract "${resolved.name}".`
3261
+ `useReason streaming output contract "${resolved.name}" requires stream: true.`
3036
3262
  );
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
3263
+ if (steps.length >= maxSteps) {
3264
+ const message = `useReason output generation requires another model pass within toolExecution.maxSteps (${maxSteps}). Increase maxSteps to allow outputs to use completed tool and interrupt results.`;
3265
+ throw new import_errors6.ProviderRequestError(message, {
3266
+ code: "REASON_OUTPUT_BUDGET_EXHAUSTED",
3267
+ operation: "useReason",
3268
+ safeMessage: message,
3269
+ retryable: false,
3270
+ details: { maxSteps, modelPasses: steps.length }
3271
+ });
3272
+ }
3273
+ const streamInstruction = call.input && typeof call.input === "object" && !Array.isArray(call.input) ? call.input.instruction : void 0;
3274
+ if (resolved.stream && (typeof streamInstruction !== "string" || streamInstruction.trim().length === 0))
3275
+ throw new Error(
3276
+ `useReason streaming output contract "${resolved.name}" requires an instruction.`
3277
+ );
3278
+ const instruction = [
3279
+ `Generate output contract "${resolved.name}": ${resolved.contract.description}`,
3280
+ "Use the completed tool and human-input results. Do not assume results for pending calls.",
3281
+ resolved.stream ? String(streamInstruction) : `Regenerate this original draft, which was written before those results were available: ${JSON.stringify(call.input)}`
3282
+ ].join("\n\n");
3283
+ const streamId = `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissions.length : "return"}`;
3284
+ const structured = {
3285
+ dataType: resolved.contract.schemaId,
3286
+ schemaId: resolved.contract.schemaId,
3287
+ schemaVersion: resolved.contract.schemaVersion,
3288
+ fields: resolved.contract.stream?.fields
3289
+ };
3290
+ const inferred = inferOutputFormat(
3291
+ resolved.contract.schema,
3292
+ void 0
3046
3293
  );
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,
3294
+ const outputRecord = {
3295
+ stepIndex: steps.length,
3058
3296
  kind: "output",
3059
- text: outputStep.text,
3297
+ text: "",
3060
3298
  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 {
3299
+ toolResults: []
3300
+ };
3301
+ steps.push(outputRecord);
3302
+ save();
3303
+ try {
3304
+ const outputStep = await reasonEngine(
3305
+ {
3306
+ ...useReasonArgs,
3307
+ tools: [],
3308
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3309
+ messages: [
3310
+ ...messages,
3311
+ ...toolCalls.filter(
3312
+ (pendingCall) => !toolResults.some(
3313
+ (result3) => result3.toolCallId === pendingCall.id
3314
+ )
3315
+ ).map(
3316
+ (pendingCall) => ({
3317
+ role: "tool",
3318
+ toolCallId: pendingCall.id,
3319
+ name: pendingCall.name,
3320
+ content: "This call has not completed yet. Use only completed results; do not assume a result for this pending call."
3321
+ })
3322
+ ),
3323
+ {
3324
+ role: "user",
3325
+ content: withOutputGuardrails(
3326
+ instruction,
3327
+ resolved.contract.schema
3328
+ )
3329
+ }
3330
+ ],
3331
+ emit: false,
3332
+ stream: resolved.stream && stream,
3333
+ ...emit && resolved.stream && stream ? {
3334
+ onTextChunk: createStructuredOutputStreamer(
3335
+ { structured },
3336
+ id,
3337
+ streamId
3338
+ )
3339
+ } : {}
3340
+ },
3341
+ { ...id ? { id } : {}, opId }
3342
+ );
3343
+ if (outputStep.toolCalls?.length)
3344
+ throw new Error(
3345
+ `Provider requested a tool during streamed output contract "${resolved.name}".`
3346
+ );
3347
+ aggregatedUsage = mergeUsage(aggregatedUsage, outputStep.usage);
3348
+ accumulateTokenUsage(outputStep.usage);
3349
+ aggregatedWarnings = mergeWarnings(
3350
+ aggregatedWarnings,
3351
+ mergeWarnings(inferred.warnings, outputStep.warnings)
3352
+ );
3353
+ aggregatedProviderMetadata = mergeProviderMetadata(
3354
+ aggregatedProviderMetadata,
3355
+ outputStep.providerMetadata
3356
+ );
3357
+ finalFinishReason = outputStep.finishReason;
3358
+ finalRaw = outputStep.raw;
3359
+ Object.assign(outputRecord, {
3360
+ stepIndex: outputRecord.stepIndex,
3361
+ kind: "output",
3362
+ text: outputStep.text,
3363
+ toolCalls: [],
3364
+ toolResults: [],
3365
+ ...outputStep.usage ? { usage: outputStep.usage } : {},
3366
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3367
+ ...outputStep.providerMetadata ? { providerMetadata: outputStep.providerMetadata } : {},
3368
+ ...outputStep.warnings ? { warnings: outputStep.warnings } : {}
3369
+ });
3370
+ save();
3371
+ data = parseReasonOutputWithSchema({
3372
+ text: outputStep.text,
3373
+ schema: resolved.contract.schema,
3374
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3375
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3376
+ label: `useReason output contract "${resolved.name}"`
3377
+ });
3378
+ } catch (error) {
3379
+ traceSpan?.addEvent?.("useReason.output.invalidated", {
3380
+ contract: resolved.name,
3381
+ kind: resolved.kind,
3382
+ schemaId: resolved.contract.schemaId,
3383
+ schemaVersion: resolved.contract.schemaVersion,
3384
+ streamId
3385
+ });
3386
+ if (emit && resolved.stream) {
3387
+ try {
3388
+ ctx.node.emit("structured_data_invalidated", {
3389
+ node: ctx.node.graph.node,
3390
+ streamId
3391
+ });
3392
+ } catch {
3393
+ }
3083
3394
  }
3395
+ throw error;
3084
3396
  }
3085
- throw error;
3397
+ } else {
3398
+ data = parseWithSchema(
3399
+ resolved.contract.schema,
3400
+ call.input,
3401
+ `useReason output contract "${resolved.name}"`
3402
+ );
3086
3403
  }
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,
3404
+ const entry = { contract: resolved.name, data };
3405
+ const emissionIndex = emissions.length;
3406
+ if (resolved.kind === "emit")
3407
+ emissions.push(entry);
3408
+ else returned = entry;
3409
+ if (emit)
3410
+ emitStructuredData({
3411
+ kind: "final",
3412
+ data,
3413
+ dataType: resolved.contract.schemaId,
3414
+ schemaId: resolved.contract.schemaId,
3415
+ schemaVersion: resolved.contract.schemaVersion,
3416
+ streamId: `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissionIndex : "return"}`,
3417
+ ...id ? { id } : {}
3418
+ });
3419
+ traceSpan?.addEvent?.("useReason.output.emitted", {
3420
+ contract: resolved.name,
3421
+ kind: resolved.kind,
3104
3422
  schemaId: resolved.contract.schemaId,
3105
3423
  schemaVersion: resolved.contract.schemaVersion,
3106
- streamId: `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissionIndex : "return"}`,
3107
- ...id ? { id } : {}
3424
+ emissionIndex,
3425
+ data
3108
3426
  });
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;
3427
+ const result2 = normalizeToolResult(call, { emitted: true, data });
3428
+ toolResults.push(result2);
3429
+ messages.push({
3430
+ role: "tool",
3431
+ content: result2.content,
3432
+ toolCallId: result2.toolCallId,
3433
+ name: result2.name
3434
+ });
3435
+ if (resolved.kind === "return") completed = true;
3121
3436
  save();
3122
- break;
3437
+ continue;
3123
3438
  }
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
- }
3137
- const interruptCalls = toolCalls.filter(
3138
- (call) => interruptByToolName.has(call.name)
3139
- );
3140
- if (interruptCalls.length > 0) {
3141
- if (toolCalls.length !== 1)
3142
- throw new Error(
3143
- "A useReason interrupt contract call must be the only tool call in its model turn."
3144
- );
3145
- const interruptCall = interruptCalls[0];
3146
- if (!interruptCall)
3147
- throw new Error("Missing useReason interrupt control call.");
3148
- if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
3149
- throw new Error(
3150
- `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3151
- );
3152
- const resolved = interruptByToolName.get(interruptCall.name);
3153
- if (!resolved)
3154
- throw new Error(
3155
- `Unknown useReason interrupt contract tool "${interruptCall.name}".`
3439
+ if (interruptByToolName.has(toolCall.name)) {
3440
+ const resolved = interruptByToolName.get(toolCall.name);
3441
+ if (!resolved)
3442
+ throw new Error(
3443
+ `Unknown useReason interrupt contract tool "${toolCall.name}".`
3444
+ );
3445
+ if (toolResults.some((result3) => result3.toolCallId === toolCall.id)) {
3446
+ continue;
3447
+ }
3448
+ if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
3449
+ throw new Error(
3450
+ `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3451
+ );
3452
+ const request = parseWithSchema(
3453
+ resolved.contract.requestSchema,
3454
+ toolCall.input,
3455
+ `useReason interrupt contract "${resolved.name}" request`
3156
3456
  );
3157
- if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
3158
- pending = void 0;
3159
- pendingStepIndex = void 0;
3457
+ traceSpan?.addEvent?.("useReason.interrupt.requested", {
3458
+ checkpointKey,
3459
+ contract: resolved.name,
3460
+ interruptIndex: interruptHistory.length
3461
+ });
3462
+ const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
3463
+ const response = await awaitInterruptInternal({
3464
+ onResolved: rememberInterrupt,
3465
+ request: {
3466
+ kind: "custom",
3467
+ request,
3468
+ contract: resolved.name,
3469
+ ...question ? { question } : {},
3470
+ schemaId: resolved.contract.schemaId,
3471
+ schemaVersion: resolved.contract.schemaVersion
3472
+ },
3473
+ responseSchema: resolved.contract.responseSchema,
3474
+ id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
3475
+ meta: {
3476
+ __kortyxReason: {
3477
+ opId,
3478
+ stepIndex,
3479
+ interruptIndex: interruptHistory.length,
3480
+ contract: resolved.name
3481
+ }
3482
+ }
3483
+ });
3484
+ const result2 = normalizeToolResult(toolCall, response);
3485
+ toolResults.push(result2);
3486
+ messages.push({
3487
+ role: "tool",
3488
+ content: result2.content,
3489
+ toolCallId: result2.toolCallId,
3490
+ name: result2.name
3491
+ });
3492
+ interruptHistory.push({
3493
+ contract: resolved.name,
3494
+ request,
3495
+ response
3496
+ });
3497
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3498
+ checkpointKey,
3499
+ contract: resolved.name,
3500
+ interruptIndex: interruptHistory.length - 1
3501
+ });
3160
3502
  save();
3161
3503
  continue;
3162
3504
  }
3163
- const request = parseWithSchema(
3164
- resolved.contract.requestSchema,
3165
- interruptCall.input,
3166
- `useReason interrupt contract "${resolved.name}" request`
3167
- );
3168
- traceSpan?.addEvent?.("useReason.interrupt.requested", {
3169
- checkpointKey,
3170
- contract: resolved.name,
3171
- interruptIndex: interruptHistory.length
3172
- });
3173
- const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
3174
- const response = await awaitInterruptInternal({
3175
- request: {
3176
- kind: "custom",
3177
- request,
3178
- contract: resolved.name,
3179
- ...question ? { question } : {},
3180
- schemaId: resolved.contract.schemaId,
3181
- schemaVersion: resolved.contract.schemaVersion
3182
- },
3183
- responseSchema: resolved.contract.responseSchema,
3184
- id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
3185
- meta: {
3186
- __kortyxReason: {
3187
- opId,
3188
- stepIndex,
3189
- interruptIndex: interruptHistory.length,
3190
- contract: resolved.name
3191
- }
3192
- }
3193
- });
3194
- const result2 = normalizeToolResult(interruptCall, response);
3195
- toolResults.push(result2);
3196
- messages.push({
3197
- role: "tool",
3198
- content: result2.content,
3199
- toolCallId: result2.toolCallId,
3200
- name: result2.name
3201
- });
3202
- interruptHistory.push({
3203
- contract: resolved.name,
3204
- request,
3205
- response
3206
- });
3207
- traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3208
- checkpointKey,
3209
- contract: resolved.name,
3210
- interruptIndex: interruptHistory.length - 1
3211
- });
3212
- pending = void 0;
3213
- pendingStepIndex = void 0;
3214
- save();
3215
- continue;
3216
- }
3217
- for (const toolCall of toolCalls) {
3218
3505
  if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
3219
3506
  await replayToolObservations(
3220
3507
  steps[stepIndex]?.toolObservations?.filter(
@@ -3260,6 +3547,7 @@ var runReasonToolLoop = async (args) => {
3260
3547
  "waiting"
3261
3548
  );
3262
3549
  const approval = await awaitInterruptInternal({
3550
+ onResolved: rememberInterrupt,
3263
3551
  id: `tool:${toolCall.id}`,
3264
3552
  request: {
3265
3553
  kind: "choice",
@@ -3374,22 +3662,24 @@ var runReasonToolLoop = async (args) => {
3374
3662
  });
3375
3663
  }
3376
3664
  }
3377
- const completed2 = toolResults.at(-1);
3378
- if (completed2)
3665
+ const completedToolResult = toolResults.at(-1);
3666
+ if (completedToolResult)
3379
3667
  messages.push({
3380
3668
  role: "tool",
3381
- content: completed2.content,
3382
- toolCallId: completed2.toolCallId,
3383
- name: completed2.name,
3384
- ...completed2.isError !== void 0 ? { isError: completed2.isError } : {}
3669
+ content: completedToolResult.content,
3670
+ toolCallId: completedToolResult.toolCallId,
3671
+ name: completedToolResult.name,
3672
+ ...completedToolResult.isError !== void 0 ? { isError: completedToolResult.isError } : {}
3385
3673
  });
3386
3674
  save();
3387
3675
  }
3388
3676
  pending = void 0;
3389
3677
  pendingStepIndex = void 0;
3390
3678
  save();
3679
+ if (completed) break;
3391
3680
  }
3392
3681
  if (!completed) {
3682
+ if (outputContractCorrections > 0) throw correctionExhausted();
3393
3683
  throw new Error(
3394
3684
  `useReason tool loop reached maxSteps (${maxSteps}) before producing a final response.`
3395
3685
  );
@@ -3451,7 +3741,11 @@ var runReasonToolLoop = async (args) => {
3451
3741
  output: useReasonArgs.telemetry?.output ?? returned?.data ?? finalOutput ?? finalText
3452
3742
  }
3453
3743
  });
3454
- ctx.currentNodeState.byKey[checkpointKey] = { status: "completed", result };
3744
+ ctx.currentNodeState.byKey[checkpointKey] = {
3745
+ status: "completed",
3746
+ result,
3747
+ resolvedInterrupts
3748
+ };
3455
3749
  ctx.stateDirty = true;
3456
3750
  return result;
3457
3751
  } catch (error) {
@@ -3528,6 +3822,8 @@ async function runReason(args) {
3528
3822
  const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
3529
3823
  const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
3530
3824
  if (existingCompleted) {
3825
+ for (const request of existingCompleted.resolvedInterrupts ?? [])
3826
+ ctx.node.replayInterrupt?.(request);
3531
3827
  await replayToolObservations(
3532
3828
  existingCompleted.result.steps?.flatMap(
3533
3829
  (step) => step.toolObservations ?? []