@kortyx/agent 0.25.1 → 0.26.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
@@ -525,6 +525,7 @@ var require_dist = __commonJS({
525
525
  WorkflowCallError: () => WorkflowCallError,
526
526
  completeResponse: () => completeResponse,
527
527
  createWorkflowHooks: () => createWorkflowHooks,
528
+ defineInterruptContract: () => defineInterruptContract,
528
529
  emitWorkflowCall: () => emitWorkflowCall2,
529
530
  parallel: () => parallel,
530
531
  reportError: () => reportError,
@@ -836,6 +837,95 @@ var require_dist = __commonJS({
836
837
  );
837
838
  return Promise.resolve(response);
838
839
  };
840
+ var CONTROL_TOOL_PREFIX = "kortyx_request_input__";
841
+ var LEGACY_CONTRACT_NAME = "default";
842
+ var legacyWarningEmitted = false;
843
+ var legacyConfigs = /* @__PURE__ */ new WeakSet();
844
+ var defineInterruptContract = (contract) => {
845
+ if (!contract || typeof contract !== "object")
846
+ throw new Error("defineInterruptContract requires a contract object.");
847
+ for (const [field, value] of [
848
+ ["description", contract.description],
849
+ ["schemaId", contract.schemaId],
850
+ ["schemaVersion", contract.schemaVersion]
851
+ ])
852
+ if (typeof value !== "string" || value.trim().length === 0)
853
+ throw new Error(
854
+ `Interrupt contract ${field} must be a non-empty string.`
855
+ );
856
+ if (typeof contract.requestSchema?.safeParse !== "function")
857
+ throw new Error(
858
+ "Interrupt contract requestSchema must provide safeParse(value)."
859
+ );
860
+ if (typeof contract.responseSchema?.safeParse !== "function")
861
+ throw new Error(
862
+ "Interrupt contract responseSchema must provide safeParse(value)."
863
+ );
864
+ return contract;
865
+ };
866
+ var interruptControlToolName = (name) => `${CONTROL_TOOL_PREFIX}${name}`;
867
+ var isInterruptControlToolName = (name) => name.startsWith(CONTROL_TOOL_PREFIX);
868
+ var assertInterruptContractName = (name) => {
869
+ if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(name))
870
+ throw new Error(
871
+ `Invalid useReason interrupt contract name "${name}". Use letters, numbers, underscores, or hyphens, starting with a letter.`
872
+ );
873
+ if (interruptControlToolName(name).length > 64)
874
+ throw new Error(
875
+ `useReason interrupt contract name "${name}" is too long for a provider tool name.`
876
+ );
877
+ };
878
+ var warnLegacyUseReasonInterrupt = () => {
879
+ if (legacyWarningEmitted) return;
880
+ legacyWarningEmitted = true;
881
+ process.emitWarning(
882
+ "useReason({ interrupt }) is deprecated and will be removed in the next major release. Define an interrupt contract with defineInterruptContract() and pass it through useReason({ interrupts: { contracts } }). See docs/internal/next-major-removals.md.",
883
+ {
884
+ code: "KORTYX_USE_REASON_INTERRUPT_DEPRECATED",
885
+ type: "DeprecationWarning"
886
+ }
887
+ );
888
+ };
889
+ var normalizeReasonInterrupts = (args) => {
890
+ if (args.interrupt && args.interrupts)
891
+ throw new Error(
892
+ "useReason cannot receive both deprecated `interrupt` and `interrupts`. Migrate the legacy configuration to an interrupt contract."
893
+ );
894
+ if (args.interrupts) {
895
+ for (const [name, contract] of Object.entries(args.interrupts.contracts)) {
896
+ assertInterruptContractName(name);
897
+ defineInterruptContract(contract);
898
+ }
899
+ if (Object.keys(args.interrupts.contracts).length === 0)
900
+ throw new Error("useReason interrupts.contracts cannot be empty.");
901
+ if (args.interrupts.maxRequests !== void 0 && (!Number.isInteger(args.interrupts.maxRequests) || args.interrupts.maxRequests < 1))
902
+ throw new Error("useReason interrupts.maxRequests must be at least 1.");
903
+ return args.interrupts;
904
+ }
905
+ if (!args.interrupt) return void 0;
906
+ warnLegacyUseReasonInterrupt();
907
+ const normalized = {
908
+ mode: args.interrupt.mode,
909
+ maxRequests: 1,
910
+ contracts: {
911
+ [LEGACY_CONTRACT_NAME]: {
912
+ description: "Ask the user for the additional information required to complete this request.",
913
+ requestSchema: args.interrupt.requestSchema,
914
+ responseSchema: args.interrupt.responseSchema ?? {
915
+ safeParse: (value) => ({
916
+ success: true,
917
+ data: value
918
+ })
919
+ },
920
+ schemaId: args.interrupt.schemaId ?? "kortyx.legacy-interrupt",
921
+ schemaVersion: args.interrupt.schemaVersion ?? "1"
922
+ }
923
+ }
924
+ };
925
+ legacyConfigs.add(normalized);
926
+ return normalized;
927
+ };
928
+ var isLegacyInterruptContracts = (config) => Boolean(config && legacyConfigs.has(config));
839
929
  var import_core62 = require("@kortyx/core");
840
930
  function safeSpan(span) {
841
931
  const guard = (fn) => (...args) => {
@@ -2812,6 +2902,7 @@ ${tryStringify2(args.response)}`
2812
2902
  };
2813
2903
  var import_core52 = require("@kortyx/core");
2814
2904
  var import_errors62 = require("@kortyx/core/errors");
2905
+ var import_zod42 = require("zod");
2815
2906
  var toToolDefinitions = (tools2) => tools2.map((tool) => ({
2816
2907
  name: tool.name,
2817
2908
  ...tool.title ? { title: tool.title } : {},
@@ -2847,7 +2938,12 @@ ${tryStringify2(args.response)}`
2847
2938
  };
2848
2939
  };
2849
2940
  var createInitialMessages = (args) => [
2850
- ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
2941
+ ...typeof args.system === "string" || args.interruptInstructions ? [
2942
+ {
2943
+ role: "system",
2944
+ content: [args.system, args.interruptInstructions].filter((value) => Boolean(value)).join("\n\n")
2945
+ }
2946
+ ] : [],
2851
2947
  { role: "user", content: String(args.input ?? "") }
2852
2948
  ];
2853
2949
  var runReasonToolLoop = async (args) => {
@@ -2862,19 +2958,23 @@ ${tryStringify2(args.response)}`
2862
2958
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
2863
2959
  const opId = checkpoint?.opId ?? args.opId;
2864
2960
  const tools2 = useReasonArgs.tools ?? [];
2961
+ const interruptConfig = normalizeReasonInterrupts({
2962
+ interrupt: useReasonArgs.interrupt,
2963
+ interrupts: useReasonArgs.interrupts
2964
+ });
2965
+ const interruptByToolName = /* @__PURE__ */ new Map();
2865
2966
  const toolByName = /* @__PURE__ */ new Map();
2866
2967
  let separateOutput = false;
2867
2968
  for (const tool of tools2) {
2969
+ if (isInterruptControlToolName(tool.name))
2970
+ throw new Error(
2971
+ `useReason tool name "${tool.name}" uses the reserved interrupt control-tool namespace.`
2972
+ );
2868
2973
  if (toolByName.has(tool.name)) {
2869
2974
  throw new Error(`useReason received duplicate tool name "${tool.name}".`);
2870
2975
  }
2871
2976
  toolByName.set(tool.name, tool);
2872
2977
  }
2873
- if (useReasonArgs.interrupt) {
2874
- throw new Error(
2875
- "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
2876
- );
2877
- }
2878
2978
  separateOutput = Boolean(
2879
2979
  useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
2880
2980
  ...useReasonArgs.model.options,
@@ -2886,21 +2986,63 @@ ${tryStringify2(args.response)}`
2886
2986
  ) ?? {}
2887
2987
  }).requiresSeparateStructuredOutput
2888
2988
  );
2889
- const toolDefinitions = toToolDefinitions(tools2);
2989
+ const interruptDefinitions = [];
2990
+ for (const [name, contract] of Object.entries(
2991
+ interruptConfig?.contracts ?? {}
2992
+ )) {
2993
+ const toolName = interruptControlToolName(name);
2994
+ if (toolByName.has(toolName))
2995
+ throw new Error(
2996
+ `useReason tool name "${toolName}" is reserved for interrupt contracts.`
2997
+ );
2998
+ let inputSchema;
2999
+ try {
3000
+ inputSchema = (0, import_zod42.toJSONSchema)(contract.requestSchema);
3001
+ } catch (error) {
3002
+ throw new Error(
3003
+ `useReason interrupt contract "${name}" requestSchema must support JSON Schema conversion.`,
3004
+ { cause: error }
3005
+ );
3006
+ }
3007
+ interruptByToolName.set(toolName, { name, contract });
3008
+ interruptDefinitions.push({
3009
+ name: toolName,
3010
+ title: `Request human input: ${name}`,
3011
+ description: [
3012
+ contract.description,
3013
+ "Call this only when human input is needed. Execution pauses until the validated response is returned as this tool's result."
3014
+ ].join(" "),
3015
+ inputSchema,
3016
+ metadata: {
3017
+ kortyxControl: "interrupt",
3018
+ contract: name,
3019
+ schemaId: contract.schemaId,
3020
+ schemaVersion: contract.schemaVersion
3021
+ }
3022
+ });
3023
+ }
3024
+ const toolDefinitions = [
3025
+ ...toToolDefinitions(tools2),
3026
+ ...interruptDefinitions
3027
+ ];
2890
3028
  let finalizing = checkpoint?.finalizing ?? false;
2891
3029
  let completed = false;
2892
3030
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
2893
3031
  const messages = checkpoint?.messages ?? createInitialMessages({
2894
3032
  system: useReasonArgs.system,
2895
- input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input
3033
+ input: useReasonArgs.outputSchema ? withOutputGuardrails(useReasonArgs.input, useReasonArgs.outputSchema) : useReasonArgs.input,
3034
+ ...interruptConfig ? {
3035
+ interruptInstructions: [
3036
+ "Human-input rules:",
3037
+ "- The kortyx_request_input__* tools pause this reasoning operation and return the validated human response as a normal tool result.",
3038
+ "- Call at most one human-input tool in a model turn, and do not combine it with other tool calls in that turn.",
3039
+ 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
+ ].join("\n")
3041
+ } : {}
2896
3042
  });
2897
3043
  const steps = checkpoint?.steps ?? [];
2898
- const allToolCalls = steps.flatMap(
2899
- (step) => step.toolCalls
2900
- );
2901
- const allToolResults = steps.flatMap(
2902
- (step) => step.toolResults
2903
- );
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));
2904
3046
  let finalText = "";
2905
3047
  let finalRaw;
2906
3048
  let finalOutput;
@@ -2916,6 +3058,7 @@ ${tryStringify2(args.response)}`
2916
3058
  );
2917
3059
  let pending = checkpoint?.pending;
2918
3060
  const approvedCalls = checkpoint?.approvedCalls ?? [];
3061
+ const interruptHistory = checkpoint?.interruptHistory ?? [];
2919
3062
  const save = () => {
2920
3063
  ctx.currentNodeState.byKey[checkpointKey] = {
2921
3064
  status: "tool_loop",
@@ -2923,6 +3066,7 @@ ${tryStringify2(args.response)}`
2923
3066
  messages,
2924
3067
  steps,
2925
3068
  approvedCalls,
3069
+ interruptHistory,
2926
3070
  finalizing,
2927
3071
  ...pending ? { pending } : {}
2928
3072
  };
@@ -3001,6 +3145,10 @@ ${tryStringify2(args.response)}`
3001
3145
  ...step.warnings ? { warnings: step.warnings } : {}
3002
3146
  });
3003
3147
  if (toolCalls.length === 0) {
3148
+ if (interruptConfig?.mode === "required" && interruptHistory.length === 0)
3149
+ throw new Error(
3150
+ "useReason interrupts.mode is required, but the model returned a final answer without requesting human input."
3151
+ );
3004
3152
  if (separateOutput && !finalizing) {
3005
3153
  if (args.allowValidatedToolOutput && useReasonArgs.outputSchema) {
3006
3154
  try {
@@ -3045,7 +3193,9 @@ ${tryStringify2(args.response)}`
3045
3193
  "Provider requested a tool during schema-only finalization."
3046
3194
  );
3047
3195
  if (!reused) {
3048
- allToolCalls.push(...toolCalls);
3196
+ allToolCalls.push(
3197
+ ...toolCalls.filter((call) => !interruptByToolName.has(call.name))
3198
+ );
3049
3199
  messages.push({
3050
3200
  role: "assistant",
3051
3201
  content: step.text,
@@ -3055,6 +3205,84 @@ ${tryStringify2(args.response)}`
3055
3205
  }
3056
3206
  pending = step;
3057
3207
  save();
3208
+ const interruptCalls = toolCalls.filter(
3209
+ (call) => interruptByToolName.has(call.name)
3210
+ );
3211
+ if (interruptCalls.length > 0) {
3212
+ if (toolCalls.length !== 1)
3213
+ throw new Error(
3214
+ "A useReason interrupt contract call must be the only tool call in its model turn."
3215
+ );
3216
+ const interruptCall = interruptCalls[0];
3217
+ if (!interruptCall)
3218
+ throw new Error("Missing useReason interrupt control call.");
3219
+ if (interruptHistory.length >= (interruptConfig?.maxRequests ?? 1))
3220
+ throw new Error(
3221
+ `useReason reached interrupts.maxRequests (${interruptConfig?.maxRequests ?? 1}).`
3222
+ );
3223
+ const resolved = interruptByToolName.get(interruptCall.name);
3224
+ if (!resolved)
3225
+ throw new Error(
3226
+ `Unknown useReason interrupt contract tool "${interruptCall.name}".`
3227
+ );
3228
+ if (toolResults.some((result3) => result3.toolCallId === interruptCall.id)) {
3229
+ pending = void 0;
3230
+ save();
3231
+ continue;
3232
+ }
3233
+ const request = parseWithSchema(
3234
+ resolved.contract.requestSchema,
3235
+ interruptCall.input,
3236
+ `useReason interrupt contract "${resolved.name}" request`
3237
+ );
3238
+ traceSpan?.addEvent?.("useReason.interrupt.requested", {
3239
+ checkpointKey,
3240
+ contract: resolved.name,
3241
+ interruptIndex: interruptHistory.length
3242
+ });
3243
+ const question = request && typeof request === "object" && !Array.isArray(request) && typeof request.question === "string" ? String(request.question) : void 0;
3244
+ const response = await awaitInterruptInternal({
3245
+ request: {
3246
+ kind: "custom",
3247
+ request,
3248
+ contract: resolved.name,
3249
+ ...question ? { question } : {},
3250
+ schemaId: resolved.contract.schemaId,
3251
+ schemaVersion: resolved.contract.schemaVersion
3252
+ },
3253
+ responseSchema: resolved.contract.responseSchema,
3254
+ id: `${id ?? "reason"}:${resolved.name}:${interruptHistory.length + 1}`,
3255
+ meta: {
3256
+ __kortyxReason: {
3257
+ opId,
3258
+ stepIndex,
3259
+ interruptIndex: interruptHistory.length,
3260
+ contract: resolved.name
3261
+ }
3262
+ }
3263
+ });
3264
+ const result2 = normalizeToolResult(interruptCall, response);
3265
+ toolResults.push(result2);
3266
+ messages.push({
3267
+ role: "tool",
3268
+ content: result2.content,
3269
+ toolCallId: result2.toolCallId,
3270
+ name: result2.name
3271
+ });
3272
+ interruptHistory.push({
3273
+ contract: resolved.name,
3274
+ request,
3275
+ response
3276
+ });
3277
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3278
+ checkpointKey,
3279
+ contract: resolved.name,
3280
+ interruptIndex: interruptHistory.length - 1
3281
+ });
3282
+ pending = void 0;
3283
+ save();
3284
+ continue;
3285
+ }
3058
3286
  for (const toolCall of toolCalls) {
3059
3287
  if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
3060
3288
  await replayToolObservations(
@@ -3264,7 +3492,11 @@ ${tryStringify2(args.response)}`
3264
3492
  ...finalOutput !== void 0 ? { output: finalOutput } : {},
3265
3493
  toolCalls: allToolCalls,
3266
3494
  toolResults: allToolResults,
3267
- steps
3495
+ steps,
3496
+ ...interruptHistory.length > 0 ? { interruptHistory } : {},
3497
+ ...isLegacyInterruptContracts(interruptConfig) && interruptHistory.length > 0 ? {
3498
+ interruptResponse: interruptHistory.at(-1)?.response
3499
+ } : {}
3268
3500
  };
3269
3501
  traceSpan?.end?.({
3270
3502
  ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
@@ -3275,7 +3507,8 @@ ${tryStringify2(args.response)}`
3275
3507
  textLength: finalText.length,
3276
3508
  toolCallCount: allToolCalls.length,
3277
3509
  toolResultCount: allToolResults.length,
3278
- toolStepCount: steps.length
3510
+ toolStepCount: steps.length,
3511
+ interruptCount: interruptHistory.length
3279
3512
  },
3280
3513
  telemetry: {
3281
3514
  ...useReasonArgs.telemetry ?? {},
@@ -3303,7 +3536,7 @@ ${tryStringify2(args.response)}`
3303
3536
  var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
3304
3537
  var assertReasoningThoughtsCompatibility = (args) => {
3305
3538
  if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
3306
- const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3539
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.interrupts) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
3307
3540
  if (!usesStructuredOutput) return;
3308
3541
  throw new Error(
3309
3542
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
@@ -3311,6 +3544,10 @@ ${tryStringify2(args.response)}`
3311
3544
  };
3312
3545
  async function runReason(args) {
3313
3546
  assertReasoningThoughtsCompatibility(args);
3547
+ const interruptConfig = normalizeReasonInterrupts({
3548
+ interrupt: args.interrupt,
3549
+ interrupts: args.interrupts
3550
+ });
3314
3551
  const ctx = getHookContext();
3315
3552
  const abortSignal = (0, import_core62.combineAbortSignals)(
3316
3553
  ctx.node.abortSignal,
@@ -3325,20 +3562,23 @@ ${tryStringify2(args.response)}`
3325
3562
  args.outputSchema,
3326
3563
  args.responseFormat ?? args.model.options?.responseFormat
3327
3564
  );
3565
+ const usesLegacyNoToolInterrupt = Boolean(
3566
+ args.interrupt && !args.interrupts && !args.tools?.length
3567
+ );
3328
3568
  args = {
3329
3569
  ...args,
3330
3570
  abortSignal,
3331
- ...!args.interrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
3571
+ ...!usesLegacyNoToolInterrupt && inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {}
3332
3572
  };
3333
3573
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
3334
- const opId = createRuntimeId2();
3574
+ const provisionalOpId = createRuntimeId2();
3335
3575
  const checkpointKey = resolveReasonCheckpointKey({
3336
3576
  ...id ? { id } : {},
3337
3577
  autoIndex: ctx.reasonCallIndex++
3338
3578
  });
3339
- const existingCompleted = readReasonCompletedCheckpoint(
3340
- ctx.currentNodeState.byKey[checkpointKey]
3341
- );
3579
+ const checkpointValue = ctx.currentNodeState.byKey[checkpointKey];
3580
+ const opId = checkpointValue && typeof checkpointValue === "object" && "status" in checkpointValue && checkpointValue.status === "tool_loop" && "opId" in checkpointValue && typeof checkpointValue.opId === "string" ? checkpointValue.opId : provisionalOpId;
3581
+ const existingCompleted = readReasonCompletedCheckpoint(checkpointValue);
3342
3582
  if (existingCompleted) {
3343
3583
  await replayToolObservations(
3344
3584
  existingCompleted.result.steps?.flatMap(
@@ -3347,9 +3587,7 @@ ${tryStringify2(args.response)}`
3347
3587
  );
3348
3588
  return existingCompleted.result;
3349
3589
  }
3350
- const existingCheckpoint = readReasonCheckpoint(
3351
- ctx.currentNodeState.byKey[checkpointKey]
3352
- );
3590
+ const existingCheckpoint = readReasonCheckpoint(checkpointValue);
3353
3591
  const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
3354
3592
  return withSafeTraceSpan2(
3355
3593
  getReasonTraceAdapter(),
@@ -3368,7 +3606,8 @@ ${tryStringify2(args.response)}`
3368
3606
  ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
3369
3607
  ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
3370
3608
  hasOutputSchema: Boolean(args.outputSchema),
3371
- hasInterrupt: Boolean(args.interrupt),
3609
+ hasInterrupt: Boolean(interruptConfig),
3610
+ interruptContractCount: Object.keys(interruptConfig?.contracts ?? {}).length,
3372
3611
  hasStructured: Boolean(args.structured),
3373
3612
  hasTools: Boolean(args.tools?.length)
3374
3613
  },
@@ -3378,7 +3617,7 @@ ${tryStringify2(args.response)}`
3378
3617
  }
3379
3618
  },
3380
3619
  async (traceSpan) => {
3381
- if (args.tools?.length) {
3620
+ if ((args.tools?.length || args.interrupts) && !existingCheckpoint) {
3382
3621
  return runReasonToolLoop({
3383
3622
  useReasonArgs: args,
3384
3623
  allowValidatedToolOutput,
@@ -3645,6 +3884,24 @@ ${tryStringify2(args.response)}`
3645
3884
  return useReason(args);
3646
3885
  }
3647
3886
  function useInterrupt(args) {
3887
+ if ("contract" in args) {
3888
+ const contract = defineInterruptContract(args.contract);
3889
+ return awaitInterruptInternal({
3890
+ request: {
3891
+ kind: "custom",
3892
+ request: parseWithSchema(
3893
+ contract.requestSchema,
3894
+ args.request,
3895
+ "useInterrupt contract request"
3896
+ ),
3897
+ schemaId: contract.schemaId,
3898
+ schemaVersion: contract.schemaVersion,
3899
+ ...args.id ? { id: args.id } : {},
3900
+ ...args.meta ? { meta: args.meta } : {}
3901
+ },
3902
+ responseSchema: contract.responseSchema
3903
+ });
3904
+ }
3648
3905
  return awaitInterruptInternal(args);
3649
3906
  }
3650
3907
  function useAbortSignal() {
@@ -4950,7 +5207,10 @@ async function orchestrateGraphStream({
4950
5207
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
4951
5208
  resumeOutcome: "failed",
4952
5209
  resumeError: (0, import_errors4.serializeFailure)(resumeError).message,
4953
- failure: (0, import_errors4.serializeFailure)(resumeError)
5210
+ failure: (0, import_errors4.serializeFailure)(resumeError),
5211
+ ...typeof config.telemetryInterruptContract === "string" ? { contract: config.telemetryInterruptContract } : {},
5212
+ ...typeof config.telemetryInterruptSchemaId === "string" ? { schemaId: config.telemetryInterruptSchemaId } : {},
5213
+ ...typeof config.telemetryInterruptSchemaVersion === "string" ? { schemaVersion: config.telemetryInterruptSchemaVersion } : {}
4954
5214
  },
4955
5215
  flush: true
4956
5216
  });
@@ -5056,6 +5316,7 @@ async function orchestrateGraphStream({
5056
5316
  const optionsList = Array.isArray(input.options) ? input.options : [];
5057
5317
  const kind = input.kind || (input.multiple ? "multi-choice" : "choice");
5058
5318
  const isText = kind === "text";
5319
+ const isCustom = kind === "custom";
5059
5320
  const record = {
5060
5321
  token,
5061
5322
  ready: false,
@@ -5066,7 +5327,17 @@ async function orchestrateGraphStream({
5066
5327
  workflow: payload.workflow || currentState.currentWorkflow,
5067
5328
  node: payload.node || "",
5068
5329
  state: { ...currentState, awaitingHumanInput: true },
5069
- schema: isText ? {
5330
+ schema: isCustom ? {
5331
+ kind: "custom",
5332
+ multiple: false,
5333
+ ...input.question ? { question: input.question } : {},
5334
+ ...typeof input.id === "string" && input.id.length > 0 ? { id: input.id } : {},
5335
+ ...typeof input.contract === "string" && input.contract.length > 0 ? { contract: input.contract } : {},
5336
+ ...Object.hasOwn(input, "request") ? { request: input.request } : {},
5337
+ ...typeof input.schemaId === "string" && input.schemaId.length > 0 ? { schemaId: input.schemaId } : {},
5338
+ ...typeof input.schemaVersion === "string" && input.schemaVersion.length > 0 ? { schemaVersion: input.schemaVersion } : {},
5339
+ ...input.meta && typeof input.meta === "object" ? { meta: input.meta } : {}
5340
+ } : isText ? {
5070
5341
  kind,
5071
5342
  multiple: Boolean(input.multiple),
5072
5343
  ...input.question ? { question: input.question } : {},
@@ -5121,6 +5392,8 @@ async function orchestrateGraphStream({
5121
5392
  kind: record.schema.kind,
5122
5393
  multiple: record.schema.multiple,
5123
5394
  question: record.schema.question,
5395
+ ...record.schema.contract ? { contract: record.schema.contract } : {},
5396
+ ...Object.hasOwn(record.schema, "request") ? { request: record.schema.request } : {},
5124
5397
  ...typeof record.schema.id === "string" && record.schema.id.length > 0 ? { id: record.schema.id } : {},
5125
5398
  ...typeof record.schema.schemaId === "string" && record.schema.schemaId.length > 0 ? { schemaId: record.schema.schemaId } : {},
5126
5399
  ...typeof record.schema.schemaVersion === "string" && record.schema.schemaVersion.length > 0 ? { schemaVersion: record.schema.schemaVersion } : {},
@@ -5132,6 +5405,11 @@ async function orchestrateGraphStream({
5132
5405
  }))
5133
5406
  }
5134
5407
  });
5408
+ const captureRequest = shouldCaptureTelemetryContent(
5409
+ telemetryConfig.captureContent,
5410
+ "output"
5411
+ );
5412
+ const structuredRequest = isRecord3(record.schema.request) ? record.schema.request : void 0;
5135
5413
  emitTelemetryEvent({
5136
5414
  config,
5137
5415
  type: "interrupt.created",
@@ -5147,11 +5425,11 @@ async function orchestrateGraphStream({
5147
5425
  interactionMode: record.schema.kind === "text" ? "freeform" : record.options.length > 0 ? "static-options" : record.schema.schemaId ? "dynamic-picker" : "unknown",
5148
5426
  ...record.schema.schemaId ? { schemaId: record.schema.schemaId } : {},
5149
5427
  ...record.schema.schemaVersion ? { schemaVersion: record.schema.schemaVersion } : {},
5150
- ...typeof record.schema.question === "string" && shouldCaptureTelemetryContent(telemetryConfig.captureContent, "output") ? { question: record.schema.question } : {},
5151
- ...shouldCaptureTelemetryContent(
5152
- telemetryConfig.captureContent,
5153
- "output"
5154
- ) ? {
5428
+ ...record.schema.contract ? { contract: record.schema.contract } : {},
5429
+ requestCaptured: Boolean(captureRequest && structuredRequest),
5430
+ ...captureRequest && structuredRequest ? { request: structuredRequest } : {},
5431
+ ...typeof record.schema.question === "string" && captureRequest ? { question: record.schema.question } : {},
5432
+ ...captureRequest ? {
5155
5433
  options: record.options.map((option) => ({
5156
5434
  id: option.id,
5157
5435
  label: option.label,
@@ -5960,6 +6238,7 @@ function parseResumeMeta(msg) {
5960
6238
  const token = typeof raw.token === "string" ? raw.token : "";
5961
6239
  const requestId = typeof raw.requestId === "string" ? raw.requestId : "";
5962
6240
  const cancel = raw.cancel === true;
6241
+ const hasValue = Object.hasOwn(raw, "value");
5963
6242
  let selected = [];
5964
6243
  const rawSelected = raw.selected;
5965
6244
  if (typeof rawSelected === "string") selected = [rawSelected];
@@ -5969,7 +6248,13 @@ function parseResumeMeta(msg) {
5969
6248
  else if (Array.isArray(raw.choices))
5970
6249
  selected = raw.choices.map((c) => isRecord4(c) ? c.id : void 0).filter((id) => typeof id === "string");
5971
6250
  if (!token || !requestId) return null;
5972
- return { token, requestId, selected, cancel };
6251
+ return {
6252
+ token,
6253
+ requestId,
6254
+ selected,
6255
+ cancel,
6256
+ ...hasValue ? { value: raw.value, hasValue: true } : {}
6257
+ };
5973
6258
  }
5974
6259
  async function tryPrepareResumeStream({
5975
6260
  limits,
@@ -6056,7 +6341,8 @@ async function tryPrepareResumeStream({
6056
6341
  },
6057
6342
  payload: {
6058
6343
  interruptId: pending.requestId,
6059
- reason: "cancelled_by_client"
6344
+ reason: "cancelled_by_client",
6345
+ ...pending.schema.contract ? { contract: pending.schema.contract } : {}
6060
6346
  },
6061
6347
  flush: true
6062
6348
  });
@@ -6136,7 +6422,7 @@ async function tryPrepareResumeStream({
6136
6422
  resumeUpdate.runtime = resumeStatePatch;
6137
6423
  }
6138
6424
  const hasResumeUpdate = !isLimitPause && Object.keys(resumeUpdate).length > 0;
6139
- const resumeValue = isLimitPause ? void 0 : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
6425
+ const resumeValue = isLimitPause ? void 0 : meta.hasValue && pending.schema.kind === "custom" ? meta.value : meta.selected?.length && pending.schema.kind === "multi-choice" ? meta.selected.map((x) => String(x)) : meta.selected?.length ? String(meta.selected[0]) : void 0;
6140
6426
  const resumeCheckpointId = typeof pending.graphCheckpointId === "string" && pending.graphCheckpointId.length > 0 ? pending.graphCheckpointId : void 0;
6141
6427
  const resumedGraph = await (0, import_runtime2.createExecutionGraph)(wf, {
6142
6428
  ...telemetryConfig,
@@ -6146,7 +6432,7 @@ async function tryPrepareResumeStream({
6146
6432
  ...hasResumeUpdate ? { resumeUpdate } : {}
6147
6433
  });
6148
6434
  await store.delete(pending.token);
6149
- const response = responseFromSelection(meta.selected);
6435
+ const response = meta.hasValue ? JSON.stringify(meta.value) : responseFromSelection(meta.selected);
6150
6436
  const telemetry = isRecord4(telemetryConfig.telemetry) ? telemetryConfig.telemetry : {};
6151
6437
  const responseCaptured = Boolean(
6152
6438
  response && shouldCaptureTelemetryContent(telemetry.captureContent, "input")
@@ -6165,6 +6451,10 @@ async function tryPrepareResumeStream({
6165
6451
  resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
6166
6452
  resumeOutcome: "resumed",
6167
6453
  responseCaptured,
6454
+ ...pending.schema.contract ? { contract: pending.schema.contract } : {},
6455
+ ...pending.schema.schemaId ? { schemaId: pending.schema.schemaId } : {},
6456
+ ...pending.schema.schemaVersion ? { schemaVersion: pending.schema.schemaVersion } : {},
6457
+ ...responseCaptured && meta.hasValue ? { responseValue: meta.value } : {},
6168
6458
  ...responseCaptured && response ? { response } : {}
6169
6459
  },
6170
6460
  flush: true
@@ -6186,6 +6476,11 @@ async function tryPrepareResumeStream({
6186
6476
  resume: true,
6187
6477
  telemetryInterruptId: pending.requestId,
6188
6478
  telemetryInterruptNodeId: pending.node,
6479
+ ...pending.schema.contract ? { telemetryInterruptContract: pending.schema.contract } : {},
6480
+ ...pending.schema.schemaId ? { telemetryInterruptSchemaId: pending.schema.schemaId } : {},
6481
+ ...pending.schema.schemaVersion ? {
6482
+ telemetryInterruptSchemaVersion: pending.schema.schemaVersion
6483
+ } : {},
6189
6484
  ...resumeValue !== void 0 ? { resumeValue } : {},
6190
6485
  ...resumeCheckpointId ? { resumeCheckpointId } : {},
6191
6486
  ...hasResumeUpdate ? { resumeUpdate } : {}
@@ -6219,6 +6514,7 @@ var responseSchema = import_zod3.z.discriminatedUnion("type", [
6219
6514
  type: import_zod3.z.literal("select"),
6220
6515
  ids: import_zod3.z.array(import_zod3.z.string().min(1)).min(1)
6221
6516
  }).strict(),
6517
+ import_zod3.z.object({ type: import_zod3.z.literal("value"), value: import_zod3.z.unknown() }).strict(),
6222
6518
  import_zod3.z.object({ type: import_zod3.z.literal("cancel") }).strict()
6223
6519
  ]);
6224
6520
  function resultFromOutcome(outcome, runId, sessionId) {
@@ -6359,7 +6655,13 @@ async function resumeWorkflow(services, args) {
6359
6655
  "Resume handle does not match the root execution and workflow version."
6360
6656
  );
6361
6657
  if (response.type === "cancel") return;
6362
- if (response.type === "text") {
6658
+ if (response.type === "value") {
6659
+ if (pending.schema.kind !== "custom")
6660
+ throw new ExecutionRequestError(
6661
+ "INVALID_RESPONSE",
6662
+ "Structured values are only valid for custom interrupts."
6663
+ );
6664
+ } else if (response.type === "text") {
6363
6665
  if (pending.schema.kind !== "text")
6364
6666
  throw new ExecutionRequestError(
6365
6667
  "INVALID_RESPONSE",
@@ -6389,6 +6691,7 @@ async function resumeWorkflow(services, args) {
6389
6691
  token: handle.token,
6390
6692
  requestId: handle.requestId,
6391
6693
  selected: response.type === "text" ? [response.text] : response.type === "select" ? response.ids : [],
6694
+ ...response.type === "value" ? { value: response.value, hasValue: true } : {},
6392
6695
  cancel: response.type === "cancel"
6393
6696
  },
6394
6697
  sessionId: handle.sessionId,