@kortyx/hooks 0.31.0 → 0.32.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
@@ -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
 
@@ -2978,7 +2983,7 @@ var runReasonToolLoop = async (args) => {
2978
2983
  interruptInstructions: [
2979
2984
  "Human-input rules:",
2980
2985
  "- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
2981
- "- 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.",
2982
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."
2983
2988
  ].join("\n")
2984
2989
  } : {},
@@ -2988,7 +2993,7 @@ var runReasonToolLoop = async (args) => {
2988
2993
  "- The kortyx_emit__* tools publish validated structured output and return an acknowledgement. You may continue with text or more tools afterward.",
2989
2994
  "- The kortyx_return__* tools publish a validated result and end this reasoning operation. If return tools are available, call exactly one before finishing.",
2990
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.",
2991
- "- 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."
2992
2997
  ].join("\n")
2993
2998
  } : {}
2994
2999
  });
@@ -3017,6 +3022,19 @@ var runReasonToolLoop = async (args) => {
3017
3022
  );
3018
3023
  let pending = checkpoint?.pending;
3019
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);
3020
3038
  const approvedCalls = checkpoint?.approvedCalls ?? [];
3021
3039
  const interruptHistory = checkpoint?.interruptHistory ?? [];
3022
3040
  const emissions = checkpoint?.emissions ?? [];
@@ -3028,8 +3046,10 @@ var runReasonToolLoop = async (args) => {
3028
3046
  messages,
3029
3047
  steps,
3030
3048
  approvedCalls,
3049
+ resolvedInterrupts,
3031
3050
  interruptHistory,
3032
3051
  emissions,
3052
+ outputContractCorrections,
3033
3053
  ...returned ? { returned } : {},
3034
3054
  finalizing,
3035
3055
  ...pending ? { pending } : {},
@@ -3037,6 +3057,10 @@ var runReasonToolLoop = async (args) => {
3037
3057
  };
3038
3058
  ctx.stateDirty = true;
3039
3059
  };
3060
+ const rememberInterrupt = (request) => {
3061
+ resolvedInterrupts.push(request);
3062
+ save();
3063
+ };
3040
3064
  const emitToolEvent = (event, payload) => {
3041
3065
  ctx.node.emit(event, {
3042
3066
  ...payload,
@@ -3046,7 +3070,7 @@ var runReasonToolLoop = async (args) => {
3046
3070
  });
3047
3071
  };
3048
3072
  try {
3049
- 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) {
3050
3074
  (0, import_core5.throwIfExecutionAborted)(abortSignal);
3051
3075
  traceSpan?.addEvent?.("useReason.tool-step.start", {
3052
3076
  stepIndex,
@@ -3183,273 +3207,301 @@ var runReasonToolLoop = async (args) => {
3183
3207
  const outputCalls = toolCalls.filter(
3184
3208
  (call) => outputByToolName.has(call.name)
3185
3209
  );
3186
- if (outputCalls.length > 0) {
3187
- if (toolCalls.length !== 1)
3188
- throw new Error(
3189
- "A useReason output contract call must be the only tool call in its model turn."
3190
- );
3191
- const call = outputCalls[0];
3192
- if (!call) throw new Error("Missing useReason output control call.");
3193
- const resolved = outputByToolName.get(call.name);
3194
- if (!resolved)
3195
- throw new Error(
3196
- `Unknown useReason output contract tool "${call.name}".`
3197
- );
3198
- if (toolResults.some((result3) => result3.toolCallId === call.id)) {
3199
- pending = void 0;
3200
- pendingStepIndex = void 0;
3201
- save();
3202
- 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;
3203
3227
  }
3204
- if (resolved.kind === "emit" && emissions.length >= (useReasonArgs.outputs?.maxEmissions ?? 16))
3205
- throw new Error(
3206
- `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)
3207
3243
  );
3208
- let data;
3209
- if (resolved.stream) {
3210
- if (!stream)
3211
- throw new Error(
3212
- `useReason streaming output contract "${resolved.name}" requires stream: true.`
3213
- );
3214
- if (stepIndex + 1 >= maxSteps)
3244
+ const resolved = outputByToolName.get(call.name);
3245
+ if (!resolved)
3215
3246
  throw new Error(
3216
- `useReason output streaming requires another model pass within maxSteps (${maxSteps}).`
3247
+ `Unknown useReason output contract tool "${call.name}".`
3217
3248
  );
3218
- const instruction = call.input && typeof call.input === "object" && !Array.isArray(call.input) ? call.input.instruction : void 0;
3219
- 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))
3220
3254
  throw new Error(
3221
- `useReason streaming output contract "${resolved.name}" requires an instruction.`
3222
- );
3223
- const streamId = `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissions.length : "return"}`;
3224
- const structured = {
3225
- dataType: resolved.contract.schemaId,
3226
- schemaId: resolved.contract.schemaId,
3227
- schemaVersion: resolved.contract.schemaVersion,
3228
- fields: resolved.contract.stream?.fields
3229
- };
3230
- const inferred = inferOutputFormat(
3231
- resolved.contract.schema,
3232
- void 0
3233
- );
3234
- try {
3235
- const outputStep = await reasonEngine(
3236
- {
3237
- ...useReasonArgs,
3238
- tools: [],
3239
- ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3240
- messages: [
3241
- ...messages,
3242
- {
3243
- role: "tool",
3244
- toolCallId: call.id,
3245
- name: call.name,
3246
- content: "Generating the requested structured output."
3247
- },
3248
- {
3249
- role: "user",
3250
- content: withOutputGuardrails(
3251
- instruction,
3252
- resolved.contract.schema
3253
- )
3254
- }
3255
- ],
3256
- emit: false,
3257
- stream,
3258
- ...emit && stream ? {
3259
- onTextChunk: createStructuredOutputStreamer(
3260
- { structured },
3261
- id,
3262
- streamId
3263
- )
3264
- } : {}
3265
- },
3266
- { ...id ? { id } : {}, opId }
3255
+ `useReason reached outputs.maxEmissions (${useReasonArgs.outputs?.maxEmissions ?? 16}).`
3267
3256
  );
3268
- if (outputStep.toolCalls?.length)
3257
+ let data;
3258
+ if (resolved.stream || hasEvidenceCalls) {
3259
+ if (resolved.stream && !stream)
3269
3260
  throw new Error(
3270
- `Provider requested a tool during streamed output contract "${resolved.name}".`
3261
+ `useReason streaming output contract "${resolved.name}" requires stream: true.`
3271
3262
  );
3272
- aggregatedUsage = mergeUsage(aggregatedUsage, outputStep.usage);
3273
- accumulateTokenUsage(outputStep.usage);
3274
- aggregatedWarnings = mergeWarnings(
3275
- aggregatedWarnings,
3276
- mergeWarnings(inferred.warnings, outputStep.warnings)
3277
- );
3278
- aggregatedProviderMetadata = mergeProviderMetadata(
3279
- aggregatedProviderMetadata,
3280
- 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
3281
3293
  );
3282
- finalFinishReason = outputStep.finishReason;
3283
- finalRaw = outputStep.raw;
3284
- data = parseReasonOutputWithSchema({
3285
- text: outputStep.text,
3286
- schema: resolved.contract.schema,
3287
- ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3288
- ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3289
- label: `useReason output contract "${resolved.name}"`
3290
- });
3291
- steps.push({
3292
- stepIndex: stepIndex + 1,
3294
+ const outputRecord = {
3295
+ stepIndex: steps.length,
3293
3296
  kind: "output",
3294
- text: outputStep.text,
3297
+ text: "",
3295
3298
  toolCalls: [],
3296
- toolResults: [],
3297
- ...outputStep.usage ? { usage: outputStep.usage } : {},
3298
- ...outputStep.finishReason ? { finishReason: outputStep.finishReason } : {},
3299
- ...outputStep.providerMetadata ? { providerMetadata: outputStep.providerMetadata } : {},
3300
- ...outputStep.warnings ? { warnings: outputStep.warnings } : {}
3301
- });
3302
- stepIndex += 1;
3303
- } catch (error) {
3304
- traceSpan?.addEvent?.("useReason.output.invalidated", {
3305
- contract: resolved.name,
3306
- kind: resolved.kind,
3307
- schemaId: resolved.contract.schemaId,
3308
- schemaVersion: resolved.contract.schemaVersion,
3309
- streamId
3310
- });
3311
- if (emit) {
3312
- try {
3313
- ctx.node.emit("structured_data_invalidated", {
3314
- node: ctx.node.graph.node,
3315
- streamId
3316
- });
3317
- } 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
+ }
3318
3394
  }
3395
+ throw error;
3319
3396
  }
3320
- throw error;
3397
+ } else {
3398
+ data = parseWithSchema(
3399
+ resolved.contract.schema,
3400
+ call.input,
3401
+ `useReason output contract "${resolved.name}"`
3402
+ );
3321
3403
  }
3322
- } else {
3323
- data = parseWithSchema(
3324
- resolved.contract.schema,
3325
- call.input,
3326
- `useReason output contract "${resolved.name}"`
3327
- );
3328
- }
3329
- const entry = { contract: resolved.name, data };
3330
- const emissionIndex = emissions.length;
3331
- if (resolved.kind === "emit")
3332
- emissions.push(entry);
3333
- else returned = entry;
3334
- if (emit)
3335
- emitStructuredData({
3336
- kind: "final",
3337
- data,
3338
- 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,
3339
3422
  schemaId: resolved.contract.schemaId,
3340
3423
  schemaVersion: resolved.contract.schemaVersion,
3341
- streamId: `${opId}:${resolved.kind}:${resolved.name}:${resolved.kind === "emit" ? emissionIndex : "return"}`,
3342
- ...id ? { id } : {}
3424
+ emissionIndex,
3425
+ data
3343
3426
  });
3344
- traceSpan?.addEvent?.("useReason.output.emitted", {
3345
- contract: resolved.name,
3346
- kind: resolved.kind,
3347
- schemaId: resolved.contract.schemaId,
3348
- schemaVersion: resolved.contract.schemaVersion,
3349
- emissionIndex,
3350
- data
3351
- });
3352
- if (resolved.kind === "return") {
3353
- pending = void 0;
3354
- pendingStepIndex = void 0;
3355
- 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;
3356
3436
  save();
3357
- break;
3437
+ continue;
3358
3438
  }
3359
- const result2 = normalizeToolResult(call, { emitted: true, data });
3360
- toolResults.push(result2);
3361
- messages.push({
3362
- role: "tool",
3363
- content: result2.content,
3364
- toolCallId: result2.toolCallId,
3365
- name: result2.name
3366
- });
3367
- pending = void 0;
3368
- pendingStepIndex = void 0;
3369
- save();
3370
- continue;
3371
- }
3372
- const interruptCalls = toolCalls.filter(
3373
- (call) => interruptByToolName.has(call.name)
3374
- );
3375
- if (interruptCalls.length > 0) {
3376
- if (toolCalls.length !== 1)
3377
- throw new Error(
3378
- "A useReason interrupt contract call must be the only tool call in its model turn."
3379
- );
3380
- const interruptCall = interruptCalls[0];
3381
- if (!interruptCall)
3382
- throw new Error("Missing useReason interrupt control call.");
3383
- if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
3384
- throw new Error(
3385
- `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3386
- );
3387
- const resolved = interruptByToolName.get(interruptCall.name);
3388
- if (!resolved)
3389
- throw new Error(
3390
- `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`
3391
3456
  );
3392
- if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
3393
- pending = void 0;
3394
- 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
+ });
3395
3502
  save();
3396
3503
  continue;
3397
3504
  }
3398
- const request = parseWithSchema(
3399
- resolved.contract.requestSchema,
3400
- interruptCall.input,
3401
- `useReason interrupt contract "${resolved.name}" request`
3402
- );
3403
- traceSpan?.addEvent?.("useReason.interrupt.requested", {
3404
- checkpointKey,
3405
- contract: resolved.name,
3406
- interruptIndex: interruptHistory.length
3407
- });
3408
- const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
3409
- const response = await awaitInterruptInternal({
3410
- request: {
3411
- kind: "custom",
3412
- request,
3413
- contract: resolved.name,
3414
- ...question ? { question } : {},
3415
- schemaId: resolved.contract.schemaId,
3416
- schemaVersion: resolved.contract.schemaVersion
3417
- },
3418
- responseSchema: resolved.contract.responseSchema,
3419
- id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
3420
- meta: {
3421
- __kortyxReason: {
3422
- opId,
3423
- stepIndex,
3424
- interruptIndex: interruptHistory.length,
3425
- contract: resolved.name
3426
- }
3427
- }
3428
- });
3429
- const result2 = normalizeToolResult(interruptCall, response);
3430
- toolResults.push(result2);
3431
- messages.push({
3432
- role: "tool",
3433
- content: result2.content,
3434
- toolCallId: result2.toolCallId,
3435
- name: result2.name
3436
- });
3437
- interruptHistory.push({
3438
- contract: resolved.name,
3439
- request,
3440
- response
3441
- });
3442
- traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3443
- checkpointKey,
3444
- contract: resolved.name,
3445
- interruptIndex: interruptHistory.length - 1
3446
- });
3447
- pending = void 0;
3448
- pendingStepIndex = void 0;
3449
- save();
3450
- continue;
3451
- }
3452
- for (const toolCall of toolCalls) {
3453
3505
  if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
3454
3506
  await replayToolObservations(
3455
3507
  steps[stepIndex]?.toolObservations?.filter(
@@ -3495,6 +3547,7 @@ var runReasonToolLoop = async (args) => {
3495
3547
  "waiting"
3496
3548
  );
3497
3549
  const approval = await awaitInterruptInternal({
3550
+ onResolved: rememberInterrupt,
3498
3551
  id: `tool:${toolCall.id}`,
3499
3552
  request: {
3500
3553
  kind: "choice",
@@ -3609,22 +3662,24 @@ var runReasonToolLoop = async (args) => {
3609
3662
  });
3610
3663
  }
3611
3664
  }
3612
- const completed2 = toolResults.at(-1);
3613
- if (completed2)
3665
+ const completedToolResult = toolResults.at(-1);
3666
+ if (completedToolResult)
3614
3667
  messages.push({
3615
3668
  role: "tool",
3616
- content: completed2.content,
3617
- toolCallId: completed2.toolCallId,
3618
- name: completed2.name,
3619
- ...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 } : {}
3620
3673
  });
3621
3674
  save();
3622
3675
  }
3623
3676
  pending = void 0;
3624
3677
  pendingStepIndex = void 0;
3625
3678
  save();
3679
+ if (completed) break;
3626
3680
  }
3627
3681
  if (!completed) {
3682
+ if (outputContractCorrections > 0) throw correctionExhausted();
3628
3683
  throw new Error(
3629
3684
  `useReason tool loop reached maxSteps (${maxSteps}) before producing a final response.`
3630
3685
  );
@@ -3686,7 +3741,11 @@ var runReasonToolLoop = async (args) => {
3686
3741
  output: useReasonArgs.telemetry?.output ?? returned?.data ?? finalOutput ?? finalText
3687
3742
  }
3688
3743
  });
3689
- ctx.currentNodeState.byKey[checkpointKey] = { status: "completed", result };
3744
+ ctx.currentNodeState.byKey[checkpointKey] = {
3745
+ status: "completed",
3746
+ result,
3747
+ resolvedInterrupts
3748
+ };
3690
3749
  ctx.stateDirty = true;
3691
3750
  return result;
3692
3751
  } catch (error) {
@@ -3763,6 +3822,8 @@ async function runReason(args) {
3763
3822
  const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
3764
3823
  const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
3765
3824
  if (existingCompleted) {
3825
+ for (const request of existingCompleted.resolvedInterrupts ?? [])
3826
+ ctx.node.replayInterrupt?.(request);
3766
3827
  await replayToolObservations(
3767
3828
  existingCompleted.result.steps?.flatMap(
3768
3829
  (step) => step.toolObservations ?? []