@kortyx/hooks 0.25.3 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  useReason: () => useReason2,
33
33
  useRuntimeContext: () => useRuntimeContext,
34
34
  useStructuredData: () => useStructuredData,
35
+ useTool: () => useTool,
35
36
  useWorkflow: () => useWorkflow,
36
37
  useWorkflowState: () => useWorkflowState,
37
38
  workflowCallFingerprint: () => workflowCallFingerprint
@@ -99,6 +100,7 @@ var createInternalContext = (ctx) => {
99
100
  workflowContextClosed: false,
100
101
  nodeStateIndex: 0,
101
102
  reasonCallIndex: 0,
103
+ toolCallIndex: 0,
102
104
  currentNodeState,
103
105
  workflowState: cloneWorkflowState(hookRuntimeState.workflowState),
104
106
  tokenUsage: cloneTokenUsage(ctx.state.runtime?.tokenUsage),
@@ -189,6 +191,12 @@ async function runWithHookContext(ctx, fn) {
189
191
  }
190
192
  });
191
193
  cleanupCompletedReasonCheckpoints(internal);
194
+ for (const key of Object.keys(internal.currentNodeState.byKey)) {
195
+ if (key.startsWith("__useTool:")) {
196
+ delete internal.currentNodeState.byKey[key];
197
+ internal.stateDirty = true;
198
+ }
199
+ }
192
200
  for (const key of Object.keys(internal.currentNodeState.byKey)) {
193
201
  if (key.startsWith("__useWorkflow:") || key.startsWith("__parallel:") || key === "__useWorkflowActivation") {
194
202
  internal.currentNodeState.byKey[key] = null;
@@ -341,7 +349,87 @@ var awaitInterruptInternal = (args) => {
341
349
  };
342
350
 
343
351
  // src/reason/use-reason.ts
344
- var import_core5 = require("@kortyx/core");
352
+ var import_core6 = require("@kortyx/core");
353
+
354
+ // src/safe-tracing.ts
355
+ function safeSpan(span) {
356
+ const guard = (fn) => (...args) => {
357
+ try {
358
+ fn?.(...args);
359
+ } catch {
360
+ }
361
+ };
362
+ let ended = false;
363
+ const finish = (fn) => (...args) => {
364
+ if (ended) return;
365
+ ended = true;
366
+ guard(fn)(...args);
367
+ };
368
+ return {
369
+ addEvent: guard(
370
+ (name, attributes) => span?.addEvent?.(name, attributes)
371
+ ),
372
+ setAttributes: guard(
373
+ (attributes) => span?.setAttributes?.(attributes)
374
+ ),
375
+ end: finish((args) => span?.end?.(args)),
376
+ fail: finish((error, args) => {
377
+ if (span?.fail) span.fail(error, args);
378
+ else span?.end?.(args);
379
+ })
380
+ };
381
+ }
382
+ function safeStartSpan(adapter, args) {
383
+ try {
384
+ return safeSpan(adapter?.startSpan?.(args));
385
+ } catch {
386
+ return safeSpan(void 0);
387
+ }
388
+ }
389
+ async function withSafeTraceSpan(adapter, args, fn) {
390
+ let execution;
391
+ const invoke = (span) => {
392
+ execution ??= Promise.resolve().then(async () => {
393
+ const observed = safeSpan(span);
394
+ try {
395
+ const result = await fn(observed);
396
+ observed.end?.();
397
+ return result;
398
+ } catch (error) {
399
+ observed.fail?.(error);
400
+ throw error;
401
+ }
402
+ });
403
+ return execution;
404
+ };
405
+ let activeScope;
406
+ try {
407
+ activeScope = adapter?.withSpan?.bind(adapter);
408
+ } catch {
409
+ }
410
+ if (activeScope) {
411
+ let started;
412
+ const invoked = new Promise((resolve) => {
413
+ started = resolve;
414
+ });
415
+ const observe = async () => {
416
+ try {
417
+ await activeScope(args, (span) => {
418
+ const result = invoke(span);
419
+ started?.();
420
+ return result;
421
+ });
422
+ } catch {
423
+ }
424
+ };
425
+ const observation = observe();
426
+ if (!execution) await Promise.race([invoked, observation]);
427
+ } else {
428
+ await invoke(safeStartSpan(adapter, args)).catch(() => {
429
+ });
430
+ }
431
+ return execution ?? invoke(safeSpan(void 0));
432
+ }
345
433
 
346
434
  // src/structured.ts
347
435
  var import_node_crypto = require("crypto");
@@ -435,18 +523,392 @@ var emitStructuredData = (args) => {
435
523
  var shouldEmitStructured = (cfg) => Boolean(cfg);
436
524
  var shouldStreamStructured = (cfg) => Boolean(cfg) && (cfg?.stream ?? true);
437
525
 
438
- // src/reason/engine.ts
526
+ // src/tool.ts
527
+ var import_node_async_hooks2 = require("async_hooks");
439
528
  var import_node_crypto3 = require("crypto");
440
- var import_core3 = require("@kortyx/core");
441
-
442
- // src/reason-engine.ts
443
- var import_node_crypto2 = require("crypto");
444
529
  var import_core2 = require("@kortyx/core");
530
+ var import_errors3 = require("@kortyx/core/errors");
531
+
532
+ // src/error-diagnostics.ts
445
533
  var import_errors2 = require("@kortyx/core/errors");
534
+ function errorDiagnostics(error, project) {
535
+ try {
536
+ const name = (0, import_errors2.errorProperty)(error, "name") ?? (error instanceof Error ? error.name : void 0);
537
+ const message = (0, import_errors2.errorProperty)(error, "message");
538
+ const details = project ? project(error) : {
539
+ type: typeof name === "string" ? name : "Error",
540
+ message: typeof error === "string" ? error : typeof message === "string" ? message : "Execution reported a fault."
541
+ };
542
+ if (!details) return {};
543
+ const projectedMessage = details.message;
544
+ const projectedType = details.type;
545
+ if (typeof projectedMessage !== "string") return {};
546
+ return {
547
+ errorMessage: projectedMessage.slice(0, 8192),
548
+ ...typeof projectedType === "string" ? { errorType: projectedType.slice(0, 256) } : {}
549
+ };
550
+ } catch {
551
+ return {};
552
+ }
553
+ }
554
+
555
+ // src/workflow-telemetry.ts
556
+ var import_node_crypto2 = require("crypto");
557
+ function emitWorkflowCall(telemetry, type, payload) {
558
+ const base = telemetry?.correlation;
559
+ if (!telemetry?.reporter || !telemetry.environment || !telemetry.service || !base?.runId || !base.workflowId)
560
+ return;
561
+ try {
562
+ const active = telemetry.trace?.getActiveContext?.();
563
+ void Promise.resolve(
564
+ telemetry.reporter.emit([
565
+ {
566
+ schemaVersion: 1,
567
+ eventId: (0, import_node_crypto2.randomUUID)(),
568
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
569
+ environment: telemetry.environment,
570
+ service: telemetry.service,
571
+ correlation: {
572
+ ...base,
573
+ runId: base.runId,
574
+ workflowId: base.workflowId,
575
+ ...active
576
+ },
577
+ type,
578
+ payload
579
+ }
580
+ ])
581
+ ).catch(() => {
582
+ });
583
+ } catch {
584
+ }
585
+ }
586
+ function workflowCallContent(telemetry, side, value) {
587
+ const policy = telemetry?.captureContent;
588
+ if (!(policy === true || policy && typeof policy === "object" && policy[side]))
589
+ return {};
590
+ const serialized = JSON.stringify(value);
591
+ return serialized && Buffer.byteLength(serialized, "utf8") <= 16384 ? { [side]: JSON.parse(serialized) } : { [`${side}Omitted`]: "Content exceeds 16 KiB" };
592
+ }
593
+
594
+ // src/tool.ts
595
+ var tools = new import_node_async_hooks2.AsyncLocalStorage();
596
+ function classification(tool, value, thrown, policy = tool.outcomes) {
597
+ const fallback = {
598
+ outcome: thrown ? "fault" : "success"
599
+ };
600
+ try {
601
+ if (value && typeof value === "object" && "isError" in value && value.isError === true)
602
+ fallback.outcome = "fault";
603
+ const result = thrown ? policy?.classifyError?.(value) : policy?.classifyResult?.(value);
604
+ if (!result || !["success", "denied", "fault"].includes(result.outcome))
605
+ return fallback;
606
+ if (result.outcome !== "denied")
607
+ return thrown || fallback.outcome === "fault" ? fallback : { outcome: result.outcome };
608
+ const code = result.code;
609
+ return {
610
+ outcome: "denied",
611
+ code: typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(code) && policy?.denialCodes?.includes(code) ? code : "DENIED"
612
+ };
613
+ } catch {
614
+ return fallback;
615
+ }
616
+ }
617
+ async function closeOwnedTools(owned) {
618
+ const closers = /* @__PURE__ */ new Map();
619
+ for (const tool of owned) {
620
+ if (tool.closeAfterUse !== false && tool.close && !closers.has(tool.close))
621
+ closers.set(tool.close, tool);
622
+ }
623
+ await Promise.all(
624
+ [...closers].map(async ([close, tool]) => {
625
+ try {
626
+ await close.call(tool);
627
+ } catch {
628
+ const ctx = getHookContext();
629
+ try {
630
+ ctx.reasonTrace?.startSpan({
631
+ name: "kortyx.tool.cleanup",
632
+ attributes: { cleanupFailed: true }
633
+ })?.end?.();
634
+ } catch {
635
+ }
636
+ }
637
+ })
638
+ );
639
+ }
640
+ function captureToolError(observation, error, policy, returned = false) {
641
+ try {
642
+ const reported = returned && error && typeof error === "object" && "isError" in error && error.isError === true && "content" in error && typeof error.content === "string" ? { name: "ToolError", message: error.content } : error;
643
+ Object.assign(
644
+ observation,
645
+ errorDiagnostics(policy?.error ? error : reported, policy?.error)
646
+ );
647
+ } catch {
648
+ }
649
+ }
650
+ function ownership() {
651
+ const ctx = getHookContext();
652
+ const correlation = ctx.node.workflowCallTelemetry?.correlation;
653
+ return {
654
+ workflowId: ctx.node.graph.name,
655
+ nodeId: ctx.node.graph.node,
656
+ ...correlation?.workflowRevisionId ? { workflowRevisionId: correlation.workflowRevisionId } : {},
657
+ ...correlation?.parentInvocationId ? { parentInvocationId: correlation.parentInvocationId } : {},
658
+ ...correlation?.runId ? { runId: correlation.runId } : {},
659
+ ...correlation?.invocationId ? { invocationId: correlation.invocationId } : {},
660
+ ...correlation?.branchId ? { branchId: correlation.branchId } : {}
661
+ };
662
+ }
663
+ function rememberToolObservation(observation) {
664
+ const ctx = getHookContext();
665
+ const existing = ctx.workflowState.__kortyxToolObservations;
666
+ const observations = Array.isArray(existing) ? existing : [];
667
+ if (!observations.some((item) => item.attemptId === observation.attemptId))
668
+ observations.push({ ...observation });
669
+ ctx.workflowState.__kortyxToolObservations = observations;
670
+ ctx.stateDirty = true;
671
+ }
672
+ function toolTrace(observation) {
673
+ const ctx = getHookContext();
674
+ const adapter = ctx.reasonTrace ?? ctx.node.workflowCallTelemetry?.trace;
675
+ if (adapter) return adapter;
676
+ const telemetry = ctx.node.workflowCallTelemetry;
677
+ if (!telemetry?.reporter) return void 0;
678
+ const correlation = { ...telemetry.correlation, ...observation };
679
+ const spanId = (0, import_node_crypto3.randomUUID)();
680
+ const emit = (type, payload) => emitWorkflowCall(
681
+ {
682
+ ...telemetry,
683
+ correlation,
684
+ trace: {
685
+ startSpan: () => void 0,
686
+ getActiveContext: () => ({
687
+ traceId: correlation.runId ?? spanId,
688
+ spanId
689
+ })
690
+ }
691
+ },
692
+ type,
693
+ payload
694
+ );
695
+ return {
696
+ startSpan: ({ attributes = {} }) => {
697
+ if (attributes.executed === true) emit("tool.started", attributes);
698
+ let ended = false;
699
+ return {
700
+ end: (args) => {
701
+ if (ended) return;
702
+ ended = true;
703
+ const payload = { ...attributes, ...args?.attributes };
704
+ const type = payload.observationKind === "reused" ? "tool.reused" : payload.observationKind === "waiting" ? "tool.waiting" : payload.suspended ? "tool.suspended" : payload.outcome === "denied" ? "tool.denied" : payload.outcome === "fault" ? "tool.failed" : payload.outcome === "cancelled" ? "tool.cancelled" : "tool.completed";
705
+ emit(type, payload);
706
+ }
707
+ };
708
+ }
709
+ };
710
+ }
711
+ async function executeObservedTool(args) {
712
+ const ctx = getHookContext();
713
+ const observation = {
714
+ version: 1,
715
+ name: args.tool.name,
716
+ toolCallId: args.toolCallId,
717
+ attemptId: (0, import_node_crypto3.randomUUID)(),
718
+ callingMode: args.callingMode,
719
+ executed: false,
720
+ ...ownership()
721
+ };
722
+ const scope = {
723
+ tool: args.tool,
724
+ input: args.input,
725
+ signal: args.abortSignal,
726
+ mode: args.callingMode,
727
+ providerToolCallId: args.providerToolCallId ?? args.toolCallId,
728
+ adopted: false
729
+ };
730
+ try {
731
+ (0, import_core2.throwIfExecutionAborted)(args.abortSignal);
732
+ } catch (error) {
733
+ observation.outcome = "cancelled";
734
+ await observeToolFact(observation);
735
+ args.onObservation?.(observation);
736
+ throw error;
737
+ }
738
+ ctx.node.consumeExecution?.("maxToolCalls");
739
+ observation.executed = true;
740
+ const started = performance.now();
741
+ return withSafeTraceSpan(
742
+ toolTrace(observation),
743
+ {
744
+ name: "kortyx.tool",
745
+ attributes: { ...observation, nodeId: ctx.node.graph.node }
746
+ },
747
+ async (span) => tools.run(scope, async () => {
748
+ try {
749
+ const result = await args.tool.execute(args.input, {
750
+ toolCallId: args.providerToolCallId ?? args.toolCallId,
751
+ ...args.abortSignal ? { abortSignal: args.abortSignal } : {}
752
+ });
753
+ (0, import_core2.throwIfExecutionAborted)(args.abortSignal);
754
+ const outcome = classification(
755
+ args.tool,
756
+ result,
757
+ false,
758
+ scope.classify ?? args.tool.outcomes
759
+ );
760
+ observation.outcome = outcome.outcome;
761
+ if (outcome.code) observation.denialCode = outcome.code;
762
+ if (observation.outcome === "fault") {
763
+ captureToolError(
764
+ observation,
765
+ result,
766
+ scope.telemetry ?? args.tool.telemetry,
767
+ true
768
+ );
769
+ }
770
+ return result;
771
+ } catch (error) {
772
+ if (args.abortSignal?.aborted) observation.outcome = "cancelled";
773
+ else if (!(0, import_errors3.isControlFlowError)(error)) {
774
+ const outcome = classification(
775
+ args.tool,
776
+ error,
777
+ true,
778
+ scope.classify ?? args.tool.outcomes
779
+ );
780
+ observation.outcome = outcome.outcome;
781
+ if (outcome.code) observation.denialCode = outcome.code;
782
+ if (observation.outcome === "fault")
783
+ captureToolError(
784
+ observation,
785
+ error,
786
+ scope.telemetry ?? args.tool.telemetry
787
+ );
788
+ }
789
+ throw error;
790
+ } finally {
791
+ observation.durationMs = Math.max(0, performance.now() - started);
792
+ span.end?.({
793
+ attributes: {
794
+ ...observation,
795
+ ...observation.outcome ? {} : { suspended: true }
796
+ }
797
+ });
798
+ rememberToolObservation(observation);
799
+ args.onObservation?.({ ...observation });
800
+ }
801
+ })
802
+ );
803
+ }
804
+ async function observeToolFact(observation, kind, preserveOwner = false) {
805
+ const ctx = getHookContext();
806
+ if (!preserveOwner) Object.assign(observation, ownership());
807
+ if (observation.outcome && kind !== "reused")
808
+ rememberToolObservation(observation);
809
+ await withSafeTraceSpan(
810
+ toolTrace(observation),
811
+ {
812
+ name: "kortyx.tool",
813
+ attributes: {
814
+ ...observation,
815
+ executed: false,
816
+ ...kind ? { observationKind: kind } : {},
817
+ nodeId: observation.nodeId ?? ctx.node.graph.node
818
+ }
819
+ },
820
+ async (span) => {
821
+ span.end?.({
822
+ attributes: {
823
+ ...observation,
824
+ executed: false,
825
+ ...kind ? { observationKind: kind } : {}
826
+ }
827
+ });
828
+ }
829
+ );
830
+ }
831
+ async function useTool(args) {
832
+ const ctx = getHookContext();
833
+ const scope = tools.getStore();
834
+ if (scope?.mode === "model" && !scope.adopted && scope.tool.name === args.tool.name && scope.input === args.input) {
835
+ scope.adopted = true;
836
+ scope.classify = args.tool.outcomes ?? scope.classify;
837
+ scope.telemetry = args.tool.telemetry ?? scope.telemetry;
838
+ return tools.run(
839
+ { ...scope, mode: "direct" },
840
+ () => args.tool.execute(args.input, {
841
+ toolCallId: scope.providerToolCallId,
842
+ ...scope.signal ? { abortSignal: scope.signal } : {}
843
+ })
844
+ );
845
+ }
846
+ const index = ctx.toolCallIndex++;
847
+ const key = `__useTool:${args.id ?? `auto:${index}`}`;
848
+ let id = ctx.currentNodeState.byKey[key];
849
+ if (typeof id !== "string") {
850
+ id = (0, import_node_crypto3.randomUUID)();
851
+ ctx.currentNodeState.byKey[key] = id;
852
+ ctx.stateDirty = true;
853
+ }
854
+ try {
855
+ return await executeObservedTool({
856
+ tool: args.tool,
857
+ input: args.input,
858
+ toolCallId: String(id),
859
+ callingMode: "direct",
860
+ abortSignal: (0, import_core2.combineAbortSignals)(ctx.node.abortSignal, args.abortSignal)
861
+ });
862
+ } finally {
863
+ await closeOwnedTools([args.tool]);
864
+ }
865
+ }
866
+ async function replayToolObservations(observations, child = false) {
867
+ for (const original of observations) {
868
+ const source = original.source ?? {
869
+ ...ownershipFrom(original),
870
+ toolCallId: original.toolCallId,
871
+ attemptId: original.attemptId
872
+ };
873
+ await observeToolFact(
874
+ {
875
+ ...original,
876
+ ...ownership(),
877
+ ...child ? {
878
+ workflowId: original.workflowId,
879
+ nodeId: original.nodeId,
880
+ workflowRevisionId: original.workflowRevisionId,
881
+ invocationId: original.invocationId,
882
+ parentInvocationId: original.parentInvocationId
883
+ } : {},
884
+ attemptId: (0, import_node_crypto3.randomUUID)(),
885
+ executed: false,
886
+ source,
887
+ durationMs: void 0
888
+ },
889
+ "reused",
890
+ child
891
+ );
892
+ }
893
+ }
894
+ var ownershipFrom = (observation) => ({
895
+ runId: observation.runId,
896
+ invocationId: observation.invocationId,
897
+ branchId: observation.branchId
898
+ });
899
+
900
+ // src/reason/engine.ts
901
+ var import_node_crypto5 = require("crypto");
902
+ var import_core4 = require("@kortyx/core");
903
+
904
+ // src/reason-engine.ts
905
+ var import_node_crypto4 = require("crypto");
906
+ var import_core3 = require("@kortyx/core");
907
+ var import_errors4 = require("@kortyx/core/errors");
446
908
  function assertModelFinish(result) {
447
909
  if (result.finishReason?.unified !== "content-filter") return;
448
910
  throw Object.assign(
449
- new import_errors2.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
911
+ new import_errors4.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
450
912
  category: "provider",
451
913
  retryable: false,
452
914
  safeMessage: "The provider refused the request."
@@ -460,7 +922,7 @@ function assertModelFinish(result) {
460
922
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
461
923
  var createRuntimeId = () => {
462
924
  try {
463
- return (0, import_node_crypto2.randomUUID)();
925
+ return (0, import_node_crypto4.randomUUID)();
464
926
  } catch {
465
927
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
466
928
  }
@@ -495,8 +957,8 @@ async function runReasonEngine(args) {
495
957
  args.model.options?.providerOptions,
496
958
  args.providerOptions
497
959
  );
498
- const tools = args.tools ?? args.model.options?.tools;
499
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
960
+ const tools2 = args.tools ?? args.model.options?.tools;
961
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
500
962
  const model = args.model.provider.getModel(args.model.modelId, {
501
963
  ...temperature !== void 0 ? { temperature } : {},
502
964
  streaming: stream,
@@ -505,10 +967,10 @@ async function runReasonEngine(args) {
505
967
  ...abortSignal !== void 0 ? { abortSignal } : {},
506
968
  ...reasoning !== void 0 ? { reasoning } : {},
507
969
  ...responseFormat !== void 0 ? { responseFormat } : {},
508
- ...tools !== void 0 ? { tools } : {},
970
+ ...tools2 !== void 0 ? { tools: tools2 } : {},
509
971
  ...providerOptions !== void 0 ? { providerOptions } : {}
510
972
  });
511
- if (tools?.length && !model.supportsToolStreaming) stream = false;
973
+ if (tools2?.length && !model.supportsToolStreaming) stream = false;
512
974
  const messages = args.messages !== void 0 ? args.messages : [
513
975
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
514
976
  { role: "user", content: String(args.input ?? "") }
@@ -520,7 +982,7 @@ async function runReasonEngine(args) {
520
982
  opId,
521
983
  segmentId
522
984
  };
523
- const traceSpan = args.reasonTrace?.startSpan({
985
+ const traceSpan = safeStartSpan(args.reasonTrace, {
524
986
  name: "runReasonEngine",
525
987
  attributes: {
526
988
  ...commonMeta,
@@ -600,7 +1062,7 @@ async function runReasonEngine(args) {
600
1062
  }
601
1063
  const response2 = await model.stream(messages);
602
1064
  for await (const chunk of response2) {
603
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1065
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
604
1066
  switch (chunk.type) {
605
1067
  case "text-delta": {
606
1068
  if (chunk.delta.length === 0) break;
@@ -666,7 +1128,7 @@ async function runReasonEngine(args) {
666
1128
  }
667
1129
  }
668
1130
  }
669
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1131
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
670
1132
  assertModelFinish({ finishReason, usage });
671
1133
  if (emit) {
672
1134
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
@@ -683,12 +1145,12 @@ async function runReasonEngine(args) {
683
1145
  ...providerMetadata !== void 0 ? { providerMetadata } : {},
684
1146
  ...warnings !== void 0 ? { warnings } : {}
685
1147
  };
686
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1148
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
687
1149
  endTrace(traceSpan, result2);
688
1150
  return result2;
689
1151
  }
690
1152
  const response = await model.invoke(messages);
691
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1153
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
692
1154
  assertModelFinish(response);
693
1155
  if (response.content) args.onTextChunk?.(response.content);
694
1156
  if (emit) {
@@ -718,7 +1180,7 @@ async function runReasonEngine(args) {
718
1180
  endTrace(traceSpan, result);
719
1181
  return result;
720
1182
  } catch (error) {
721
- const failure = abortSignal?.aborted ? (0, import_core2.createExecutionCancelledError)() : error;
1183
+ const failure = abortSignal?.aborted ? (0, import_core3.createExecutionCancelledError)() : error;
722
1184
  failTrace(traceSpan, failure);
723
1185
  throw failure;
724
1186
  }
@@ -727,19 +1189,19 @@ async function runReasonEngine(args) {
727
1189
  // src/reason/engine.ts
728
1190
  var createRuntimeId2 = () => {
729
1191
  try {
730
- return (0, import_node_crypto3.randomUUID)();
1192
+ return (0, import_node_crypto5.randomUUID)();
731
1193
  } catch {
732
1194
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
733
1195
  }
734
1196
  };
735
1197
  async function reasonEngine(args, meta, inputOverride) {
736
1198
  const ctx = getHookContext();
737
- const abortSignal = (0, import_core3.combineAbortSignals)(
1199
+ const abortSignal = (0, import_core4.combineAbortSignals)(
738
1200
  ctx.node.abortSignal,
739
1201
  args.abortSignal,
740
1202
  args.model.options?.abortSignal
741
1203
  );
742
- (0, import_core3.throwIfExecutionAborted)(abortSignal);
1204
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
743
1205
  ctx.node.consumeExecution?.("maxModelPasses");
744
1206
  try {
745
1207
  return await runReasonEngine({
@@ -1489,7 +1951,7 @@ function createStructuredOutputStreamer(args, id, opId) {
1489
1951
  }
1490
1952
 
1491
1953
  // src/reason/parsing.ts
1492
- var import_errors3 = require("@kortyx/core/errors");
1954
+ var import_errors5 = require("@kortyx/core/errors");
1493
1955
  var tryJsonParse = (value) => {
1494
1956
  try {
1495
1957
  return { ok: true, value: JSON.parse(value) };
@@ -1527,12 +1989,12 @@ var resolveOutputCandidate = (text) => {
1527
1989
  }
1528
1990
  return text;
1529
1991
  };
1530
- var createTruncatedStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
1992
+ var createTruncatedStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
1531
1993
  "OUTPUT_TRUNCATED",
1532
1994
  `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`,
1533
1995
  cause
1534
1996
  );
1535
- var createInvalidStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
1997
+ var createInvalidStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
1536
1998
  "INVALID_MODEL_JSON",
1537
1999
  `${label} did not produce valid structured output. The model returned text instead of the expected JSON payload. Check outputSchema and provider structured-output settings.`,
1538
2000
  cause
@@ -1544,7 +2006,7 @@ var parseReasonOutputWithSchema = (args) => {
1544
2006
  try {
1545
2007
  return parseWithSchema(args.schema, candidate, args.label);
1546
2008
  } catch (error) {
1547
- if (!(error instanceof import_errors3.ValidationError)) throw error;
2009
+ if (!(error instanceof import_errors5.ValidationError)) throw error;
1548
2010
  const failure = args.finishReason?.unified === "length" ? createTruncatedStructuredOutputError(args.label, error) : typeof candidate === "string" && !validJson ? createInvalidStructuredOutputError(args.label, error) : error;
1549
2011
  if (failure && typeof failure === "object")
1550
2012
  Object.assign(failure, {
@@ -1564,7 +2026,7 @@ var parseInterruptFirstPassResult = (args) => {
1564
2026
  if (typeof candidate === "string" && candidate.trim().length > 0) {
1565
2027
  throw createInvalidStructuredOutputError("useReason output");
1566
2028
  }
1567
- throw new import_errors3.ValidationError(
2029
+ throw new import_errors5.ValidationError(
1568
2030
  "MODEL_OUTPUT_SCHEMA",
1569
2031
  "useReason first pass with interrupt must return a JSON object."
1570
2032
  );
@@ -1573,7 +2035,7 @@ var parseInterruptFirstPassResult = (args) => {
1573
2035
  let interruptRequired = true;
1574
2036
  if (mode === "optional") {
1575
2037
  if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1576
- throw new import_errors3.ValidationError(
2038
+ throw new import_errors5.ValidationError(
1577
2039
  "MODEL_OUTPUT_SCHEMA",
1578
2040
  'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1579
2041
  );
@@ -1586,7 +2048,7 @@ var parseInterruptFirstPassResult = (args) => {
1586
2048
  candidate.interrupt
1587
2049
  ].some((value) => value !== null && value !== void 0);
1588
2050
  if (hasUnexpectedRequest) {
1589
- throw new import_errors3.ValidationError(
2051
+ throw new import_errors5.ValidationError(
1590
2052
  "MODEL_OUTPUT_SCHEMA",
1591
2053
  'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1592
2054
  );
@@ -1620,7 +2082,7 @@ var parseInterruptFirstPassResult = (args) => {
1620
2082
  "useReason output"
1621
2083
  );
1622
2084
  } catch (error) {
1623
- if (!(error instanceof import_errors3.ValidationError)) throw error;
2085
+ if (!(error instanceof import_errors5.ValidationError)) throw error;
1624
2086
  if (args.finishReason?.unified === "length") {
1625
2087
  throw createTruncatedStructuredOutputError("useReason output", error);
1626
2088
  }
@@ -1629,7 +2091,7 @@ var parseInterruptFirstPassResult = (args) => {
1629
2091
  }
1630
2092
  const explicitDraft = typeof candidate.draftText === "string" && candidate.draftText.length > 0 ? candidate.draftText : typeof candidate.text === "string" && candidate.text.length > 0 ? candidate.text : void 0;
1631
2093
  if (!args.outputSchema && !explicitDraft) {
1632
- throw new import_errors3.ValidationError(
2094
+ throw new import_errors5.ValidationError(
1633
2095
  "MODEL_OUTPUT_SCHEMA",
1634
2096
  "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1635
2097
  );
@@ -1819,6 +2281,7 @@ var mergeUsage = (left, right) => {
1819
2281
  const reasoning = sum(left.reasoning, right.reasoning);
1820
2282
  const cacheRead = sum(left.cacheRead, right.cacheRead);
1821
2283
  const cacheWrite = sum(left.cacheWrite, right.cacheWrite);
2284
+ const cacheWrite1h = sum(left.cacheWrite1h, right.cacheWrite1h);
1822
2285
  const merged = {
1823
2286
  ...right.outputIncludesReasoning !== void 0 ? { outputIncludesReasoning: right.outputIncludesReasoning } : {},
1824
2287
  ...right.inputIncludesCacheWrite !== void 0 ? { inputIncludesCacheWrite: right.inputIncludesCacheWrite } : {},
@@ -1830,6 +2293,7 @@ var mergeUsage = (left, right) => {
1830
2293
  if (reasoning !== void 0) merged.reasoning = reasoning;
1831
2294
  if (cacheRead !== void 0) merged.cacheRead = cacheRead;
1832
2295
  if (cacheWrite !== void 0) merged.cacheWrite = cacheWrite;
2296
+ if (cacheWrite1h !== void 0) merged.cacheWrite1h = cacheWrite1h;
1833
2297
  if (raw) merged.raw = raw;
1834
2298
  return merged;
1835
2299
  };
@@ -1868,9 +2332,9 @@ var emitReasonStructuredOutput = (args) => {
1868
2332
  };
1869
2333
 
1870
2334
  // src/reason/tools.ts
1871
- var import_core4 = require("@kortyx/core");
1872
- var import_errors4 = require("@kortyx/core/errors");
1873
- var toToolDefinitions = (tools) => tools.map((tool) => ({
2335
+ var import_core5 = require("@kortyx/core");
2336
+ var import_errors6 = require("@kortyx/core/errors");
2337
+ var toToolDefinitions = (tools2) => tools2.map((tool) => ({
1874
2338
  name: tool.name,
1875
2339
  ...tool.title ? { title: tool.title } : {},
1876
2340
  ...tool.description ? { description: tool.description } : {},
@@ -1887,12 +2351,13 @@ var resolveToolExecutionBoolean = (value, toolName, fallback) => {
1887
2351
  return fallback;
1888
2352
  };
1889
2353
  var normalizeToolResult = (toolCall, value) => {
1890
- if (value && typeof value === "object" && !Array.isArray(value) && "toolCallId" in value && "name" in value && "content" in value) {
1891
- return value;
2354
+ if (value && typeof value === "object" && !Array.isArray(value) && "toolCallId" in value && "name" in value && "content" in value && typeof value.content === "string") {
2355
+ const result = value;
2356
+ return { ...result, toolCallId: toolCall.id, name: toolCall.name };
1892
2357
  }
1893
2358
  let content;
1894
2359
  try {
1895
- content = typeof value === "string" ? value : JSON.stringify(value);
2360
+ content = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
1896
2361
  } catch {
1897
2362
  content = String(value);
1898
2363
  }
@@ -1903,28 +2368,6 @@ var normalizeToolResult = (toolCall, value) => {
1903
2368
  ...value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
1904
2369
  };
1905
2370
  };
1906
- var closeOwnedTools = async (tools, preserveFailure = false, traceSpan) => {
1907
- const closeFns = /* @__PURE__ */ new Set();
1908
- for (const tool of tools) {
1909
- if (tool.closeAfterUse === false || !tool.close) continue;
1910
- closeFns.add(tool.close);
1911
- }
1912
- try {
1913
- await Promise.all(
1914
- [...closeFns].map(async (close) => {
1915
- await close();
1916
- })
1917
- );
1918
- } catch (error) {
1919
- if (!preserveFailure) throw error;
1920
- try {
1921
- traceSpan?.addEvent?.("useReason.cleanup.failed", {
1922
- failure: (0, import_errors4.serializeFailure)(error)
1923
- });
1924
- } catch {
1925
- }
1926
- }
1927
- };
1928
2371
  var createInitialMessages = (args) => [
1929
2372
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
1930
2373
  { role: "user", content: String(args.input ?? "") }
@@ -1932,7 +2375,7 @@ var createInitialMessages = (args) => [
1932
2375
  var runReasonToolLoop = async (args) => {
1933
2376
  const { useReasonArgs, id, traceSpan, checkpointKey } = args;
1934
2377
  const ctx = getHookContext();
1935
- const abortSignal = (0, import_core4.combineAbortSignals)(
2378
+ const abortSignal = (0, import_core5.combineAbortSignals)(
1936
2379
  ctx.node.abortSignal,
1937
2380
  useReasonArgs.abortSignal,
1938
2381
  useReasonArgs.model.options?.abortSignal
@@ -1940,43 +2383,32 @@ var runReasonToolLoop = async (args) => {
1940
2383
  const saved = ctx.currentNodeState.byKey[checkpointKey];
1941
2384
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
1942
2385
  const opId = checkpoint?.opId ?? args.opId;
1943
- let primaryFailure = false;
1944
- const tools = useReasonArgs.tools ?? [];
2386
+ const tools2 = useReasonArgs.tools ?? [];
1945
2387
  const toolByName = /* @__PURE__ */ new Map();
1946
- let validationCompleted = false;
1947
2388
  let separateOutput = false;
1948
- try {
1949
- for (const tool of tools) {
1950
- if (toolByName.has(tool.name)) {
1951
- throw new Error(
1952
- `useReason received duplicate tool name "${tool.name}".`
1953
- );
1954
- }
1955
- toolByName.set(tool.name, tool);
1956
- }
1957
- if (useReasonArgs.interrupt) {
1958
- throw new Error(
1959
- "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
1960
- );
2389
+ for (const tool of tools2) {
2390
+ if (toolByName.has(tool.name)) {
2391
+ throw new Error(`useReason received duplicate tool name "${tool.name}".`);
1961
2392
  }
1962
- separateOutput = Boolean(
1963
- useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
1964
- ...useReasonArgs.model.options,
1965
- ...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
1966
- ...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
1967
- providerOptions: resolveProviderOptions(
1968
- useReasonArgs.model.options?.providerOptions,
1969
- useReasonArgs.providerOptions
1970
- ) ?? {}
1971
- }).requiresSeparateStructuredOutput
2393
+ toolByName.set(tool.name, tool);
2394
+ }
2395
+ if (useReasonArgs.interrupt) {
2396
+ throw new Error(
2397
+ "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
1972
2398
  );
1973
- validationCompleted = true;
1974
- } finally {
1975
- if (!validationCompleted) {
1976
- await closeOwnedTools(tools, true, traceSpan);
1977
- }
1978
2399
  }
1979
- const toolDefinitions = toToolDefinitions(tools);
2400
+ separateOutput = Boolean(
2401
+ useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
2402
+ ...useReasonArgs.model.options,
2403
+ ...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
2404
+ ...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
2405
+ providerOptions: resolveProviderOptions(
2406
+ useReasonArgs.model.options?.providerOptions,
2407
+ useReasonArgs.providerOptions
2408
+ ) ?? {}
2409
+ }).requiresSeparateStructuredOutput
2410
+ );
2411
+ const toolDefinitions = toToolDefinitions(tools2);
1980
2412
  let finalizing = checkpoint?.finalizing ?? false;
1981
2413
  let completed = false;
1982
2414
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
@@ -2028,7 +2460,7 @@ var runReasonToolLoop = async (args) => {
2028
2460
  };
2029
2461
  try {
2030
2462
  for (let stepIndex = pending ? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
2031
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2463
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2032
2464
  traceSpan?.addEvent?.("useReason.tool-step.start", {
2033
2465
  stepIndex,
2034
2466
  toolCount: toolDefinitions.length
@@ -2072,6 +2504,12 @@ var runReasonToolLoop = async (args) => {
2072
2504
  );
2073
2505
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, step.warnings);
2074
2506
  const toolCalls = step.toolCalls ?? [];
2507
+ const ids = /* @__PURE__ */ new Set();
2508
+ for (const call of toolCalls) {
2509
+ if (!call.id || ids.has(call.id) || !reused && allToolCalls.some((prior) => prior.id === call.id))
2510
+ throw new Error("Model returned duplicate or empty tool call IDs.");
2511
+ ids.add(call.id);
2512
+ }
2075
2513
  const toolResults = reused ? steps[stepIndex]?.toolResults ?? [] : [];
2076
2514
  if (!reused)
2077
2515
  steps.push({
@@ -2140,9 +2578,15 @@ var runReasonToolLoop = async (args) => {
2140
2578
  pending = step;
2141
2579
  save();
2142
2580
  for (const toolCall of toolCalls) {
2143
- if (toolResults.some((result2) => result2.toolCallId === toolCall.id))
2581
+ if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
2582
+ await replayToolObservations(
2583
+ steps[stepIndex]?.toolObservations?.filter(
2584
+ (observation) => observation.toolCallId === `${opId}:${toolCall.id}`
2585
+ ) ?? []
2586
+ );
2144
2587
  continue;
2145
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2588
+ }
2589
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2146
2590
  const tool = toolByName.get(toolCall.name);
2147
2591
  if (!tool) {
2148
2592
  throw new Error(
@@ -2159,11 +2603,6 @@ var runReasonToolLoop = async (args) => {
2159
2603
  tool.name,
2160
2604
  false
2161
2605
  );
2162
- traceSpan?.addEvent?.("useReason.tool-call.start", {
2163
- stepIndex,
2164
- tool: tool.name,
2165
- toolCallId: toolCall.id
2166
- });
2167
2606
  if (shouldEmitTool) {
2168
2607
  emitToolEvent("tool-call-start", {
2169
2608
  tool: tool.name,
@@ -2172,6 +2611,17 @@ var runReasonToolLoop = async (args) => {
2172
2611
  });
2173
2612
  }
2174
2613
  if (shouldApproveTool && !approvedCalls.includes(toolCall.id)) {
2614
+ await observeToolFact(
2615
+ {
2616
+ version: 1,
2617
+ name: tool.name,
2618
+ toolCallId: `${opId}:${toolCall.id}`,
2619
+ attemptId: `${opId}:${toolCall.id}:approval`,
2620
+ callingMode: "model",
2621
+ executed: false
2622
+ },
2623
+ "waiting"
2624
+ );
2175
2625
  const approval = await awaitInterruptInternal({
2176
2626
  id: `tool:${toolCall.id}`,
2177
2627
  request: {
@@ -2190,6 +2640,22 @@ var runReasonToolLoop = async (args) => {
2190
2640
  });
2191
2641
  const approved = Array.isArray(approval) ? approval.includes("approve") : approval === "approve";
2192
2642
  if (!approved) {
2643
+ const deniedObservation = {
2644
+ version: 1,
2645
+ name: tool.name,
2646
+ toolCallId: `${opId}:${toolCall.id}`,
2647
+ attemptId: `${opId}:${toolCall.id}:approval`,
2648
+ callingMode: "model",
2649
+ executed: false,
2650
+ outcome: "denied",
2651
+ denialCode: "APPROVAL_DENIED"
2652
+ };
2653
+ await observeToolFact(deniedObservation);
2654
+ const entry = steps[stepIndex];
2655
+ if (entry) {
2656
+ entry.toolObservations ??= [];
2657
+ entry.toolObservations.push(deniedObservation);
2658
+ }
2193
2659
  const denied = {
2194
2660
  toolCallId: toolCall.id,
2195
2661
  name: tool.name,
@@ -2220,13 +2686,24 @@ var runReasonToolLoop = async (args) => {
2220
2686
  save();
2221
2687
  }
2222
2688
  try {
2223
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2224
- ctx.node.consumeExecution?.("maxToolCalls");
2225
- const rawResult = await tool.execute(toolCall.input, {
2226
- toolCallId: toolCall.id,
2227
- ...abortSignal ? { abortSignal } : {}
2689
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2690
+ const rawResult = await executeObservedTool({
2691
+ tool,
2692
+ input: toolCall.input,
2693
+ toolCallId: `${opId}:${toolCall.id}`,
2694
+ providerToolCallId: toolCall.id,
2695
+ callingMode: "model",
2696
+ abortSignal,
2697
+ onObservation: (observation) => {
2698
+ const entry = steps[stepIndex];
2699
+ if (entry) {
2700
+ entry.toolObservations ??= [];
2701
+ entry.toolObservations.push(observation);
2702
+ save();
2703
+ }
2704
+ }
2228
2705
  });
2229
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2706
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2230
2707
  const result2 = normalizeToolResult(toolCall, rawResult);
2231
2708
  toolResults.push(result2);
2232
2709
  allToolResults.push(result2);
@@ -2239,20 +2716,14 @@ var runReasonToolLoop = async (args) => {
2239
2716
  ...result2.isError !== void 0 ? { isError: result2.isError } : {}
2240
2717
  });
2241
2718
  }
2242
- traceSpan?.addEvent?.("useReason.tool-call.complete", {
2243
- stepIndex,
2244
- tool: result2.name,
2245
- toolCallId: result2.toolCallId,
2246
- isError: Boolean(result2.isError)
2247
- });
2248
2719
  } catch (error) {
2249
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2250
- if ((0, import_errors4.isControlFlowError)(error)) throw error;
2720
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2721
+ if ((0, import_errors6.isControlFlowError)(error)) throw error;
2251
2722
  const result2 = {
2252
2723
  toolCallId: toolCall.id,
2253
2724
  name: tool.name,
2254
- content: (0, import_errors4.serializeFailure)(error).message,
2255
- failure: (0, import_errors4.serializeFailure)(error),
2725
+ content: (0, import_errors6.serializeFailure)(error).message,
2726
+ failure: (0, import_errors6.serializeFailure)(error),
2256
2727
  isError: true
2257
2728
  };
2258
2729
  toolResults.push(result2);
@@ -2265,13 +2736,6 @@ var runReasonToolLoop = async (args) => {
2265
2736
  failure: result2.failure
2266
2737
  });
2267
2738
  }
2268
- traceSpan?.addEvent?.("useReason.tool-call.error", {
2269
- stepIndex,
2270
- tool: tool.name,
2271
- toolCallId: toolCall.id,
2272
- message: result2.content,
2273
- failure: result2.failure
2274
- });
2275
2739
  }
2276
2740
  const completed2 = toolResults.at(-1);
2277
2741
  if (completed2)
@@ -2344,7 +2808,6 @@ var runReasonToolLoop = async (args) => {
2344
2808
  ctx.stateDirty = true;
2345
2809
  return result;
2346
2810
  } catch (error) {
2347
- primaryFailure = true;
2348
2811
  try {
2349
2812
  traceSpan?.fail?.(error, {
2350
2813
  attributes: {
@@ -2355,8 +2818,6 @@ var runReasonToolLoop = async (args) => {
2355
2818
  } catch {
2356
2819
  }
2357
2820
  throw error;
2358
- } finally {
2359
- await closeOwnedTools(tools, primaryFailure, traceSpan);
2360
2821
  }
2361
2822
  };
2362
2823
 
@@ -2372,15 +2833,15 @@ var assertReasoningThoughtsCompatibility = (args) => {
2372
2833
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
2373
2834
  );
2374
2835
  };
2375
- async function useReason(args) {
2836
+ async function runReason(args) {
2376
2837
  assertReasoningThoughtsCompatibility(args);
2377
2838
  const ctx = getHookContext();
2378
- const abortSignal = (0, import_core5.combineAbortSignals)(
2839
+ const abortSignal = (0, import_core6.combineAbortSignals)(
2379
2840
  ctx.node.abortSignal,
2380
2841
  args.abortSignal,
2381
2842
  args.model.options?.abortSignal
2382
2843
  );
2383
- (0, import_core5.throwIfExecutionAborted)(abortSignal);
2844
+ (0, import_core6.throwIfExecutionAborted)(abortSignal);
2384
2845
  const allowValidatedToolOutput = Boolean(
2385
2846
  args.outputSchema && !(args.responseFormat ?? args.model.options?.responseFormat)
2386
2847
  );
@@ -2403,286 +2864,303 @@ async function useReason(args) {
2403
2864
  ctx.currentNodeState.byKey[checkpointKey]
2404
2865
  );
2405
2866
  if (existingCompleted) {
2867
+ await replayToolObservations(
2868
+ existingCompleted.result.steps?.flatMap(
2869
+ (step) => step.toolObservations ?? []
2870
+ ) ?? []
2871
+ );
2406
2872
  return existingCompleted.result;
2407
2873
  }
2408
2874
  const existingCheckpoint = readReasonCheckpoint(
2409
2875
  ctx.currentNodeState.byKey[checkpointKey]
2410
2876
  );
2411
2877
  const reasonMeta = typeof id === "string" ? { id, opId } : { opId };
2412
- const traceSpan = getReasonTraceAdapter()?.startSpan({
2413
- name: "useReason",
2414
- attributes: {
2415
- ...id ? { id } : {},
2416
- opId,
2417
- nodeId: ctx.node.graph.node,
2418
- providerId: args.model.provider.id,
2419
- modelId: args.model.modelId,
2420
- stream: args.stream ?? args.model.options?.streaming ?? true,
2421
- emit: args.emit ?? true,
2422
- ...args.telemetry?.operation ? { operation: args.telemetry.operation } : {},
2423
- ...args.telemetry?.prompt?.name ? { promptName: args.telemetry.prompt.name } : {},
2424
- ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
2425
- ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
2426
- hasOutputSchema: Boolean(args.outputSchema),
2427
- hasInterrupt: Boolean(args.interrupt),
2428
- hasStructured: Boolean(args.structured),
2429
- hasTools: Boolean(args.tools?.length)
2430
- },
2431
- telemetry: {
2432
- ...args.telemetry ?? {},
2433
- input: args.telemetry?.input ?? args.input
2434
- }
2435
- });
2436
- if (args.tools?.length) {
2437
- return runReasonToolLoop({
2438
- useReasonArgs: args,
2439
- allowValidatedToolOutput,
2440
- checkpointKey,
2441
- initialWarnings: inferred.warnings,
2442
- ...id ? { id } : {},
2443
- opId,
2444
- ...traceSpan ? { traceSpan } : {}
2445
- });
2446
- }
2447
- const suppressTextStream = Boolean(args.outputSchema || args.interrupt);
2448
- const setFieldPaths = resolveSetFieldPaths(args.structured);
2449
- const appendFieldPaths = resolveAppendFieldPaths(args.structured);
2450
- const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
2451
- const useStructuredIncrementalStreaming = Boolean(
2452
- args.outputSchema && !args.interrupt && (setFieldPaths.length > 0 || appendFieldPaths.length > 0 || textDeltaFieldPaths.length > 0) && args.stream !== false && shouldStreamReasonStructured(args)
2453
- );
2454
- let firstText = "";
2455
- let firstRaw;
2456
- let firstOutput;
2457
- let firstInterruptRequest;
2458
- let firstInterruptRequired = false;
2459
- let finalText = "";
2460
- let finalRaw;
2461
- let finalOutput;
2462
- let aggregatedUsage;
2463
- let finalFinishReason;
2464
- let aggregatedProviderMetadata;
2465
- let aggregatedWarnings = inferred.warnings;
2466
- if (existingCheckpoint) {
2467
- firstText = existingCheckpoint.firstText;
2468
- firstRaw = existingCheckpoint.firstRaw;
2469
- firstOutput = existingCheckpoint.firstOutput;
2470
- finalText = firstText;
2471
- finalRaw = firstRaw;
2472
- finalOutput = firstOutput;
2473
- aggregatedUsage = existingCheckpoint.firstUsage;
2474
- finalFinishReason = existingCheckpoint.firstFinishReason;
2475
- aggregatedProviderMetadata = existingCheckpoint.firstProviderMetadata;
2476
- aggregatedWarnings = existingCheckpoint.firstWarnings;
2477
- traceSpan?.addEvent?.("useReason.resume", {
2478
- checkpointKey
2479
- });
2480
- } else {
2481
- const first = await reasonEngine(
2482
- {
2483
- ...args,
2484
- emit: suppressTextStream ? false : args.emit,
2485
- stream: useStructuredIncrementalStreaming ? true : suppressTextStream ? false : args.stream,
2486
- ...useStructuredIncrementalStreaming ? {
2487
- onTextChunk: createStructuredOutputStreamer(args, id, opId)
2488
- } : {}
2878
+ return withSafeTraceSpan(
2879
+ getReasonTraceAdapter(),
2880
+ {
2881
+ name: "useReason",
2882
+ attributes: {
2883
+ ...id ? { id } : {},
2884
+ opId,
2885
+ nodeId: ctx.node.graph.node,
2886
+ providerId: args.model.provider.id,
2887
+ modelId: args.model.modelId,
2888
+ stream: args.stream ?? args.model.options?.streaming ?? true,
2889
+ emit: args.emit ?? true,
2890
+ ...args.telemetry?.operation ? { operation: args.telemetry.operation } : {},
2891
+ ...args.telemetry?.prompt?.name ? { promptName: args.telemetry.prompt.name } : {},
2892
+ ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
2893
+ ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
2894
+ hasOutputSchema: Boolean(args.outputSchema),
2895
+ hasInterrupt: Boolean(args.interrupt),
2896
+ hasStructured: Boolean(args.structured),
2897
+ hasTools: Boolean(args.tools?.length)
2489
2898
  },
2490
- reasonMeta,
2491
- args.interrupt ? defaultInterruptFirstPassInput({
2492
- input: args.input,
2493
- requestSchema: args.interrupt.requestSchema,
2494
- mode: args.interrupt.mode ?? "required",
2495
- ...args.outputSchema ? { outputSchema: args.outputSchema } : {}
2496
- }) : args.outputSchema ? withOutputGuardrails(
2497
- withStructuredStreamHints(args.input, {
2498
- ...setFieldPaths.length > 0 ? { setFieldPaths } : {},
2499
- ...appendFieldPaths.length > 0 ? { appendFieldPaths } : {},
2500
- ...textDeltaFieldPaths.length > 0 ? { textDeltaFieldPaths } : {}
2501
- }),
2502
- args.outputSchema
2503
- ) : void 0
2504
- );
2505
- firstRaw = first.raw;
2506
- finalRaw = first.raw;
2507
- accumulateTokenUsage(first.usage);
2508
- aggregatedUsage = mergeUsage(aggregatedUsage, first.usage);
2509
- finalFinishReason = first.finishReason;
2510
- aggregatedProviderMetadata = mergeProviderMetadata(
2511
- aggregatedProviderMetadata,
2512
- first.providerMetadata
2513
- );
2514
- aggregatedWarnings = mergeWarnings(aggregatedWarnings, first.warnings);
2515
- traceSpan?.addEvent?.("useReason.first-pass.complete", {
2516
- textLength: first.text.length,
2517
- hasInterrupt: Boolean(args.interrupt)
2518
- });
2519
- if (args.interrupt) {
2520
- const firstPass = parseInterruptFirstPassResult({
2521
- text: first.text,
2522
- requestSchema: args.interrupt.requestSchema,
2523
- ...first.finishReason ? { finishReason: first.finishReason } : {},
2524
- ...first.usage ? { usage: first.usage } : {},
2525
- ...args.outputSchema ? { outputSchema: args.outputSchema } : {},
2526
- mode: args.interrupt.mode ?? "required"
2527
- });
2528
- firstText = firstPass.draftText;
2529
- finalText = firstPass.draftText;
2530
- firstInterruptRequired = firstPass.interruptRequired;
2531
- firstInterruptRequest = firstPass.request;
2532
- firstOutput = firstPass.output;
2533
- finalOutput = firstPass.output;
2534
- } else {
2535
- firstText = first.text;
2536
- finalText = first.text;
2537
- if (args.outputSchema) {
2538
- firstOutput = parseReasonOutputWithSchema({
2539
- text: first.text,
2540
- schema: args.outputSchema,
2541
- ...first.finishReason ? { finishReason: first.finishReason } : {},
2542
- ...first.usage ? { usage: first.usage } : {},
2543
- label: "useReason output"
2899
+ telemetry: {
2900
+ ...args.telemetry ?? {},
2901
+ input: args.telemetry?.input ?? args.input
2902
+ }
2903
+ },
2904
+ async (traceSpan) => {
2905
+ if (args.tools?.length) {
2906
+ return runReasonToolLoop({
2907
+ useReasonArgs: args,
2908
+ allowValidatedToolOutput,
2909
+ checkpointKey,
2910
+ initialWarnings: inferred.warnings,
2911
+ ...id ? { id } : {},
2912
+ opId,
2913
+ ...traceSpan ? { traceSpan } : {}
2544
2914
  });
2545
2915
  }
2546
- finalOutput = firstOutput;
2547
- }
2548
- }
2549
- let interruptResponse;
2550
- if (args.interrupt && (existingCheckpoint || firstInterruptRequired)) {
2551
- const requestSchema = args.interrupt.requestSchema;
2552
- const interruptRequest = existingCheckpoint ? parseWithSchema(
2553
- requestSchema,
2554
- existingCheckpoint.request,
2555
- "useReason interrupt.request"
2556
- ) : firstInterruptRequest;
2557
- if (!interruptRequest) {
2558
- throw new Error(
2559
- "useReason interrupt request is missing; first pass did not produce a valid interrupt payload."
2916
+ const suppressTextStream = Boolean(args.outputSchema || args.interrupt);
2917
+ const setFieldPaths = resolveSetFieldPaths(args.structured);
2918
+ const appendFieldPaths = resolveAppendFieldPaths(args.structured);
2919
+ const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
2920
+ const useStructuredIncrementalStreaming = Boolean(
2921
+ args.outputSchema && !args.interrupt && (setFieldPaths.length > 0 || appendFieldPaths.length > 0 || textDeltaFieldPaths.length > 0) && args.stream !== false && shouldStreamReasonStructured(args)
2560
2922
  );
2561
- }
2562
- if (!existingCheckpoint) {
2563
- ctx.currentNodeState.byKey[checkpointKey] = {
2564
- status: "awaiting_interrupt",
2565
- request: interruptRequest,
2566
- firstText,
2567
- ...firstRaw !== void 0 ? { firstRaw } : {},
2568
- ...aggregatedUsage !== void 0 ? { firstUsage: aggregatedUsage } : {},
2569
- ...finalFinishReason !== void 0 ? { firstFinishReason: finalFinishReason } : {},
2570
- ...aggregatedProviderMetadata !== void 0 ? { firstProviderMetadata: aggregatedProviderMetadata } : {},
2571
- ...aggregatedWarnings !== void 0 ? { firstWarnings: aggregatedWarnings } : {},
2572
- ...firstOutput !== void 0 ? { firstOutput } : {}
2573
- };
2574
- ctx.stateDirty = true;
2575
- }
2576
- const resumeStatePatch = resolveHookStatePatch({
2577
- nodeId: ctx.node.graph.node,
2578
- currentNodeState: ctx.currentNodeState,
2579
- workflowState: ctx.workflowState
2580
- });
2581
- interruptResponse = await awaitInterruptInternal({
2582
- request: interruptRequest,
2583
- requestSchema,
2584
- responseSchema: args.interrupt.responseSchema,
2585
- ...args.interrupt.schemaId ? { schemaId: args.interrupt.schemaId } : {},
2586
- ...args.interrupt.schemaVersion ? { schemaVersion: args.interrupt.schemaVersion } : {},
2587
- ...id ? { id } : {},
2588
- meta: {
2589
- __kortyxResumeStatePatch: resumeStatePatch
2923
+ let firstText = "";
2924
+ let firstRaw;
2925
+ let firstOutput;
2926
+ let firstInterruptRequest;
2927
+ let firstInterruptRequired = false;
2928
+ let finalText = "";
2929
+ let finalRaw;
2930
+ let finalOutput;
2931
+ let aggregatedUsage;
2932
+ let finalFinishReason;
2933
+ let aggregatedProviderMetadata;
2934
+ let aggregatedWarnings = inferred.warnings;
2935
+ if (existingCheckpoint) {
2936
+ firstText = existingCheckpoint.firstText;
2937
+ firstRaw = existingCheckpoint.firstRaw;
2938
+ firstOutput = existingCheckpoint.firstOutput;
2939
+ finalText = firstText;
2940
+ finalRaw = firstRaw;
2941
+ finalOutput = firstOutput;
2942
+ aggregatedUsage = existingCheckpoint.firstUsage;
2943
+ finalFinishReason = existingCheckpoint.firstFinishReason;
2944
+ aggregatedProviderMetadata = existingCheckpoint.firstProviderMetadata;
2945
+ aggregatedWarnings = existingCheckpoint.firstWarnings;
2946
+ traceSpan?.addEvent?.("useReason.resume", {
2947
+ checkpointKey
2948
+ });
2949
+ } else {
2950
+ const first = await reasonEngine(
2951
+ {
2952
+ ...args,
2953
+ emit: suppressTextStream ? false : args.emit,
2954
+ stream: useStructuredIncrementalStreaming ? true : suppressTextStream ? false : args.stream,
2955
+ ...useStructuredIncrementalStreaming ? {
2956
+ onTextChunk: createStructuredOutputStreamer(args, id, opId)
2957
+ } : {}
2958
+ },
2959
+ reasonMeta,
2960
+ args.interrupt ? defaultInterruptFirstPassInput({
2961
+ input: args.input,
2962
+ requestSchema: args.interrupt.requestSchema,
2963
+ mode: args.interrupt.mode ?? "required",
2964
+ ...args.outputSchema ? { outputSchema: args.outputSchema } : {}
2965
+ }) : args.outputSchema ? withOutputGuardrails(
2966
+ withStructuredStreamHints(args.input, {
2967
+ ...setFieldPaths.length > 0 ? { setFieldPaths } : {},
2968
+ ...appendFieldPaths.length > 0 ? { appendFieldPaths } : {},
2969
+ ...textDeltaFieldPaths.length > 0 ? { textDeltaFieldPaths } : {}
2970
+ }),
2971
+ args.outputSchema
2972
+ ) : void 0
2973
+ );
2974
+ firstRaw = first.raw;
2975
+ finalRaw = first.raw;
2976
+ accumulateTokenUsage(first.usage);
2977
+ aggregatedUsage = mergeUsage(aggregatedUsage, first.usage);
2978
+ finalFinishReason = first.finishReason;
2979
+ aggregatedProviderMetadata = mergeProviderMetadata(
2980
+ aggregatedProviderMetadata,
2981
+ first.providerMetadata
2982
+ );
2983
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, first.warnings);
2984
+ traceSpan?.addEvent?.("useReason.first-pass.complete", {
2985
+ textLength: first.text.length,
2986
+ hasInterrupt: Boolean(args.interrupt)
2987
+ });
2988
+ if (args.interrupt) {
2989
+ const firstPass = parseInterruptFirstPassResult({
2990
+ text: first.text,
2991
+ requestSchema: args.interrupt.requestSchema,
2992
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
2993
+ ...first.usage ? { usage: first.usage } : {},
2994
+ ...args.outputSchema ? { outputSchema: args.outputSchema } : {},
2995
+ mode: args.interrupt.mode ?? "required"
2996
+ });
2997
+ firstText = firstPass.draftText;
2998
+ finalText = firstPass.draftText;
2999
+ firstInterruptRequired = firstPass.interruptRequired;
3000
+ firstInterruptRequest = firstPass.request;
3001
+ firstOutput = firstPass.output;
3002
+ finalOutput = firstPass.output;
3003
+ } else {
3004
+ firstText = first.text;
3005
+ finalText = first.text;
3006
+ if (args.outputSchema) {
3007
+ firstOutput = parseReasonOutputWithSchema({
3008
+ text: first.text,
3009
+ schema: args.outputSchema,
3010
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
3011
+ ...first.usage ? { usage: first.usage } : {},
3012
+ label: "useReason output"
3013
+ });
3014
+ }
3015
+ finalOutput = firstOutput;
3016
+ }
2590
3017
  }
2591
- });
2592
- traceSpan?.addEvent?.("useReason.interrupt.resolved", {
2593
- checkpointKey
2594
- });
2595
- if (Object.hasOwn(ctx.currentNodeState.byKey, checkpointKey)) {
2596
- delete ctx.currentNodeState.byKey[checkpointKey];
2597
- ctx.stateDirty = true;
2598
- }
2599
- const continuationInput = defaultContinuationInput({
2600
- input: args.input,
2601
- draftText: firstText,
2602
- ...firstOutput !== void 0 ? { draftOutput: firstOutput } : {},
2603
- request: interruptRequest,
2604
- response: interruptResponse
2605
- });
2606
- const second = await reasonEngine(
2607
- {
2608
- ...args,
2609
- ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
2610
- emit: suppressTextStream ? false : args.emit,
2611
- stream: suppressTextStream ? false : args.stream
2612
- },
2613
- reasonMeta,
2614
- args.outputSchema ? withOutputGuardrails(
2615
- continuationInput,
2616
- args.outputSchema
2617
- ) : continuationInput
2618
- );
2619
- finalText = second.text;
2620
- finalRaw = second.raw;
2621
- accumulateTokenUsage(second.usage);
2622
- aggregatedUsage = mergeUsage(aggregatedUsage, second.usage);
2623
- finalFinishReason = second.finishReason;
2624
- aggregatedProviderMetadata = mergeProviderMetadata(
2625
- aggregatedProviderMetadata,
2626
- second.providerMetadata
2627
- );
2628
- aggregatedWarnings = mergeWarnings(aggregatedWarnings, second.warnings);
2629
- traceSpan?.addEvent?.("useReason.continuation.complete", {
2630
- textLength: second.text.length
2631
- });
2632
- if (args.outputSchema) {
2633
- finalOutput = parseReasonOutputWithSchema({
3018
+ let interruptResponse;
3019
+ if (args.interrupt && (existingCheckpoint || firstInterruptRequired)) {
3020
+ const requestSchema = args.interrupt.requestSchema;
3021
+ const interruptRequest = existingCheckpoint ? parseWithSchema(
3022
+ requestSchema,
3023
+ existingCheckpoint.request,
3024
+ "useReason interrupt.request"
3025
+ ) : firstInterruptRequest;
3026
+ if (!interruptRequest) {
3027
+ throw new Error(
3028
+ "useReason interrupt request is missing; first pass did not produce a valid interrupt payload."
3029
+ );
3030
+ }
3031
+ if (!existingCheckpoint) {
3032
+ ctx.currentNodeState.byKey[checkpointKey] = {
3033
+ status: "awaiting_interrupt",
3034
+ request: interruptRequest,
3035
+ firstText,
3036
+ ...firstRaw !== void 0 ? { firstRaw } : {},
3037
+ ...aggregatedUsage !== void 0 ? { firstUsage: aggregatedUsage } : {},
3038
+ ...finalFinishReason !== void 0 ? { firstFinishReason: finalFinishReason } : {},
3039
+ ...aggregatedProviderMetadata !== void 0 ? { firstProviderMetadata: aggregatedProviderMetadata } : {},
3040
+ ...aggregatedWarnings !== void 0 ? { firstWarnings: aggregatedWarnings } : {},
3041
+ ...firstOutput !== void 0 ? { firstOutput } : {}
3042
+ };
3043
+ ctx.stateDirty = true;
3044
+ }
3045
+ const resumeStatePatch = resolveHookStatePatch({
3046
+ nodeId: ctx.node.graph.node,
3047
+ currentNodeState: ctx.currentNodeState,
3048
+ workflowState: ctx.workflowState
3049
+ });
3050
+ interruptResponse = await awaitInterruptInternal({
3051
+ request: interruptRequest,
3052
+ requestSchema,
3053
+ responseSchema: args.interrupt.responseSchema,
3054
+ ...args.interrupt.schemaId ? { schemaId: args.interrupt.schemaId } : {},
3055
+ ...args.interrupt.schemaVersion ? { schemaVersion: args.interrupt.schemaVersion } : {},
3056
+ ...id ? { id } : {},
3057
+ meta: {
3058
+ __kortyxResumeStatePatch: resumeStatePatch
3059
+ }
3060
+ });
3061
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3062
+ checkpointKey
3063
+ });
3064
+ if (Object.hasOwn(ctx.currentNodeState.byKey, checkpointKey)) {
3065
+ delete ctx.currentNodeState.byKey[checkpointKey];
3066
+ ctx.stateDirty = true;
3067
+ }
3068
+ const continuationInput = defaultContinuationInput({
3069
+ input: args.input,
3070
+ draftText: firstText,
3071
+ ...firstOutput !== void 0 ? { draftOutput: firstOutput } : {},
3072
+ request: interruptRequest,
3073
+ response: interruptResponse
3074
+ });
3075
+ const second = await reasonEngine(
3076
+ {
3077
+ ...args,
3078
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3079
+ emit: suppressTextStream ? false : args.emit,
3080
+ stream: suppressTextStream ? false : args.stream
3081
+ },
3082
+ reasonMeta,
3083
+ args.outputSchema ? withOutputGuardrails(
3084
+ continuationInput,
3085
+ args.outputSchema
3086
+ ) : continuationInput
3087
+ );
3088
+ finalText = second.text;
3089
+ finalRaw = second.raw;
3090
+ accumulateTokenUsage(second.usage);
3091
+ aggregatedUsage = mergeUsage(aggregatedUsage, second.usage);
3092
+ finalFinishReason = second.finishReason;
3093
+ aggregatedProviderMetadata = mergeProviderMetadata(
3094
+ aggregatedProviderMetadata,
3095
+ second.providerMetadata
3096
+ );
3097
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, second.warnings);
3098
+ traceSpan?.addEvent?.("useReason.continuation.complete", {
3099
+ textLength: second.text.length
3100
+ });
3101
+ if (args.outputSchema) {
3102
+ finalOutput = parseReasonOutputWithSchema({
3103
+ text: finalText,
3104
+ schema: args.outputSchema,
3105
+ ...second.finishReason ? { finishReason: second.finishReason } : {},
3106
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3107
+ label: "useReason output"
3108
+ });
3109
+ }
3110
+ }
3111
+ if (finalOutput !== void 0) {
3112
+ emitReasonStructuredOutput({
3113
+ ...id ? { id } : {},
3114
+ opId,
3115
+ output: finalOutput,
3116
+ structured: args.structured,
3117
+ emit: args.emit ?? true
3118
+ });
3119
+ }
3120
+ const result = {
3121
+ ...id ? { id } : {},
3122
+ opId,
2634
3123
  text: finalText,
2635
- schema: args.outputSchema,
2636
- ...second.finishReason ? { finishReason: second.finishReason } : {},
2637
- ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2638
- label: "useReason output"
3124
+ ...finalRaw !== void 0 ? { raw: finalRaw } : {},
3125
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3126
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
3127
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
3128
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
3129
+ ...finalOutput !== void 0 ? { output: finalOutput } : {},
3130
+ ...interruptResponse !== void 0 ? { interruptResponse } : {}
3131
+ };
3132
+ if (args.interrupt) {
3133
+ ctx.currentNodeState.byKey[checkpointKey] = {
3134
+ status: "completed",
3135
+ result
3136
+ };
3137
+ ctx.stateDirty = true;
3138
+ }
3139
+ traceSpan?.end?.({
3140
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3141
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
3142
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
3143
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
3144
+ attributes: {
3145
+ textLength: finalText.length,
3146
+ resumedFromCheckpoint: Boolean(existingCheckpoint),
3147
+ interrupted: Boolean(interruptResponse !== void 0)
3148
+ },
3149
+ telemetry: {
3150
+ ...args.telemetry ?? {},
3151
+ output: args.telemetry?.output ?? finalOutput ?? finalText
3152
+ }
2639
3153
  });
3154
+ return result;
2640
3155
  }
3156
+ );
3157
+ }
3158
+ async function useReason(args) {
3159
+ try {
3160
+ return await runReason(args);
3161
+ } finally {
3162
+ await closeOwnedTools(args.tools ?? []);
2641
3163
  }
2642
- if (finalOutput !== void 0) {
2643
- emitReasonStructuredOutput({
2644
- ...id ? { id } : {},
2645
- opId,
2646
- output: finalOutput,
2647
- structured: args.structured,
2648
- emit: args.emit ?? true
2649
- });
2650
- }
2651
- const result = {
2652
- ...id ? { id } : {},
2653
- opId,
2654
- text: finalText,
2655
- ...finalRaw !== void 0 ? { raw: finalRaw } : {},
2656
- ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
2657
- ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
2658
- ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
2659
- ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
2660
- ...finalOutput !== void 0 ? { output: finalOutput } : {},
2661
- ...interruptResponse !== void 0 ? { interruptResponse } : {}
2662
- };
2663
- if (args.interrupt) {
2664
- ctx.currentNodeState.byKey[checkpointKey] = {
2665
- status: "completed",
2666
- result
2667
- };
2668
- ctx.stateDirty = true;
2669
- }
2670
- traceSpan?.end?.({
2671
- ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
2672
- ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
2673
- ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
2674
- ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
2675
- attributes: {
2676
- textLength: finalText.length,
2677
- resumedFromCheckpoint: Boolean(existingCheckpoint),
2678
- interrupted: Boolean(interruptResponse !== void 0)
2679
- },
2680
- telemetry: {
2681
- ...args.telemetry ?? {},
2682
- output: args.telemetry?.output ?? finalOutput ?? finalText
2683
- }
2684
- });
2685
- return result;
2686
3164
  }
2687
3165
 
2688
3166
  // src/hooks.ts
@@ -2741,8 +3219,8 @@ function useWorkflowState(key, ...rest) {
2741
3219
  }
2742
3220
 
2743
3221
  // src/parallel.ts
2744
- var import_core6 = require("@kortyx/core");
2745
- var import_errors5 = require("@kortyx/core/errors");
3222
+ var import_core7 = require("@kortyx/core");
3223
+ var import_errors7 = require("@kortyx/core/errors");
2746
3224
  var ParallelChildWaiting = class extends Error {
2747
3225
  name = "ParallelChildWaiting";
2748
3226
  };
@@ -2756,14 +3234,14 @@ var ParallelError = class extends AggregateError {
2756
3234
  );
2757
3235
  this.results = results;
2758
3236
  this.name = "ParallelError";
2759
- this.failure = (0, import_errors5.serializeFailure)({
3237
+ this.failure = (0, import_errors7.serializeFailure)({
2760
3238
  version: 1,
2761
3239
  code: this.code,
2762
3240
  category: "workflow",
2763
3241
  message: this.message,
2764
3242
  retryable: null,
2765
3243
  results: results.map(
2766
- (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors5.serializeFailure)(result.reason) }
3244
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors7.serializeFailure)(result.reason) }
2767
3245
  )
2768
3246
  });
2769
3247
  }
@@ -2771,7 +3249,7 @@ var ParallelError = class extends AggregateError {
2771
3249
  code = "PARALLEL_FAILED";
2772
3250
  failure;
2773
3251
  toJSON() {
2774
- return (0, import_errors5.serializeFailure)(this);
3252
+ return (0, import_errors7.serializeFailure)(this);
2775
3253
  }
2776
3254
  };
2777
3255
  function registerWorkflowTask(id) {
@@ -2867,13 +3345,13 @@ function parallel(values) {
2867
3345
  );
2868
3346
  }
2869
3347
  if (ready[0]?.status === "rejected") throw ready[0].reason;
2870
- (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
3348
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2871
3349
  if (ctx.workflowControlError) throw ctx.workflowControlError;
2872
3350
  const rejected = outcomes.filter(
2873
3351
  (outcome) => outcome.status === "rejected"
2874
3352
  );
2875
3353
  const control = rejected.find(
2876
- (outcome) => (0, import_core6.isExecutionCancelled)(outcome.reason) || (0, import_core6.isExecutionLimitReached)(outcome.reason)
3354
+ (outcome) => (0, import_core7.isExecutionCancelled)(outcome.reason) || (0, import_core7.isExecutionLimitReached)(outcome.reason)
2877
3355
  );
2878
3356
  if (control) {
2879
3357
  ctx.workflowControlError = control.reason;
@@ -2910,54 +3388,13 @@ function parallel(values) {
2910
3388
  }
2911
3389
 
2912
3390
  // src/workflow.ts
2913
- var import_node_crypto5 = require("crypto");
2914
- var import_core7 = require("@kortyx/core");
2915
- var import_errors6 = require("@kortyx/core/errors");
2916
-
2917
- // src/workflow-telemetry.ts
2918
- var import_node_crypto4 = require("crypto");
2919
- function emitWorkflowCall(telemetry, type, payload) {
2920
- const base = telemetry?.correlation;
2921
- if (!telemetry?.reporter || !telemetry.environment || !telemetry.service || !base?.runId || !base.workflowId)
2922
- return;
2923
- try {
2924
- const active = telemetry.trace?.getActiveContext?.();
2925
- void Promise.resolve(
2926
- telemetry.reporter.emit([
2927
- {
2928
- schemaVersion: 1,
2929
- eventId: (0, import_node_crypto4.randomUUID)(),
2930
- occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
2931
- environment: telemetry.environment,
2932
- service: telemetry.service,
2933
- correlation: {
2934
- ...base,
2935
- runId: base.runId,
2936
- workflowId: base.workflowId,
2937
- ...active
2938
- },
2939
- type,
2940
- payload
2941
- }
2942
- ])
2943
- ).catch(() => {
2944
- });
2945
- } catch {
2946
- }
2947
- }
2948
- function workflowCallContent(telemetry, side, value) {
2949
- const policy = telemetry?.captureContent;
2950
- if (!(policy === true || policy && typeof policy === "object" && policy[side]))
2951
- return {};
2952
- const serialized = JSON.stringify(value);
2953
- return serialized && Buffer.byteLength(serialized, "utf8") <= 16384 ? { [side]: JSON.parse(serialized) } : { [`${side}Omitted`]: "Content exceeds 16 KiB" };
2954
- }
2955
-
2956
- // src/workflow.ts
2957
- var WorkflowCallError = class extends import_errors6.KortyxError {
3391
+ var import_node_crypto6 = require("crypto");
3392
+ var import_core8 = require("@kortyx/core");
3393
+ var import_errors8 = require("@kortyx/core/errors");
3394
+ var WorkflowCallError = class extends import_errors8.KortyxError {
2958
3395
  constructor(workflow, message, cause, failure) {
2959
- const detail = (0, import_errors6.serializeFailure)(
2960
- failure ?? cause ?? new import_errors6.WorkflowContractError(message)
3396
+ const detail = (0, import_errors8.serializeFailure)(
3397
+ failure ?? cause ?? new import_errors8.WorkflowContractError(message)
2961
3398
  );
2962
3399
  super(detail.code, `Workflow '${workflow}': ${message}`, {
2963
3400
  ...detail,
@@ -2978,12 +3415,12 @@ function workflowCallFingerprint(value) {
2978
3415
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
2979
3416
  if (typeof v === "number" && Number.isFinite(v)) return v;
2980
3417
  if (typeof v !== "object" || !v || seen.has(v)) {
2981
- throw new import_errors6.WorkflowContractError(
3418
+ throw new import_errors8.WorkflowContractError(
2982
3419
  "Workflow input and output must be JSON serializable."
2983
3420
  );
2984
3421
  }
2985
3422
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
2986
- throw new import_errors6.WorkflowContractError(
3423
+ throw new import_errors8.WorkflowContractError(
2987
3424
  "Workflow input and output must contain plain JSON objects."
2988
3425
  );
2989
3426
  }
@@ -3004,7 +3441,7 @@ function createWorkflowHooks(workflows) {
3004
3441
  useWorkflow: (args) => {
3005
3442
  const workflow = workflows[args.workflow];
3006
3443
  if (!workflow || workflow.id !== args.workflow)
3007
- throw new import_errors6.WorkflowContractError(
3444
+ throw new import_errors8.WorkflowContractError(
3008
3445
  "Workflow registry keys must match definition ids."
3009
3446
  );
3010
3447
  return useWorkflow({ ...args, workflow });
@@ -3018,7 +3455,7 @@ async function useWorkflow(args) {
3018
3455
  if (task.group) await task.group.ready;
3019
3456
  return await runWorkflowCall(args, task);
3020
3457
  } catch (error) {
3021
- if ((0, import_core7.isExecutionCancelled)(error) || (0, import_core7.isExecutionLimitReached)(error))
3458
+ if ((0, import_core8.isExecutionCancelled)(error) || (0, import_core8.isExecutionLimitReached)(error))
3022
3459
  getHookContext().workflowControlError = error;
3023
3460
  throw error;
3024
3461
  } finally {
@@ -3027,26 +3464,26 @@ async function useWorkflow(args) {
3027
3464
  }
3028
3465
  async function runWorkflowCall(args, task) {
3029
3466
  const ctx = getHookContext();
3030
- (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3467
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3031
3468
  const service = ctx.node.callWorkflow;
3032
3469
  if (!service)
3033
- throw new import_errors6.WorkflowContractError(
3470
+ throw new import_errors8.WorkflowContractError(
3034
3471
  "useWorkflow requires an agent workflow registry."
3035
3472
  );
3036
3473
  if (!args.id.trim())
3037
- throw new import_errors6.WorkflowContractError(
3474
+ throw new import_errors8.WorkflowContractError(
3038
3475
  "useWorkflow requires a stable, nonempty id."
3039
3476
  );
3040
3477
  if (ctx.workflowCallActive && !task.group)
3041
- throw new import_errors6.WorkflowContractError(
3478
+ throw new import_errors8.WorkflowContractError(
3042
3479
  "Concurrent workflow calls require parallel([...]); otherwise await each call."
3043
3480
  );
3044
3481
  if (ctx.workflowCallIds.has(args.id))
3045
- throw new import_errors6.WorkflowContractError(
3482
+ throw new import_errors8.WorkflowContractError(
3046
3483
  `Duplicate workflow call id '${args.id}' in one node activation.`
3047
3484
  );
3048
3485
  if (ctx.workflowCallIds.size >= 64)
3049
- throw new import_errors6.WorkflowContractError(
3486
+ throw new import_errors8.WorkflowContractError(
3050
3487
  "A node may call at most 64 child workflows per activation."
3051
3488
  );
3052
3489
  ctx.workflowCallIds.add(args.id);
@@ -3073,7 +3510,7 @@ async function runWorkflowCall(args, task) {
3073
3510
  if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
3074
3511
  record ??= {
3075
3512
  fingerprint,
3076
- invocationId: (0, import_node_crypto5.randomUUID)(),
3513
+ invocationId: (0, import_node_crypto6.randomUUID)(),
3077
3514
  status: "running",
3078
3515
  interrupts: [],
3079
3516
  ...task.group ? { parallelKey: task.group.key } : {}
@@ -3083,7 +3520,7 @@ async function runWorkflowCall(args, task) {
3083
3520
  if (!task.group) ctx.workflowCallActive = true;
3084
3521
  const telemetry = ctx.node.workflowCallTelemetry;
3085
3522
  const activationKey = "__useWorkflowActivation";
3086
- ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
3523
+ ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto6.randomUUID)();
3087
3524
  const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
3088
3525
  const metadata = {
3089
3526
  invocationId: record.invocationId,
@@ -3127,7 +3564,7 @@ async function runWorkflowCall(args, task) {
3127
3564
  workflow,
3128
3565
  record.error ?? "child failed",
3129
3566
  void 0,
3130
- record.failure ?? (0, import_errors6.serializeFailure)(void 0)
3567
+ record.failure ?? (0, import_errors8.serializeFailure)(void 0)
3131
3568
  );
3132
3569
  if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
3133
3570
  throw new ParallelChildWaiting();
@@ -3151,9 +3588,9 @@ async function runWorkflowCall(args, task) {
3151
3588
  } catch (error) {
3152
3589
  const child = error;
3153
3590
  recordUsage(child?.__kortyxChildUsage);
3154
- (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3155
- if ((0, import_core7.isExecutionCancelled)(error)) throw error;
3156
- if ((0, import_core7.isExecutionLimitReached)(error)) {
3591
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3592
+ if ((0, import_core8.isExecutionCancelled)(error)) throw error;
3593
+ if ((0, import_core8.isExecutionLimitReached)(error)) {
3157
3594
  if (child?.__kortyxChildSnapshot)
3158
3595
  record.snapshot = child.__kortyxChildSnapshot;
3159
3596
  report("suspended", {
@@ -3163,7 +3600,7 @@ async function runWorkflowCall(args, task) {
3163
3600
  throw error;
3164
3601
  }
3165
3602
  record.status = "failed";
3166
- record.failure = (0, import_errors6.serializeFailure)(error);
3603
+ record.failure = (0, import_errors8.serializeFailure)(error);
3167
3604
  record.error = record.failure.message;
3168
3605
  report("failed", { error: "Child workflow failed" });
3169
3606
  throw new WorkflowCallError(
@@ -3173,12 +3610,15 @@ async function runWorkflowCall(args, task) {
3173
3610
  record.failure
3174
3611
  );
3175
3612
  }
3176
- (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3613
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3177
3614
  recordUsage(outcome.usage);
3178
3615
  if (outcome.status === "completed") {
3179
3616
  record.data = outcome.data;
3180
3617
  workflowCallFingerprint(record.data);
3181
3618
  record.status = "completed";
3619
+ record.toolObservations = outcome.toolObservations ?? [];
3620
+ for (const observation of record.toolObservations)
3621
+ rememberToolObservation(observation);
3182
3622
  delete record.snapshot;
3183
3623
  report(
3184
3624
  "completed",
@@ -3201,8 +3641,12 @@ async function runWorkflowCall(args, task) {
3201
3641
  });
3202
3642
  }
3203
3643
  }
3204
- if (cached)
3644
+ if (cached) {
3205
3645
  report("reused", workflowCallContent(telemetry, "output", record.data));
3646
+ if (record.restoredToolBranch !== telemetry?.correlation?.branchId)
3647
+ await replayToolObservations(record.toolObservations ?? [], true);
3648
+ delete record.restoredToolBranch;
3649
+ }
3206
3650
  const data = record.data ?? {};
3207
3651
  return {
3208
3652
  data: JSON.parse(JSON.stringify(data))
@@ -3226,6 +3670,7 @@ async function runWorkflowCall(args, task) {
3226
3670
  useReason,
3227
3671
  useRuntimeContext,
3228
3672
  useStructuredData,
3673
+ useTool,
3229
3674
  useWorkflow,
3230
3675
  useWorkflowState,
3231
3676
  workflowCallFingerprint