@kortyx/agent 0.26.2 → 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
@@ -61,7 +61,7 @@ var require_internal = __commonJS({
61
61
  withSafeTraceSpan: () => withSafeTraceSpan2
62
62
  });
63
63
  module2.exports = __toCommonJS2(internal_exports);
64
- var import_errors6 = require("@kortyx/core/errors");
64
+ var import_errors8 = require("@kortyx/core/errors");
65
65
  var MAX_STACK_LENGTH = 32768;
66
66
  var MAX_CAUSE_DEPTH = 4;
67
67
  var projectDetails = (value, depth, seen) => {
@@ -71,16 +71,16 @@ var require_internal = __commonJS({
71
71
  return { type: "Error", message: "Circular error cause." };
72
72
  seen.add(objectValue);
73
73
  }
74
- const name = (0, import_errors6.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
75
- const message = (0, import_errors6.errorProperty)(value, "message");
76
- const stack = (0, import_errors6.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
74
+ const name = (0, import_errors8.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
75
+ const message = (0, import_errors8.errorProperty)(value, "message");
76
+ const stack = (0, import_errors8.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
77
77
  try {
78
78
  return value.stack;
79
79
  } catch {
80
80
  return void 0;
81
81
  }
82
82
  })() : void 0);
83
- const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors6.errorProperty)(value, "cause") : null;
83
+ const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors8.errorProperty)(value, "cause") : null;
84
84
  return {
85
85
  type: typeof name === "string" ? name.slice(0, 256) : "Error",
86
86
  message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
@@ -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,
@@ -789,14 +790,14 @@ var require_dist = __commonJS({
789
790
  workflowState: args.workflowState
790
791
  }
791
792
  });
792
- var import_errors6 = require("@kortyx/core/errors");
793
+ var import_errors8 = require("@kortyx/core/errors");
793
794
  var parseWithSchema = (schema, value, label) => {
794
795
  if (!schema) return value;
795
796
  const parsed = schema.safeParse(value);
796
797
  if (parsed.success) return parsed.data;
797
798
  const parsedError = "error" in parsed && parsed.error ? parsed.error : void 0;
798
799
  const reason = typeof parsedError?.message === "string" && parsedError.message.length > 0 ? parsedError.message : "invalid payload";
799
- throw new import_errors6.ValidationError(
800
+ throw new import_errors8.ValidationError(
800
801
  label === "useReason output" ? "MODEL_OUTPUT_SCHEMA" : "SCHEMA_VALIDATION",
801
802
  `${label} validation failed: ${reason}`,
802
803
  parsedError
@@ -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) {
@@ -3950,7 +4388,7 @@ ${tryStringify2(args.response)}`
3950
4388
  return [getValue(), setValue];
3951
4389
  }
3952
4390
  var import_core7 = require("@kortyx/core");
3953
- var import_errors7 = require("@kortyx/core/errors");
4391
+ var import_errors72 = require("@kortyx/core/errors");
3954
4392
  var ParallelChildWaiting = class extends Error {
3955
4393
  name = "ParallelChildWaiting";
3956
4394
  };
@@ -3964,14 +4402,14 @@ ${tryStringify2(args.response)}`
3964
4402
  );
3965
4403
  this.results = results;
3966
4404
  this.name = "ParallelError";
3967
- this.failure = (0, import_errors7.serializeFailure)({
4405
+ this.failure = (0, import_errors72.serializeFailure)({
3968
4406
  version: 1,
3969
4407
  code: this.code,
3970
4408
  category: "workflow",
3971
4409
  message: this.message,
3972
4410
  retryable: null,
3973
4411
  results: results.map(
3974
- (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors7.serializeFailure)(result.reason) }
4412
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors72.serializeFailure)(result.reason) }
3975
4413
  )
3976
4414
  });
3977
4415
  }
@@ -3979,7 +4417,7 @@ ${tryStringify2(args.response)}`
3979
4417
  code = "PARALLEL_FAILED";
3980
4418
  failure;
3981
4419
  toJSON() {
3982
- return (0, import_errors7.serializeFailure)(this);
4420
+ return (0, import_errors72.serializeFailure)(this);
3983
4421
  }
3984
4422
  };
3985
4423
  function registerWorkflowTask(id) {
@@ -4124,11 +4562,11 @@ ${tryStringify2(args.response)}`
4124
4562
  }
4125
4563
  var import_node_crypto6 = require("crypto");
4126
4564
  var import_core8 = require("@kortyx/core");
4127
- var import_errors8 = require("@kortyx/core/errors");
4128
- var WorkflowCallError = class extends import_errors8.KortyxError {
4565
+ var import_errors82 = require("@kortyx/core/errors");
4566
+ var WorkflowCallError = class extends import_errors82.KortyxError {
4129
4567
  constructor(workflow, message, cause, failure) {
4130
- const detail = (0, import_errors8.serializeFailure)(
4131
- failure ?? cause ?? new import_errors8.WorkflowContractError(message)
4568
+ const detail = (0, import_errors82.serializeFailure)(
4569
+ failure ?? cause ?? new import_errors82.WorkflowContractError(message)
4132
4570
  );
4133
4571
  super(detail.code, `Workflow '${workflow}': ${message}`, {
4134
4572
  ...detail,
@@ -4149,12 +4587,12 @@ ${tryStringify2(args.response)}`
4149
4587
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
4150
4588
  if (typeof v === "number" && Number.isFinite(v)) return v;
4151
4589
  if (typeof v !== "object" || !v || seen.has(v)) {
4152
- throw new import_errors8.WorkflowContractError(
4590
+ throw new import_errors82.WorkflowContractError(
4153
4591
  "Workflow input and output must be JSON serializable."
4154
4592
  );
4155
4593
  }
4156
4594
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
4157
- throw new import_errors8.WorkflowContractError(
4595
+ throw new import_errors82.WorkflowContractError(
4158
4596
  "Workflow input and output must contain plain JSON objects."
4159
4597
  );
4160
4598
  }
@@ -4175,7 +4613,7 @@ ${tryStringify2(args.response)}`
4175
4613
  useWorkflow: (args) => {
4176
4614
  const workflow = workflows[args.workflow];
4177
4615
  if (!workflow || workflow.id !== args.workflow)
4178
- throw new import_errors8.WorkflowContractError(
4616
+ throw new import_errors82.WorkflowContractError(
4179
4617
  "Workflow registry keys must match definition ids."
4180
4618
  );
4181
4619
  return useWorkflow({ ...args, workflow });
@@ -4201,23 +4639,23 @@ ${tryStringify2(args.response)}`
4201
4639
  (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
4202
4640
  const service = ctx.node.callWorkflow;
4203
4641
  if (!service)
4204
- throw new import_errors8.WorkflowContractError(
4642
+ throw new import_errors82.WorkflowContractError(
4205
4643
  "useWorkflow requires an agent workflow registry."
4206
4644
  );
4207
4645
  if (!args.id.trim())
4208
- throw new import_errors8.WorkflowContractError(
4646
+ throw new import_errors82.WorkflowContractError(
4209
4647
  "useWorkflow requires a stable, nonempty id."
4210
4648
  );
4211
4649
  if (ctx.workflowCallActive && !task.group)
4212
- throw new import_errors8.WorkflowContractError(
4650
+ throw new import_errors82.WorkflowContractError(
4213
4651
  "Concurrent workflow calls require parallel([...]); otherwise await each call."
4214
4652
  );
4215
4653
  if (ctx.workflowCallIds.has(args.id))
4216
- throw new import_errors8.WorkflowContractError(
4654
+ throw new import_errors82.WorkflowContractError(
4217
4655
  `Duplicate workflow call id '${args.id}' in one node activation.`
4218
4656
  );
4219
4657
  if (ctx.workflowCallIds.size >= 64)
4220
- throw new import_errors8.WorkflowContractError(
4658
+ throw new import_errors82.WorkflowContractError(
4221
4659
  "A node may call at most 64 child workflows per activation."
4222
4660
  );
4223
4661
  ctx.workflowCallIds.add(args.id);
@@ -4298,7 +4736,7 @@ ${tryStringify2(args.response)}`
4298
4736
  workflow,
4299
4737
  record.error ?? "child failed",
4300
4738
  void 0,
4301
- record.failure ?? (0, import_errors8.serializeFailure)(void 0)
4739
+ record.failure ?? (0, import_errors82.serializeFailure)(void 0)
4302
4740
  );
4303
4741
  if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
4304
4742
  throw new ParallelChildWaiting();
@@ -4334,7 +4772,7 @@ ${tryStringify2(args.response)}`
4334
4772
  throw error;
4335
4773
  }
4336
4774
  record.status = "failed";
4337
- record.failure = (0, import_errors8.serializeFailure)(error);
4775
+ record.failure = (0, import_errors82.serializeFailure)(error);
4338
4776
  record.error = record.failure.message;
4339
4777
  report("failed", { error: "Child workflow failed" });
4340
4778
  throw new WorkflowCallError(
@@ -4395,6 +4833,7 @@ ${tryStringify2(args.response)}`
4395
4833
  // src/index.ts
4396
4834
  var index_exports = {};
4397
4835
  __export(index_exports, {
4836
+ ChatLifecycleHookError: () => ChatLifecycleHookError,
4398
4837
  ExecutionRequestError: () => ExecutionRequestError,
4399
4838
  createAgent: () => createAgent,
4400
4839
  createChatRouteHandler: () => createChatRouteHandler,
@@ -4417,198 +4856,44 @@ __export(index_exports, {
4417
4856
  module.exports = __toCommonJS(index_exports);
4418
4857
 
4419
4858
  // src/adapters/http.ts
4859
+ var import_errors5 = require("@kortyx/core/errors");
4860
+ var import_stream2 = require("@kortyx/stream");
4861
+ var import_zod2 = require("zod");
4862
+
4863
+ // src/chat/lifecycle.ts
4420
4864
  var import_errors = require("@kortyx/core/errors");
4421
- var import_stream = require("@kortyx/stream");
4422
- var import_zod = require("zod");
4423
- var chatMessageSchema = import_zod.z.object({
4424
- role: import_zod.z.enum(["user", "assistant", "system"]),
4425
- content: import_zod.z.string(),
4426
- metadata: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
4427
- id: import_zod.z.string().optional(),
4428
- timestamp: import_zod.z.number().refine(Number.isFinite).optional()
4429
- }).strict();
4430
- var chatRequestBodySchema = import_zod.z.looseObject({
4431
- sessionId: import_zod.z.string().optional(),
4432
- workflowId: import_zod.z.string().optional(),
4433
- stream: import_zod.z.boolean().optional(),
4434
- context: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional(),
4435
- messages: import_zod.z.array(chatMessageSchema)
4436
- });
4437
- var checkpointRequestBodySchema = import_zod.z.discriminatedUnion("action", [
4438
- import_zod.z.object({
4439
- action: import_zod.z.literal("list"),
4440
- sessionId: import_zod.z.string().min(1)
4441
- }).strict(),
4442
- import_zod.z.object({
4443
- action: import_zod.z.literal("get"),
4444
- checkpointId: import_zod.z.string().min(1)
4445
- }).strict(),
4446
- import_zod.z.object({
4447
- action: import_zod.z.literal("rollback"),
4448
- checkpointId: import_zod.z.string().min(1)
4449
- }).strict(),
4450
- import_zod.z.object({
4451
- action: import_zod.z.literal("fork"),
4452
- checkpointId: import_zod.z.string().min(1),
4453
- newSessionId: import_zod.z.string().min(1).optional()
4454
- }).strict()
4455
- ]);
4456
- async function readRequestJson(request) {
4457
- try {
4458
- const value = await request.json();
4459
- if (!value || typeof value !== "object" || Array.isArray(value))
4460
- throw new Error("Expected a JSON object.");
4461
- return value;
4462
- } catch (cause) {
4463
- throw new import_errors.KortyxError(
4464
- "INVALID_REQUEST",
4465
- "Request body is not valid JSON.",
4865
+ var ChatLifecycleHookError = class extends import_errors.KortyxError {
4866
+ phase;
4867
+ constructor(phase, cause) {
4868
+ super(
4869
+ "CHAT_LIFECYCLE_HOOK_FAILED",
4870
+ `Chat lifecycle hook failed: ${phase}`,
4466
4871
  {
4467
- category: "request",
4468
- retryable: false,
4469
- safeMessage: "The request body must contain a valid JSON object.",
4872
+ category: "persistence",
4873
+ retryable: null,
4874
+ safeMessage: "Chat state could not be saved.",
4470
4875
  cause
4471
4876
  }
4472
4877
  );
4878
+ this.name = "ChatLifecycleHookError";
4879
+ this.phase = phase;
4473
4880
  }
4474
- }
4475
- function createFailureResponse(error, status) {
4476
- const failure = (0, import_errors.serializeFailure)(error);
4477
- return Response.json(
4478
- { error: failure.message, failure },
4479
- { status: status ?? (0, import_errors.failureHttpStatus)(failure) }
4480
- );
4481
- }
4482
- function parseChatRequestBody(value) {
4483
- const parsed = chatRequestBodySchema.safeParse(value);
4484
- if (!parsed.success) {
4485
- const [firstIssue] = parsed.error.issues;
4486
- throw new import_errors.KortyxError("INVALID_REQUEST", firstIssue.message, {
4881
+ };
4882
+
4883
+ // src/interrupt/resume-handler.ts
4884
+ var import_core3 = require("@kortyx/core");
4885
+ var import_runtime2 = require("@kortyx/runtime");
4886
+
4887
+ // src/execution/types.ts
4888
+ var import_errors2 = require("@kortyx/core/errors");
4889
+ var ExecutionRequestError = class extends import_errors2.KortyxError {
4890
+ name = "ExecutionRequestError";
4891
+ constructor(code, message, cause) {
4892
+ super(code, message, {
4487
4893
  category: "request",
4488
4894
  retryable: false,
4489
- safeMessage: "The request body is invalid."
4490
- });
4491
- }
4492
- const sessionId = parsed.data.sessionId?.trim();
4493
- const workflowId = parsed.data.workflowId?.trim();
4494
- return {
4495
- ...sessionId ? { sessionId } : {},
4496
- ...workflowId ? { workflowId } : {},
4497
- ...typeof parsed.data.stream === "boolean" ? { stream: parsed.data.stream } : {},
4498
- ...parsed.data.context ? { context: parsed.data.context } : {},
4499
- messages: parsed.data.messages
4500
- };
4501
- }
4502
- function parseCheckpointRequestBody(value) {
4503
- const parsed = checkpointRequestBodySchema.safeParse(value);
4504
- if (parsed.success) return parsed.data;
4505
- const [firstIssue] = parsed.error.issues;
4506
- throw new import_errors.KortyxError("INVALID_REQUEST", firstIssue.message, {
4507
- category: "request",
4508
- retryable: false,
4509
- safeMessage: "The request body is invalid."
4510
- });
4511
- }
4512
- async function handleChatRequestBody(args) {
4513
- const { agent, body } = args;
4514
- const stream = await agent.streamChat(body.messages, {
4515
- ...args.onExecution ? { onExecution: args.onExecution } : {},
4516
- ...args.abortSignal ? { abortSignal: args.abortSignal } : {},
4517
- sessionId: body.sessionId,
4518
- workflowId: body.workflowId,
4519
- context: body.context
4520
- });
4521
- if (body.stream === false) {
4522
- const buffered = await (0, import_stream.collectBufferedStream)(stream);
4523
- return new Response(JSON.stringify(buffered), {
4524
- headers: {
4525
- "content-type": "application/json"
4526
- }
4527
- });
4528
- }
4529
- return (0, import_stream.toSSE)(stream);
4530
- }
4531
- function createChatRouteHandler(args) {
4532
- const { agent, errorStatus } = args;
4533
- return async function POST(request) {
4534
- try {
4535
- const body = parseChatRequestBody(await readRequestJson(request));
4536
- return await handleChatRequestBody({
4537
- agent,
4538
- body,
4539
- abortSignal: request.signal,
4540
- onExecution: args.onExecution
4541
- });
4542
- } catch (error) {
4543
- return createFailureResponse(error, errorStatus);
4544
- }
4545
- };
4546
- }
4547
- async function handleCheckpointRequestBody(args) {
4548
- const { agent, body } = args;
4549
- const result = body.action === "list" ? await agent.listCheckpoints(body.sessionId) : body.action === "get" ? await agent.getCheckpoint(body.checkpointId) : body.action === "rollback" ? await agent.rollbackTo(body.checkpointId) : await agent.fork(body.checkpointId, {
4550
- ...body.newSessionId ? { newSessionId: body.newSessionId } : {}
4551
- });
4552
- return new Response(JSON.stringify(result), {
4553
- headers: {
4554
- "content-type": "application/json"
4555
- }
4556
- });
4557
- }
4558
- function createCheckpointRouteHandler(args) {
4559
- const { agent, errorStatus } = args;
4560
- return async function POST(request) {
4561
- try {
4562
- const body = parseCheckpointRequestBody(await readRequestJson(request));
4563
- return await handleCheckpointRequestBody({ agent, body });
4564
- } catch (error) {
4565
- return createFailureResponse(error, errorStatus);
4566
- }
4567
- };
4568
- }
4569
-
4570
- // src/adapters/http-client.ts
4571
- var import_browser = require("@kortyx/stream/browser");
4572
- async function* streamChatFromRoute(args) {
4573
- yield* (0, import_browser.streamFromRoute)({
4574
- endpoint: args.endpoint,
4575
- ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {},
4576
- ...args.headers ? { headers: args.headers } : {},
4577
- body: {
4578
- sessionId: args.sessionId,
4579
- workflowId: args.workflowId,
4580
- messages: args.messages
4581
- }
4582
- });
4583
- }
4584
-
4585
- // src/chat/create-agent.ts
4586
- var import_node_path = require("path");
4587
- var import_core5 = require("@kortyx/core");
4588
- var import_providers = require("@kortyx/providers");
4589
- var import_runtime5 = require("@kortyx/runtime");
4590
- var import_zod5 = require("zod");
4591
-
4592
- // src/execution/execute.ts
4593
- var import_core4 = require("@kortyx/core");
4594
- var import_errors5 = require("@kortyx/core/errors");
4595
- var import_runtime3 = require("@kortyx/runtime");
4596
- var import_zod3 = require("zod");
4597
-
4598
- // src/interrupt/resume-handler.ts
4599
- var import_core3 = require("@kortyx/core");
4600
- var import_runtime2 = require("@kortyx/runtime");
4601
-
4602
- // src/execution/types.ts
4603
- var import_errors2 = require("@kortyx/core/errors");
4604
- var ExecutionRequestError = class extends import_errors2.KortyxError {
4605
- name = "ExecutionRequestError";
4606
- constructor(code, message, cause) {
4607
- super(code, message, {
4608
- category: "request",
4609
- retryable: false,
4610
- safeMessage: "The execution request is invalid. Check the workflow contract and request fields.",
4611
- cause
4895
+ safeMessage: "The execution request is invalid. Check the workflow contract and request fields.",
4896
+ cause
4612
4897
  });
4613
4898
  }
4614
4899
  };
@@ -4619,11 +4904,12 @@ var import_core2 = require("@kortyx/core");
4619
4904
  var import_errors4 = require("@kortyx/core/errors");
4620
4905
  var import_internal = __toESM(require_internal());
4621
4906
  var import_runtime = require("@kortyx/runtime");
4907
+ var import_stream = require("@kortyx/stream");
4622
4908
  var import_langgraph = require("@langchain/langgraph");
4623
4909
 
4624
4910
  // src/execution/contracts.ts
4625
4911
  var import_errors3 = require("@kortyx/core/errors");
4626
- var import_zod2 = require("zod");
4912
+ var import_zod = require("zod");
4627
4913
  async function resolveExecutionWorkflow(select, reference) {
4628
4914
  const id = typeof reference === "string" ? reference : reference.id;
4629
4915
  let workflow;
@@ -4655,12 +4941,12 @@ async function resolveExecutionWorkflow(select, reference) {
4655
4941
  }
4656
4942
  function parseExecutionInput(workflow, input) {
4657
4943
  try {
4658
- import_zod2.z.json().parse(input);
4944
+ import_zod.z.json().parse(input);
4659
4945
  const parsed = workflow.inputSchema ? workflow.inputSchema.parse(input) : input;
4660
- import_zod2.z.json().parse(parsed);
4946
+ import_zod.z.json().parse(parsed);
4661
4947
  return parsed;
4662
4948
  } catch (error) {
4663
- if (!(error instanceof import_zod2.z.ZodError)) throw error;
4949
+ if (!(error instanceof import_zod.z.ZodError)) throw error;
4664
4950
  throw new ExecutionRequestError("INVALID_INPUT", error.message, error);
4665
4951
  }
4666
4952
  }
@@ -4678,9 +4964,9 @@ async function validateExecutionOutput(state, select) {
4678
4964
  for (const schema of /* @__PURE__ */ new Set([terminal.outputSchema, root.outputSchema])) {
4679
4965
  if (schema) data = schema.parse(data);
4680
4966
  }
4681
- if (terminal.outputSchema || root.outputSchema) import_zod2.z.json().parse(data);
4967
+ if (terminal.outputSchema || root.outputSchema) import_zod.z.json().parse(data);
4682
4968
  } catch (error) {
4683
- if (!(error instanceof import_zod2.z.ZodError)) throw error;
4969
+ if (!(error instanceof import_zod.z.ZodError)) throw error;
4684
4970
  const failure = new import_errors3.ValidationError(
4685
4971
  "INVALID_OUTPUT",
4686
4972
  `Workflow output validation failed: ${error.message}`,
@@ -5091,6 +5377,9 @@ async function orchestrateGraphStream({
5091
5377
  abortSignal: requestSignal,
5092
5378
  executionSignal,
5093
5379
  onExecution,
5380
+ clientTurnId,
5381
+ continueOnDisconnect = false,
5382
+ onResponseFinalized,
5094
5383
  emitOutput = true,
5095
5384
  onOutcome,
5096
5385
  selectWorkflow,
@@ -5099,12 +5388,34 @@ async function orchestrateGraphStream({
5099
5388
  executionLeaseRelease
5100
5389
  }) {
5101
5390
  const out = new import_node_stream.PassThrough({ objectMode: true });
5391
+ const visibleMessage = emitOutput && clientTurnId && onResponseFinalized && initialConfig.responseCompleted !== true ? (0, import_stream.createFinalizedChatMessageAccumulator)(clientTurnId) : void 0;
5392
+ let responseFinalized = false;
5393
+ const finalizeVisibleResponse = async (status) => {
5394
+ if (!visibleMessage || !onResponseFinalized || !clientTurnId || !sessionId || responseFinalized)
5395
+ return;
5396
+ responseFinalized = true;
5397
+ try {
5398
+ await onResponseFinalized({
5399
+ sessionId,
5400
+ runId,
5401
+ clientTurnId,
5402
+ status,
5403
+ message: visibleMessage.message(),
5404
+ ...sessionCheckpointId ? { checkpointId: sessionCheckpointId } : {}
5405
+ });
5406
+ } catch (error) {
5407
+ console.error("[chat:onResponseFinalized]", (0, import_errors4.serializeFailure)(error));
5408
+ }
5409
+ };
5102
5410
  const write = (chunk) => {
5103
5411
  if (isRecord3(chunk) && chunk.type === "done" && isGraphState(chunk.data)) {
5104
5412
  const { __kortyxParallelGraph: _journal, ...runtime } = chunk.data.runtime;
5105
5413
  chunk = { ...chunk, data: { ...chunk.data, runtime } };
5106
5414
  }
5107
- if (emitOutput && !response.closed && !out.destroyed) out.write(chunk);
5415
+ if (emitOutput && !response.closed) {
5416
+ visibleMessage?.apply(chunk);
5417
+ if (!out.destroyed) out.write(chunk);
5418
+ }
5108
5419
  };
5109
5420
  let outcomeState = state;
5110
5421
  let outcomeError;
@@ -5119,7 +5430,7 @@ async function orchestrateGraphStream({
5119
5430
  const response = createResponseLifecycle({
5120
5431
  enabled: emitOutput,
5121
5432
  restored: initialConfig.responseCompleted === true,
5122
- requestSignal,
5433
+ requestSignal: continueOnDisconnect ? void 0 : requestSignal,
5123
5434
  executionSignal: executionSignal ? AbortSignal.any([executionSignal, leaseAbort.signal]) : leaseAbort.signal,
5124
5435
  onClosed: () => {
5125
5436
  out.write({ type: "done" });
@@ -5183,11 +5494,12 @@ async function orchestrateGraphStream({
5183
5494
  },
5184
5495
  flush: true
5185
5496
  });
5497
+ await finalizeVisibleResponse("completed");
5186
5498
  }
5187
5499
  });
5188
5500
  const abortSignal = response.signal;
5189
5501
  out.on("close", () => {
5190
- if (!finished) response.disconnect();
5502
+ if (!finished && !continueOnDisconnect) response.disconnect();
5191
5503
  });
5192
5504
  let structuredSeq = 0;
5193
5505
  const debugEnabled = Boolean(config?.features?.tracing);
@@ -5618,6 +5930,13 @@ async function orchestrateGraphStream({
5618
5930
  });
5619
5931
  return;
5620
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
+ }
5621
5940
  if (event === "transition") {
5622
5941
  write({
5623
5942
  type: "transition",
@@ -5940,6 +6259,9 @@ async function orchestrateGraphStream({
5940
6259
  );
5941
6260
  }
5942
6261
  }
6262
+ await finalizeVisibleResponse(
6263
+ outcomeError ? "failed" : shouldKeepFrameworkState ? "interrupted" : "completed"
6264
+ );
5943
6265
  write({ type: "done", data: workflowFinalState });
5944
6266
  out.end();
5945
6267
  return;
@@ -6017,6 +6339,9 @@ async function orchestrateGraphStream({
6017
6339
  });
6018
6340
  finished = true;
6019
6341
  runTraceSpan.end?.(runTraceEndArgs());
6342
+ await finalizeVisibleResponse(
6343
+ outcomeError ? "failed" : pendingRecordToken || naturalFinalState.awaitingHumanInput ? "interrupted" : "completed"
6344
+ );
6020
6345
  write({ type: "done", data: naturalFinalState });
6021
6346
  out.end();
6022
6347
  return;
@@ -6077,6 +6402,7 @@ async function orchestrateGraphStream({
6077
6402
  fallbackRunSpan?.setAttributes?.({ "kortyx.run.cancelled": true });
6078
6403
  fallbackRunSpan?.end?.();
6079
6404
  write({ type: "cancelled", runId, reason: "Execution cancelled." });
6405
+ await finalizeVisibleResponse("cancelled");
6080
6406
  write({ type: "done" });
6081
6407
  finished = true;
6082
6408
  out.end();
@@ -6223,6 +6549,9 @@ async function orchestrateGraphStream({
6223
6549
  console.error("[execution:releaseLease]", (0, import_errors4.serializeFailure)(error));
6224
6550
  }
6225
6551
  response.dispose();
6552
+ await finalizeVisibleResponse(
6553
+ outcomeError ? (0, import_core2.isExecutionCancelled)(outcomeError) ? "cancelled" : "failed" : pendingRecordToken || outcomeState.awaitingHumanInput ? "interrupted" : "completed"
6554
+ );
6226
6555
  onOutcome?.({
6227
6556
  state: outcomeState,
6228
6557
  pending: pendingRecordToken ? activePendingRequests.get(pendingRecordToken) : void 0,
@@ -6272,6 +6601,9 @@ async function tryPrepareResumeStream({
6272
6601
  abortSignal,
6273
6602
  executionSignal,
6274
6603
  onExecution,
6604
+ clientTurnId,
6605
+ continueOnDisconnect,
6606
+ onResponseFinalized,
6275
6607
  lastMessage,
6276
6608
  meta: suppliedMeta,
6277
6609
  emitOutput,
@@ -6490,6 +6822,9 @@ async function tryPrepareResumeStream({
6490
6822
  executionSignal: continuedSignal ? AbortSignal.any([continuedSignal, leaseAbort.signal]) : leaseAbort.signal,
6491
6823
  executionLeaseRelease: releaseLease,
6492
6824
  onExecution,
6825
+ clientTurnId,
6826
+ continueOnDisconnect,
6827
+ onResponseFinalized,
6493
6828
  emitOutput,
6494
6829
  onOutcome,
6495
6830
  sessionId,
@@ -6526,7 +6861,316 @@ async function tryPrepareResumeStream({
6526
6861
  }
6527
6862
  }
6528
6863
 
6864
+ // src/adapters/http.ts
6865
+ var reportLifecycleError = async (callback, event) => {
6866
+ if (!callback) {
6867
+ console.error("[chat:lifecycle]", event.error);
6868
+ return;
6869
+ }
6870
+ try {
6871
+ await callback(event);
6872
+ } catch (error) {
6873
+ console.error("[chat:onLifecycleError]", (0, import_errors5.serializeFailure)(error));
6874
+ }
6875
+ };
6876
+ var chatMessageSchema = import_zod2.z.object({
6877
+ role: import_zod2.z.enum(["user", "assistant", "system"]),
6878
+ content: import_zod2.z.string(),
6879
+ metadata: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
6880
+ id: import_zod2.z.string().optional(),
6881
+ timestamp: import_zod2.z.number().refine(Number.isFinite).optional()
6882
+ }).strict();
6883
+ var chatRequestBodySchema = import_zod2.z.looseObject({
6884
+ sessionId: import_zod2.z.string().optional(),
6885
+ clientTurnId: import_zod2.z.string().max(128).optional(),
6886
+ workflowId: import_zod2.z.string().optional(),
6887
+ stream: import_zod2.z.boolean().optional(),
6888
+ context: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.unknown()).optional(),
6889
+ messages: import_zod2.z.array(chatMessageSchema)
6890
+ });
6891
+ var checkpointRequestBodySchema = import_zod2.z.discriminatedUnion("action", [
6892
+ import_zod2.z.object({
6893
+ action: import_zod2.z.literal("list"),
6894
+ sessionId: import_zod2.z.string().min(1)
6895
+ }).strict(),
6896
+ import_zod2.z.object({
6897
+ action: import_zod2.z.literal("get"),
6898
+ checkpointId: import_zod2.z.string().min(1)
6899
+ }).strict(),
6900
+ import_zod2.z.object({
6901
+ action: import_zod2.z.literal("rollback"),
6902
+ checkpointId: import_zod2.z.string().min(1)
6903
+ }).strict(),
6904
+ import_zod2.z.object({
6905
+ action: import_zod2.z.literal("fork"),
6906
+ checkpointId: import_zod2.z.string().min(1),
6907
+ newSessionId: import_zod2.z.string().min(1).optional()
6908
+ }).strict()
6909
+ ]);
6910
+ async function readRequestJson(request) {
6911
+ try {
6912
+ const value = await request.json();
6913
+ if (!value || typeof value !== "object" || Array.isArray(value))
6914
+ throw new Error("Expected a JSON object.");
6915
+ return value;
6916
+ } catch (cause) {
6917
+ throw new import_errors5.KortyxError(
6918
+ "INVALID_REQUEST",
6919
+ "Request body is not valid JSON.",
6920
+ {
6921
+ category: "request",
6922
+ retryable: false,
6923
+ safeMessage: "The request body must contain a valid JSON object.",
6924
+ cause
6925
+ }
6926
+ );
6927
+ }
6928
+ }
6929
+ function createFailureResponse(error, status) {
6930
+ const failure = (0, import_errors5.serializeFailure)(error);
6931
+ return Response.json(
6932
+ { error: failure.message, failure },
6933
+ { status: status ?? (0, import_errors5.failureHttpStatus)(failure) }
6934
+ );
6935
+ }
6936
+ function parseChatRequestBody(value) {
6937
+ const parsed = chatRequestBodySchema.safeParse(value);
6938
+ if (!parsed.success) {
6939
+ const [firstIssue] = parsed.error.issues;
6940
+ throw new import_errors5.KortyxError("INVALID_REQUEST", firstIssue.message, {
6941
+ category: "request",
6942
+ retryable: false,
6943
+ safeMessage: "The request body is invalid."
6944
+ });
6945
+ }
6946
+ const sessionId = parsed.data.sessionId?.trim();
6947
+ const workflowId = parsed.data.workflowId?.trim();
6948
+ const clientTurnId = parsed.data.clientTurnId?.trim();
6949
+ return {
6950
+ ...sessionId ? { sessionId } : {},
6951
+ ...workflowId ? { workflowId } : {},
6952
+ ...clientTurnId ? { clientTurnId } : {},
6953
+ ...typeof parsed.data.stream === "boolean" ? { stream: parsed.data.stream } : {},
6954
+ ...parsed.data.context ? { context: parsed.data.context } : {},
6955
+ messages: parsed.data.messages
6956
+ };
6957
+ }
6958
+ function parseCheckpointRequestBody(value) {
6959
+ const parsed = checkpointRequestBodySchema.safeParse(value);
6960
+ if (parsed.success) return parsed.data;
6961
+ const [firstIssue] = parsed.error.issues;
6962
+ throw new import_errors5.KortyxError("INVALID_REQUEST", firstIssue.message, {
6963
+ category: "request",
6964
+ retryable: false,
6965
+ safeMessage: "The request body is invalid."
6966
+ });
6967
+ }
6968
+ async function handleChatRequestBody(args) {
6969
+ const { agent, body } = args;
6970
+ if (args.onResponseFinalized && (!body.sessionId || !body.clientTurnId)) {
6971
+ throw new import_errors5.KortyxError(
6972
+ "INVALID_REQUEST",
6973
+ "sessionId and clientTurnId are required with onResponseFinalized.",
6974
+ {
6975
+ category: "request",
6976
+ retryable: false,
6977
+ safeMessage: "This chat request needs a session and turn ID."
6978
+ }
6979
+ );
6980
+ }
6981
+ const stream = await agent.streamChat(body.messages, {
6982
+ ...args.onExecution ? { onExecution: args.onExecution } : {},
6983
+ ...body.clientTurnId ? { clientTurnId: body.clientTurnId } : {},
6984
+ ...args.continueOnDisconnect ? { continueOnDisconnect: true } : {},
6985
+ ...args.onResponseFinalized ? { onResponseFinalized: args.onResponseFinalized } : {},
6986
+ ...args.abortSignal && !args.continueOnDisconnect ? { abortSignal: args.abortSignal } : {},
6987
+ sessionId: body.sessionId,
6988
+ workflowId: body.workflowId,
6989
+ context: body.context
6990
+ });
6991
+ if (body.stream === false) {
6992
+ const buffered = await (0, import_stream2.collectBufferedStream)(stream);
6993
+ return new Response(JSON.stringify(buffered), {
6994
+ headers: {
6995
+ "content-type": "application/json"
6996
+ }
6997
+ });
6998
+ }
6999
+ return (0, import_stream2.toSSE)(stream);
7000
+ }
7001
+ function createChatRouteHandler(args) {
7002
+ const { agent, errorStatus } = args;
7003
+ if (args.disconnect === "continue" && !args.onExecution) {
7004
+ throw new Error(
7005
+ 'disconnect: "continue" requires onExecution to retain host lifetime.'
7006
+ );
7007
+ }
7008
+ return async function POST(request) {
7009
+ try {
7010
+ const body = parseChatRequestBody(await readRequestJson(request));
7011
+ if (args.onTurnAccepted || args.onResponseFinalized) {
7012
+ if (!body.sessionId || !body.clientTurnId) {
7013
+ throw new import_errors5.KortyxError(
7014
+ "INVALID_REQUEST",
7015
+ "sessionId and clientTurnId are required with chat lifecycle hooks.",
7016
+ {
7017
+ category: "request",
7018
+ retryable: false,
7019
+ safeMessage: "This chat request needs a session and turn ID."
7020
+ }
7021
+ );
7022
+ }
7023
+ const userMessage = body.messages.at(-1);
7024
+ if (!userMessage || userMessage.role !== "user") {
7025
+ throw new import_errors5.KortyxError(
7026
+ "INVALID_REQUEST",
7027
+ "The last chat message must be from the user.",
7028
+ {
7029
+ category: "request",
7030
+ retryable: false,
7031
+ safeMessage: "The chat request needs a user message."
7032
+ }
7033
+ );
7034
+ }
7035
+ if (args.onTurnAccepted) {
7036
+ try {
7037
+ await args.onTurnAccepted({
7038
+ request,
7039
+ sessionId: body.sessionId,
7040
+ clientTurnId: body.clientTurnId,
7041
+ userMessage: { ...userMessage, role: "user" },
7042
+ kind: parseResumeMeta(userMessage) ? "interrupt-response" : "prompt"
7043
+ });
7044
+ } catch (cause) {
7045
+ const error = new ChatLifecycleHookError("turn-accepted", cause);
7046
+ await reportLifecycleError(args.onLifecycleError, {
7047
+ phase: "turn-accepted",
7048
+ clientTurnId: body.clientTurnId,
7049
+ error
7050
+ });
7051
+ throw error;
7052
+ }
7053
+ }
7054
+ }
7055
+ return await handleChatRequestBody({
7056
+ agent,
7057
+ body,
7058
+ abortSignal: request.signal,
7059
+ onExecution: args.onExecution,
7060
+ continueOnDisconnect: args.disconnect === "continue",
7061
+ ...args.onResponseFinalized ? {
7062
+ onResponseFinalized: async (event) => {
7063
+ try {
7064
+ await args.onResponseFinalized?.({ ...event, request });
7065
+ } catch (cause) {
7066
+ await reportLifecycleError(args.onLifecycleError, {
7067
+ phase: "response-finalized",
7068
+ clientTurnId: event.clientTurnId,
7069
+ error: new ChatLifecycleHookError(
7070
+ "response-finalized",
7071
+ cause
7072
+ )
7073
+ });
7074
+ }
7075
+ }
7076
+ } : {}
7077
+ });
7078
+ } catch (error) {
7079
+ return createFailureResponse(error, errorStatus);
7080
+ }
7081
+ };
7082
+ }
7083
+ async function handleCheckpointRequestBody(args) {
7084
+ const { agent, body } = args;
7085
+ const result = body.action === "list" ? await agent.listCheckpoints(body.sessionId) : body.action === "get" ? await agent.getCheckpoint(body.checkpointId) : body.action === "rollback" ? await agent.rollbackTo(body.checkpointId) : await agent.fork(body.checkpointId, {
7086
+ ...body.newSessionId ? { newSessionId: body.newSessionId } : {}
7087
+ });
7088
+ if (args.request && body.action === "fork" && args.onForked) {
7089
+ const fork = result;
7090
+ try {
7091
+ await args.onForked({
7092
+ request: args.request,
7093
+ sourceSessionId: fork.parentSessionId,
7094
+ newSessionId: fork.sessionId,
7095
+ sourceCheckpointId: fork.forkedFrom,
7096
+ newCheckpointId: fork.checkpoint.id
7097
+ });
7098
+ } catch (cause) {
7099
+ await reportLifecycleError(args.onLifecycleError, {
7100
+ phase: "forked",
7101
+ error: new ChatLifecycleHookError("forked", cause)
7102
+ });
7103
+ }
7104
+ }
7105
+ if (args.request && body.action === "rollback" && args.onRolledBack) {
7106
+ const rollback = result;
7107
+ try {
7108
+ await args.onRolledBack({
7109
+ request: args.request,
7110
+ sessionId: rollback.sessionId,
7111
+ headCheckpointId: rollback.head,
7112
+ invalidatedStructuredStreamIds: rollback.invalidatedStructuredStreamIds
7113
+ });
7114
+ } catch (cause) {
7115
+ await reportLifecycleError(args.onLifecycleError, {
7116
+ phase: "rolled-back",
7117
+ error: new ChatLifecycleHookError("rolled-back", cause)
7118
+ });
7119
+ }
7120
+ }
7121
+ return new Response(JSON.stringify(result), {
7122
+ headers: {
7123
+ "content-type": "application/json"
7124
+ }
7125
+ });
7126
+ }
7127
+ function createCheckpointRouteHandler(args) {
7128
+ const { agent, errorStatus } = args;
7129
+ return async function POST(request) {
7130
+ try {
7131
+ const body = parseCheckpointRequestBody(await readRequestJson(request));
7132
+ return await handleCheckpointRequestBody({
7133
+ agent,
7134
+ body,
7135
+ request,
7136
+ onForked: args.onForked,
7137
+ onRolledBack: args.onRolledBack,
7138
+ onLifecycleError: args.onLifecycleError
7139
+ });
7140
+ } catch (error) {
7141
+ return createFailureResponse(error, errorStatus);
7142
+ }
7143
+ };
7144
+ }
7145
+
7146
+ // src/adapters/http-client.ts
7147
+ var import_browser = require("@kortyx/stream/browser");
7148
+ async function* streamChatFromRoute(args) {
7149
+ yield* (0, import_browser.streamFromRoute)({
7150
+ endpoint: args.endpoint,
7151
+ ...args.fetchImpl ? { fetchImpl: args.fetchImpl } : {},
7152
+ ...args.headers ? { headers: args.headers } : {},
7153
+ body: {
7154
+ sessionId: args.sessionId,
7155
+ clientTurnId: args.clientTurnId,
7156
+ workflowId: args.workflowId,
7157
+ messages: args.messages
7158
+ }
7159
+ });
7160
+ }
7161
+
7162
+ // src/chat/create-agent.ts
7163
+ var import_node_path = require("path");
7164
+ var import_core5 = require("@kortyx/core");
7165
+ var import_providers = require("@kortyx/providers");
7166
+ var import_runtime5 = require("@kortyx/runtime");
7167
+ var import_zod5 = require("zod");
7168
+
6529
7169
  // src/execution/execute.ts
7170
+ var import_core4 = require("@kortyx/core");
7171
+ var import_errors6 = require("@kortyx/core/errors");
7172
+ var import_runtime3 = require("@kortyx/runtime");
7173
+ var import_zod3 = require("zod");
6530
7174
  var handleSchema = import_zod3.z.object({
6531
7175
  token: import_zod3.z.string().min(1),
6532
7176
  requestId: import_zod3.z.string().min(1),
@@ -6557,7 +7201,7 @@ function resultFromOutcome(outcome, runId, sessionId) {
6557
7201
  return {
6558
7202
  ...info,
6559
7203
  status: "failed",
6560
- error: (0, import_errors5.serializeFailure)(error)
7204
+ error: (0, import_errors6.serializeFailure)(error)
6561
7205
  };
6562
7206
  }
6563
7207
  if (outcome.cancelled)
@@ -6738,7 +7382,7 @@ async function resumeWorkflow(services, args) {
6738
7382
  );
6739
7383
  }).catch(
6740
7384
  (error) => reject(
6741
- error instanceof Error ? error : new import_errors5.KortyxError("RESUME_FAILED", "Resume failed.", {
7385
+ error instanceof Error ? error : new import_errors6.KortyxError("RESUME_FAILED", "Resume failed.", {
6742
7386
  category: "persistence",
6743
7387
  cause: error,
6744
7388
  safeMessage: "The saved execution could not be resumed."
@@ -6941,6 +7585,7 @@ function restoreWorkflowCallBranch(requests, telemetry, sourceRunId) {
6941
7585
  }
6942
7586
 
6943
7587
  // src/chat/process-chat.ts
7588
+ var import_errors7 = require("@kortyx/core/errors");
6944
7589
  var import_runtime4 = require("@kortyx/runtime");
6945
7590
 
6946
7591
  // src/utils/extract-latest-message.ts
@@ -6961,6 +7606,9 @@ async function streamChat({
6961
7606
  abortSignal,
6962
7607
  executionSignal,
6963
7608
  onExecution,
7609
+ clientTurnId,
7610
+ continueOnDisconnect,
7611
+ onResponseFinalized,
6964
7612
  options,
6965
7613
  sessionId,
6966
7614
  defaultWorkflowId,
@@ -6972,6 +7620,18 @@ async function streamChat({
6972
7620
  getProvider,
6973
7621
  applyResumeSelection
6974
7622
  }) {
7623
+ const suppliedSessionId = sessionId || options?.sessionId;
7624
+ if (onResponseFinalized && (!suppliedSessionId || !clientTurnId?.trim() || clientTurnId.length > 128)) {
7625
+ throw new import_errors7.KortyxError(
7626
+ "INVALID_REQUEST",
7627
+ "sessionId and clientTurnId are required with onResponseFinalized.",
7628
+ {
7629
+ category: "request",
7630
+ retryable: false,
7631
+ safeMessage: "This chat request needs a session and turn ID."
7632
+ }
7633
+ );
7634
+ }
6975
7635
  const config = await loadRuntimeConfig(options);
6976
7636
  let runtimeConfig2 = {
6977
7637
  ...config,
@@ -7010,6 +7670,9 @@ async function streamChat({
7010
7670
  abortSignal,
7011
7671
  executionSignal,
7012
7672
  onExecution,
7673
+ clientTurnId,
7674
+ continueOnDisconnect,
7675
+ onResponseFinalized,
7013
7676
  lastMessage: last,
7014
7677
  sessionId: resolvedSessionId,
7015
7678
  config: runtimeConfig2,
@@ -7083,6 +7746,9 @@ async function streamChat({
7083
7746
  abortSignal,
7084
7747
  executionSignal,
7085
7748
  onExecution,
7749
+ clientTurnId,
7750
+ continueOnDisconnect,
7751
+ onResponseFinalized,
7086
7752
  sessionId: resolvedSessionId,
7087
7753
  runId,
7088
7754
  graph,
@@ -7101,6 +7767,9 @@ var agentProcessOptionsSchema = import_zod5.z.object({
7101
7767
  onExecution: import_zod5.z.custom(
7102
7768
  (value) => typeof value === "function"
7103
7769
  ).optional(),
7770
+ clientTurnId: import_zod5.z.string().min(1).max(128).optional(),
7771
+ continueOnDisconnect: import_zod5.z.boolean().optional(),
7772
+ onResponseFinalized: import_zod5.z.custom((value) => typeof value === "function").optional(),
7104
7773
  limits: import_core5.ExecutionLimitsSchema.optional(),
7105
7774
  abortSignal: import_zod5.z.custom((value) => value instanceof AbortSignal).optional(),
7106
7775
  sessionId: import_zod5.z.string().optional(),
@@ -7247,6 +7916,9 @@ function createAgent(args) {
7247
7916
  abortSignal: parsedOptions?.abortSignal,
7248
7917
  executionSignal: parsedOptions?.executionSignal,
7249
7918
  onExecution: parsedOptions?.onExecution,
7919
+ clientTurnId: parsedOptions?.clientTurnId,
7920
+ continueOnDisconnect: parsedOptions?.continueOnDisconnect,
7921
+ onResponseFinalized: parsedOptions?.onResponseFinalized,
7250
7922
  options: parsedOptions,
7251
7923
  workflowRegistry: registry,
7252
7924
  ...knownWorkflowIds ? { knownWorkflowIds } : {},
@@ -7403,6 +8075,7 @@ function createAgent(args) {
7403
8075
  }
7404
8076
  // Annotate the CommonJS export names for ESM import in node:
7405
8077
  0 && (module.exports = {
8078
+ ChatLifecycleHookError,
7406
8079
  ExecutionRequestError,
7407
8080
  createAgent,
7408
8081
  createChatRouteHandler,