@kortyx/agent 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -526,6 +526,7 @@ var require_dist = __commonJS({
526
526
  completeResponse: () => completeResponse,
527
527
  createWorkflowHooks: () => createWorkflowHooks,
528
528
  defineInterruptContract: () => defineInterruptContract,
529
+ defineOutputContract: () => defineOutputContract,
529
530
  emitWorkflowCall: () => emitWorkflowCall2,
530
531
  parallel: () => parallel,
531
532
  reportError: () => reportError,
@@ -926,6 +927,112 @@ var require_dist = __commonJS({
926
927
  return normalized;
927
928
  };
928
929
  var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
930
+ var import_zod22 = require("zod");
931
+ var parseStructuredPath = (path, source) => {
932
+ if (path.length === 0) {
933
+ throw new Error(`${source} path must be a non-empty dot-separated string.`);
934
+ }
935
+ const parts = path.split(".");
936
+ if (parts.some((part) => part.length === 0)) {
937
+ throw new Error(
938
+ `${source} path "${path}" must not contain empty segments.`
939
+ );
940
+ }
941
+ return parts;
942
+ };
943
+ var assertStructuredPath = (path, source) => {
944
+ parseStructuredPath(path, source);
945
+ };
946
+ var EMIT_PREFIX = "kortyx_emit__";
947
+ var RETURN_PREFIX = "kortyx_return__";
948
+ var STREAM_EMIT_PREFIX = "kortyx_stream_emit__";
949
+ var STREAM_RETURN_PREFIX = "kortyx_stream_return__";
950
+ var legacyOutputWarningEmitted = false;
951
+ var warnLegacyReasonOutput = () => {
952
+ if (legacyOutputWarningEmitted) return;
953
+ legacyOutputWarningEmitted = true;
954
+ process.emitWarning(
955
+ "useReason({ outputSchema, structured }) and result.output are deprecated and will be removed in the next major release. Define reusable contracts with defineOutputContract() and pass them through useReason({ outputs }). See docs/internal/next-major-removals.md.",
956
+ { code: "KORTYX_USE_REASON_OUTPUT_DEPRECATED", type: "DeprecationWarning" }
957
+ );
958
+ };
959
+ var defineOutputContract = (contract) => {
960
+ if (!contract || typeof contract !== "object")
961
+ throw new Error("defineOutputContract requires a contract object.");
962
+ for (const [field, value] of [
963
+ ["description", contract.description],
964
+ ["schemaId", contract.schemaId],
965
+ ["schemaVersion", contract.schemaVersion]
966
+ ])
967
+ if (typeof value !== "string" || value.trim().length === 0)
968
+ throw new Error(`Output contract ${field} must be a non-empty string.`);
969
+ if (typeof contract.schema?.safeParse !== "function")
970
+ throw new Error("Output contract schema must provide safeParse(value).");
971
+ if (contract.stream) {
972
+ if (!contract.stream.fields || Object.keys(contract.stream.fields).length === 0)
973
+ throw new Error("Output contract stream.fields must not be empty.");
974
+ for (const [path, mode] of Object.entries(contract.stream.fields)) {
975
+ assertStructuredPath(path, "Output contract stream.fields");
976
+ if (!["set", "append", "text-delta"].includes(mode))
977
+ throw new Error(`Invalid output contract stream mode for "${path}".`);
978
+ }
979
+ }
980
+ return contract;
981
+ };
982
+ var outputControlToolName = (kind, name) => `${kind === "emit" ? EMIT_PREFIX : kind === "return" ? RETURN_PREFIX : kind === "stream-emit" ? STREAM_EMIT_PREFIX : STREAM_RETURN_PREFIX}${name}`;
983
+ var isOutputControlToolName = (name) => name.startsWith(EMIT_PREFIX) || name.startsWith(RETURN_PREFIX) || name.startsWith(STREAM_EMIT_PREFIX) || name.startsWith(STREAM_RETURN_PREFIX);
984
+ var normalizeReasonOutputs = (config, hasLegacyOutput) => {
985
+ if (!config) return void 0;
986
+ if (hasLegacyOutput)
987
+ throw new Error(
988
+ "useReason cannot combine `outputs` with deprecated `outputSchema` or `structured`."
989
+ );
990
+ const entries = [
991
+ ...Object.entries(config.emit ?? {}).map(([name, contract]) => ({
992
+ name,
993
+ contract,
994
+ kind: "emit"
995
+ })),
996
+ ...Object.entries(config.return ?? {}).map(([name, contract]) => ({
997
+ name,
998
+ contract,
999
+ kind: "return"
1000
+ }))
1001
+ ];
1002
+ if (entries.length === 0)
1003
+ throw new Error(
1004
+ "useReason outputs must include at least one emit or return contract."
1005
+ );
1006
+ for (const { name, contract, kind } of entries) {
1007
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name) || outputControlToolName(kind, name).length > 64)
1008
+ throw new Error(`Invalid useReason output contract name "${name}".`);
1009
+ defineOutputContract(contract);
1010
+ if (contract.schema["~kortyx"]?.nativeOutput === true)
1011
+ throw new Error(
1012
+ `useReason output contract "${name}" uses a provider-native output schema that cannot be selected through tools. Continue using outputSchema for this provider until a non-tool contract path is available.`
1013
+ );
1014
+ if (contract.stream && outputControlToolName(
1015
+ kind === "emit" ? "stream-emit" : "stream-return",
1016
+ name
1017
+ ).length > 64)
1018
+ throw new Error(
1019
+ `Invalid useReason output contract name "${name}" for streaming tools.`
1020
+ );
1021
+ try {
1022
+ const json = (0, import_zod22.toJSONSchema)(contract.schema);
1023
+ if (json.type !== "object")
1024
+ throw new Error("The tool input schema must have an object root.");
1025
+ } catch (error) {
1026
+ throw new Error(
1027
+ `useReason output contract "${name}" schema must support JSON Schema conversion.`,
1028
+ { cause: error }
1029
+ );
1030
+ }
1031
+ }
1032
+ if (config.maxEmissions !== void 0 && (!Number.isInteger(config.maxEmissions) || config.maxEmissions < 1))
1033
+ throw new Error("useReason outputs.maxEmissions must be at least 1.");
1034
+ return config;
1035
+ };
929
1036
  var import_core62 = require("@kortyx/core");
930
1037
  function safeSpan(span) {
931
1038
  const guard = (fn) => (...args) => {
@@ -1006,21 +1113,6 @@ var require_dist = __commonJS({
1006
1113
  return execution ?? invoke(safeSpan(void 0));
1007
1114
  }
1008
1115
  var import_node_crypto4 = require("crypto");
1009
- var parseStructuredPath = (path, source) => {
1010
- if (path.length === 0) {
1011
- throw new Error(`${source} path must be a non-empty dot-separated string.`);
1012
- }
1013
- const parts = path.split(".");
1014
- if (parts.some((part) => part.length === 0)) {
1015
- throw new Error(
1016
- `${source} path "${path}" must not contain empty segments.`
1017
- );
1018
- }
1019
- return parts;
1020
- };
1021
- var assertStructuredPath = (path, source) => {
1022
- parseStructuredPath(path, source);
1023
- };
1024
1116
  var createStructuredStreamId = () => {
1025
1117
  try {
1026
1118
  return (0, import_node_crypto4.randomUUID)();
@@ -1816,12 +1908,12 @@ var require_dist = __commonJS({
1816
1908
  throw error;
1817
1909
  }
1818
1910
  }
1819
- var import_zod22 = require("zod");
1911
+ var import_zod32 = require("zod");
1820
1912
  function inferOutputFormat(schema, explicit) {
1821
1913
  if (explicit) return { responseFormat: explicit };
1822
1914
  if (!schema) return {};
1823
1915
  try {
1824
- const json = (0, import_zod22.toJSONSchema)(schema);
1916
+ const json = (0, import_zod32.toJSONSchema)(schema);
1825
1917
  const clean = (value) => {
1826
1918
  if (Array.isArray(value)) return value.map(clean);
1827
1919
  if (!value || typeof value !== "object") return value;
@@ -2570,7 +2662,7 @@ var require_dist = __commonJS({
2570
2662
  );
2571
2663
  var createInvalidStructuredOutputError = (label, cause) => new import_errors52.ValidationError(
2572
2664
  "INVALID_MODEL_JSON",
2573
- `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check outputSchema and provider structured-output settings.`,
2665
+ `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check the output contract or outputSchema and provider structured-output settings.`,
2574
2666
  cause
2575
2667
  );
2576
2668
  var parseReasonOutputWithSchema = (args) => {
@@ -2693,7 +2785,7 @@ var require_dist = __commonJS({
2693
2785
  throw error;
2694
2786
  }
2695
2787
  };
2696
- var import_zod32 = require("zod");
2788
+ var import_zod42 = require("zod");
2697
2789
  var tryStringify2 = (value) => {
2698
2790
  if (typeof value === "string") return value;
2699
2791
  try {
@@ -2704,7 +2796,7 @@ var require_dist = __commonJS({
2704
2796
  };
2705
2797
  var tryGetSchemaHint = (schema) => {
2706
2798
  try {
2707
- const hint = (0, import_zod32.toJSONSchema)(schema);
2799
+ const hint = (0, import_zod42.toJSONSchema)(schema);
2708
2800
  return JSON.stringify(hint, null, 2);
2709
2801
  } catch {
2710
2802
  return void 0;
@@ -2902,7 +2994,7 @@ ${tryStringify2(args.response)}`
2902
2994
  };
2903
2995
  var import_core52 = require("@kortyx/core");
2904
2996
  var import_errors62 = require("@kortyx/core/errors");
2905
- var import_zod42 = require("zod");
2997
+ var import_zod52 = require("zod");
2906
2998
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2907
2999
  name: tool.name,
2908
3000
  ...tool.title ? { title: tool.title } : {},
@@ -2938,10 +3030,14 @@ ${tryStringify2(args.response)}`
2938
3030
  };
2939
3031
  };
2940
3032
  var createInitialMessages = (args) => [
2941
- ...typeof args.system === "string" || args.interruptInstructions ? [
3033
+ ...typeof args.system === "string" || args.interruptInstructions || args.outputInstructions ? [
2942
3034
  {
2943
3035
  role: "system",
2944
- content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
3036
+ content: [
3037
+ args.system,
3038
+ args.interruptInstructions,
3039
+ args.outputInstructions
3040
+ ].filter((value) => Boolean(value)).join("\n\n")
2945
3041
  }
2946
3042
  ] : [],
2947
3043
  { role: "user", content: String(args.input ?? "") }
@@ -2963,12 +3059,13 @@ ${tryStringify2(args.response)}`
2963
3059
  interrupts: useReasonArgs.interrupts
2964
3060
  });
2965
3061
  const interruptByToolName = /* @__PURE__ */ new Map();
3062
+ const outputByToolName = /* @__PURE__ */ new Map();
2966
3063
  const toolByName = /* @__PURE__ */ new Map();
2967
3064
  let separateOutput = false;
2968
3065
  for (const tool of tools2) {
2969
- if (isInterruptControlToolName(tool.name))
3066
+ if (isInterruptControlToolName(tool.name) || isOutputControlToolName(tool.name))
2970
3067
  throw new Error(
2971
- `useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
3068
+ `useReason tool name "${tool.name}" uses a reserved control-tool namespace.`
2972
3069
  );
2973
3070
  if (toolByName.has(tool.name)) {
2974
3071
  throw new Error(`useReason received duplicate tool name "${tool.name}".`);
@@ -2997,7 +3094,7 @@ ${tryStringify2(args.response)}`
2997
3094
  );
2998
3095
  let inputSchema;
2999
3096
  try {
3000
- inputSchema = (0, import_zod42.toJSONSchema)(contract.requestSchema);
3097
+ inputSchema = (0, import_zod52.toJSONSchema)(contract.requestSchema);
3001
3098
  } catch (error) {
3002
3099
  throw new Error(
3003
3100
  `useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
@@ -3021,9 +3118,66 @@ ${tryStringify2(args.response)}`
3021
3118
  }
3022
3119
  });
3023
3120
  }
3121
+ const outputDefinitions = [];
3122
+ for (const kind of ["emit", "return"]) {
3123
+ for (const [name, contract] of Object.entries(
3124
+ useReasonArgs.outputs?.[kind] ?? {}
3125
+ )) {
3126
+ if (contract.stream) {
3127
+ const streamName = outputControlToolName(
3128
+ kind === "emit" ? "stream-emit" : "stream-return",
3129
+ name
3130
+ );
3131
+ outputByToolName.set(streamName, {
3132
+ name,
3133
+ kind,
3134
+ contract,
3135
+ stream: true
3136
+ });
3137
+ outputDefinitions.push({
3138
+ name: streamName,
3139
+ title: `Stream structured output: ${name}`,
3140
+ description: `${contract.description} Call this to start realtime generation of this structured value. The value is generated in a separate streamed model pass, validated, and ${kind === "emit" ? "returned as a tool result so you can continue" : "used to finish this operation"}.`,
3141
+ inputSchema: {
3142
+ type: "object",
3143
+ properties: {
3144
+ instruction: {
3145
+ type: "string",
3146
+ description: "What the streamed output should contain."
3147
+ }
3148
+ },
3149
+ required: ["instruction"],
3150
+ additionalProperties: false
3151
+ },
3152
+ metadata: {
3153
+ kortyxControl: kind === "emit" ? "output.stream.emit" : "output.stream.return",
3154
+ contract: name,
3155
+ schemaId: contract.schemaId,
3156
+ schemaVersion: contract.schemaVersion
3157
+ }
3158
+ });
3159
+ } else {
3160
+ const toolName = outputControlToolName(kind, name);
3161
+ outputByToolName.set(toolName, { name, kind, contract, stream: false });
3162
+ outputDefinitions.push({
3163
+ name: toolName,
3164
+ title: `${kind === "emit" ? "Emit" : "Return"} structured output: ${name}`,
3165
+ description: `${contract.description} ${kind === "emit" ? "Emit this structured value and continue reasoning." : "Return this structured value and finish reasoning."}`,
3166
+ inputSchema: (0, import_zod52.toJSONSchema)(contract.schema),
3167
+ metadata: {
3168
+ kortyxControl: kind === "emit" ? "output.emit" : "output.return",
3169
+ contract: name,
3170
+ schemaId: contract.schemaId,
3171
+ schemaVersion: contract.schemaVersion
3172
+ }
3173
+ });
3174
+ }
3175
+ }
3176
+ }
3024
3177
  const toolDefinitions = [
3025
3178
  ...toToolDefinitions(tools2),
3026
- ...interruptDefinitions
3179
+ ...interruptDefinitions,
3180
+ ...outputDefinitions
3027
3181
  ];
3028
3182
  let finalizing = checkpoint?.finalizing ?? false;
3029
3183
  let completed = false;
@@ -3038,11 +3192,27 @@ ${tryStringify2(args.response)}`
3038
3192
  "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
3039
3193
  interruptConfig.mode === "required" ? "- You must call at least one human-input tool before returning the final answer." : "- Human input is optional; call a human-input tool only when the request cannot be completed safely without it."
3040
3194
  ].join("\n")
3195
+ } : {},
3196
+ ...useReasonArgs.outputs ? {
3197
+ outputInstructions: [
3198
+ "Structured output rules:",
3199
+ "- The kortyx_emit__* tools publish validated structured output and return an acknowledgement. You may continue with text or more tools afterward.",
3200
+ "- The kortyx_return__* tools publish a validated result and end this reasoning operation. If return tools are available, call exactly one before finishing.",
3201
+ "- 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.",
3202
+ "- Call a structured output control tool alone in its model turn."
3203
+ ].join("\n")
3041
3204
  } : {}
3042
3205
  });
3043
3206
  const steps = checkpoint?.steps ?? [];
3044
- const allToolCalls = steps.flatMap((step) => step.toolCalls).filter((call) => !interruptByToolName.has(call.name));
3045
- const allToolResults = steps.flatMap((step) => step.toolResults).filter((result) => !interruptByToolName.has(result.name));
3207
+ const allToolCalls = steps.flatMap((step) => step.toolCalls).filter(
3208
+ (call) => !interruptByToolName.has(call.name) && !outputByToolName.has(call.name)
3209
+ );
3210
+ const knownToolCallIds = new Set(
3211
+ steps.flatMap((step) => step.toolCalls.map((call) => call.id))
3212
+ );
3213
+ const allToolResults = steps.flatMap((step) => step.toolResults).filter(
3214
+ (result) => !interruptByToolName.has(result.name) && !outputByToolName.has(result.name)
3215
+ );
3046
3216
  let finalText = "";
3047
3217
  let finalRaw;
3048
3218
  let finalOutput;
@@ -3057,8 +3227,11 @@ ${tryStringify2(args.response)}`
3057
3227
  args.initialWarnings
3058
3228
  );
3059
3229
  let pending = checkpoint?.pending;
3230
+ let pendingStepIndex = checkpoint?.pendingStepIndex;
3060
3231
  const approvedCalls = checkpoint?.approvedCalls ?? [];
3061
3232
  const interruptHistory = checkpoint?.interruptHistory ?? [];
3233
+ const emissions = checkpoint?.emissions ?? [];
3234
+ let returned = checkpoint?.returned;
3062
3235
  const save = () => {
3063
3236
  ctx.currentNodeState.byKey[checkpointKey] = {
3064
3237
  status: "tool_loop",
@@ -3067,8 +3240,11 @@ ${tryStringify2(args.response)}`
3067
3240
  steps,
3068
3241
  approvedCalls,
3069
3242
  interruptHistory,
3243
+ emissions,
3244
+ ...returned ? { returned } : {},
3070
3245
  finalizing,
3071
- ...pending ? { pending } : {}
3246
+ ...pending ? { pending } : {},
3247
+ ...pending && pendingStepIndex !== void 0 ? { pendingStepIndex } : {}
3072
3248
  };
3073
3249
  ctx.stateDirty = true;
3074
3250
  };
@@ -3081,7 +3257,7 @@ ${tryStringify2(args.response)}`
3081
3257
  });
3082
3258
  };
3083
3259
  try {
3084
- for (let stepIndex = pending ? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
3260
+ for (let stepIndex = pending ? pendingStepIndex ?? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
3085
3261
  (0, import_core52.throwIfExecutionAborted)(abortSignal);
3086
3262
  traceSpan?.addEvent?.("useReason.tool-step.start", {
3087
3263
  stepIndex,
@@ -3091,6 +3267,7 @@ ${tryStringify2(args.response)}`
3091
3267
  const stream = useReasonArgs.stream ?? useReasonArgs.model.options?.streaming ?? true;
3092
3268
  const emit = useReasonArgs.emit ?? true;
3093
3269
  let textStarted = false;
3270
+ const segmentId = `step-${stepIndex}`;
3094
3271
  const structuredChunk = useReasonArgs.outputSchema && (!separateOutput || finalizing) && stream && emit && shouldStreamStructured(useReasonArgs.structured) ? createStructuredOutputStreamer(useReasonArgs, id, opId) : void 0;
3095
3272
  const step = pending ?? await reasonEngine(
3096
3273
  {
@@ -3104,15 +3281,15 @@ ${tryStringify2(args.response)}`
3104
3281
  if (structuredChunk) structuredChunk(delta);
3105
3282
  if (!emit || useReasonArgs.outputSchema || !delta) return;
3106
3283
  if (!textStarted) {
3107
- emitToolEvent("text-start", {});
3284
+ emitToolEvent("text-start", { segmentId });
3108
3285
  textStarted = true;
3109
3286
  }
3110
- emitToolEvent("text-delta", { delta });
3287
+ emitToolEvent("text-delta", { delta, segmentId });
3111
3288
  }
3112
3289
  },
3113
3290
  { ...id ? { id } : {}, opId }
3114
3291
  );
3115
- if (textStarted) emitToolEvent("text-end", {});
3292
+ if (textStarted) emitToolEvent("text-end", { segmentId });
3116
3293
  finalText = step.text;
3117
3294
  finalRaw = step.raw;
3118
3295
  if (!reused) {
@@ -3128,7 +3305,7 @@ ${tryStringify2(args.response)}`
3128
3305
  const toolCalls = step.toolCalls ?? [];
3129
3306
  const ids = /* @__PURE__ */ new Set();
3130
3307
  for (const call of toolCalls) {
3131
- if (!call.id || ids.has(call.id) || !reused && allToolCalls.some((prior) => prior.id === call.id))
3308
+ if (!call.id || ids.has(call.id) || !reused && knownToolCallIds.has(call.id))
3132
3309
  throw new Error("Model returned duplicate or empty tool call IDs.");
3133
3310
  ids.add(call.id);
3134
3311
  }
@@ -3144,7 +3321,12 @@ ${tryStringify2(args.response)}`
3144
3321
  ...step.providerMetadata ? { providerMetadata: step.providerMetadata } : {},
3145
3322
  ...step.warnings ? { warnings: step.warnings } : {}
3146
3323
  });
3324
+ if (!reused) for (const call of toolCalls) knownToolCallIds.add(call.id);
3147
3325
  if (toolCalls.length === 0) {
3326
+ if (Object.keys(useReasonArgs.outputs?.return ?? {}).length > 0 && !returned)
3327
+ throw new Error(
3328
+ "useReason outputs.return requires one terminal output contract call."
3329
+ );
3148
3330
  if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
3149
3331
  throw new Error(
3150
3332
  "useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
@@ -3182,6 +3364,7 @@ ${tryStringify2(args.response)}`
3182
3364
  });
3183
3365
  finalizing = true;
3184
3366
  pending = void 0;
3367
+ pendingStepIndex = void 0;
3185
3368
  save();
3186
3369
  continue;
3187
3370
  }
@@ -3194,7 +3377,9 @@ ${tryStringify2(args.response)}`
3194
3377
  );
3195
3378
  if (!reused) {
3196
3379
  allToolCalls.push(
3197
- ...toolCalls.filter((call) => !interruptByToolName.has(call.name))
3380
+ ...toolCalls.filter(
3381
+ (call) => !interruptByToolName.has(call.name) && !outputByToolName.has(call.name)
3382
+ )
3198
3383
  );
3199
3384
  messages.push({
3200
3385
  role: "assistant",
@@ -3204,7 +3389,197 @@ ${tryStringify2(args.response)}`
3204
3389
  });
3205
3390
  }
3206
3391
  pending = step;
3392
+ pendingStepIndex = stepIndex;
3207
3393
  save();
3394
+ const outputCalls = toolCalls.filter(
3395
+ (call) => outputByToolName.has(call.name)
3396
+ );
3397
+ if (outputCalls.length > 0) {
3398
+ if (toolCalls.length !== 1)
3399
+ throw new Error(
3400
+ "A useReason output contract call must be the only tool call in its model turn."
3401
+ );
3402
+ const call = outputCalls[0];
3403
+ if (!call) throw new Error("Missing useReason output control call.");
3404
+ const resolved = outputByToolName.get(call.name);
3405
+ if (!resolved)
3406
+ throw new Error(
3407
+ `Unknown useReason output contract tool "${call.name}".`
3408
+ );
3409
+ if (toolResults.some((result3) => result3.toolCallId === call.id)) {
3410
+ pending = void 0;
3411
+ pendingStepIndex = void 0;
3412
+ save();
3413
+ continue;
3414
+ }
3415
+ if (resolved.kind === "emit" && emissions.length >= (useReasonArgs.outputs?.maxEmissions ?? 16))
3416
+ throw new Error(
3417
+ `useReason reached outputs.maxEmissions (${useReasonArgs.outputs?.maxEmissions ?? 16}).`
3418
+ );
3419
+ let data;
3420
+ if (resolved.stream) {
3421
+ if (!stream)
3422
+ throw new Error(
3423
+ `useReason streaming output contract "${resolved.name}" requires stream: true.`
3424
+ );
3425
+ if (stepIndex + 1 >= maxSteps)
3426
+ throw new Error(
3427
+ `useReason output streaming requires another model pass within maxSteps (${maxSteps}).`
3428
+ );
3429
+ const instruction = call.input && typeof call.input === "object" && !Array.isArray(call.input) ? call.input.instruction : void 0;
3430
+ if (typeof instruction !== "string" || instruction.trim().length === 0)
3431
+ throw new Error(
3432
+ `useReason streaming output contract "${resolved.name}" requires an instruction.`
3433
+ );
3434
+ const streamId = `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissions.length : "return"}`;
3435
+ const structured = {
3436
+ dataType: resolved.contract.schemaId,
3437
+ schemaId: resolved.contract.schemaId,
3438
+ schemaVersion: resolved.contract.schemaVersion,
3439
+ fields: resolved.contract.stream?.fields
3440
+ };
3441
+ const inferred = inferOutputFormat(
3442
+ resolved.contract.schema,
3443
+ void 0
3444
+ );
3445
+ try {
3446
+ const outputStep = await reasonEngine(
3447
+ {
3448
+ ...useReasonArgs,
3449
+ tools: [],
3450
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3451
+ messages: [
3452
+ ...messages,
3453
+ {
3454
+ role: "tool",
3455
+ toolCallId: call.id,
3456
+ name: call.name,
3457
+ content: "Generating the requested structured output."
3458
+ },
3459
+ {
3460
+ role: "user",
3461
+ content: withOutputGuardrails(
3462
+ instruction,
3463
+ resolved.contract.schema
3464
+ )
3465
+ }
3466
+ ],
3467
+ emit: false,
3468
+ stream,
3469
+ ...emit && stream ? {
3470
+ onTextChunk: createStructuredOutputStreamer(
3471
+ { structured },
3472
+ id,
3473
+ streamId
3474
+ )
3475
+ } : {}
3476
+ },
3477
+ { ...id ? { id } : {}, opId }
3478
+ );
3479
+ if (outputStep.toolCalls?.length)
3480
+ throw new Error(
3481
+ `Provider requested a tool during streamed output contract "${resolved.name}".`
3482
+ );
3483
+ aggregatedUsage = mergeUsage(aggregatedUsage, outputStep.usage);
3484
+ accumulateTokenUsage(outputStep.usage);
3485
+ aggregatedWarnings = mergeWarnings(
3486
+ aggregatedWarnings,
3487
+ mergeWarnings(inferred.warnings, outputStep.warnings)
3488
+ );
3489
+ aggregatedProviderMetadata = mergeProviderMetadata(
3490
+ aggregatedProviderMetadata,
3491
+ outputStep.providerMetadata
3492
+ );
3493
+ finalFinishReason = outputStep.finishReason;
3494
+ finalRaw = outputStep.raw;
3495
+ data = parseReasonOutputWithSchema({
3496
+ text: outputStep.text,
3497
+ schema: resolved.contract.schema,
3498
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3499
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3500
+ label: `useReason output contract "${resolved.name}"`
3501
+ });
3502
+ steps.push({
3503
+ stepIndex: stepIndex + 1,
3504
+ kind: "output",
3505
+ text: outputStep.text,
3506
+ toolCalls: [],
3507
+ toolResults: [],
3508
+ ...outputStep.usage ? { usage: outputStep.usage } : {},
3509
+ ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3510
+ ...outputStep.providerMetadata ? { providerMetadata: outputStep.providerMetadata } : {},
3511
+ ...outputStep.warnings ? { warnings: outputStep.warnings } : {}
3512
+ });
3513
+ stepIndex += 1;
3514
+ } catch (error) {
3515
+ traceSpan?.addEvent?.("useReason.output.invalidated", {
3516
+ contract: resolved.name,
3517
+ kind: resolved.kind,
3518
+ schemaId: resolved.contract.schemaId,
3519
+ schemaVersion: resolved.contract.schemaVersion,
3520
+ streamId
3521
+ });
3522
+ if (emit) {
3523
+ try {
3524
+ ctx.node.emit("structured_data_invalidated", {
3525
+ node: ctx.node.graph.node,
3526
+ streamId
3527
+ });
3528
+ } catch {
3529
+ }
3530
+ }
3531
+ throw error;
3532
+ }
3533
+ } else {
3534
+ data = parseWithSchema(
3535
+ resolved.contract.schema,
3536
+ call.input,
3537
+ `useReason output contract "${resolved.name}"`
3538
+ );
3539
+ }
3540
+ const entry = { contract: resolved.name, data };
3541
+ const emissionIndex = emissions.length;
3542
+ if (resolved.kind === "emit")
3543
+ emissions.push(entry);
3544
+ else returned = entry;
3545
+ if (emit)
3546
+ emitStructuredData({
3547
+ kind: "final",
3548
+ data,
3549
+ dataType: resolved.contract.schemaId,
3550
+ schemaId: resolved.contract.schemaId,
3551
+ schemaVersion: resolved.contract.schemaVersion,
3552
+ streamId: `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissionIndex : "return"}`,
3553
+ ...id ? { id } : {}
3554
+ });
3555
+ traceSpan?.addEvent?.("useReason.output.emitted", {
3556
+ contract: resolved.name,
3557
+ kind: resolved.kind,
3558
+ schemaId: resolved.contract.schemaId,
3559
+ schemaVersion: resolved.contract.schemaVersion,
3560
+ emissionIndex,
3561
+ data
3562
+ });
3563
+ if (resolved.kind === "return") {
3564
+ pending = void 0;
3565
+ pendingStepIndex = void 0;
3566
+ completed = true;
3567
+ save();
3568
+ break;
3569
+ }
3570
+ const result2 = normalizeToolResult(call, { emitted: true, data });
3571
+ toolResults.push(result2);
3572
+ messages.push({
3573
+ role: "tool",
3574
+ content: result2.content,
3575
+ toolCallId: result2.toolCallId,
3576
+ name: result2.name
3577
+ });
3578
+ pending = void 0;
3579
+ pendingStepIndex = void 0;
3580
+ save();
3581
+ continue;
3582
+ }
3208
3583
  const interruptCalls = toolCalls.filter(
3209
3584
  (call) => interruptByToolName.has(call.name)
3210
3585
  );
@@ -3227,6 +3602,7 @@ ${tryStringify2(args.response)}`
3227
3602
  );
3228
3603
  if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
3229
3604
  pending = void 0;
3605
+ pendingStepIndex = void 0;
3230
3606
  save();
3231
3607
  continue;
3232
3608
  }
@@ -3280,6 +3656,7 @@ ${tryStringify2(args.response)}`
3280
3656
  interruptIndex: interruptHistory.length - 1
3281
3657
  });
3282
3658
  pending = void 0;
3659
+ pendingStepIndex = void 0;
3283
3660
  save();
3284
3661
  continue;
3285
3662
  }
@@ -3455,6 +3832,7 @@ ${tryStringify2(args.response)}`
3455
3832
  save();
3456
3833
  }
3457
3834
  pending = void 0;
3835
+ pendingStepIndex = void 0;
3458
3836
  save();
3459
3837
  }
3460
3838
  if (!completed) {
@@ -3483,7 +3861,7 @@ ${tryStringify2(args.response)}`
3483
3861
  const result = {
3484
3862
  ...id ? { id } : {},
3485
3863
  opId,
3486
- text: finalText,
3864
+ text: useReasonArgs.outputs ? steps.filter((step) => step.kind !== "output").map((step) => step.text).filter(Boolean).join("\n") : finalText,
3487
3865
  ...finalRaw !== void 0 ? { raw: finalRaw } : {},
3488
3866
  ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3489
3867
  ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
@@ -3493,6 +3871,8 @@ ${tryStringify2(args.response)}`
3493
3871
  toolCalls: allToolCalls,
3494
3872
  toolResults: allToolResults,
3495
3873
  steps,
3874
+ ...emissions.length ? { emissions } : {},
3875
+ ...returned ? { returned } : {},
3496
3876
  ...interruptHistory.length > 0 ? { interruptHistory } : {},
3497
3877
  ...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
3498
3878
  interruptResponse: interruptHistory.at(-1)?.response
@@ -3508,11 +3888,13 @@ ${tryStringify2(args.response)}`
3508
3888
  toolCallCount: allToolCalls.length,
3509
3889
  toolResultCount: allToolResults.length,
3510
3890
  toolStepCount: steps.length,
3511
- interruptCount: interruptHistory.length
3891
+ interruptCount: interruptHistory.length,
3892
+ emissionCount: emissions.length,
3893
+ ...returned ? { returnContract: returned.contract } : {}
3512
3894
  },
3513
3895
  telemetry: {
3514
3896
  ...useReasonArgs.telemetry ?? {},
3515
- output: useReasonArgs.telemetry?.output ?? finalOutput ?? finalText
3897
+ output: useReasonArgs.telemetry?.output ?? returned?.data ?? finalOutput ?? finalText
3516
3898
  }
3517
3899
  });
3518
3900
  ctx.currentNodeState.byKey[checkpointKey] = { status: "completed", result };
@@ -3537,7 +3919,7 @@ ${tryStringify2(args.response)}`
3537
3919
  var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
3538
3920
  var assertReasoningThoughtsCompatibility = (args) => {
3539
3921
  if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
3540
- const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3922
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || Boolean(args.outputs) || resolveEffectiveResponseFormatType(args) === "json";
3541
3923
  if (!usesStructuredOutput) return;
3542
3924
  throw new Error(
3543
3925
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
@@ -3545,6 +3927,15 @@ ${tryStringify2(args.response)}`
3545
3927
  };
3546
3928
  async function runReason(args) {
3547
3929
  assertReasoningThoughtsCompatibility(args);
3930
+ normalizeReasonOutputs(
3931
+ args.outputs,
3932
+ Boolean(args.outputSchema || args.structured)
3933
+ );
3934
+ if (args.outputSchema || args.structured) warnLegacyReasonOutput();
3935
+ if (args.outputs && resolveEffectiveResponseFormatType(args) === "json")
3936
+ throw new Error(
3937
+ "useReason output contracts use tool calls and cannot be combined with JSON responseFormat."
3938
+ );
3548
3939
  const interruptConfig = normalizeReasonInterrupts({
3549
3940
  interrupt: args.interrupt,
3550
3941
  interrupts: args.interrupts
@@ -3618,7 +4009,7 @@ ${tryStringify2(args.response)}`
3618
4009
  }
3619
4010
  },
3620
4011
  async (traceSpan) => {
3621
- if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
4012
+ if ((args.tools?.length || args.interrupts || args.outputs) && !existingCheckpoint) {
3622
4013
  return runReasonToolLoop({
3623
4014
  useReasonArgs: args,
3624
4015
  allowValidatedToolOutput,
@@ -3879,6 +4270,53 @@ ${tryStringify2(args.response)}`
3879
4270
  }
3880
4271
  }
3881
4272
  function useStructuredData(args) {
4273
+ if ("contract" in args) {
4274
+ const contract = defineOutputContract(args.contract);
4275
+ const base = {
4276
+ dataType: contract.schemaId,
4277
+ schemaId: contract.schemaId,
4278
+ schemaVersion: contract.schemaVersion,
4279
+ ...args.id ? { id: args.id } : {},
4280
+ ...args.streamId ? { streamId: args.streamId } : {}
4281
+ };
4282
+ if (args.kind === "set" || args.kind === "append" || args.kind === "text-delta") {
4283
+ if (contract.stream?.fields[args.path] !== args.kind)
4284
+ throw new Error(
4285
+ `Output contract does not allow ${args.kind} at "${args.path}".`
4286
+ );
4287
+ if (args.kind === "set")
4288
+ emitStructuredData({
4289
+ ...base,
4290
+ kind: "set",
4291
+ path: args.path,
4292
+ value: args.value,
4293
+ ...args.valueSchema ? { valueSchema: args.valueSchema } : {}
4294
+ });
4295
+ else if (args.kind === "append")
4296
+ emitStructuredData({
4297
+ ...base,
4298
+ kind: "append",
4299
+ path: args.path,
4300
+ items: args.items,
4301
+ ...args.itemSchema ? { itemSchema: args.itemSchema } : {}
4302
+ });
4303
+ else
4304
+ emitStructuredData({
4305
+ ...base,
4306
+ kind: "text-delta",
4307
+ path: args.path,
4308
+ delta: args.delta
4309
+ });
4310
+ return;
4311
+ }
4312
+ emitStructuredData({
4313
+ ...base,
4314
+ kind: "final",
4315
+ data: args.data,
4316
+ dataSchema: contract.schema
4317
+ });
4318
+ return;
4319
+ }
3882
4320
  emitStructuredData(args);
3883
4321
  }
3884
4322
  function useReason2(args) {
@@ -5492,6 +5930,13 @@ async function orchestrateGraphStream({
5492
5930
  });
5493
5931
  return;
5494
5932
  }
5933
+ if (event === "structured_data_invalidated") {
5934
+ const streamId = payload?.streamId;
5935
+ if (typeof streamId !== "string" || !streamId) return;
5936
+ structuredStreamIds.delete(streamId);
5937
+ write({ type: "structured-data-invalidated", streamId });
5938
+ return;
5939
+ }
5495
5940
  if (event === "transition") {
5496
5941
  write({
5497
5942
  type: "transition",