@kortyx/hooks 0.9.0 → 0.10.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
@@ -44,6 +44,20 @@ var normalizeNodeState = (value) => {
44
44
  return { byIndex: [], byKey: {} };
45
45
  };
46
46
  var cloneWorkflowState = (value) => isRecord(value) ? { ...value } : {};
47
+ var cloneTokenUsage = (value) => {
48
+ if (!isRecord(value)) return void 0;
49
+ const input = typeof value.input === "number" ? value.input : void 0;
50
+ const output = typeof value.output === "number" ? value.output : void 0;
51
+ const total = typeof value.total === "number" ? value.total : void 0;
52
+ if (input === void 0 && output === void 0 && total === void 0) {
53
+ return void 0;
54
+ }
55
+ return {
56
+ input: input ?? 0,
57
+ output: output ?? 0,
58
+ total: total ?? 0
59
+ };
60
+ };
47
61
  var getHookRuntimeState = (state) => {
48
62
  const runtime = state.runtime ?? {};
49
63
  const internal = isRecord(runtime.__kortyx) ? runtime.__kortyx : void 0;
@@ -67,21 +81,60 @@ var createInternalContext = (ctx) => {
67
81
  reasonCallIndex: 0,
68
82
  currentNodeState,
69
83
  workflowState: cloneWorkflowState(hookRuntimeState.workflowState),
70
- dirty: false
84
+ tokenUsage: cloneTokenUsage(ctx.state.runtime?.tokenUsage),
85
+ stateDirty: false,
86
+ runtimeDirty: false
71
87
  };
72
88
  };
73
89
  var buildRuntimeStateUpdates = (ctx) => {
74
- if (!ctx.dirty) return null;
75
- return {
76
- __kortyx: {
90
+ const updates = {};
91
+ if (ctx.stateDirty) {
92
+ updates.__kortyx = {
77
93
  nodeState: {
78
94
  nodeId: ctx.node.graph.node,
79
95
  state: ctx.currentNodeState
80
96
  },
81
97
  workflowState: ctx.workflowState
82
- }
98
+ };
99
+ }
100
+ if (ctx.runtimeDirty && ctx.tokenUsage) {
101
+ updates.tokenUsage = ctx.tokenUsage;
102
+ }
103
+ return Object.keys(updates).length > 0 ? updates : null;
104
+ };
105
+ var toTokenUsageDelta = (usage) => {
106
+ if (!usage) return void 0;
107
+ const input = usage.input ?? 0;
108
+ const output = usage.output ?? 0;
109
+ const total = usage.total ?? input + output + (usage.reasoning ?? 0);
110
+ if (input === 0 && output === 0 && total === 0) {
111
+ return void 0;
112
+ }
113
+ return {
114
+ input,
115
+ output,
116
+ total
83
117
  };
84
118
  };
119
+ function accumulateTokenUsage(usage) {
120
+ const delta = toTokenUsageDelta(usage);
121
+ if (!delta) return;
122
+ const ctx = getHookContext();
123
+ const current = ctx.tokenUsage ?? {
124
+ input: 0,
125
+ output: 0,
126
+ total: 0
127
+ };
128
+ ctx.tokenUsage = {
129
+ input: current.input + delta.input,
130
+ output: current.output + delta.output,
131
+ total: current.total + delta.total
132
+ };
133
+ ctx.runtimeDirty = true;
134
+ }
135
+ function getReasonTraceAdapter() {
136
+ return getHookContext().reasonTrace;
137
+ }
85
138
  async function runWithHookContext(ctx, fn) {
86
139
  const internal = createInternalContext(ctx);
87
140
  try {
@@ -146,6 +199,25 @@ var awaitInterruptInternal = (args) => {
146
199
 
147
200
  // src/structured.ts
148
201
  var import_node_crypto = require("crypto");
202
+
203
+ // src/structured-path.ts
204
+ var parseStructuredPath = (path, source) => {
205
+ if (path.length === 0) {
206
+ throw new Error(`${source} path must be a non-empty dot-separated string.`);
207
+ }
208
+ const parts = path.split(".");
209
+ if (parts.some((part) => part.length === 0)) {
210
+ throw new Error(
211
+ `${source} path "${path}" must not contain empty segments.`
212
+ );
213
+ }
214
+ return parts;
215
+ };
216
+ var assertStructuredPath = (path, source) => {
217
+ parseStructuredPath(path, source);
218
+ };
219
+
220
+ // src/structured.ts
149
221
  var createStructuredStreamId = () => {
150
222
  try {
151
223
  return (0, import_node_crypto.randomUUID)();
@@ -166,6 +238,7 @@ var emitStructuredData = (args) => {
166
238
  ...typeof args.id === "string" && args.id.length > 0 ? { id: args.id } : {}
167
239
  };
168
240
  if (args.kind === "set") {
241
+ assertStructuredPath(args.path, "useStructuredData");
169
242
  const value = parseWithSchema(
170
243
  args.valueSchema,
171
244
  args.value,
@@ -180,6 +253,7 @@ var emitStructuredData = (args) => {
180
253
  return;
181
254
  }
182
255
  if (args.kind === "append") {
256
+ assertStructuredPath(args.path, "useStructuredData");
183
257
  const items = args.items.map(
184
258
  (item) => parseWithSchema(args.itemSchema, item, "useStructuredData append item")
185
259
  );
@@ -192,6 +266,7 @@ var emitStructuredData = (args) => {
192
266
  return;
193
267
  }
194
268
  if (args.kind === "text-delta") {
269
+ assertStructuredPath(args.path, "useStructuredData");
195
270
  ctx.node.emit("structured_data", {
196
271
  ...base,
197
272
  kind: "text-delta",
@@ -226,6 +301,12 @@ var readReasonCheckpoint = (raw) => {
226
301
  request: value.request,
227
302
  firstText: value.firstText,
228
303
  ...Object.hasOwn(value, "firstRaw") ? { firstRaw: value.firstRaw } : {},
304
+ ...Object.hasOwn(value, "firstUsage") ? { firstUsage: value.firstUsage } : {},
305
+ ...Object.hasOwn(value, "firstFinishReason") ? { firstFinishReason: value.firstFinishReason } : {},
306
+ ...Object.hasOwn(value, "firstProviderMetadata") ? {
307
+ firstProviderMetadata: value.firstProviderMetadata
308
+ } : {},
309
+ ...Object.hasOwn(value, "firstWarnings") ? { firstWarnings: value.firstWarnings } : {},
229
310
  ...Object.hasOwn(value, "firstOutput") ? { firstOutput: value.firstOutput } : {}
230
311
  };
231
312
  };
@@ -244,12 +325,7 @@ var import_node_crypto3 = require("crypto");
244
325
 
245
326
  // src/reason-engine.ts
246
327
  var import_node_crypto2 = require("crypto");
247
- var chunkToText = (chunk) => {
248
- if (typeof chunk === "string") return chunk;
249
- if (typeof chunk.text === "string") return chunk.text;
250
- if (typeof chunk.content === "string") return chunk.content;
251
- return "";
252
- };
328
+ var toError = (value) => value instanceof Error ? value : new Error(String(value));
253
329
  var createRuntimeId = () => {
254
330
  try {
255
331
  return (0, import_node_crypto2.randomUUID)();
@@ -268,9 +344,21 @@ async function runReasonEngine(args) {
268
344
  const stream = args.stream ?? args.model.options?.streaming ?? true;
269
345
  const emit = args.emit ?? true;
270
346
  const temperature = args.temperature ?? args.model.options?.temperature ?? args.defaultTemperature;
271
- const model = args.getProvider(args.model.providerId, args.model.modelId, {
347
+ const maxOutputTokens = args.maxOutputTokens ?? args.model.options?.maxOutputTokens;
348
+ const stopSequences = args.stopSequences ?? args.model.options?.stopSequences;
349
+ const abortSignal = args.abortSignal ?? args.model.options?.abortSignal;
350
+ const reasoning = args.reasoning ?? args.model.options?.reasoning;
351
+ const responseFormat = args.responseFormat ?? args.model.options?.responseFormat;
352
+ const providerOptions = args.providerOptions ?? args.model.options?.providerOptions;
353
+ const model = args.model.provider.getModel(args.model.modelId, {
272
354
  ...temperature !== void 0 ? { temperature } : {},
273
- streaming: stream
355
+ streaming: stream,
356
+ ...maxOutputTokens !== void 0 ? { maxOutputTokens } : {},
357
+ ...stopSequences !== void 0 ? { stopSequences } : {},
358
+ ...abortSignal !== void 0 ? { abortSignal } : {},
359
+ ...reasoning !== void 0 ? { reasoning } : {},
360
+ ...responseFormat !== void 0 ? { responseFormat } : {},
361
+ ...providerOptions !== void 0 ? { providerOptions } : {}
274
362
  });
275
363
  const messages = [];
276
364
  if (typeof args.system === "string" && args.system.length > 0) {
@@ -284,59 +372,154 @@ async function runReasonEngine(args) {
284
372
  opId,
285
373
  segmentId
286
374
  };
287
- if (stream) {
288
- let final = "";
289
- let raw;
375
+ const traceSpan = args.reasonTrace?.startSpan({
376
+ name: "runReasonEngine",
377
+ attributes: {
378
+ ...commonMeta,
379
+ providerId: args.model.provider.id,
380
+ modelId: args.model.modelId,
381
+ stream,
382
+ emit,
383
+ ...typeof args.nodeId === "string" && args.nodeId.length > 0 ? { nodeId: args.nodeId } : {}
384
+ }
385
+ });
386
+ const endTrace = (span, result) => {
387
+ span?.end?.({
388
+ ...result.usage !== void 0 ? { usage: result.usage } : {},
389
+ ...result.finishReason !== void 0 ? { finishReason: result.finishReason } : {},
390
+ ...result.providerMetadata !== void 0 ? { providerMetadata: result.providerMetadata } : {},
391
+ ...result.warnings !== void 0 ? { warnings: result.warnings } : {},
392
+ attributes: {
393
+ textLength: result.text.length
394
+ }
395
+ });
396
+ };
397
+ const failTrace = (span, error) => {
398
+ span?.fail?.(error, {
399
+ attributes: {
400
+ providerId: args.model.provider.id,
401
+ modelId: args.model.modelId
402
+ }
403
+ });
404
+ };
405
+ try {
406
+ if (stream) {
407
+ let final = "";
408
+ let raw;
409
+ let usage;
410
+ let finishReason;
411
+ let providerMetadata;
412
+ let warnings;
413
+ if (emit) {
414
+ emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
415
+ ...commonMeta
416
+ });
417
+ }
418
+ const response2 = await model.stream(messages);
419
+ for await (const chunk of response2) {
420
+ switch (chunk.type) {
421
+ case "text-delta": {
422
+ if (chunk.delta.length === 0) break;
423
+ final += chunk.delta;
424
+ args.onTextChunk?.(chunk.delta);
425
+ if (chunk.raw !== void 0) {
426
+ raw = chunk.raw;
427
+ }
428
+ if (chunk.providerMetadata !== void 0) {
429
+ providerMetadata = chunk.providerMetadata;
430
+ }
431
+ if (emit) {
432
+ emitNodeEvent(args.emitEvent, args.nodeId, "text-delta", {
433
+ ...commonMeta,
434
+ delta: chunk.delta
435
+ });
436
+ }
437
+ break;
438
+ }
439
+ case "finish": {
440
+ if (chunk.raw !== void 0) {
441
+ raw = chunk.raw;
442
+ }
443
+ if (chunk.usage !== void 0) {
444
+ usage = chunk.usage;
445
+ }
446
+ if (chunk.finishReason !== void 0) {
447
+ finishReason = chunk.finishReason;
448
+ }
449
+ if (chunk.providerMetadata !== void 0) {
450
+ providerMetadata = chunk.providerMetadata;
451
+ }
452
+ if (chunk.warnings !== void 0) {
453
+ warnings = chunk.warnings;
454
+ }
455
+ break;
456
+ }
457
+ case "raw": {
458
+ raw = chunk.raw;
459
+ if (chunk.providerMetadata !== void 0) {
460
+ providerMetadata = chunk.providerMetadata;
461
+ }
462
+ break;
463
+ }
464
+ case "error": {
465
+ if (chunk.raw !== void 0) {
466
+ raw = chunk.raw;
467
+ }
468
+ if (chunk.providerMetadata !== void 0) {
469
+ providerMetadata = chunk.providerMetadata;
470
+ }
471
+ if (chunk.warnings !== void 0) {
472
+ warnings = chunk.warnings;
473
+ }
474
+ throw toError(chunk.error);
475
+ }
476
+ }
477
+ }
478
+ if (emit) {
479
+ emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
480
+ ...commonMeta
481
+ });
482
+ }
483
+ const result2 = {
484
+ text: final,
485
+ ...raw !== void 0 ? { raw } : {},
486
+ ...usage !== void 0 ? { usage } : {},
487
+ ...finishReason !== void 0 ? { finishReason } : {},
488
+ ...providerMetadata !== void 0 ? { providerMetadata } : {},
489
+ ...warnings !== void 0 ? { warnings } : {}
490
+ };
491
+ endTrace(traceSpan, result2);
492
+ return result2;
493
+ }
494
+ const response = await model.invoke(messages);
290
495
  if (emit) {
291
496
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
292
497
  ...commonMeta
293
498
  });
294
- }
295
- const response2 = await model.stream(messages);
296
- for await (const chunk of response2) {
297
- const text = chunkToText(chunk);
298
- if (!text) continue;
299
- final += text;
300
- args.onTextChunk?.(text);
301
- if (typeof chunk === "object" && chunk && "raw" in chunk) {
302
- raw = chunk.raw;
303
- }
304
- if (emit) {
499
+ if (response.content) {
305
500
  emitNodeEvent(args.emitEvent, args.nodeId, "text-delta", {
306
501
  ...commonMeta,
307
- delta: text
502
+ delta: response.content
308
503
  });
309
504
  }
310
- }
311
- if (emit) {
312
505
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
313
506
  ...commonMeta
314
507
  });
315
508
  }
316
- return {
317
- text: final,
318
- ...raw !== void 0 ? { raw } : {}
509
+ const result = {
510
+ text: response.content,
511
+ ...response.raw !== void 0 ? { raw: response.raw } : {},
512
+ ...response.usage !== void 0 ? { usage: response.usage } : {},
513
+ ...response.finishReason !== void 0 ? { finishReason: response.finishReason } : {},
514
+ ...response.providerMetadata !== void 0 ? { providerMetadata: response.providerMetadata } : {},
515
+ ...response.warnings !== void 0 ? { warnings: response.warnings } : {}
319
516
  };
517
+ endTrace(traceSpan, result);
518
+ return result;
519
+ } catch (error) {
520
+ failTrace(traceSpan, error);
521
+ throw error;
320
522
  }
321
- const response = await model.invoke(messages);
322
- if (emit) {
323
- emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
324
- ...commonMeta
325
- });
326
- if (response.content) {
327
- emitNodeEvent(args.emitEvent, args.nodeId, "text-delta", {
328
- ...commonMeta,
329
- delta: response.content
330
- });
331
- }
332
- emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
333
- ...commonMeta
334
- });
335
- }
336
- return {
337
- text: response.content,
338
- ...response.raw !== void 0 ? { raw: response.raw } : {}
339
- };
340
523
  }
341
524
 
342
525
  // src/reason/engine.ts
@@ -349,22 +532,24 @@ var createRuntimeId2 = () => {
349
532
  };
350
533
  async function reasonEngine(args, meta, inputOverride) {
351
534
  const ctx = getHookContext();
352
- const getProvider = ctx.getProvider;
353
- if (!getProvider) {
354
- throw new Error("useReason requires a provider factory in runtime config.");
355
- }
356
535
  return runReasonEngine({
357
- getProvider,
358
536
  model: args.model,
359
537
  input: inputOverride ?? args.input,
360
538
  system: args.system,
361
539
  temperature: args.temperature,
540
+ maxOutputTokens: args.maxOutputTokens,
541
+ stopSequences: args.stopSequences,
542
+ abortSignal: args.abortSignal,
543
+ reasoning: args.reasoning,
544
+ responseFormat: args.responseFormat,
545
+ providerOptions: args.providerOptions,
362
546
  defaultTemperature: ctx.node.config?.model?.temperature,
363
547
  stream: args.stream,
364
548
  emit: args.emit,
365
549
  onTextChunk: args.onTextChunk,
366
550
  nodeId: ctx.node.graph.node,
367
551
  emitEvent: ctx.node.emit,
552
+ reasonTrace: ctx.reasonTrace,
368
553
  id: meta.id,
369
554
  opId: meta.opId
370
555
  });
@@ -374,8 +559,11 @@ async function reasonEngine(args, meta, inputOverride) {
374
559
  var tryJsonParse = (value) => {
375
560
  try {
376
561
  return { ok: true, value: JSON.parse(value) };
377
- } catch {
378
- return { ok: false };
562
+ } catch (error) {
563
+ return {
564
+ ok: false,
565
+ error: error instanceof Error ? error.message : String(error)
566
+ };
379
567
  }
380
568
  };
381
569
  var extractJsonCodeBlock = (value) => {
@@ -405,9 +593,50 @@ var resolveOutputCandidate = (text) => {
405
593
  }
406
594
  return text;
407
595
  };
596
+ var looksLikeStructuredJsonAttempt = (value) => {
597
+ const trimmed = value.trim();
598
+ if (!trimmed) return false;
599
+ return trimmed.startsWith("{") || trimmed.startsWith("[") || /^```(?:json)?/i.test(trimmed);
600
+ };
601
+ var isLikelyTruncatedStructuredOutput = (text) => {
602
+ const trimmed = text.trim();
603
+ if (!looksLikeStructuredJsonAttempt(trimmed)) return false;
604
+ const direct = tryJsonParse(trimmed);
605
+ if (!direct.ok && direct.error) return true;
606
+ const block = extractJsonCodeBlock(trimmed);
607
+ if (!block) return false;
608
+ const parsedBlock = tryJsonParse(block);
609
+ return !parsedBlock.ok && Boolean(parsedBlock.error);
610
+ };
611
+ var createTruncatedStructuredOutputError = (label) => new Error(
612
+ `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`
613
+ );
614
+ var createInvalidStructuredOutputError = (label) => new Error(
615
+ `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. This often happens when output is truncated or the provider does not follow JSON mode. Increase maxOutputTokens or simplify the requested output.`
616
+ );
617
+ var parseReasonOutputWithSchema = (args) => {
618
+ const candidate = resolveOutputCandidate(args.text);
619
+ try {
620
+ return parseWithSchema(args.schema, candidate, args.label);
621
+ } catch (error) {
622
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
623
+ throw createTruncatedStructuredOutputError(args.label);
624
+ }
625
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
626
+ throw createInvalidStructuredOutputError(args.label);
627
+ }
628
+ throw error;
629
+ }
630
+ };
408
631
  var parseInterruptFirstPassResult = (args) => {
409
632
  const candidate = resolveOutputCandidate(args.text);
410
633
  if (!isRecord2(candidate)) {
634
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
635
+ throw createTruncatedStructuredOutputError("useReason output");
636
+ }
637
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
638
+ throw createInvalidStructuredOutputError("useReason output");
639
+ }
411
640
  throw new Error(
412
641
  "useReason first pass with interrupt must return a JSON object."
413
642
  );
@@ -429,11 +658,21 @@ var parseInterruptFirstPassResult = (args) => {
429
658
  let output;
430
659
  if (args.outputSchema) {
431
660
  const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
432
- output = parseWithSchema(
433
- args.outputSchema,
434
- outputPayload,
435
- "useReason output"
436
- );
661
+ try {
662
+ output = parseWithSchema(
663
+ args.outputSchema,
664
+ outputPayload,
665
+ "useReason output"
666
+ );
667
+ } catch (error) {
668
+ if (args.finishReason?.unified === "length" || isLikelyTruncatedStructuredOutput(args.text)) {
669
+ throw createTruncatedStructuredOutputError("useReason output");
670
+ }
671
+ if (typeof outputPayload === "string" && outputPayload.trim().length > 0) {
672
+ throw createInvalidStructuredOutputError("useReason output");
673
+ }
674
+ throw error;
675
+ }
437
676
  }
438
677
  const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
439
678
  if (!args.outputSchema && !explicitDraft) {
@@ -582,9 +821,10 @@ var resolveAppendFieldPaths = (cfg) => {
582
821
  if (!fields) return [];
583
822
  const appendPaths = Object.entries(fields).filter(([, mode]) => mode === "append").map(([path]) => path);
584
823
  for (const path of appendPaths) {
585
- if (!path || path.includes(".")) {
824
+ assertStructuredPath(path, "useReason structured append");
825
+ if (path.includes(".")) {
586
826
  throw new Error(
587
- "useReason structured append streaming currently supports top-level array fields only."
827
+ "useReason structured append streaming requires non-empty top-level array field keys."
588
828
  );
589
829
  }
590
830
  }
@@ -595,9 +835,10 @@ var resolveSetFieldPaths = (cfg) => {
595
835
  if (!fields) return [];
596
836
  const setPaths = Object.entries(fields).filter(([, mode]) => mode === "set").map(([path]) => path);
597
837
  for (const path of setPaths) {
598
- if (!path || path.includes(".")) {
838
+ assertStructuredPath(path, "useReason structured set");
839
+ if (path.includes(".")) {
599
840
  throw new Error(
600
- "useReason structured set streaming currently supports top-level fields only."
841
+ "useReason structured set streaming requires non-empty top-level field keys."
601
842
  );
602
843
  }
603
844
  }
@@ -608,9 +849,10 @@ var resolveTextDeltaFieldPaths = (cfg) => {
608
849
  if (!fields) return [];
609
850
  const textPaths = Object.entries(fields).filter(([, mode]) => mode === "text-delta").map(([path]) => path);
610
851
  for (const path of textPaths) {
611
- if (!path || path.includes(".")) {
852
+ assertStructuredPath(path, "useReason structured text-delta");
853
+ if (path.includes(".")) {
612
854
  throw new Error(
613
- "useReason structured text-delta streaming currently supports top-level string fields only."
855
+ "useReason structured text-delta streaming requires non-empty top-level string field keys."
614
856
  );
615
857
  }
616
858
  }
@@ -948,8 +1190,53 @@ var extractStreamingStringValue = (args) => {
948
1190
  };
949
1191
 
950
1192
  // src/reason/use-reason.ts
1193
+ var mergeUsage = (left, right) => {
1194
+ if (!left) return right;
1195
+ if (!right) return left;
1196
+ const sum = (a, b) => a === void 0 && b === void 0 ? void 0 : (a ?? 0) + (b ?? 0);
1197
+ const raw = left.raw || right.raw ? {
1198
+ ...left.raw ?? {},
1199
+ ...right.raw ?? {}
1200
+ } : void 0;
1201
+ const input = sum(left.input, right.input);
1202
+ const output = sum(left.output, right.output);
1203
+ const total = sum(left.total, right.total);
1204
+ const reasoning = sum(left.reasoning, right.reasoning);
1205
+ const cacheRead = sum(left.cacheRead, right.cacheRead);
1206
+ const cacheWrite = sum(left.cacheWrite, right.cacheWrite);
1207
+ const merged = {};
1208
+ if (input !== void 0) merged.input = input;
1209
+ if (output !== void 0) merged.output = output;
1210
+ if (total !== void 0) merged.total = total;
1211
+ if (reasoning !== void 0) merged.reasoning = reasoning;
1212
+ if (cacheRead !== void 0) merged.cacheRead = cacheRead;
1213
+ if (cacheWrite !== void 0) merged.cacheWrite = cacheWrite;
1214
+ if (raw) merged.raw = raw;
1215
+ return merged;
1216
+ };
1217
+ var mergeWarnings = (left, right) => {
1218
+ if (!left?.length) return right;
1219
+ if (!right?.length) return left;
1220
+ const seen = /* @__PURE__ */ new Set();
1221
+ const merged = [];
1222
+ for (const warning of [...left, ...right]) {
1223
+ const key = JSON.stringify(warning);
1224
+ if (seen.has(key)) continue;
1225
+ seen.add(key);
1226
+ merged.push(warning);
1227
+ }
1228
+ return merged;
1229
+ };
1230
+ var mergeProviderMetadata = (left, right) => {
1231
+ if (!left) return right;
1232
+ if (!right) return left;
1233
+ return {
1234
+ ...left,
1235
+ ...right
1236
+ };
1237
+ };
951
1238
  var emitReasonStructuredOutput = (args) => {
952
- if (!shouldEmitStructured(args.structured)) return;
1239
+ if (!args.emit || !shouldEmitStructured(args.structured)) return;
953
1240
  emitStructuredData({
954
1241
  dataType: args.structured?.dataType ?? "reason-output",
955
1242
  data: args.output,
@@ -960,7 +1247,19 @@ var emitReasonStructuredOutput = (args) => {
960
1247
  streamId: args.opId
961
1248
  });
962
1249
  };
1250
+ var shouldEmitReasonStructured = (args) => (args.emit ?? true) && shouldEmitStructured(args.structured);
1251
+ var resolveEffectiveReasoningIncludeThoughts = (args) => args.reasoning?.includeThoughts ?? args.model.options?.reasoning?.includeThoughts ?? false;
1252
+ var resolveEffectiveResponseFormatType = (args) => args.responseFormat?.type ?? args.model.options?.responseFormat?.type;
1253
+ var assertReasoningThoughtsCompatibility = (args) => {
1254
+ if (!resolveEffectiveReasoningIncludeThoughts(args)) return;
1255
+ const usesStructuredOutput = Boolean(args.outputSchema) || Boolean(args.interrupt) || Boolean(args.structured) || resolveEffectiveResponseFormatType(args) === "json";
1256
+ if (!usesStructuredOutput) return;
1257
+ throw new Error(
1258
+ "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
1259
+ );
1260
+ };
963
1261
  async function useReason(args) {
1262
+ assertReasoningThoughtsCompatibility(args);
964
1263
  const ctx = getHookContext();
965
1264
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
966
1265
  const opId = createRuntimeId2();
@@ -972,12 +1271,27 @@ async function useReason(args) {
972
1271
  ctx.currentNodeState.byKey[checkpointKey]
973
1272
  );
974
1273
  const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
1274
+ const traceSpan = getReasonTraceAdapter()?.startSpan({
1275
+ name: "useReason",
1276
+ attributes: {
1277
+ ...id ? { id } : {},
1278
+ opId,
1279
+ nodeId: ctx.node.graph.node,
1280
+ providerId: args.model.provider.id,
1281
+ modelId: args.model.modelId,
1282
+ stream: args.stream ?? args.model.options?.streaming ?? true,
1283
+ emit: args.emit ?? true,
1284
+ hasOutputSchema: Boolean(args.outputSchema),
1285
+ hasInterrupt: Boolean(args.interrupt),
1286
+ hasStructured: Boolean(args.structured)
1287
+ }
1288
+ });
975
1289
  const suppressTextStream = Boolean(args.outputSchema || args.interrupt);
976
1290
  const setFieldPaths = resolveSetFieldPaths(args.structured);
977
1291
  const appendFieldPaths = resolveAppendFieldPaths(args.structured);
978
1292
  const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
979
1293
  const useStructuredIncrementalStreaming = Boolean(
980
- args.outputSchema && !args.interrupt && (setFieldPaths.length > 0 || appendFieldPaths.length > 0 || textDeltaFieldPaths.length > 0) && args.stream !== false && shouldEmitStructured(args.structured)
1294
+ args.outputSchema && !args.interrupt && (setFieldPaths.length > 0 || appendFieldPaths.length > 0 || textDeltaFieldPaths.length > 0) && args.stream !== false && shouldEmitReasonStructured(args)
981
1295
  );
982
1296
  let firstText = "";
983
1297
  let firstRaw;
@@ -986,6 +1300,10 @@ async function useReason(args) {
986
1300
  let finalText = "";
987
1301
  let finalRaw;
988
1302
  let finalOutput;
1303
+ let aggregatedUsage;
1304
+ let finalFinishReason;
1305
+ let aggregatedProviderMetadata;
1306
+ let aggregatedWarnings;
989
1307
  const emittedSetValues = /* @__PURE__ */ new Map();
990
1308
  const emittedAppendCounts = /* @__PURE__ */ new Map();
991
1309
  const emittedTextValues = /* @__PURE__ */ new Map();
@@ -996,6 +1314,13 @@ async function useReason(args) {
996
1314
  finalText = firstText;
997
1315
  finalRaw = firstRaw;
998
1316
  finalOutput = firstOutput;
1317
+ aggregatedUsage = existingCheckpoint.firstUsage;
1318
+ finalFinishReason = existingCheckpoint.firstFinishReason;
1319
+ aggregatedProviderMetadata = existingCheckpoint.firstProviderMetadata;
1320
+ aggregatedWarnings = existingCheckpoint.firstWarnings;
1321
+ traceSpan?.addEvent?.("useReason.resume", {
1322
+ checkpointKey
1323
+ });
999
1324
  } else {
1000
1325
  const first = await reasonEngine(
1001
1326
  {
@@ -1090,10 +1415,23 @@ async function useReason(args) {
1090
1415
  );
1091
1416
  firstRaw = first.raw;
1092
1417
  finalRaw = first.raw;
1418
+ accumulateTokenUsage(first.usage);
1419
+ aggregatedUsage = mergeUsage(aggregatedUsage, first.usage);
1420
+ finalFinishReason = first.finishReason;
1421
+ aggregatedProviderMetadata = mergeProviderMetadata(
1422
+ aggregatedProviderMetadata,
1423
+ first.providerMetadata
1424
+ );
1425
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, first.warnings);
1426
+ traceSpan?.addEvent?.("useReason.first-pass.complete", {
1427
+ textLength: first.text.length,
1428
+ hasInterrupt: Boolean(args.interrupt)
1429
+ });
1093
1430
  if (args.interrupt) {
1094
1431
  const firstPass = parseInterruptFirstPassResult({
1095
1432
  text: first.text,
1096
1433
  requestSchema: args.interrupt.requestSchema,
1434
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
1097
1435
  ...args.outputSchema ? { outputSchema: args.outputSchema } : {}
1098
1436
  });
1099
1437
  firstText = firstPass.draftText;
@@ -1105,11 +1443,12 @@ async function useReason(args) {
1105
1443
  firstText = first.text;
1106
1444
  finalText = first.text;
1107
1445
  if (args.outputSchema) {
1108
- firstOutput = parseWithSchema(
1109
- args.outputSchema,
1110
- resolveOutputCandidate(first.text),
1111
- "useReason output"
1112
- );
1446
+ firstOutput = parseReasonOutputWithSchema({
1447
+ text: first.text,
1448
+ schema: args.outputSchema,
1449
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
1450
+ label: "useReason output"
1451
+ });
1113
1452
  }
1114
1453
  finalOutput = firstOutput;
1115
1454
  }
@@ -1133,9 +1472,13 @@ async function useReason(args) {
1133
1472
  request: interruptRequest,
1134
1473
  firstText,
1135
1474
  ...firstRaw !== void 0 ? { firstRaw } : {},
1475
+ ...aggregatedUsage !== void 0 ? { firstUsage: aggregatedUsage } : {},
1476
+ ...finalFinishReason !== void 0 ? { firstFinishReason: finalFinishReason } : {},
1477
+ ...aggregatedProviderMetadata !== void 0 ? { firstProviderMetadata: aggregatedProviderMetadata } : {},
1478
+ ...aggregatedWarnings !== void 0 ? { firstWarnings: aggregatedWarnings } : {},
1136
1479
  ...firstOutput !== void 0 ? { firstOutput } : {}
1137
1480
  };
1138
- ctx.dirty = true;
1481
+ ctx.stateDirty = true;
1139
1482
  }
1140
1483
  const resumeStatePatch = resolveHookStatePatch({
1141
1484
  nodeId: ctx.node.graph.node,
@@ -1153,9 +1496,12 @@ async function useReason(args) {
1153
1496
  __kortyxResumeStatePatch: resumeStatePatch
1154
1497
  }
1155
1498
  });
1499
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
1500
+ checkpointKey
1501
+ });
1156
1502
  if (Object.hasOwn(ctx.currentNodeState.byKey, checkpointKey)) {
1157
1503
  delete ctx.currentNodeState.byKey[checkpointKey];
1158
- ctx.dirty = true;
1504
+ ctx.stateDirty = true;
1159
1505
  }
1160
1506
  const continuationInput = defaultContinuationInput({
1161
1507
  input: args.input,
@@ -1178,12 +1524,24 @@ async function useReason(args) {
1178
1524
  );
1179
1525
  finalText = second.text;
1180
1526
  finalRaw = second.raw;
1527
+ accumulateTokenUsage(second.usage);
1528
+ aggregatedUsage = mergeUsage(aggregatedUsage, second.usage);
1529
+ finalFinishReason = second.finishReason;
1530
+ aggregatedProviderMetadata = mergeProviderMetadata(
1531
+ aggregatedProviderMetadata,
1532
+ second.providerMetadata
1533
+ );
1534
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, second.warnings);
1535
+ traceSpan?.addEvent?.("useReason.continuation.complete", {
1536
+ textLength: second.text.length
1537
+ });
1181
1538
  if (args.outputSchema) {
1182
- finalOutput = parseWithSchema(
1183
- args.outputSchema,
1184
- resolveOutputCandidate(finalText),
1185
- "useReason output"
1186
- );
1539
+ finalOutput = parseReasonOutputWithSchema({
1540
+ text: finalText,
1541
+ schema: args.outputSchema,
1542
+ ...second.finishReason ? { finishReason: second.finishReason } : {},
1543
+ label: "useReason output"
1544
+ });
1187
1545
  }
1188
1546
  }
1189
1547
  if (finalOutput !== void 0) {
@@ -1191,17 +1549,34 @@ async function useReason(args) {
1191
1549
  ...id ? { id } : {},
1192
1550
  opId,
1193
1551
  output: finalOutput,
1194
- structured: args.structured
1552
+ structured: args.structured,
1553
+ emit: args.emit ?? true
1195
1554
  });
1196
1555
  }
1197
- return {
1556
+ const result = {
1198
1557
  ...id ? { id } : {},
1199
1558
  opId,
1200
1559
  text: finalText,
1201
1560
  ...finalRaw !== void 0 ? { raw: finalRaw } : {},
1561
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
1562
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
1563
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
1564
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
1202
1565
  ...finalOutput !== void 0 ? { output: finalOutput } : {},
1203
1566
  ...interruptResponse !== void 0 ? { interruptResponse } : {}
1204
1567
  };
1568
+ traceSpan?.end?.({
1569
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
1570
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
1571
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
1572
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
1573
+ attributes: {
1574
+ textLength: finalText.length,
1575
+ resumedFromCheckpoint: Boolean(existingCheckpoint),
1576
+ interrupted: Boolean(interruptResponse !== void 0)
1577
+ }
1578
+ });
1579
+ return result;
1205
1580
  }
1206
1581
 
1207
1582
  // src/hooks.ts
@@ -1220,14 +1595,14 @@ function useNodeState(initialValue) {
1220
1595
  const index = ctx.nodeStateIndex++;
1221
1596
  if (index >= nodeState.byIndex.length) {
1222
1597
  nodeState.byIndex[index] = initialValue;
1223
- ctx.dirty = true;
1598
+ ctx.stateDirty = true;
1224
1599
  }
1225
1600
  const getValue = () => nodeState.byIndex[index];
1226
1601
  const setValue = (next) => {
1227
1602
  const prev = getValue();
1228
1603
  const resolved = typeof next === "function" ? next(prev) : next;
1229
1604
  nodeState.byIndex[index] = resolved;
1230
- ctx.dirty = true;
1605
+ ctx.stateDirty = true;
1231
1606
  };
1232
1607
  return [getValue(), setValue];
1233
1608
  }
@@ -1238,14 +1613,14 @@ function useWorkflowState(key, ...rest) {
1238
1613
  const initialValue = rest[0];
1239
1614
  if (!Object.hasOwn(workflowState, key) && hasInitial) {
1240
1615
  workflowState[key] = initialValue;
1241
- ctx.dirty = true;
1616
+ ctx.stateDirty = true;
1242
1617
  }
1243
1618
  const getValue = () => workflowState[key];
1244
1619
  const setValue = (next) => {
1245
1620
  const prev = getValue();
1246
1621
  const resolved = typeof next === "function" ? next(prev) : next;
1247
1622
  workflowState[key] = resolved;
1248
- ctx.dirty = true;
1623
+ ctx.stateDirty = true;
1249
1624
  };
1250
1625
  return [getValue(), setValue];
1251
1626
  }