@kortyx/hooks 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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 {
@@ -248,6 +301,12 @@ var readReasonCheckpoint = (raw) => {
248
301
  request: value.request,
249
302
  firstText: value.firstText,
250
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 } : {},
251
310
  ...Object.hasOwn(value, "firstOutput") ? { firstOutput: value.firstOutput } : {}
252
311
  };
253
312
  };
@@ -266,12 +325,7 @@ var import_node_crypto3 = require("crypto");
266
325
 
267
326
  // src/reason-engine.ts
268
327
  var import_node_crypto2 = require("crypto");
269
- var chunkToText = (chunk) => {
270
- if (typeof chunk === "string") return chunk;
271
- if (typeof chunk.text === "string") return chunk.text;
272
- if (typeof chunk.content === "string") return chunk.content;
273
- return "";
274
- };
328
+ var toError = (value) => value instanceof Error ? value : new Error(String(value));
275
329
  var createRuntimeId = () => {
276
330
  try {
277
331
  return (0, import_node_crypto2.randomUUID)();
@@ -290,9 +344,21 @@ async function runReasonEngine(args) {
290
344
  const stream = args.stream ?? args.model.options?.streaming ?? true;
291
345
  const emit = args.emit ?? true;
292
346
  const temperature = args.temperature ?? args.model.options?.temperature ?? args.defaultTemperature;
293
- 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, {
294
354
  ...temperature !== void 0 ? { temperature } : {},
295
- 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 } : {}
296
362
  });
297
363
  const messages = [];
298
364
  if (typeof args.system === "string" && args.system.length > 0) {
@@ -306,59 +372,154 @@ async function runReasonEngine(args) {
306
372
  opId,
307
373
  segmentId
308
374
  };
309
- if (stream) {
310
- let final = "";
311
- 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);
312
495
  if (emit) {
313
496
  emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
314
497
  ...commonMeta
315
498
  });
316
- }
317
- const response2 = await model.stream(messages);
318
- for await (const chunk of response2) {
319
- const text = chunkToText(chunk);
320
- if (!text) continue;
321
- final += text;
322
- args.onTextChunk?.(text);
323
- if (typeof chunk === "object" && chunk && "raw" in chunk) {
324
- raw = chunk.raw;
325
- }
326
- if (emit) {
499
+ if (response.content) {
327
500
  emitNodeEvent(args.emitEvent, args.nodeId, "text-delta", {
328
501
  ...commonMeta,
329
- delta: text
502
+ delta: response.content
330
503
  });
331
504
  }
332
- }
333
- if (emit) {
334
505
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
335
506
  ...commonMeta
336
507
  });
337
508
  }
338
- return {
339
- text: final,
340
- ...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 } : {}
341
516
  };
517
+ endTrace(traceSpan, result);
518
+ return result;
519
+ } catch (error) {
520
+ failTrace(traceSpan, error);
521
+ throw error;
342
522
  }
343
- const response = await model.invoke(messages);
344
- if (emit) {
345
- emitNodeEvent(args.emitEvent, args.nodeId, "text-start", {
346
- ...commonMeta
347
- });
348
- if (response.content) {
349
- emitNodeEvent(args.emitEvent, args.nodeId, "text-delta", {
350
- ...commonMeta,
351
- delta: response.content
352
- });
353
- }
354
- emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
355
- ...commonMeta
356
- });
357
- }
358
- return {
359
- text: response.content,
360
- ...response.raw !== void 0 ? { raw: response.raw } : {}
361
- };
362
523
  }
363
524
 
364
525
  // src/reason/engine.ts
@@ -371,22 +532,24 @@ var createRuntimeId2 = () => {
371
532
  };
372
533
  async function reasonEngine(args, meta, inputOverride) {
373
534
  const ctx = getHookContext();
374
- const getProvider = ctx.getProvider;
375
- if (!getProvider) {
376
- throw new Error("useReason requires a provider factory in runtime config.");
377
- }
378
535
  return runReasonEngine({
379
- getProvider,
380
536
  model: args.model,
381
537
  input: inputOverride ?? args.input,
382
538
  system: args.system,
383
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,
384
546
  defaultTemperature: ctx.node.config?.model?.temperature,
385
547
  stream: args.stream,
386
548
  emit: args.emit,
387
549
  onTextChunk: args.onTextChunk,
388
550
  nodeId: ctx.node.graph.node,
389
551
  emitEvent: ctx.node.emit,
552
+ reasonTrace: ctx.reasonTrace,
390
553
  id: meta.id,
391
554
  opId: meta.opId
392
555
  });
@@ -396,8 +559,11 @@ async function reasonEngine(args, meta, inputOverride) {
396
559
  var tryJsonParse = (value) => {
397
560
  try {
398
561
  return { ok: true, value: JSON.parse(value) };
399
- } catch {
400
- return { ok: false };
562
+ } catch (error) {
563
+ return {
564
+ ok: false,
565
+ error: error instanceof Error ? error.message : String(error)
566
+ };
401
567
  }
402
568
  };
403
569
  var extractJsonCodeBlock = (value) => {
@@ -427,9 +593,50 @@ var resolveOutputCandidate = (text) => {
427
593
  }
428
594
  return text;
429
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
+ };
430
631
  var parseInterruptFirstPassResult = (args) => {
431
632
  const candidate = resolveOutputCandidate(args.text);
432
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
+ }
433
640
  throw new Error(
434
641
  "useReason first pass with interrupt must return a JSON object."
435
642
  );
@@ -451,11 +658,21 @@ var parseInterruptFirstPassResult = (args) => {
451
658
  let output;
452
659
  if (args.outputSchema) {
453
660
  const outputPayload = Object.hasOwn(candidate, "output") ? candidate.output : candidate;
454
- output = parseWithSchema(
455
- args.outputSchema,
456
- outputPayload,
457
- "useReason output"
458
- );
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
+ }
459
676
  }
460
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;
461
678
  if (!args.outputSchema && !explicitDraft) {
@@ -973,8 +1190,53 @@ var extractStreamingStringValue = (args) => {
973
1190
  };
974
1191
 
975
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
+ };
976
1238
  var emitReasonStructuredOutput = (args) => {
977
- if (!shouldEmitStructured(args.structured)) return;
1239
+ if (!args.emit || !shouldEmitStructured(args.structured)) return;
978
1240
  emitStructuredData({
979
1241
  dataType: args.structured?.dataType ?? "reason-output",
980
1242
  data: args.output,
@@ -985,7 +1247,19 @@ var emitReasonStructuredOutput = (args) => {
985
1247
  streamId: args.opId
986
1248
  });
987
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
+ };
988
1261
  async function useReason(args) {
1262
+ assertReasoningThoughtsCompatibility(args);
989
1263
  const ctx = getHookContext();
990
1264
  const id = typeof args.id === "string" && args.id.length > 0 ? args.id : void 0;
991
1265
  const opId = createRuntimeId2();
@@ -997,12 +1271,27 @@ async function useReason(args) {
997
1271
  ctx.currentNodeState.byKey[checkpointKey]
998
1272
  );
999
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
+ });
1000
1289
  const suppressTextStream = Boolean(args.outputSchema || args.interrupt);
1001
1290
  const setFieldPaths = resolveSetFieldPaths(args.structured);
1002
1291
  const appendFieldPaths = resolveAppendFieldPaths(args.structured);
1003
1292
  const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
1004
1293
  const useStructuredIncrementalStreaming = Boolean(
1005
- 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)
1006
1295
  );
1007
1296
  let firstText = "";
1008
1297
  let firstRaw;
@@ -1011,6 +1300,10 @@ async function useReason(args) {
1011
1300
  let finalText = "";
1012
1301
  let finalRaw;
1013
1302
  let finalOutput;
1303
+ let aggregatedUsage;
1304
+ let finalFinishReason;
1305
+ let aggregatedProviderMetadata;
1306
+ let aggregatedWarnings;
1014
1307
  const emittedSetValues = /* @__PURE__ */ new Map();
1015
1308
  const emittedAppendCounts = /* @__PURE__ */ new Map();
1016
1309
  const emittedTextValues = /* @__PURE__ */ new Map();
@@ -1021,6 +1314,13 @@ async function useReason(args) {
1021
1314
  finalText = firstText;
1022
1315
  finalRaw = firstRaw;
1023
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
+ });
1024
1324
  } else {
1025
1325
  const first = await reasonEngine(
1026
1326
  {
@@ -1115,10 +1415,23 @@ async function useReason(args) {
1115
1415
  );
1116
1416
  firstRaw = first.raw;
1117
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
+ });
1118
1430
  if (args.interrupt) {
1119
1431
  const firstPass = parseInterruptFirstPassResult({
1120
1432
  text: first.text,
1121
1433
  requestSchema: args.interrupt.requestSchema,
1434
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
1122
1435
  ...args.outputSchema ? { outputSchema: args.outputSchema } : {}
1123
1436
  });
1124
1437
  firstText = firstPass.draftText;
@@ -1130,11 +1443,12 @@ async function useReason(args) {
1130
1443
  firstText = first.text;
1131
1444
  finalText = first.text;
1132
1445
  if (args.outputSchema) {
1133
- firstOutput = parseWithSchema(
1134
- args.outputSchema,
1135
- resolveOutputCandidate(first.text),
1136
- "useReason output"
1137
- );
1446
+ firstOutput = parseReasonOutputWithSchema({
1447
+ text: first.text,
1448
+ schema: args.outputSchema,
1449
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
1450
+ label: "useReason output"
1451
+ });
1138
1452
  }
1139
1453
  finalOutput = firstOutput;
1140
1454
  }
@@ -1158,9 +1472,13 @@ async function useReason(args) {
1158
1472
  request: interruptRequest,
1159
1473
  firstText,
1160
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 } : {},
1161
1479
  ...firstOutput !== void 0 ? { firstOutput } : {}
1162
1480
  };
1163
- ctx.dirty = true;
1481
+ ctx.stateDirty = true;
1164
1482
  }
1165
1483
  const resumeStatePatch = resolveHookStatePatch({
1166
1484
  nodeId: ctx.node.graph.node,
@@ -1178,9 +1496,12 @@ async function useReason(args) {
1178
1496
  __kortyxResumeStatePatch: resumeStatePatch
1179
1497
  }
1180
1498
  });
1499
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
1500
+ checkpointKey
1501
+ });
1181
1502
  if (Object.hasOwn(ctx.currentNodeState.byKey, checkpointKey)) {
1182
1503
  delete ctx.currentNodeState.byKey[checkpointKey];
1183
- ctx.dirty = true;
1504
+ ctx.stateDirty = true;
1184
1505
  }
1185
1506
  const continuationInput = defaultContinuationInput({
1186
1507
  input: args.input,
@@ -1203,12 +1524,24 @@ async function useReason(args) {
1203
1524
  );
1204
1525
  finalText = second.text;
1205
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
+ });
1206
1538
  if (args.outputSchema) {
1207
- finalOutput = parseWithSchema(
1208
- args.outputSchema,
1209
- resolveOutputCandidate(finalText),
1210
- "useReason output"
1211
- );
1539
+ finalOutput = parseReasonOutputWithSchema({
1540
+ text: finalText,
1541
+ schema: args.outputSchema,
1542
+ ...second.finishReason ? { finishReason: second.finishReason } : {},
1543
+ label: "useReason output"
1544
+ });
1212
1545
  }
1213
1546
  }
1214
1547
  if (finalOutput !== void 0) {
@@ -1216,17 +1549,34 @@ async function useReason(args) {
1216
1549
  ...id ? { id } : {},
1217
1550
  opId,
1218
1551
  output: finalOutput,
1219
- structured: args.structured
1552
+ structured: args.structured,
1553
+ emit: args.emit ?? true
1220
1554
  });
1221
1555
  }
1222
- return {
1556
+ const result = {
1223
1557
  ...id ? { id } : {},
1224
1558
  opId,
1225
1559
  text: finalText,
1226
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 } : {},
1227
1565
  ...finalOutput !== void 0 ? { output: finalOutput } : {},
1228
1566
  ...interruptResponse !== void 0 ? { interruptResponse } : {}
1229
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;
1230
1580
  }
1231
1581
 
1232
1582
  // src/hooks.ts
@@ -1245,14 +1595,14 @@ function useNodeState(initialValue) {
1245
1595
  const index = ctx.nodeStateIndex++;
1246
1596
  if (index >= nodeState.byIndex.length) {
1247
1597
  nodeState.byIndex[index] = initialValue;
1248
- ctx.dirty = true;
1598
+ ctx.stateDirty = true;
1249
1599
  }
1250
1600
  const getValue = () => nodeState.byIndex[index];
1251
1601
  const setValue = (next) => {
1252
1602
  const prev = getValue();
1253
1603
  const resolved = typeof next === "function" ? next(prev) : next;
1254
1604
  nodeState.byIndex[index] = resolved;
1255
- ctx.dirty = true;
1605
+ ctx.stateDirty = true;
1256
1606
  };
1257
1607
  return [getValue(), setValue];
1258
1608
  }
@@ -1263,14 +1613,14 @@ function useWorkflowState(key, ...rest) {
1263
1613
  const initialValue = rest[0];
1264
1614
  if (!Object.hasOwn(workflowState, key) && hasInitial) {
1265
1615
  workflowState[key] = initialValue;
1266
- ctx.dirty = true;
1616
+ ctx.stateDirty = true;
1267
1617
  }
1268
1618
  const getValue = () => workflowState[key];
1269
1619
  const setValue = (next) => {
1270
1620
  const prev = getValue();
1271
1621
  const resolved = typeof next === "function" ? next(prev) : next;
1272
1622
  workflowState[key] = resolved;
1273
- ctx.dirty = true;
1623
+ ctx.stateDirty = true;
1274
1624
  };
1275
1625
  return [getValue(), setValue];
1276
1626
  }