@kortyx/hooks 0.25.3 → 0.27.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
@@ -25,6 +25,7 @@ __export(index_exports, {
25
25
  createWorkflowHooks: () => createWorkflowHooks,
26
26
  emitWorkflowCall: () => emitWorkflowCall,
27
27
  parallel: () => parallel,
28
+ reportError: () => reportError,
28
29
  runWithHookContext: () => runWithHookContext,
29
30
  useAbortSignal: () => useAbortSignal,
30
31
  useInterrupt: () => useInterrupt,
@@ -32,6 +33,7 @@ __export(index_exports, {
32
33
  useReason: () => useReason2,
33
34
  useRuntimeContext: () => useRuntimeContext,
34
35
  useStructuredData: () => useStructuredData,
36
+ useTool: () => useTool,
35
37
  useWorkflow: () => useWorkflow,
36
38
  useWorkflowState: () => useWorkflowState,
37
39
  workflowCallFingerprint: () => workflowCallFingerprint
@@ -99,6 +101,7 @@ var createInternalContext = (ctx) => {
99
101
  workflowContextClosed: false,
100
102
  nodeStateIndex: 0,
101
103
  reasonCallIndex: 0,
104
+ toolCallIndex: 0,
102
105
  currentNodeState,
103
106
  workflowState: cloneWorkflowState(hookRuntimeState.workflowState),
104
107
  tokenUsage: cloneTokenUsage(ctx.state.runtime?.tokenUsage),
@@ -189,6 +192,12 @@ async function runWithHookContext(ctx, fn) {
189
192
  }
190
193
  });
191
194
  cleanupCompletedReasonCheckpoints(internal);
195
+ for (const key of Object.keys(internal.currentNodeState.byKey)) {
196
+ if (key.startsWith("__useTool:")) {
197
+ delete internal.currentNodeState.byKey[key];
198
+ internal.stateDirty = true;
199
+ }
200
+ }
192
201
  for (const key of Object.keys(internal.currentNodeState.byKey)) {
193
202
  if (key.startsWith("__useWorkflow:") || key.startsWith("__parallel:") || key === "__useWorkflowActivation") {
194
203
  internal.currentNodeState.byKey[key] = null;
@@ -341,7 +350,87 @@ var awaitInterruptInternal = (args) => {
341
350
  };
342
351
 
343
352
  // src/reason/use-reason.ts
344
- var import_core5 = require("@kortyx/core");
353
+ var import_core6 = require("@kortyx/core");
354
+
355
+ // src/safe-tracing.ts
356
+ function safeSpan(span) {
357
+ const guard = (fn) => (...args) => {
358
+ try {
359
+ fn?.(...args);
360
+ } catch {
361
+ }
362
+ };
363
+ let ended = false;
364
+ const finish = (fn) => (...args) => {
365
+ if (ended) return;
366
+ ended = true;
367
+ guard(fn)(...args);
368
+ };
369
+ return {
370
+ addEvent: guard(
371
+ (name, attributes) => span?.addEvent?.(name, attributes)
372
+ ),
373
+ setAttributes: guard(
374
+ (attributes) => span?.setAttributes?.(attributes)
375
+ ),
376
+ end: finish((args) => span?.end?.(args)),
377
+ fail: finish((error, args) => {
378
+ if (span?.fail) span.fail(error, args);
379
+ else span?.end?.(args);
380
+ })
381
+ };
382
+ }
383
+ function safeStartSpan(adapter, args) {
384
+ try {
385
+ return safeSpan(adapter?.startSpan?.(args));
386
+ } catch {
387
+ return safeSpan(void 0);
388
+ }
389
+ }
390
+ async function withSafeTraceSpan(adapter, args, fn) {
391
+ let execution;
392
+ const invoke = (span) => {
393
+ execution ??= Promise.resolve().then(async () => {
394
+ const observed = safeSpan(span);
395
+ try {
396
+ const result = await fn(observed);
397
+ observed.end?.();
398
+ return result;
399
+ } catch (error) {
400
+ observed.fail?.(error);
401
+ throw error;
402
+ }
403
+ });
404
+ return execution;
405
+ };
406
+ let activeScope;
407
+ try {
408
+ activeScope = adapter?.withSpan?.bind(adapter);
409
+ } catch {
410
+ }
411
+ if (activeScope) {
412
+ let started;
413
+ const invoked = new Promise((resolve) => {
414
+ started = resolve;
415
+ });
416
+ const observe = async () => {
417
+ try {
418
+ await activeScope(args, (span) => {
419
+ const result = invoke(span);
420
+ started?.();
421
+ return result;
422
+ });
423
+ } catch {
424
+ }
425
+ };
426
+ const observation = observe();
427
+ if (!execution) await Promise.race([invoked, observation]);
428
+ } else {
429
+ await invoke(safeStartSpan(adapter, args)).catch(() => {
430
+ });
431
+ }
432
+ return execution ?? invoke(safeSpan(void 0));
433
+ }
345
434
 
346
435
  // src/structured.ts
347
436
  var import_node_crypto = require("crypto");
@@ -435,18 +524,419 @@ var emitStructuredData = (args) => {
435
524
  var shouldEmitStructured = (cfg) => Boolean(cfg);
436
525
  var shouldStreamStructured = (cfg) => Boolean(cfg) && (cfg?.stream ?? true);
437
526
 
438
- // src/reason/engine.ts
527
+ // src/tool.ts
528
+ var import_node_async_hooks2 = require("async_hooks");
439
529
  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
530
  var import_core2 = require("@kortyx/core");
531
+ var import_errors3 = require("@kortyx/core/errors");
532
+
533
+ // src/error-diagnostics.ts
445
534
  var import_errors2 = require("@kortyx/core/errors");
535
+ var MAX_STACK_LENGTH = 32768;
536
+ var MAX_CAUSE_DEPTH = 4;
537
+ var projectDetails = (value, depth, seen) => {
538
+ const objectValue = value && typeof value === "object" ? value : void 0;
539
+ if (objectValue) {
540
+ if (seen.has(objectValue))
541
+ return { type: "Error", message: "Circular error cause." };
542
+ seen.add(objectValue);
543
+ }
544
+ const name = (0, import_errors2.errorProperty)(value, "name") ?? (value instanceof Error ? value.name : void 0);
545
+ const message = (0, import_errors2.errorProperty)(value, "message");
546
+ const stack = (0, import_errors2.errorProperty)(value, "stack") ?? (value instanceof Error ? (() => {
547
+ try {
548
+ return value.stack;
549
+ } catch {
550
+ return void 0;
551
+ }
552
+ })() : void 0);
553
+ const cause = depth < MAX_CAUSE_DEPTH ? (0, import_errors2.errorProperty)(value, "cause") : null;
554
+ return {
555
+ type: typeof name === "string" ? name.slice(0, 256) : "Error",
556
+ message: (typeof value === "string" ? value : typeof message === "string" ? message : "Execution reported a fault.").slice(0, 8192),
557
+ ...typeof stack === "string" ? { stack: stack.slice(0, MAX_STACK_LENGTH) } : {},
558
+ ...cause !== null && cause !== void 0 ? { cause: projectDetails(cause, depth + 1, seen) } : {}
559
+ };
560
+ };
561
+ var sanitizeProjectedDetails = (details, depth = 0) => ({
562
+ ...typeof details.type === "string" ? { type: details.type.slice(0, 256) } : {},
563
+ message: typeof details.message === "string" ? details.message.slice(0, 8192) : "Execution reported a fault.",
564
+ ...typeof details.stack === "string" ? { stack: details.stack.slice(0, MAX_STACK_LENGTH) } : {},
565
+ ...depth < MAX_CAUSE_DEPTH && details.cause ? { cause: sanitizeProjectedDetails(details.cause, depth + 1) } : {}
566
+ });
567
+ function exceptionDiagnostics(error, project) {
568
+ try {
569
+ const details = project ? project(error) : projectDetails(error, 0, /* @__PURE__ */ new WeakSet());
570
+ return details ? sanitizeProjectedDetails(details) : null;
571
+ } catch {
572
+ return null;
573
+ }
574
+ }
575
+ function errorDiagnostics(error, project) {
576
+ const details = exceptionDiagnostics(error, project);
577
+ return details ? {
578
+ errorMessage: details.message,
579
+ ...details.type ? { errorType: details.type } : {}
580
+ } : {};
581
+ }
582
+
583
+ // src/workflow-telemetry.ts
584
+ var import_node_crypto2 = require("crypto");
585
+ function emitWorkflowCall(telemetry, type, payload) {
586
+ const base = telemetry?.correlation;
587
+ if (!telemetry?.reporter || !telemetry.environment || !telemetry.service || !base?.runId || !base.workflowId)
588
+ return;
589
+ try {
590
+ const active = telemetry.trace?.getActiveContext?.();
591
+ void Promise.resolve(
592
+ telemetry.reporter.emit([
593
+ {
594
+ schemaVersion: 1,
595
+ eventId: (0, import_node_crypto2.randomUUID)(),
596
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
597
+ environment: telemetry.environment,
598
+ service: telemetry.service,
599
+ correlation: {
600
+ ...base,
601
+ runId: base.runId,
602
+ workflowId: base.workflowId,
603
+ ...active
604
+ },
605
+ type,
606
+ payload
607
+ }
608
+ ])
609
+ ).catch(() => {
610
+ });
611
+ } catch {
612
+ }
613
+ }
614
+ function workflowCallContent(telemetry, side, value) {
615
+ const policy = telemetry?.captureContent;
616
+ if (!(policy === true || policy && typeof policy === "object" && policy[side]))
617
+ return {};
618
+ const serialized = JSON.stringify(value);
619
+ return serialized && Buffer.byteLength(serialized, "utf8") <= 16384 ? { [side]: JSON.parse(serialized) } : { [`${side}Omitted`]: "Content exceeds 16 KiB" };
620
+ }
621
+
622
+ // src/tool.ts
623
+ var tools = new import_node_async_hooks2.AsyncLocalStorage();
624
+ function classification(tool, value, thrown, policy = tool.outcomes) {
625
+ const fallback = {
626
+ outcome: thrown ? "fault" : "success"
627
+ };
628
+ try {
629
+ if (value && typeof value === "object" && "isError" in value && value.isError === true)
630
+ fallback.outcome = "fault";
631
+ const result = thrown ? policy?.classifyError?.(value) : policy?.classifyResult?.(value);
632
+ if (!result || !["success", "denied", "fault"].includes(result.outcome))
633
+ return fallback;
634
+ if (result.outcome !== "denied")
635
+ return thrown || fallback.outcome === "fault" ? fallback : { outcome: result.outcome };
636
+ const code = result.code;
637
+ return {
638
+ outcome: "denied",
639
+ code: typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(code) && policy?.denialCodes?.includes(code) ? code : "DENIED"
640
+ };
641
+ } catch {
642
+ return fallback;
643
+ }
644
+ }
645
+ async function closeOwnedTools(owned) {
646
+ const closers = /* @__PURE__ */ new Map();
647
+ for (const tool of owned) {
648
+ if (tool.closeAfterUse !== false && tool.close && !closers.has(tool.close))
649
+ closers.set(tool.close, tool);
650
+ }
651
+ await Promise.all(
652
+ [...closers].map(async ([close, tool]) => {
653
+ try {
654
+ await close.call(tool);
655
+ } catch {
656
+ const ctx = getHookContext();
657
+ try {
658
+ ctx.reasonTrace?.startSpan({
659
+ name: "kortyx.tool.cleanup",
660
+ attributes: { cleanupFailed: true }
661
+ })?.end?.();
662
+ } catch {
663
+ }
664
+ }
665
+ })
666
+ );
667
+ }
668
+ function captureToolError(observation, error, policy, returned = false) {
669
+ try {
670
+ 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;
671
+ Object.assign(
672
+ observation,
673
+ errorDiagnostics(policy?.error ? error : reported, policy?.error)
674
+ );
675
+ } catch {
676
+ }
677
+ }
678
+ function ownership() {
679
+ const ctx = getHookContext();
680
+ const correlation = ctx.node.workflowCallTelemetry?.correlation;
681
+ return {
682
+ workflowId: ctx.node.graph.name,
683
+ nodeId: ctx.node.graph.node,
684
+ ...correlation?.workflowRevisionId ? { workflowRevisionId: correlation.workflowRevisionId } : {},
685
+ ...correlation?.parentInvocationId ? { parentInvocationId: correlation.parentInvocationId } : {},
686
+ ...correlation?.runId ? { runId: correlation.runId } : {},
687
+ ...correlation?.invocationId ? { invocationId: correlation.invocationId } : {},
688
+ ...correlation?.branchId ? { branchId: correlation.branchId } : {}
689
+ };
690
+ }
691
+ function rememberToolObservation(observation) {
692
+ const ctx = getHookContext();
693
+ const existing = ctx.workflowState.__kortyxToolObservations;
694
+ const observations = Array.isArray(existing) ? existing : [];
695
+ if (!observations.some((item) => item.attemptId === observation.attemptId))
696
+ observations.push({ ...observation });
697
+ ctx.workflowState.__kortyxToolObservations = observations;
698
+ ctx.stateDirty = true;
699
+ }
700
+ function toolTrace(observation) {
701
+ const ctx = getHookContext();
702
+ const adapter = ctx.reasonTrace ?? ctx.node.workflowCallTelemetry?.trace;
703
+ if (adapter) return adapter;
704
+ const telemetry = ctx.node.workflowCallTelemetry;
705
+ if (!telemetry?.reporter) return void 0;
706
+ const correlation = { ...telemetry.correlation, ...observation };
707
+ const spanId = (0, import_node_crypto3.randomUUID)();
708
+ const emit = (type, payload) => emitWorkflowCall(
709
+ {
710
+ ...telemetry,
711
+ correlation,
712
+ trace: {
713
+ startSpan: () => void 0,
714
+ getActiveContext: () => ({
715
+ traceId: correlation.runId ?? spanId,
716
+ spanId
717
+ })
718
+ }
719
+ },
720
+ type,
721
+ payload
722
+ );
723
+ return {
724
+ startSpan: ({ attributes = {} }) => {
725
+ if (attributes.executed === true) emit("tool.started", attributes);
726
+ let ended = false;
727
+ return {
728
+ end: (args) => {
729
+ if (ended) return;
730
+ ended = true;
731
+ const payload = { ...attributes, ...args?.attributes };
732
+ 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";
733
+ emit(type, payload);
734
+ }
735
+ };
736
+ }
737
+ };
738
+ }
739
+ async function executeObservedTool(args) {
740
+ const ctx = getHookContext();
741
+ const observation = {
742
+ version: 1,
743
+ name: args.tool.name,
744
+ toolCallId: args.toolCallId,
745
+ attemptId: (0, import_node_crypto3.randomUUID)(),
746
+ callingMode: args.callingMode,
747
+ executed: false,
748
+ ...ownership()
749
+ };
750
+ const scope = {
751
+ tool: args.tool,
752
+ input: args.input,
753
+ signal: args.abortSignal,
754
+ mode: args.callingMode,
755
+ providerToolCallId: args.providerToolCallId ?? args.toolCallId,
756
+ adopted: false
757
+ };
758
+ try {
759
+ (0, import_core2.throwIfExecutionAborted)(args.abortSignal);
760
+ } catch (error) {
761
+ observation.outcome = "cancelled";
762
+ await observeToolFact(observation);
763
+ args.onObservation?.(observation);
764
+ throw error;
765
+ }
766
+ ctx.node.consumeExecution?.("maxToolCalls");
767
+ observation.executed = true;
768
+ const started = performance.now();
769
+ return withSafeTraceSpan(
770
+ toolTrace(observation),
771
+ {
772
+ name: "kortyx.tool",
773
+ attributes: { ...observation, nodeId: ctx.node.graph.node }
774
+ },
775
+ async (span) => tools.run(scope, async () => {
776
+ try {
777
+ const result = await args.tool.execute(args.input, {
778
+ toolCallId: args.providerToolCallId ?? args.toolCallId,
779
+ ...args.abortSignal ? { abortSignal: args.abortSignal } : {}
780
+ });
781
+ (0, import_core2.throwIfExecutionAborted)(args.abortSignal);
782
+ const outcome = classification(
783
+ args.tool,
784
+ result,
785
+ false,
786
+ scope.classify ?? args.tool.outcomes
787
+ );
788
+ observation.outcome = outcome.outcome;
789
+ if (outcome.code) observation.denialCode = outcome.code;
790
+ if (observation.outcome === "fault") {
791
+ captureToolError(
792
+ observation,
793
+ result,
794
+ scope.telemetry ?? args.tool.telemetry,
795
+ true
796
+ );
797
+ }
798
+ return result;
799
+ } catch (error) {
800
+ if (args.abortSignal?.aborted) observation.outcome = "cancelled";
801
+ else if (!(0, import_errors3.isControlFlowError)(error)) {
802
+ const outcome = classification(
803
+ args.tool,
804
+ error,
805
+ true,
806
+ scope.classify ?? args.tool.outcomes
807
+ );
808
+ observation.outcome = outcome.outcome;
809
+ if (outcome.code) observation.denialCode = outcome.code;
810
+ if (observation.outcome === "fault")
811
+ captureToolError(
812
+ observation,
813
+ error,
814
+ scope.telemetry ?? args.tool.telemetry
815
+ );
816
+ }
817
+ throw error;
818
+ } finally {
819
+ observation.durationMs = Math.max(0, performance.now() - started);
820
+ span.end?.({
821
+ attributes: {
822
+ ...observation,
823
+ ...observation.outcome ? {} : { suspended: true }
824
+ }
825
+ });
826
+ rememberToolObservation(observation);
827
+ args.onObservation?.({ ...observation });
828
+ }
829
+ })
830
+ );
831
+ }
832
+ async function observeToolFact(observation, kind, preserveOwner = false) {
833
+ const ctx = getHookContext();
834
+ if (!preserveOwner) Object.assign(observation, ownership());
835
+ if (observation.outcome && kind !== "reused")
836
+ rememberToolObservation(observation);
837
+ await withSafeTraceSpan(
838
+ toolTrace(observation),
839
+ {
840
+ name: "kortyx.tool",
841
+ attributes: {
842
+ ...observation,
843
+ executed: false,
844
+ ...kind ? { observationKind: kind } : {},
845
+ nodeId: observation.nodeId ?? ctx.node.graph.node
846
+ }
847
+ },
848
+ async (span) => {
849
+ span.end?.({
850
+ attributes: {
851
+ ...observation,
852
+ executed: false,
853
+ ...kind ? { observationKind: kind } : {}
854
+ }
855
+ });
856
+ }
857
+ );
858
+ }
859
+ async function useTool(args) {
860
+ const ctx = getHookContext();
861
+ const scope = tools.getStore();
862
+ if (scope?.mode === "model" && !scope.adopted && scope.tool.name === args.tool.name && scope.input === args.input) {
863
+ scope.adopted = true;
864
+ scope.classify = args.tool.outcomes ?? scope.classify;
865
+ scope.telemetry = args.tool.telemetry ?? scope.telemetry;
866
+ return tools.run(
867
+ { ...scope, mode: "direct" },
868
+ () => args.tool.execute(args.input, {
869
+ toolCallId: scope.providerToolCallId,
870
+ ...scope.signal ? { abortSignal: scope.signal } : {}
871
+ })
872
+ );
873
+ }
874
+ const index = ctx.toolCallIndex++;
875
+ const key = `__useTool:${args.id ?? `auto:${index}`}`;
876
+ let id = ctx.currentNodeState.byKey[key];
877
+ if (typeof id !== "string") {
878
+ id = (0, import_node_crypto3.randomUUID)();
879
+ ctx.currentNodeState.byKey[key] = id;
880
+ ctx.stateDirty = true;
881
+ }
882
+ try {
883
+ return await executeObservedTool({
884
+ tool: args.tool,
885
+ input: args.input,
886
+ toolCallId: String(id),
887
+ callingMode: "direct",
888
+ abortSignal: (0, import_core2.combineAbortSignals)(ctx.node.abortSignal, args.abortSignal)
889
+ });
890
+ } finally {
891
+ await closeOwnedTools([args.tool]);
892
+ }
893
+ }
894
+ async function replayToolObservations(observations, child = false) {
895
+ for (const original of observations) {
896
+ const source = original.source ?? {
897
+ ...ownershipFrom(original),
898
+ toolCallId: original.toolCallId,
899
+ attemptId: original.attemptId
900
+ };
901
+ await observeToolFact(
902
+ {
903
+ ...original,
904
+ ...ownership(),
905
+ ...child ? {
906
+ workflowId: original.workflowId,
907
+ nodeId: original.nodeId,
908
+ workflowRevisionId: original.workflowRevisionId,
909
+ invocationId: original.invocationId,
910
+ parentInvocationId: original.parentInvocationId
911
+ } : {},
912
+ attemptId: (0, import_node_crypto3.randomUUID)(),
913
+ executed: false,
914
+ source,
915
+ durationMs: void 0
916
+ },
917
+ "reused",
918
+ child
919
+ );
920
+ }
921
+ }
922
+ var ownershipFrom = (observation) => ({
923
+ runId: observation.runId,
924
+ invocationId: observation.invocationId,
925
+ branchId: observation.branchId
926
+ });
927
+
928
+ // src/reason/engine.ts
929
+ var import_node_crypto5 = require("crypto");
930
+ var import_core4 = require("@kortyx/core");
931
+
932
+ // src/reason-engine.ts
933
+ var import_node_crypto4 = require("crypto");
934
+ var import_core3 = require("@kortyx/core");
935
+ var import_errors4 = require("@kortyx/core/errors");
446
936
  function assertModelFinish(result) {
447
937
  if (result.finishReason?.unified !== "content-filter") return;
448
938
  throw Object.assign(
449
- new import_errors2.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
939
+ new import_errors4.KortyxError("PROVIDER_REFUSAL", "The provider refused the request.", {
450
940
  category: "provider",
451
941
  retryable: false,
452
942
  safeMessage: "The provider refused the request."
@@ -460,7 +950,7 @@ function assertModelFinish(result) {
460
950
  var toError = (value) => value instanceof Error ? value : new Error(String(value));
461
951
  var createRuntimeId = () => {
462
952
  try {
463
- return (0, import_node_crypto2.randomUUID)();
953
+ return (0, import_node_crypto4.randomUUID)();
464
954
  } catch {
465
955
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
466
956
  }
@@ -495,8 +985,8 @@ async function runReasonEngine(args) {
495
985
  args.model.options?.providerOptions,
496
986
  args.providerOptions
497
987
  );
498
- const tools = args.tools ?? args.model.options?.tools;
499
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
988
+ const tools2 = args.tools ?? args.model.options?.tools;
989
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
500
990
  const model = args.model.provider.getModel(args.model.modelId, {
501
991
  ...temperature !== void 0 ? { temperature } : {},
502
992
  streaming: stream,
@@ -505,10 +995,10 @@ async function runReasonEngine(args) {
505
995
  ...abortSignal !== void 0 ? { abortSignal } : {},
506
996
  ...reasoning !== void 0 ? { reasoning } : {},
507
997
  ...responseFormat !== void 0 ? { responseFormat } : {},
508
- ...tools !== void 0 ? { tools } : {},
998
+ ...tools2 !== void 0 ? { tools: tools2 } : {},
509
999
  ...providerOptions !== void 0 ? { providerOptions } : {}
510
1000
  });
511
- if (tools?.length && !model.supportsToolStreaming) stream = false;
1001
+ if (tools2?.length && !model.supportsToolStreaming) stream = false;
512
1002
  const messages = args.messages !== void 0 ? args.messages : [
513
1003
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
514
1004
  { role: "user", content: String(args.input ?? "") }
@@ -520,7 +1010,7 @@ async function runReasonEngine(args) {
520
1010
  opId,
521
1011
  segmentId
522
1012
  };
523
- const traceSpan = args.reasonTrace?.startSpan({
1013
+ const traceSpan = safeStartSpan(args.reasonTrace, {
524
1014
  name: "runReasonEngine",
525
1015
  attributes: {
526
1016
  ...commonMeta,
@@ -600,7 +1090,7 @@ async function runReasonEngine(args) {
600
1090
  }
601
1091
  const response2 = await model.stream(messages);
602
1092
  for await (const chunk of response2) {
603
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1093
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
604
1094
  switch (chunk.type) {
605
1095
  case "text-delta": {
606
1096
  if (chunk.delta.length === 0) break;
@@ -666,7 +1156,7 @@ async function runReasonEngine(args) {
666
1156
  }
667
1157
  }
668
1158
  }
669
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1159
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
670
1160
  assertModelFinish({ finishReason, usage });
671
1161
  if (emit) {
672
1162
  emitNodeEvent(args.emitEvent, args.nodeId, "text-end", {
@@ -683,12 +1173,12 @@ async function runReasonEngine(args) {
683
1173
  ...providerMetadata !== void 0 ? { providerMetadata } : {},
684
1174
  ...warnings !== void 0 ? { warnings } : {}
685
1175
  };
686
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1176
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
687
1177
  endTrace(traceSpan, result2);
688
1178
  return result2;
689
1179
  }
690
1180
  const response = await model.invoke(messages);
691
- (0, import_core2.throwIfExecutionAborted)(abortSignal);
1181
+ (0, import_core3.throwIfExecutionAborted)(abortSignal);
692
1182
  assertModelFinish(response);
693
1183
  if (response.content) args.onTextChunk?.(response.content);
694
1184
  if (emit) {
@@ -718,7 +1208,7 @@ async function runReasonEngine(args) {
718
1208
  endTrace(traceSpan, result);
719
1209
  return result;
720
1210
  } catch (error) {
721
- const failure = abortSignal?.aborted ? (0, import_core2.createExecutionCancelledError)() : error;
1211
+ const failure = abortSignal?.aborted ? (0, import_core3.createExecutionCancelledError)() : error;
722
1212
  failTrace(traceSpan, failure);
723
1213
  throw failure;
724
1214
  }
@@ -727,19 +1217,19 @@ async function runReasonEngine(args) {
727
1217
  // src/reason/engine.ts
728
1218
  var createRuntimeId2 = () => {
729
1219
  try {
730
- return (0, import_node_crypto3.randomUUID)();
1220
+ return (0, import_node_crypto5.randomUUID)();
731
1221
  } catch {
732
1222
  return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
733
1223
  }
734
1224
  };
735
1225
  async function reasonEngine(args, meta, inputOverride) {
736
1226
  const ctx = getHookContext();
737
- const abortSignal = (0, import_core3.combineAbortSignals)(
1227
+ const abortSignal = (0, import_core4.combineAbortSignals)(
738
1228
  ctx.node.abortSignal,
739
1229
  args.abortSignal,
740
1230
  args.model.options?.abortSignal
741
1231
  );
742
- (0, import_core3.throwIfExecutionAborted)(abortSignal);
1232
+ (0, import_core4.throwIfExecutionAborted)(abortSignal);
743
1233
  ctx.node.consumeExecution?.("maxModelPasses");
744
1234
  try {
745
1235
  return await runReasonEngine({
@@ -1489,7 +1979,7 @@ function createStructuredOutputStreamer(args, id, opId) {
1489
1979
  }
1490
1980
 
1491
1981
  // src/reason/parsing.ts
1492
- var import_errors3 = require("@kortyx/core/errors");
1982
+ var import_errors5 = require("@kortyx/core/errors");
1493
1983
  var tryJsonParse = (value) => {
1494
1984
  try {
1495
1985
  return { ok: true, value: JSON.parse(value) };
@@ -1527,12 +2017,12 @@ var resolveOutputCandidate = (text) => {
1527
2017
  }
1528
2018
  return text;
1529
2019
  };
1530
- var createTruncatedStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
2020
+ var createTruncatedStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
1531
2021
  "OUTPUT_TRUNCATED",
1532
2022
  `${label} was truncated before producing valid structured output. The model stopped due to output length. Increase maxOutputTokens or simplify the requested output.`,
1533
2023
  cause
1534
2024
  );
1535
- var createInvalidStructuredOutputError = (label, cause) => new import_errors3.ValidationError(
2025
+ var createInvalidStructuredOutputError = (label, cause) => new import_errors5.ValidationError(
1536
2026
  "INVALID_MODEL_JSON",
1537
2027
  `${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
2028
  cause
@@ -1544,7 +2034,7 @@ var parseReasonOutputWithSchema = (args) => {
1544
2034
  try {
1545
2035
  return parseWithSchema(args.schema, candidate, args.label);
1546
2036
  } catch (error) {
1547
- if (!(error instanceof import_errors3.ValidationError)) throw error;
2037
+ if (!(error instanceof import_errors5.ValidationError)) throw error;
1548
2038
  const failure = args.finishReason?.unified === "length" ? createTruncatedStructuredOutputError(args.label, error) : typeof candidate === "string" && !validJson ? createInvalidStructuredOutputError(args.label, error) : error;
1549
2039
  if (failure && typeof failure === "object")
1550
2040
  Object.assign(failure, {
@@ -1564,7 +2054,7 @@ var parseInterruptFirstPassResult = (args) => {
1564
2054
  if (typeof candidate === "string" && candidate.trim().length > 0) {
1565
2055
  throw createInvalidStructuredOutputError("useReason output");
1566
2056
  }
1567
- throw new import_errors3.ValidationError(
2057
+ throw new import_errors5.ValidationError(
1568
2058
  "MODEL_OUTPUT_SCHEMA",
1569
2059
  "useReason first pass with interrupt must return a JSON object."
1570
2060
  );
@@ -1573,7 +2063,7 @@ var parseInterruptFirstPassResult = (args) => {
1573
2063
  let interruptRequired = true;
1574
2064
  if (mode === "optional") {
1575
2065
  if (candidate.decision !== "continue" && candidate.decision !== "interrupt") {
1576
- throw new import_errors3.ValidationError(
2066
+ throw new import_errors5.ValidationError(
1577
2067
  "MODEL_OUTPUT_SCHEMA",
1578
2068
  'useReason optional interrupt first pass must include decision "continue" or "interrupt".'
1579
2069
  );
@@ -1586,7 +2076,7 @@ var parseInterruptFirstPassResult = (args) => {
1586
2076
  candidate.interrupt
1587
2077
  ].some((value) => value !== null && value !== void 0);
1588
2078
  if (hasUnexpectedRequest) {
1589
- throw new import_errors3.ValidationError(
2079
+ throw new import_errors5.ValidationError(
1590
2080
  "MODEL_OUTPUT_SCHEMA",
1591
2081
  'useReason optional interrupt first pass with decision "continue" must not include an interrupt request.'
1592
2082
  );
@@ -1620,7 +2110,7 @@ var parseInterruptFirstPassResult = (args) => {
1620
2110
  "useReason output"
1621
2111
  );
1622
2112
  } catch (error) {
1623
- if (!(error instanceof import_errors3.ValidationError)) throw error;
2113
+ if (!(error instanceof import_errors5.ValidationError)) throw error;
1624
2114
  if (args.finishReason?.unified === "length") {
1625
2115
  throw createTruncatedStructuredOutputError("useReason output", error);
1626
2116
  }
@@ -1629,7 +2119,7 @@ var parseInterruptFirstPassResult = (args) => {
1629
2119
  }
1630
2120
  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
2121
  if (!args.outputSchema && !explicitDraft) {
1632
- throw new import_errors3.ValidationError(
2122
+ throw new import_errors5.ValidationError(
1633
2123
  "MODEL_OUTPUT_SCHEMA",
1634
2124
  "useReason first pass with interrupt requires `draftText` when outputSchema is not provided."
1635
2125
  );
@@ -1819,6 +2309,7 @@ var mergeUsage = (left, right) => {
1819
2309
  const reasoning = sum(left.reasoning, right.reasoning);
1820
2310
  const cacheRead = sum(left.cacheRead, right.cacheRead);
1821
2311
  const cacheWrite = sum(left.cacheWrite, right.cacheWrite);
2312
+ const cacheWrite1h = sum(left.cacheWrite1h, right.cacheWrite1h);
1822
2313
  const merged = {
1823
2314
  ...right.outputIncludesReasoning !== void 0 ? { outputIncludesReasoning: right.outputIncludesReasoning } : {},
1824
2315
  ...right.inputIncludesCacheWrite !== void 0 ? { inputIncludesCacheWrite: right.inputIncludesCacheWrite } : {},
@@ -1830,6 +2321,7 @@ var mergeUsage = (left, right) => {
1830
2321
  if (reasoning !== void 0) merged.reasoning = reasoning;
1831
2322
  if (cacheRead !== void 0) merged.cacheRead = cacheRead;
1832
2323
  if (cacheWrite !== void 0) merged.cacheWrite = cacheWrite;
2324
+ if (cacheWrite1h !== void 0) merged.cacheWrite1h = cacheWrite1h;
1833
2325
  if (raw) merged.raw = raw;
1834
2326
  return merged;
1835
2327
  };
@@ -1868,9 +2360,9 @@ var emitReasonStructuredOutput = (args) => {
1868
2360
  };
1869
2361
 
1870
2362
  // 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) => ({
2363
+ var import_core5 = require("@kortyx/core");
2364
+ var import_errors6 = require("@kortyx/core/errors");
2365
+ var toToolDefinitions = (tools2) => tools2.map((tool) => ({
1874
2366
  name: tool.name,
1875
2367
  ...tool.title ? { title: tool.title } : {},
1876
2368
  ...tool.description ? { description: tool.description } : {},
@@ -1887,12 +2379,13 @@ var resolveToolExecutionBoolean = (value, toolName, fallback) => {
1887
2379
  return fallback;
1888
2380
  };
1889
2381
  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;
2382
+ if (value && typeof value === "object" && !Array.isArray(value) && "toolCallId" in value && "name" in value && "content" in value && typeof value.content === "string") {
2383
+ const result = value;
2384
+ return { ...result, toolCallId: toolCall.id, name: toolCall.name };
1892
2385
  }
1893
2386
  let content;
1894
2387
  try {
1895
- content = typeof value === "string" ? value : JSON.stringify(value);
2388
+ content = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
1896
2389
  } catch {
1897
2390
  content = String(value);
1898
2391
  }
@@ -1903,28 +2396,6 @@ var normalizeToolResult = (toolCall, value) => {
1903
2396
  ...value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
1904
2397
  };
1905
2398
  };
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
2399
  var createInitialMessages = (args) => [
1929
2400
  ...typeof args.system === "string" && args.system.length > 0 ? [{ role: "system", content: args.system }] : [],
1930
2401
  { role: "user", content: String(args.input ?? "") }
@@ -1932,7 +2403,7 @@ var createInitialMessages = (args) => [
1932
2403
  var runReasonToolLoop = async (args) => {
1933
2404
  const { useReasonArgs, id, traceSpan, checkpointKey } = args;
1934
2405
  const ctx = getHookContext();
1935
- const abortSignal = (0, import_core4.combineAbortSignals)(
2406
+ const abortSignal = (0, import_core5.combineAbortSignals)(
1936
2407
  ctx.node.abortSignal,
1937
2408
  useReasonArgs.abortSignal,
1938
2409
  useReasonArgs.model.options?.abortSignal
@@ -1940,43 +2411,32 @@ var runReasonToolLoop = async (args) => {
1940
2411
  const saved = ctx.currentNodeState.byKey[checkpointKey];
1941
2412
  const checkpoint = saved?.status === "tool_loop" ? saved : void 0;
1942
2413
  const opId = checkpoint?.opId ?? args.opId;
1943
- let primaryFailure = false;
1944
- const tools = useReasonArgs.tools ?? [];
2414
+ const tools2 = useReasonArgs.tools ?? [];
1945
2415
  const toolByName = /* @__PURE__ */ new Map();
1946
- let validationCompleted = false;
1947
2416
  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);
2417
+ for (const tool of tools2) {
2418
+ if (toolByName.has(tool.name)) {
2419
+ throw new Error(`useReason received duplicate tool name "${tool.name}".`);
1956
2420
  }
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
- );
1961
- }
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
2421
+ toolByName.set(tool.name, tool);
2422
+ }
2423
+ if (useReasonArgs.interrupt) {
2424
+ throw new Error(
2425
+ "useReason tools cannot be combined with useReason interrupt mode yet. Use toolExecution.approval for tool approval."
1972
2426
  );
1973
- validationCompleted = true;
1974
- } finally {
1975
- if (!validationCompleted) {
1976
- await closeOwnedTools(tools, true, traceSpan);
1977
- }
1978
2427
  }
1979
- const toolDefinitions = toToolDefinitions(tools);
2428
+ separateOutput = Boolean(
2429
+ useReasonArgs.model.provider.getModel(useReasonArgs.model.modelId, {
2430
+ ...useReasonArgs.model.options,
2431
+ ...useReasonArgs.reasoning !== void 0 ? { reasoning: useReasonArgs.reasoning } : {},
2432
+ ...useReasonArgs.responseFormat ? { responseFormat: useReasonArgs.responseFormat } : {},
2433
+ providerOptions: resolveProviderOptions(
2434
+ useReasonArgs.model.options?.providerOptions,
2435
+ useReasonArgs.providerOptions
2436
+ ) ?? {}
2437
+ }).requiresSeparateStructuredOutput
2438
+ );
2439
+ const toolDefinitions = toToolDefinitions(tools2);
1980
2440
  let finalizing = checkpoint?.finalizing ?? false;
1981
2441
  let completed = false;
1982
2442
  const maxSteps = Math.max(1, useReasonArgs.toolExecution?.maxSteps ?? 3);
@@ -2028,7 +2488,7 @@ var runReasonToolLoop = async (args) => {
2028
2488
  };
2029
2489
  try {
2030
2490
  for (let stepIndex = pending ? steps.length - 1 : steps.length; stepIndex < maxSteps; stepIndex += 1) {
2031
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2491
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2032
2492
  traceSpan?.addEvent?.("useReason.tool-step.start", {
2033
2493
  stepIndex,
2034
2494
  toolCount: toolDefinitions.length
@@ -2072,6 +2532,12 @@ var runReasonToolLoop = async (args) => {
2072
2532
  );
2073
2533
  aggregatedWarnings = mergeWarnings(aggregatedWarnings, step.warnings);
2074
2534
  const toolCalls = step.toolCalls ?? [];
2535
+ const ids = /* @__PURE__ */ new Set();
2536
+ for (const call of toolCalls) {
2537
+ if (!call.id || ids.has(call.id) || !reused && allToolCalls.some((prior) => prior.id === call.id))
2538
+ throw new Error("Model returned duplicate or empty tool call IDs.");
2539
+ ids.add(call.id);
2540
+ }
2075
2541
  const toolResults = reused ? steps[stepIndex]?.toolResults ?? [] : [];
2076
2542
  if (!reused)
2077
2543
  steps.push({
@@ -2140,9 +2606,15 @@ var runReasonToolLoop = async (args) => {
2140
2606
  pending = step;
2141
2607
  save();
2142
2608
  for (const toolCall of toolCalls) {
2143
- if (toolResults.some((result2) => result2.toolCallId === toolCall.id))
2609
+ if (toolResults.some((result2) => result2.toolCallId === toolCall.id)) {
2610
+ await replayToolObservations(
2611
+ steps[stepIndex]?.toolObservations?.filter(
2612
+ (observation) => observation.toolCallId === `${opId}:${toolCall.id}`
2613
+ ) ?? []
2614
+ );
2144
2615
  continue;
2145
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2616
+ }
2617
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2146
2618
  const tool = toolByName.get(toolCall.name);
2147
2619
  if (!tool) {
2148
2620
  throw new Error(
@@ -2159,11 +2631,6 @@ var runReasonToolLoop = async (args) => {
2159
2631
  tool.name,
2160
2632
  false
2161
2633
  );
2162
- traceSpan?.addEvent?.("useReason.tool-call.start", {
2163
- stepIndex,
2164
- tool: tool.name,
2165
- toolCallId: toolCall.id
2166
- });
2167
2634
  if (shouldEmitTool) {
2168
2635
  emitToolEvent("tool-call-start", {
2169
2636
  tool: tool.name,
@@ -2172,6 +2639,17 @@ var runReasonToolLoop = async (args) => {
2172
2639
  });
2173
2640
  }
2174
2641
  if (shouldApproveTool && !approvedCalls.includes(toolCall.id)) {
2642
+ await observeToolFact(
2643
+ {
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
+ },
2651
+ "waiting"
2652
+ );
2175
2653
  const approval = await awaitInterruptInternal({
2176
2654
  id: `tool:${toolCall.id}`,
2177
2655
  request: {
@@ -2190,6 +2668,22 @@ var runReasonToolLoop = async (args) => {
2190
2668
  });
2191
2669
  const approved = Array.isArray(approval) ? approval.includes("approve") : approval === "approve";
2192
2670
  if (!approved) {
2671
+ const deniedObservation = {
2672
+ version: 1,
2673
+ name: tool.name,
2674
+ toolCallId: `${opId}:${toolCall.id}`,
2675
+ attemptId: `${opId}:${toolCall.id}:approval`,
2676
+ callingMode: "model",
2677
+ executed: false,
2678
+ outcome: "denied",
2679
+ denialCode: "APPROVAL_DENIED"
2680
+ };
2681
+ await observeToolFact(deniedObservation);
2682
+ const entry = steps[stepIndex];
2683
+ if (entry) {
2684
+ entry.toolObservations ??= [];
2685
+ entry.toolObservations.push(deniedObservation);
2686
+ }
2193
2687
  const denied = {
2194
2688
  toolCallId: toolCall.id,
2195
2689
  name: tool.name,
@@ -2220,13 +2714,24 @@ var runReasonToolLoop = async (args) => {
2220
2714
  save();
2221
2715
  }
2222
2716
  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 } : {}
2717
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2718
+ const rawResult = await executeObservedTool({
2719
+ tool,
2720
+ input: toolCall.input,
2721
+ toolCallId: `${opId}:${toolCall.id}`,
2722
+ providerToolCallId: toolCall.id,
2723
+ callingMode: "model",
2724
+ abortSignal,
2725
+ onObservation: (observation) => {
2726
+ const entry = steps[stepIndex];
2727
+ if (entry) {
2728
+ entry.toolObservations ??= [];
2729
+ entry.toolObservations.push(observation);
2730
+ save();
2731
+ }
2732
+ }
2228
2733
  });
2229
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2734
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2230
2735
  const result2 = normalizeToolResult(toolCall, rawResult);
2231
2736
  toolResults.push(result2);
2232
2737
  allToolResults.push(result2);
@@ -2239,20 +2744,14 @@ var runReasonToolLoop = async (args) => {
2239
2744
  ...result2.isError !== void 0 ? { isError: result2.isError } : {}
2240
2745
  });
2241
2746
  }
2242
- traceSpan?.addEvent?.("useReason.tool-call.complete", {
2243
- stepIndex,
2244
- tool: result2.name,
2245
- toolCallId: result2.toolCallId,
2246
- isError: Boolean(result2.isError)
2247
- });
2248
2747
  } catch (error) {
2249
- (0, import_core4.throwIfExecutionAborted)(abortSignal);
2250
- if ((0, import_errors4.isControlFlowError)(error)) throw error;
2748
+ (0, import_core5.throwIfExecutionAborted)(abortSignal);
2749
+ if ((0, import_errors6.isControlFlowError)(error)) throw error;
2251
2750
  const result2 = {
2252
2751
  toolCallId: toolCall.id,
2253
2752
  name: tool.name,
2254
- content: (0, import_errors4.serializeFailure)(error).message,
2255
- failure: (0, import_errors4.serializeFailure)(error),
2753
+ content: (0, import_errors6.serializeFailure)(error).message,
2754
+ failure: (0, import_errors6.serializeFailure)(error),
2256
2755
  isError: true
2257
2756
  };
2258
2757
  toolResults.push(result2);
@@ -2265,13 +2764,6 @@ var runReasonToolLoop = async (args) => {
2265
2764
  failure: result2.failure
2266
2765
  });
2267
2766
  }
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
2767
  }
2276
2768
  const completed2 = toolResults.at(-1);
2277
2769
  if (completed2)
@@ -2344,7 +2836,6 @@ var runReasonToolLoop = async (args) => {
2344
2836
  ctx.stateDirty = true;
2345
2837
  return result;
2346
2838
  } catch (error) {
2347
- primaryFailure = true;
2348
2839
  try {
2349
2840
  traceSpan?.fail?.(error, {
2350
2841
  attributes: {
@@ -2355,8 +2846,6 @@ var runReasonToolLoop = async (args) => {
2355
2846
  } catch {
2356
2847
  }
2357
2848
  throw error;
2358
- } finally {
2359
- await closeOwnedTools(tools, primaryFailure, traceSpan);
2360
2849
  }
2361
2850
  };
2362
2851
 
@@ -2372,15 +2861,15 @@ var assertReasoningThoughtsCompatibility = (args) => {
2372
2861
  "useReason does not support reasoning.includeThoughts with structured output, interrupt mode, or JSON responseFormat. Disable includeThoughts for this call."
2373
2862
  );
2374
2863
  };
2375
- async function useReason(args) {
2864
+ async function runReason(args) {
2376
2865
  assertReasoningThoughtsCompatibility(args);
2377
2866
  const ctx = getHookContext();
2378
- const abortSignal = (0, import_core5.combineAbortSignals)(
2867
+ const abortSignal = (0, import_core6.combineAbortSignals)(
2379
2868
  ctx.node.abortSignal,
2380
2869
  args.abortSignal,
2381
2870
  args.model.options?.abortSignal
2382
2871
  );
2383
- (0, import_core5.throwIfExecutionAborted)(abortSignal);
2872
+ (0, import_core6.throwIfExecutionAborted)(abortSignal);
2384
2873
  const allowValidatedToolOutput = Boolean(
2385
2874
  args.outputSchema && !(args.responseFormat ?? args.model.options?.responseFormat)
2386
2875
  );
@@ -2403,286 +2892,303 @@ async function useReason(args) {
2403
2892
  ctx.currentNodeState.byKey[checkpointKey]
2404
2893
  );
2405
2894
  if (existingCompleted) {
2895
+ await replayToolObservations(
2896
+ existingCompleted.result.steps?.flatMap(
2897
+ (step) => step.toolObservations ?? []
2898
+ ) ?? []
2899
+ );
2406
2900
  return existingCompleted.result;
2407
2901
  }
2408
2902
  const existingCheckpoint = readReasonCheckpoint(
2409
2903
  ctx.currentNodeState.byKey[checkpointKey]
2410
2904
  );
2411
2905
  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
- } : {}
2906
+ return withSafeTraceSpan(
2907
+ getReasonTraceAdapter(),
2908
+ {
2909
+ name: "useReason",
2910
+ attributes: {
2911
+ ...id ? { id } : {},
2912
+ opId,
2913
+ nodeId: ctx.node.graph.node,
2914
+ providerId: args.model.provider.id,
2915
+ modelId: args.model.modelId,
2916
+ stream: args.stream ?? args.model.options?.streaming ?? true,
2917
+ emit: args.emit ?? true,
2918
+ ...args.telemetry?.operation ? { operation: args.telemetry.operation } : {},
2919
+ ...args.telemetry?.prompt?.name ? { promptName: args.telemetry.prompt.name } : {},
2920
+ ...args.telemetry?.prompt?.version !== void 0 ? { promptVersion: args.telemetry.prompt.version } : {},
2921
+ ...args.telemetry?.prompt?.type ? { promptType: args.telemetry.prompt.type } : {},
2922
+ hasOutputSchema: Boolean(args.outputSchema),
2923
+ hasInterrupt: Boolean(args.interrupt),
2924
+ hasStructured: Boolean(args.structured),
2925
+ hasTools: Boolean(args.tools?.length)
2489
2926
  },
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"
2927
+ telemetry: {
2928
+ ...args.telemetry ?? {},
2929
+ input: args.telemetry?.input ?? args.input
2930
+ }
2931
+ },
2932
+ async (traceSpan) => {
2933
+ if (args.tools?.length) {
2934
+ return runReasonToolLoop({
2935
+ useReasonArgs: args,
2936
+ allowValidatedToolOutput,
2937
+ checkpointKey,
2938
+ initialWarnings: inferred.warnings,
2939
+ ...id ? { id } : {},
2940
+ opId,
2941
+ ...traceSpan ? { traceSpan } : {}
2544
2942
  });
2545
2943
  }
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."
2944
+ const suppressTextStream = Boolean(args.outputSchema || args.interrupt);
2945
+ const setFieldPaths = resolveSetFieldPaths(args.structured);
2946
+ const appendFieldPaths = resolveAppendFieldPaths(args.structured);
2947
+ const textDeltaFieldPaths = resolveTextDeltaFieldPaths(args.structured);
2948
+ const useStructuredIncrementalStreaming = Boolean(
2949
+ args.outputSchema && !args.interrupt && (setFieldPaths.length > 0 || appendFieldPaths.length > 0 || textDeltaFieldPaths.length > 0) && args.stream !== false && shouldStreamReasonStructured(args)
2560
2950
  );
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
2951
+ let firstText = "";
2952
+ let firstRaw;
2953
+ let firstOutput;
2954
+ let firstInterruptRequest;
2955
+ let firstInterruptRequired = false;
2956
+ let finalText = "";
2957
+ let finalRaw;
2958
+ let finalOutput;
2959
+ let aggregatedUsage;
2960
+ let finalFinishReason;
2961
+ let aggregatedProviderMetadata;
2962
+ let aggregatedWarnings = inferred.warnings;
2963
+ if (existingCheckpoint) {
2964
+ firstText = existingCheckpoint.firstText;
2965
+ firstRaw = existingCheckpoint.firstRaw;
2966
+ firstOutput = existingCheckpoint.firstOutput;
2967
+ finalText = firstText;
2968
+ finalRaw = firstRaw;
2969
+ finalOutput = firstOutput;
2970
+ aggregatedUsage = existingCheckpoint.firstUsage;
2971
+ finalFinishReason = existingCheckpoint.firstFinishReason;
2972
+ aggregatedProviderMetadata = existingCheckpoint.firstProviderMetadata;
2973
+ aggregatedWarnings = existingCheckpoint.firstWarnings;
2974
+ traceSpan?.addEvent?.("useReason.resume", {
2975
+ checkpointKey
2976
+ });
2977
+ } else {
2978
+ const first = await reasonEngine(
2979
+ {
2980
+ ...args,
2981
+ emit: suppressTextStream ? false : args.emit,
2982
+ stream: useStructuredIncrementalStreaming ? true : suppressTextStream ? false : args.stream,
2983
+ ...useStructuredIncrementalStreaming ? {
2984
+ onTextChunk: createStructuredOutputStreamer(args, id, opId)
2985
+ } : {}
2986
+ },
2987
+ reasonMeta,
2988
+ args.interrupt ? defaultInterruptFirstPassInput({
2989
+ input: args.input,
2990
+ requestSchema: args.interrupt.requestSchema,
2991
+ mode: args.interrupt.mode ?? "required",
2992
+ ...args.outputSchema ? { outputSchema: args.outputSchema } : {}
2993
+ }) : args.outputSchema ? withOutputGuardrails(
2994
+ withStructuredStreamHints(args.input, {
2995
+ ...setFieldPaths.length > 0 ? { setFieldPaths } : {},
2996
+ ...appendFieldPaths.length > 0 ? { appendFieldPaths } : {},
2997
+ ...textDeltaFieldPaths.length > 0 ? { textDeltaFieldPaths } : {}
2998
+ }),
2999
+ args.outputSchema
3000
+ ) : void 0
3001
+ );
3002
+ firstRaw = first.raw;
3003
+ finalRaw = first.raw;
3004
+ accumulateTokenUsage(first.usage);
3005
+ aggregatedUsage = mergeUsage(aggregatedUsage, first.usage);
3006
+ finalFinishReason = first.finishReason;
3007
+ aggregatedProviderMetadata = mergeProviderMetadata(
3008
+ aggregatedProviderMetadata,
3009
+ first.providerMetadata
3010
+ );
3011
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, first.warnings);
3012
+ traceSpan?.addEvent?.("useReason.first-pass.complete", {
3013
+ textLength: first.text.length,
3014
+ hasInterrupt: Boolean(args.interrupt)
3015
+ });
3016
+ if (args.interrupt) {
3017
+ const firstPass = parseInterruptFirstPassResult({
3018
+ text: first.text,
3019
+ requestSchema: args.interrupt.requestSchema,
3020
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
3021
+ ...first.usage ? { usage: first.usage } : {},
3022
+ ...args.outputSchema ? { outputSchema: args.outputSchema } : {},
3023
+ mode: args.interrupt.mode ?? "required"
3024
+ });
3025
+ firstText = firstPass.draftText;
3026
+ finalText = firstPass.draftText;
3027
+ firstInterruptRequired = firstPass.interruptRequired;
3028
+ firstInterruptRequest = firstPass.request;
3029
+ firstOutput = firstPass.output;
3030
+ finalOutput = firstPass.output;
3031
+ } else {
3032
+ firstText = first.text;
3033
+ finalText = first.text;
3034
+ if (args.outputSchema) {
3035
+ firstOutput = parseReasonOutputWithSchema({
3036
+ text: first.text,
3037
+ schema: args.outputSchema,
3038
+ ...first.finishReason ? { finishReason: first.finishReason } : {},
3039
+ ...first.usage ? { usage: first.usage } : {},
3040
+ label: "useReason output"
3041
+ });
3042
+ }
3043
+ finalOutput = firstOutput;
3044
+ }
2590
3045
  }
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({
3046
+ let interruptResponse;
3047
+ if (args.interrupt && (existingCheckpoint || firstInterruptRequired)) {
3048
+ const requestSchema = args.interrupt.requestSchema;
3049
+ const interruptRequest = existingCheckpoint ? parseWithSchema(
3050
+ requestSchema,
3051
+ existingCheckpoint.request,
3052
+ "useReason interrupt.request"
3053
+ ) : firstInterruptRequest;
3054
+ if (!interruptRequest) {
3055
+ throw new Error(
3056
+ "useReason interrupt request is missing; first pass did not produce a valid interrupt payload."
3057
+ );
3058
+ }
3059
+ if (!existingCheckpoint) {
3060
+ ctx.currentNodeState.byKey[checkpointKey] = {
3061
+ status: "awaiting_interrupt",
3062
+ request: interruptRequest,
3063
+ firstText,
3064
+ ...firstRaw !== void 0 ? { firstRaw } : {},
3065
+ ...aggregatedUsage !== void 0 ? { firstUsage: aggregatedUsage } : {},
3066
+ ...finalFinishReason !== void 0 ? { firstFinishReason: finalFinishReason } : {},
3067
+ ...aggregatedProviderMetadata !== void 0 ? { firstProviderMetadata: aggregatedProviderMetadata } : {},
3068
+ ...aggregatedWarnings !== void 0 ? { firstWarnings: aggregatedWarnings } : {},
3069
+ ...firstOutput !== void 0 ? { firstOutput } : {}
3070
+ };
3071
+ ctx.stateDirty = true;
3072
+ }
3073
+ const resumeStatePatch = resolveHookStatePatch({
3074
+ nodeId: ctx.node.graph.node,
3075
+ currentNodeState: ctx.currentNodeState,
3076
+ workflowState: ctx.workflowState
3077
+ });
3078
+ interruptResponse = await awaitInterruptInternal({
3079
+ request: interruptRequest,
3080
+ requestSchema,
3081
+ responseSchema: args.interrupt.responseSchema,
3082
+ ...args.interrupt.schemaId ? { schemaId: args.interrupt.schemaId } : {},
3083
+ ...args.interrupt.schemaVersion ? { schemaVersion: args.interrupt.schemaVersion } : {},
3084
+ ...id ? { id } : {},
3085
+ meta: {
3086
+ __kortyxResumeStatePatch: resumeStatePatch
3087
+ }
3088
+ });
3089
+ traceSpan?.addEvent?.("useReason.interrupt.resolved", {
3090
+ checkpointKey
3091
+ });
3092
+ if (Object.hasOwn(ctx.currentNodeState.byKey, checkpointKey)) {
3093
+ delete ctx.currentNodeState.byKey[checkpointKey];
3094
+ ctx.stateDirty = true;
3095
+ }
3096
+ const continuationInput = defaultContinuationInput({
3097
+ input: args.input,
3098
+ draftText: firstText,
3099
+ ...firstOutput !== void 0 ? { draftOutput: firstOutput } : {},
3100
+ request: interruptRequest,
3101
+ response: interruptResponse
3102
+ });
3103
+ const second = await reasonEngine(
3104
+ {
3105
+ ...args,
3106
+ ...inferred.responseFormat ? { responseFormat: inferred.responseFormat } : {},
3107
+ emit: suppressTextStream ? false : args.emit,
3108
+ stream: suppressTextStream ? false : args.stream
3109
+ },
3110
+ reasonMeta,
3111
+ args.outputSchema ? withOutputGuardrails(
3112
+ continuationInput,
3113
+ args.outputSchema
3114
+ ) : continuationInput
3115
+ );
3116
+ finalText = second.text;
3117
+ finalRaw = second.raw;
3118
+ accumulateTokenUsage(second.usage);
3119
+ aggregatedUsage = mergeUsage(aggregatedUsage, second.usage);
3120
+ finalFinishReason = second.finishReason;
3121
+ aggregatedProviderMetadata = mergeProviderMetadata(
3122
+ aggregatedProviderMetadata,
3123
+ second.providerMetadata
3124
+ );
3125
+ aggregatedWarnings = mergeWarnings(aggregatedWarnings, second.warnings);
3126
+ traceSpan?.addEvent?.("useReason.continuation.complete", {
3127
+ textLength: second.text.length
3128
+ });
3129
+ if (args.outputSchema) {
3130
+ finalOutput = parseReasonOutputWithSchema({
3131
+ text: finalText,
3132
+ schema: args.outputSchema,
3133
+ ...second.finishReason ? { finishReason: second.finishReason } : {},
3134
+ ...aggregatedUsage ? { usage: aggregatedUsage } : {},
3135
+ label: "useReason output"
3136
+ });
3137
+ }
3138
+ }
3139
+ if (finalOutput !== void 0) {
3140
+ emitReasonStructuredOutput({
3141
+ ...id ? { id } : {},
3142
+ opId,
3143
+ output: finalOutput,
3144
+ structured: args.structured,
3145
+ emit: args.emit ?? true
3146
+ });
3147
+ }
3148
+ const result = {
3149
+ ...id ? { id } : {},
3150
+ opId,
2634
3151
  text: finalText,
2635
- schema: args.outputSchema,
2636
- ...second.finishReason ? { finishReason: second.finishReason } : {},
2637
- ...aggregatedUsage ? { usage: aggregatedUsage } : {},
2638
- label: "useReason output"
3152
+ ...finalRaw !== void 0 ? { raw: finalRaw } : {},
3153
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3154
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
3155
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
3156
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
3157
+ ...finalOutput !== void 0 ? { output: finalOutput } : {},
3158
+ ...interruptResponse !== void 0 ? { interruptResponse } : {}
3159
+ };
3160
+ if (args.interrupt) {
3161
+ ctx.currentNodeState.byKey[checkpointKey] = {
3162
+ status: "completed",
3163
+ result
3164
+ };
3165
+ ctx.stateDirty = true;
3166
+ }
3167
+ traceSpan?.end?.({
3168
+ ...aggregatedUsage !== void 0 ? { usage: aggregatedUsage } : {},
3169
+ ...finalFinishReason !== void 0 ? { finishReason: finalFinishReason } : {},
3170
+ ...aggregatedProviderMetadata !== void 0 ? { providerMetadata: aggregatedProviderMetadata } : {},
3171
+ ...aggregatedWarnings !== void 0 ? { warnings: aggregatedWarnings } : {},
3172
+ attributes: {
3173
+ textLength: finalText.length,
3174
+ resumedFromCheckpoint: Boolean(existingCheckpoint),
3175
+ interrupted: Boolean(interruptResponse !== void 0)
3176
+ },
3177
+ telemetry: {
3178
+ ...args.telemetry ?? {},
3179
+ output: args.telemetry?.output ?? finalOutput ?? finalText
3180
+ }
2639
3181
  });
3182
+ return result;
2640
3183
  }
3184
+ );
3185
+ }
3186
+ async function useReason(args) {
3187
+ try {
3188
+ return await runReason(args);
3189
+ } finally {
3190
+ await closeOwnedTools(args.tools ?? []);
2641
3191
  }
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
3192
  }
2687
3193
 
2688
3194
  // src/hooks.ts
@@ -2741,8 +3247,8 @@ function useWorkflowState(key, ...rest) {
2741
3247
  }
2742
3248
 
2743
3249
  // src/parallel.ts
2744
- var import_core6 = require("@kortyx/core");
2745
- var import_errors5 = require("@kortyx/core/errors");
3250
+ var import_core7 = require("@kortyx/core");
3251
+ var import_errors7 = require("@kortyx/core/errors");
2746
3252
  var ParallelChildWaiting = class extends Error {
2747
3253
  name = "ParallelChildWaiting";
2748
3254
  };
@@ -2756,14 +3262,14 @@ var ParallelError = class extends AggregateError {
2756
3262
  );
2757
3263
  this.results = results;
2758
3264
  this.name = "ParallelError";
2759
- this.failure = (0, import_errors5.serializeFailure)({
3265
+ this.failure = (0, import_errors7.serializeFailure)({
2760
3266
  version: 1,
2761
3267
  code: this.code,
2762
3268
  category: "workflow",
2763
3269
  message: this.message,
2764
3270
  retryable: null,
2765
3271
  results: results.map(
2766
- (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors5.serializeFailure)(result.reason) }
3272
+ (result) => result.status === "fulfilled" ? { status: "fulfilled" } : { status: "rejected", failure: (0, import_errors7.serializeFailure)(result.reason) }
2767
3273
  )
2768
3274
  });
2769
3275
  }
@@ -2771,7 +3277,7 @@ var ParallelError = class extends AggregateError {
2771
3277
  code = "PARALLEL_FAILED";
2772
3278
  failure;
2773
3279
  toJSON() {
2774
- return (0, import_errors5.serializeFailure)(this);
3280
+ return (0, import_errors7.serializeFailure)(this);
2775
3281
  }
2776
3282
  };
2777
3283
  function registerWorkflowTask(id) {
@@ -2867,13 +3373,13 @@ function parallel(values) {
2867
3373
  );
2868
3374
  }
2869
3375
  if (ready[0]?.status === "rejected") throw ready[0].reason;
2870
- (0, import_core6.throwIfExecutionAborted)(ctx.node.abortSignal);
3376
+ (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
2871
3377
  if (ctx.workflowControlError) throw ctx.workflowControlError;
2872
3378
  const rejected = outcomes.filter(
2873
3379
  (outcome) => outcome.status === "rejected"
2874
3380
  );
2875
3381
  const control = rejected.find(
2876
- (outcome) => (0, import_core6.isExecutionCancelled)(outcome.reason) || (0, import_core6.isExecutionLimitReached)(outcome.reason)
3382
+ (outcome) => (0, import_core7.isExecutionCancelled)(outcome.reason) || (0, import_core7.isExecutionLimitReached)(outcome.reason)
2877
3383
  );
2878
3384
  if (control) {
2879
3385
  ctx.workflowControlError = control.reason;
@@ -2909,55 +3415,22 @@ function parallel(values) {
2909
3415
  return promise;
2910
3416
  }
2911
3417
 
2912
- // 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;
3418
+ // src/report-error.ts
3419
+ function reportError(error, options = {}) {
2923
3420
  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
- });
3421
+ getReasonTraceAdapter()?.reportError?.(error, options);
2945
3422
  } catch {
2946
3423
  }
2947
3424
  }
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
3425
 
2956
3426
  // src/workflow.ts
2957
- var WorkflowCallError = class extends import_errors6.KortyxError {
3427
+ var import_node_crypto6 = require("crypto");
3428
+ var import_core8 = require("@kortyx/core");
3429
+ var import_errors8 = require("@kortyx/core/errors");
3430
+ var WorkflowCallError = class extends import_errors8.KortyxError {
2958
3431
  constructor(workflow, message, cause, failure) {
2959
- const detail = (0, import_errors6.serializeFailure)(
2960
- failure ?? cause ?? new import_errors6.WorkflowContractError(message)
3432
+ const detail = (0, import_errors8.serializeFailure)(
3433
+ failure ?? cause ?? new import_errors8.WorkflowContractError(message)
2961
3434
  );
2962
3435
  super(detail.code, `Workflow '${workflow}': ${message}`, {
2963
3436
  ...detail,
@@ -2978,12 +3451,12 @@ function workflowCallFingerprint(value) {
2978
3451
  if (v === null || typeof v === "string" || typeof v === "boolean") return v;
2979
3452
  if (typeof v === "number" && Number.isFinite(v)) return v;
2980
3453
  if (typeof v !== "object" || !v || seen.has(v)) {
2981
- throw new import_errors6.WorkflowContractError(
3454
+ throw new import_errors8.WorkflowContractError(
2982
3455
  "Workflow input and output must be JSON serializable."
2983
3456
  );
2984
3457
  }
2985
3458
  if (!Array.isArray(v) && Object.getPrototypeOf(v) !== Object.prototype) {
2986
- throw new import_errors6.WorkflowContractError(
3459
+ throw new import_errors8.WorkflowContractError(
2987
3460
  "Workflow input and output must contain plain JSON objects."
2988
3461
  );
2989
3462
  }
@@ -3004,7 +3477,7 @@ function createWorkflowHooks(workflows) {
3004
3477
  useWorkflow: (args) => {
3005
3478
  const workflow = workflows[args.workflow];
3006
3479
  if (!workflow || workflow.id !== args.workflow)
3007
- throw new import_errors6.WorkflowContractError(
3480
+ throw new import_errors8.WorkflowContractError(
3008
3481
  "Workflow registry keys must match definition ids."
3009
3482
  );
3010
3483
  return useWorkflow({ ...args, workflow });
@@ -3018,7 +3491,7 @@ async function useWorkflow(args) {
3018
3491
  if (task.group) await task.group.ready;
3019
3492
  return await runWorkflowCall(args, task);
3020
3493
  } catch (error) {
3021
- if ((0, import_core7.isExecutionCancelled)(error) || (0, import_core7.isExecutionLimitReached)(error))
3494
+ if ((0, import_core8.isExecutionCancelled)(error) || (0, import_core8.isExecutionLimitReached)(error))
3022
3495
  getHookContext().workflowControlError = error;
3023
3496
  throw error;
3024
3497
  } finally {
@@ -3027,26 +3500,26 @@ async function useWorkflow(args) {
3027
3500
  }
3028
3501
  async function runWorkflowCall(args, task) {
3029
3502
  const ctx = getHookContext();
3030
- (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3503
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3031
3504
  const service = ctx.node.callWorkflow;
3032
3505
  if (!service)
3033
- throw new import_errors6.WorkflowContractError(
3506
+ throw new import_errors8.WorkflowContractError(
3034
3507
  "useWorkflow requires an agent workflow registry."
3035
3508
  );
3036
3509
  if (!args.id.trim())
3037
- throw new import_errors6.WorkflowContractError(
3510
+ throw new import_errors8.WorkflowContractError(
3038
3511
  "useWorkflow requires a stable, nonempty id."
3039
3512
  );
3040
3513
  if (ctx.workflowCallActive && !task.group)
3041
- throw new import_errors6.WorkflowContractError(
3514
+ throw new import_errors8.WorkflowContractError(
3042
3515
  "Concurrent workflow calls require parallel([...]); otherwise await each call."
3043
3516
  );
3044
3517
  if (ctx.workflowCallIds.has(args.id))
3045
- throw new import_errors6.WorkflowContractError(
3518
+ throw new import_errors8.WorkflowContractError(
3046
3519
  `Duplicate workflow call id '${args.id}' in one node activation.`
3047
3520
  );
3048
3521
  if (ctx.workflowCallIds.size >= 64)
3049
- throw new import_errors6.WorkflowContractError(
3522
+ throw new import_errors8.WorkflowContractError(
3050
3523
  "A node may call at most 64 child workflows per activation."
3051
3524
  );
3052
3525
  ctx.workflowCallIds.add(args.id);
@@ -3073,7 +3546,7 @@ async function runWorkflowCall(args, task) {
3073
3546
  if (!record) ctx.node.consumeExecution?.("maxChildInvocations");
3074
3547
  record ??= {
3075
3548
  fingerprint,
3076
- invocationId: (0, import_node_crypto5.randomUUID)(),
3549
+ invocationId: (0, import_node_crypto6.randomUUID)(),
3077
3550
  status: "running",
3078
3551
  interrupts: [],
3079
3552
  ...task.group ? { parallelKey: task.group.key } : {}
@@ -3083,7 +3556,7 @@ async function runWorkflowCall(args, task) {
3083
3556
  if (!task.group) ctx.workflowCallActive = true;
3084
3557
  const telemetry = ctx.node.workflowCallTelemetry;
3085
3558
  const activationKey = "__useWorkflowActivation";
3086
- ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto5.randomUUID)();
3559
+ ctx.currentNodeState.byKey[activationKey] ??= (0, import_node_crypto6.randomUUID)();
3087
3560
  const callerNodeExecutionId = ctx.currentNodeState.byKey[activationKey];
3088
3561
  const metadata = {
3089
3562
  invocationId: record.invocationId,
@@ -3127,7 +3600,7 @@ async function runWorkflowCall(args, task) {
3127
3600
  workflow,
3128
3601
  record.error ?? "child failed",
3129
3602
  void 0,
3130
- record.failure ?? (0, import_errors6.serializeFailure)(void 0)
3603
+ record.failure ?? (0, import_errors8.serializeFailure)(void 0)
3131
3604
  );
3132
3605
  if (task.group && record.status === "interrupted" && record.interrupts.at(-1)?.response === void 0)
3133
3606
  throw new ParallelChildWaiting();
@@ -3151,9 +3624,9 @@ async function runWorkflowCall(args, task) {
3151
3624
  } catch (error) {
3152
3625
  const child = error;
3153
3626
  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)) {
3627
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3628
+ if ((0, import_core8.isExecutionCancelled)(error)) throw error;
3629
+ if ((0, import_core8.isExecutionLimitReached)(error)) {
3157
3630
  if (child?.__kortyxChildSnapshot)
3158
3631
  record.snapshot = child.__kortyxChildSnapshot;
3159
3632
  report("suspended", {
@@ -3163,7 +3636,7 @@ async function runWorkflowCall(args, task) {
3163
3636
  throw error;
3164
3637
  }
3165
3638
  record.status = "failed";
3166
- record.failure = (0, import_errors6.serializeFailure)(error);
3639
+ record.failure = (0, import_errors8.serializeFailure)(error);
3167
3640
  record.error = record.failure.message;
3168
3641
  report("failed", { error: "Child workflow failed" });
3169
3642
  throw new WorkflowCallError(
@@ -3173,12 +3646,15 @@ async function runWorkflowCall(args, task) {
3173
3646
  record.failure
3174
3647
  );
3175
3648
  }
3176
- (0, import_core7.throwIfExecutionAborted)(ctx.node.abortSignal);
3649
+ (0, import_core8.throwIfExecutionAborted)(ctx.node.abortSignal);
3177
3650
  recordUsage(outcome.usage);
3178
3651
  if (outcome.status === "completed") {
3179
3652
  record.data = outcome.data;
3180
3653
  workflowCallFingerprint(record.data);
3181
3654
  record.status = "completed";
3655
+ record.toolObservations = outcome.toolObservations ?? [];
3656
+ for (const observation of record.toolObservations)
3657
+ rememberToolObservation(observation);
3182
3658
  delete record.snapshot;
3183
3659
  report(
3184
3660
  "completed",
@@ -3201,8 +3677,12 @@ async function runWorkflowCall(args, task) {
3201
3677
  });
3202
3678
  }
3203
3679
  }
3204
- if (cached)
3680
+ if (cached) {
3205
3681
  report("reused", workflowCallContent(telemetry, "output", record.data));
3682
+ if (record.restoredToolBranch !== telemetry?.correlation?.branchId)
3683
+ await replayToolObservations(record.toolObservations ?? [], true);
3684
+ delete record.restoredToolBranch;
3685
+ }
3206
3686
  const data = record.data ?? {};
3207
3687
  return {
3208
3688
  data: JSON.parse(JSON.stringify(data))
@@ -3219,6 +3699,7 @@ async function runWorkflowCall(args, task) {
3219
3699
  createWorkflowHooks,
3220
3700
  emitWorkflowCall,
3221
3701
  parallel,
3702
+ reportError,
3222
3703
  runWithHookContext,
3223
3704
  useAbortSignal,
3224
3705
  useInterrupt,
@@ -3226,6 +3707,7 @@ async function runWorkflowCall(args, task) {
3226
3707
  useReason,
3227
3708
  useRuntimeContext,
3228
3709
  useStructuredData,
3710
+ useTool,
3229
3711
  useWorkflow,
3230
3712
  useWorkflowState,
3231
3713
  workflowCallFingerprint