@librechat/agents 3.4.0 → 3.4.2

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.
Files changed (204) hide show
  1. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  2. package/dist/cjs/graphs/Graph.cjs +165 -57
  3. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  4. package/dist/cjs/graphs/MultiAgentGraph.cjs +81 -33
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  6. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs +14 -0
  7. package/dist/cjs/graphs/applyGraphRuntimeConfig.cjs.map +1 -0
  8. package/dist/cjs/graphs/createGraph.cjs +15 -0
  9. package/dist/cjs/graphs/createGraph.cjs.map +1 -0
  10. package/dist/cjs/graphs/index.cjs +1 -0
  11. package/dist/cjs/hooks/HookRegistry.cjs +83 -0
  12. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  13. package/dist/cjs/hooks/createWorkspacePolicyHook.cjs +1 -1
  14. package/dist/cjs/hooks/executeHooks.cjs +60 -14
  15. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  16. package/dist/cjs/hooks/index.cjs.map +1 -1
  17. package/dist/cjs/hooks/types.cjs +2 -0
  18. package/dist/cjs/hooks/types.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +3 -3
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/main.cjs +34 -24
  22. package/dist/cjs/messages/format.cjs +40 -23
  23. package/dist/cjs/messages/format.cjs.map +1 -1
  24. package/dist/cjs/run.cjs +130 -53
  25. package/dist/cjs/run.cjs.map +1 -1
  26. package/dist/cjs/session/AgentSession.cjs +37 -2
  27. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  28. package/dist/cjs/session/JsonlSessionStore.cjs +2 -2
  29. package/dist/cjs/stream.cjs +1 -1
  30. package/dist/cjs/summarization/node.cjs +6 -3
  31. package/dist/cjs/summarization/node.cjs.map +1 -1
  32. package/dist/cjs/tools/BashExecutor.cjs +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +1 -1
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +1 -1
  35. package/dist/cjs/tools/SubagentTool.cjs +4 -4
  36. package/dist/cjs/tools/SubagentTool.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +321 -95
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolSearch.cjs +1 -1
  40. package/dist/cjs/tools/eagerEventExecution.cjs +1 -0
  41. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +3 -3
  42. package/dist/cjs/tools/local/LocalProgrammaticToolCalling.cjs +2 -2
  43. package/dist/cjs/tools/search/rerankers.cjs +154 -1
  44. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  45. package/dist/cjs/tools/search/tool.cjs +4 -1
  46. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  47. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs +601 -0
  48. package/dist/cjs/tools/subagent/SubagentExecutionRegistry.cjs.map +1 -0
  49. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1114 -233
  50. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  51. package/dist/cjs/tools/subagent/SubagentReplay.cjs +177 -0
  52. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -0
  53. package/dist/cjs/tools/subagent/childGraphConfig.cjs +278 -0
  54. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -0
  55. package/dist/cjs/tools/subagent/index.cjs +1 -0
  56. package/dist/cjs/tools/subagent/runtimeLimits.cjs +5 -0
  57. package/dist/cjs/tools/subagent/runtimeLimits.cjs.map +1 -0
  58. package/dist/cjs/tools/toolOutputReferences.cjs +20 -0
  59. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  60. package/dist/cjs/utils/index.cjs +2 -2
  61. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  62. package/dist/esm/graphs/Graph.mjs +165 -57
  63. package/dist/esm/graphs/Graph.mjs.map +1 -1
  64. package/dist/esm/graphs/MultiAgentGraph.mjs +81 -33
  65. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  66. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs +14 -0
  67. package/dist/esm/graphs/applyGraphRuntimeConfig.mjs.map +1 -0
  68. package/dist/esm/graphs/createGraph.mjs +15 -0
  69. package/dist/esm/graphs/createGraph.mjs.map +1 -0
  70. package/dist/esm/graphs/index.mjs +1 -0
  71. package/dist/esm/hooks/HookRegistry.mjs +83 -0
  72. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  73. package/dist/esm/hooks/createWorkspacePolicyHook.mjs +1 -1
  74. package/dist/esm/hooks/executeHooks.mjs +60 -14
  75. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  76. package/dist/esm/hooks/index.mjs.map +1 -1
  77. package/dist/esm/hooks/types.mjs +2 -1
  78. package/dist/esm/hooks/types.mjs.map +1 -1
  79. package/dist/esm/llm/init.mjs +1 -1
  80. package/dist/esm/llm/invoke.mjs +2 -2
  81. package/dist/esm/main.mjs +21 -19
  82. package/dist/esm/messages/format.mjs +40 -23
  83. package/dist/esm/messages/format.mjs.map +1 -1
  84. package/dist/esm/run.mjs +131 -54
  85. package/dist/esm/run.mjs.map +1 -1
  86. package/dist/esm/session/AgentSession.mjs +37 -2
  87. package/dist/esm/session/AgentSession.mjs.map +1 -1
  88. package/dist/esm/session/JsonlSessionStore.mjs +2 -2
  89. package/dist/esm/stream.mjs +1 -1
  90. package/dist/esm/summarization/node.mjs +6 -3
  91. package/dist/esm/summarization/node.mjs.map +1 -1
  92. package/dist/esm/tools/BashExecutor.mjs +1 -1
  93. package/dist/esm/tools/CodeExecutor.mjs +1 -1
  94. package/dist/esm/tools/ProgrammaticToolCalling.mjs +1 -1
  95. package/dist/esm/tools/SubagentTool.mjs +4 -4
  96. package/dist/esm/tools/SubagentTool.mjs.map +1 -1
  97. package/dist/esm/tools/ToolNode.mjs +322 -96
  98. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  99. package/dist/esm/tools/ToolSearch.mjs +1 -1
  100. package/dist/esm/tools/eagerEventExecution.mjs +1 -1
  101. package/dist/esm/tools/local/LocalExecutionEngine.mjs +3 -3
  102. package/dist/esm/tools/local/LocalProgrammaticToolCalling.mjs +2 -2
  103. package/dist/esm/tools/search/rerankers.mjs +154 -1
  104. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  105. package/dist/esm/tools/search/tool.mjs +4 -1
  106. package/dist/esm/tools/search/tool.mjs.map +1 -1
  107. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs +597 -0
  108. package/dist/esm/tools/subagent/SubagentExecutionRegistry.mjs.map +1 -0
  109. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1114 -233
  110. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  111. package/dist/esm/tools/subagent/SubagentReplay.mjs +170 -0
  112. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -0
  113. package/dist/esm/tools/subagent/childGraphConfig.mjs +272 -0
  114. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -0
  115. package/dist/esm/tools/subagent/index.mjs +1 -0
  116. package/dist/esm/tools/subagent/runtimeLimits.mjs +5 -0
  117. package/dist/esm/tools/subagent/runtimeLimits.mjs.map +1 -0
  118. package/dist/esm/tools/toolOutputReferences.mjs +20 -0
  119. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  120. package/dist/esm/utils/index.mjs +2 -2
  121. package/dist/types/agents/AgentContext.d.ts +1 -1
  122. package/dist/types/graphs/Graph.d.ts +22 -2
  123. package/dist/types/graphs/MultiAgentGraph.d.ts +7 -1
  124. package/dist/types/graphs/applyGraphRuntimeConfig.d.ts +3 -0
  125. package/dist/types/graphs/createGraph.d.ts +13 -0
  126. package/dist/types/graphs/graphFactory.d.ts +13 -0
  127. package/dist/types/graphs/index.d.ts +2 -0
  128. package/dist/types/hooks/HookRegistry.d.ts +10 -1
  129. package/dist/types/hooks/executeHooks.d.ts +5 -1
  130. package/dist/types/hooks/index.d.ts +2 -2
  131. package/dist/types/hooks/types.d.ts +12 -0
  132. package/dist/types/run.d.ts +6 -3
  133. package/dist/types/session/AgentSession.d.ts +1 -0
  134. package/dist/types/tools/SubagentTool.d.ts +1 -1
  135. package/dist/types/tools/ToolNode.d.ts +23 -15
  136. package/dist/types/tools/search/rerankers.d.ts +28 -0
  137. package/dist/types/tools/search/types.d.ts +38 -1
  138. package/dist/types/tools/subagent/SubagentExecutionRegistry.d.ts +171 -0
  139. package/dist/types/tools/subagent/SubagentExecutor.d.ts +70 -36
  140. package/dist/types/tools/subagent/SubagentReplay.d.ts +85 -0
  141. package/dist/types/tools/subagent/childGraphConfig.d.ts +37 -0
  142. package/dist/types/tools/subagent/index.d.ts +1 -1
  143. package/dist/types/tools/subagent/runtimeLimits.d.ts +4 -0
  144. package/dist/types/tools/toolOutputReferences.d.ts +12 -0
  145. package/dist/types/types/graph.d.ts +189 -24
  146. package/dist/types/types/hitl.d.ts +15 -0
  147. package/package.json +8 -2
  148. package/src/agents/AgentContext.ts +1 -1
  149. package/src/graphs/Graph.ts +349 -104
  150. package/src/graphs/MultiAgentGraph.ts +149 -47
  151. package/src/graphs/__tests__/Graph.breakerLifecycle.test.ts +48 -9
  152. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1 -1
  153. package/src/graphs/__tests__/Graph.subagentResumeState.test.ts +80 -0
  154. package/src/graphs/__tests__/MultiAgentGraph.test.ts +86 -0
  155. package/src/graphs/__tests__/composition.smoke.test.ts +152 -0
  156. package/src/graphs/__tests__/createGraph.test.ts +206 -0
  157. package/src/graphs/applyGraphRuntimeConfig.ts +25 -0
  158. package/src/graphs/createGraph.ts +32 -0
  159. package/src/graphs/graphFactory.ts +12 -0
  160. package/src/graphs/index.ts +2 -0
  161. package/src/hooks/HookRegistry.ts +151 -1
  162. package/src/hooks/__tests__/HookRegistry.test.ts +96 -0
  163. package/src/hooks/__tests__/executeHooks.test.ts +160 -0
  164. package/src/hooks/executeHooks.ts +133 -15
  165. package/src/hooks/index.ts +3 -1
  166. package/src/hooks/types.ts +16 -0
  167. package/src/messages/format.ts +54 -26
  168. package/src/messages/formatAgentMessages.reducer.test.ts +162 -0
  169. package/src/messages/formatAgentMessages.steer.test.ts +16 -11
  170. package/src/messages/formatAgentMessages.test.ts +7 -2
  171. package/src/run.ts +214 -56
  172. package/src/session/AgentSession.ts +52 -2
  173. package/src/session/__tests__/JsonlSessionStore.test.ts +53 -0
  174. package/src/specs/graph-subagent.live.test.ts +242 -0
  175. package/src/specs/graph-subagent.test.ts +322 -0
  176. package/src/specs/langfuse-routing.integration.test.ts +120 -1
  177. package/src/specs/subagent.test.ts +130 -4
  178. package/src/summarization/__tests__/node.test.ts +32 -14
  179. package/src/summarization/node.ts +15 -11
  180. package/src/tools/SubagentTool.ts +4 -4
  181. package/src/tools/ToolNode.ts +643 -157
  182. package/src/tools/__tests__/SubagentExecutor.test.ts +740 -15
  183. package/src/tools/__tests__/SubagentReplay.test.ts +329 -0
  184. package/src/tools/__tests__/ToolNode.breakerSignal.test.ts +208 -2
  185. package/src/tools/__tests__/directToolHITLResumeScope.test.ts +512 -2
  186. package/src/tools/__tests__/hitl.test.ts +234 -5
  187. package/src/tools/__tests__/subagentHooks.test.ts +1126 -61
  188. package/src/tools/__tests__/toolOutputReferences.test.ts +19 -2
  189. package/src/tools/search/rag-api-reranker.test.ts +891 -0
  190. package/src/tools/search/rerankers.ts +260 -0
  191. package/src/tools/search/tool.ts +6 -0
  192. package/src/tools/search/types.ts +45 -1
  193. package/src/tools/subagent/SubagentExecutionRegistry.ts +1159 -0
  194. package/src/tools/subagent/SubagentExecutor.ts +2232 -312
  195. package/src/tools/subagent/SubagentReplay.ts +584 -0
  196. package/src/tools/subagent/__tests__/SubagentExecutionRegistry.test.ts +637 -0
  197. package/src/tools/subagent/__tests__/SubagentExecutor.lazy.test.ts +3054 -0
  198. package/src/tools/subagent/__tests__/childGraphConfig.test.ts +521 -0
  199. package/src/tools/subagent/childGraphConfig.ts +694 -0
  200. package/src/tools/subagent/index.ts +6 -0
  201. package/src/tools/subagent/runtimeLimits.ts +7 -0
  202. package/src/tools/toolOutputReferences.ts +40 -1
  203. package/src/types/graph.ts +222 -24
  204. package/src/types/hitl.ts +16 -0
@@ -1 +1 @@
1
- {"version":3,"file":"toolOutputReferences.mjs","names":[],"sources":["../../../src/tools/toolOutputReferences.ts"],"sourcesContent":["/**\n * Tool output reference registry.\n *\n * When enabled via `RunConfig.toolOutputReferences.enabled`, ToolNode\n * stores each successful tool output under a stable key\n * (`tool<idx>turn<turn>`) where `idx` is the tool's position within a\n * ToolNode batch and `turn` is the batch index within the run\n * (incremented once per ToolNode invocation).\n *\n * Subsequent tool calls can pipe a previous output into their args by\n * embedding `{{tool<idx>turn<turn>}}` inside any string argument;\n * {@link ToolOutputReferenceRegistry.resolve} walks the args and\n * substitutes the placeholders immediately before invocation.\n *\n * The registry stores the *raw, untruncated* tool output so a later\n * `{{…}}` substitution pipes the full payload into the next tool —\n * even when the LLM only saw a head+tail-truncated preview in\n * `ToolMessage.content`. Outputs are stored without any annotation\n * (the `_ref` key or the `[ref: ...]` prefix seen by the LLM is\n * strictly a UX signal attached to `ToolMessage.content`). Keeping the\n * registry pristine means downstream bash/jq piping receives the\n * complete, verbatim output with no injected fields.\n */\n\nimport { ToolMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport {\n calculateMaxTotalToolOutputSize,\n HARD_MAX_TOOL_RESULT_CHARS,\n HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,\n} from '@/utils/truncation';\nimport { isComputerCallOutputMessage } from '@/utils/toolContent';\nimport { INTENT_ARG } from '@/tools/intentArg';\n\n/** Parses a stringified JSON object arg; undefined for anything else. */\nfunction parseStringifiedArgsObject(\n value: string\n): Record<string, unknown> | undefined {\n if (!value.trim().startsWith('{')) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(value) as unknown;\n if (parsed != null && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\n/**\n * Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.\n * Exported for consumers that want to detect references (e.g., syntax\n * highlighting, docs). The stateful `g` variant lives inside the\n * registry so nobody trips on `lastIndex`.\n */\nexport const TOOL_OUTPUT_REF_PATTERN = /\\{\\{(tool\\d+turn\\d+)\\}\\}/;\n\n/** Object key used when a parsed-object output has `_ref` injected. */\nexport const TOOL_OUTPUT_REF_KEY = '_ref';\n\n/**\n * Object key used to carry unresolved reference warnings on a parsed-\n * object output. Using a dedicated field instead of a trailing text\n * line keeps the annotated `ToolMessage.content` parseable as JSON for\n * downstream consumers that rely on the object shape.\n */\nexport const TOOL_OUTPUT_UNRESOLVED_KEY = '_unresolved_refs';\n\n/** Single-line prefix prepended to non-object tool outputs so the LLM sees the reference key. */\nexport function buildReferencePrefix(key: string): string {\n return `[ref: ${key}]`;\n}\n\n/** Stable registry key for a tool output. */\nexport function buildReferenceKey(toolIndex: number, turn: number): string {\n return `tool${toolIndex}turn${turn}`;\n}\n\nexport type ToolOutputReferenceRegistryOptions = {\n /** Maximum characters stored per registered output. */\n maxOutputSize?: number;\n /** Maximum total characters retained across all registered outputs. */\n maxTotalSize?: number;\n /**\n * Upper bound on the number of concurrently-tracked runs. When\n * exceeded, the oldest run bucket is evicted (FIFO). Defaults to 32.\n */\n maxActiveRuns?: number;\n};\n\n/**\n * Result of resolving placeholders in tool args.\n */\nexport type ResolveResult<T> = {\n /** Arguments with placeholders replaced. Same shape as the input. */\n resolved: T;\n /** Reference keys that were referenced but had no stored value. */\n unresolved: string[];\n};\n\n/**\n * Read-only view over a frozen registry snapshot. Returned by\n * {@link ToolOutputReferenceRegistry.snapshot} for callers that need\n * to resolve placeholders against the registry state at a specific\n * point in time, ignoring any subsequent registrations.\n */\nexport interface ToolOutputResolveView {\n resolve<T>(args: T, options?: ResolveOptions): ResolveResult<T>;\n}\n\n/**\n * Per-call resolution options. `substituteIntentKey` opts the top-level\n * `intent` key back INTO placeholder substitution: the exemption protects\n * the injected display label, but a tool whose own schema declares a\n * business parameter named `intent` (the injectors skip such tools) still\n * needs references piped into it like any other argument.\n */\nexport type ResolveOptions = {\n substituteIntentKey?: boolean;\n};\n\n/**\n * Pre-resolved arg map keyed by `toolCallId`. Used by the mixed\n * direct+event dispatch path to feed event calls' resolved args\n * (captured pre-batch) into the dispatcher without re-resolving\n * against the now-stale live registry.\n */\nexport type PreResolvedArgsMap = Map<\n string,\n { resolved: Record<string, unknown>; unresolved: string[] }\n>;\n\n/**\n * Per-call sink for resolved args, keyed by `toolCallId`. Threaded\n * as a per-batch local map so concurrent `ToolNode.run()` calls do\n * not race on shared sink state.\n */\nexport type ResolvedArgsByCallId = Map<string, Record<string, unknown>>;\n\nconst EMPTY_ENTRIES: ReadonlyMap<string, string> = new Map<string, string>();\n\n/**\n * Per-run state bucket held inside the registry. Each distinct\n * `run_id` gets its own bucket so overlapping concurrent runs on a\n * shared registry cannot leak outputs, turn counters, or warn-memos\n * into one another.\n */\nclass RunStateBucket {\n entries: Map<string, string> = new Map();\n totalSize: number = 0;\n turnCounter: number = 0;\n warnedNonStringTools: Set<string> = new Set();\n}\n\n/**\n * Anonymous (`run_id` absent) bucket key. Anonymous batches are\n * treated as fresh runs on every invocation — see `nextTurn`.\n */\nconst ANON_RUN_KEY = '\\0anon';\n\n/**\n * Default upper bound on the number of concurrently-tracked runs per\n * registry. When exceeded, the oldest run's bucket (by insertion\n * order) is evicted. Keeps memory bounded when a ToolNode is reused\n * across many runs without explicit `releaseRun` calls.\n */\nconst DEFAULT_MAX_ACTIVE_RUNS = 32;\n\n/**\n * Ordered map of reference-key → stored output, partitioned by run so\n * concurrent / interleaved runs sharing one registry cannot leak\n * outputs between each other.\n *\n * Each public method takes a `runId` which selects the run's bucket.\n * Hosts typically get one registry per run via `Graph`, in which\n * case only a single bucket is ever populated; the partitioning\n * exists so the registry also behaves correctly when a single\n * instance is reused directly.\n */\nexport class ToolOutputReferenceRegistry {\n private runStates: Map<string, RunStateBucket> = new Map();\n private readonly maxOutputSize: number;\n private readonly maxTotalSize: number;\n private readonly maxActiveRuns: number;\n /**\n * Local stateful matcher used only by `replaceInString`. Kept\n * off-module so callers of the exported `TOOL_OUTPUT_REF_PATTERN`\n * never see a stale `lastIndex`.\n */\n private static readonly PLACEHOLDER_MATCHER = /\\{\\{(tool\\d+turn\\d+)\\}\\}/g;\n\n constructor(options: ToolOutputReferenceRegistryOptions = {}) {\n /**\n * Per-output default is the same ~400 KB budget as the standard\n * tool-result truncation (`HARD_MAX_TOOL_RESULT_CHARS`). This\n * keeps a single `{{…}}` substitution at a size that is safe to\n * pass through typical shell `ARG_MAX` limits and matches what\n * the LLM would otherwise have seen. Hosts that want larger per-\n * output payloads (API consumers, long JSON streams) can raise\n * the cap explicitly up to the 5 MB total budget.\n */\n const perOutput =\n options.maxOutputSize != null && options.maxOutputSize > 0\n ? options.maxOutputSize\n : HARD_MAX_TOOL_RESULT_CHARS;\n /**\n * Clamp a caller-supplied `maxTotalSize` to\n * `HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE` (5 MB) so the documented\n * absolute cap is enforced regardless of host config —\n * `calculateMaxTotalToolOutputSize` already applies the same\n * upper bound on its computed default, but the user-provided\n * branch was bypassing it.\n */\n const totalRaw =\n options.maxTotalSize != null && options.maxTotalSize > 0\n ? Math.min(options.maxTotalSize, HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE)\n : calculateMaxTotalToolOutputSize(perOutput);\n this.maxTotalSize = totalRaw;\n /**\n * The per-output cap can never exceed the per-run aggregate cap:\n * if a single entry were allowed to be larger than `maxTotalSize`,\n * the eviction loop would either blow the cap (to keep the entry)\n * or self-evict a just-stored value. Clamping here turns\n * `maxTotalSize` into a hard upper bound on *any* state the\n * registry retains per run.\n */\n this.maxOutputSize = Math.min(perOutput, totalRaw);\n this.maxActiveRuns =\n options.maxActiveRuns != null && options.maxActiveRuns > 0\n ? options.maxActiveRuns\n : DEFAULT_MAX_ACTIVE_RUNS;\n }\n\n private keyFor(runId: string | undefined): string {\n return runId ?? ANON_RUN_KEY;\n }\n\n private getOrCreate(runId: string | undefined): RunStateBucket {\n const key = this.keyFor(runId);\n let state = this.runStates.get(key);\n if (state == null) {\n state = new RunStateBucket();\n this.runStates.set(key, state);\n if (this.runStates.size > this.maxActiveRuns) {\n const oldest = this.runStates.keys().next().value;\n if (oldest != null && oldest !== key) {\n this.runStates.delete(oldest);\n }\n }\n }\n return state;\n }\n\n /** Registers (or replaces) the output stored under `key` for `runId`. */\n set(runId: string | undefined, key: string, value: string): void {\n const bucket = this.getOrCreate(runId);\n const clipped =\n value.length > this.maxOutputSize\n ? value.slice(0, this.maxOutputSize)\n : value;\n const existing = bucket.entries.get(key);\n if (existing != null) {\n bucket.totalSize -= existing.length;\n bucket.entries.delete(key);\n }\n bucket.entries.set(key, clipped);\n bucket.totalSize += clipped.length;\n this.evictWithinBucket(bucket);\n }\n\n /** Returns the stored value for `key` in `runId`'s bucket, or `undefined`. */\n get(runId: string | undefined, key: string): string | undefined {\n return this.runStates.get(this.keyFor(runId))?.entries.get(key);\n }\n\n /**\n * Returns `true` when `key` is currently stored in `runId`'s bucket.\n * Used by {@link annotateMessagesForLLM} to gate transient annotation\n * on whether the registry still owns the referenced output (a stale\n * `_refKey` from a prior run silently no-ops here).\n */\n has(runId: string | undefined, key: string): boolean {\n return this.runStates.get(this.keyFor(runId))?.entries.has(key) ?? false;\n }\n\n /** Total number of registered outputs across every run bucket. */\n get size(): number {\n let n = 0;\n for (const bucket of this.runStates.values()) {\n n += bucket.entries.size;\n }\n return n;\n }\n\n /** Maximum characters retained per output (post-clip). */\n get perOutputLimit(): number {\n return this.maxOutputSize;\n }\n\n /** Maximum total characters retained *per run*. */\n get totalLimit(): number {\n return this.maxTotalSize;\n }\n\n /** Drops every run's state. */\n clear(): void {\n this.runStates.clear();\n }\n\n /**\n * Explicitly release `runId`'s state. Safe to call when a run has\n * finished. Hosts sharing one registry across runs should call this\n * to reclaim memory deterministically; otherwise LRU eviction kicks\n * in when `maxActiveRuns` runs accumulate.\n */\n releaseRun(runId: string | undefined): void {\n this.runStates.delete(this.keyFor(runId));\n }\n\n /**\n * Claims the next batch turn synchronously from `runId`'s bucket.\n *\n * Must be called once at the start of each ToolNode batch before\n * any `await`, so concurrent invocations within the same run see\n * distinct turn values (reads are effectively atomic by JS's\n * single-threaded execution of the sync prefix).\n *\n * If `runId` is missing the anonymous bucket is dropped and a\n * fresh one created so each anonymous call behaves as its own run.\n */\n nextTurn(runId: string | undefined): number {\n if (runId == null) {\n this.runStates.delete(ANON_RUN_KEY);\n }\n const bucket = this.getOrCreate(runId);\n return bucket.turnCounter++;\n }\n\n /**\n * Records that `toolName` has been warned about in `runId` (returns\n * `true` on the first call per run, `false` after). Used by\n * ToolNode to emit one log line per offending tool per run when a\n * `ToolMessage.content` isn't a string.\n */\n claimWarnOnce(runId: string | undefined, toolName: string): boolean {\n const bucket = this.getOrCreate(runId);\n if (bucket.warnedNonStringTools.has(toolName)) {\n return false;\n }\n bucket.warnedNonStringTools.add(toolName);\n return true;\n }\n\n /**\n * Walks `args` and replaces every `{{tool<i>turn<n>}}` placeholder in\n * string values with the stored output *from `runId`'s bucket*. Non-\n * string values and object keys are left untouched. Unresolved\n * references are left in-place and reported so the caller can\n * surface them to the LLM. When no placeholder appears anywhere in\n * the serialized args, the original input is returned without\n * walking the tree.\n */\n resolve<T>(\n runId: string | undefined,\n args: T,\n options?: ResolveOptions\n ): ResolveResult<T> {\n if (!hasAnyPlaceholder(args)) {\n return { resolved: args, unresolved: [] };\n }\n const bucket = this.runStates.get(this.keyFor(runId));\n return this.resolveAgainst(bucket?.entries ?? EMPTY_ENTRIES, args, options);\n }\n\n /**\n * Captures a frozen snapshot of `runId`'s current entries and\n * returns a view that resolves placeholders against *only* that\n * snapshot. The snapshot is decoupled from the live registry, so\n * subsequent `set()` calls (for example, same-turn direct outputs\n * registering while an event branch is still in flight) are\n * invisible to the snapshot's `resolve`. Used by the mixed\n * direct+event dispatch path to preserve same-turn isolation when\n * a `PreToolUse` hook rewrites event args after directs have\n * completed.\n */\n snapshot(runId: string | undefined): ToolOutputResolveView {\n const bucket = this.runStates.get(this.keyFor(runId));\n const entries: ReadonlyMap<string, string> = bucket\n ? new Map(bucket.entries)\n : EMPTY_ENTRIES;\n return {\n resolve: <T>(args: T, options?: ResolveOptions): ResolveResult<T> =>\n this.resolveAgainst(entries, args, options),\n };\n }\n\n private resolveAgainst<T>(\n entries: ReadonlyMap<string, string>,\n args: T,\n options?: ResolveOptions\n ): ResolveResult<T> {\n if (!hasAnyPlaceholder(args)) {\n return { resolved: args, unresolved: [] };\n }\n const exemptIntentKey = options?.substituteIntentKey !== true;\n const unresolved = new Set<string>();\n /**\n * Providers may deliver the args OBJECT as a JSON string. A plain\n * string-root transform would expand placeholders inside the `intent`\n * label too, bypassing the top-level exclusion below — parse, transform\n * key-aware, and re-serialize so the label stays verbatim while every\n * other field still substitutes. Only taken when an `intent` key is\n * actually present; other strings keep the fast raw-string path.\n */\n if (exemptIntentKey && typeof args === 'string') {\n const parsedRoot = parseStringifiedArgsObject(args);\n if (parsedRoot != null && INTENT_ARG in parsedRoot) {\n const resolved = JSON.stringify(\n this.transform(entries, parsedRoot, unresolved, true)\n ) as T;\n return { resolved, unresolved: Array.from(unresolved) };\n }\n }\n const resolved = this.transform(\n entries,\n args,\n unresolved,\n exemptIntentKey\n ) as T;\n return { resolved, unresolved: Array.from(unresolved) };\n }\n\n private transform(\n entries: ReadonlyMap<string, string>,\n value: unknown,\n unresolved: Set<string>,\n exemptRootIntent = false\n ): unknown {\n if (typeof value === 'string') {\n return this.replaceInString(entries, value, unresolved);\n }\n if (Array.isArray(value)) {\n return value.map((item) => this.transform(entries, item, unresolved));\n }\n if (value !== null && typeof value === 'object') {\n const source = value as Record<string, unknown>;\n const next: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(source)) {\n /**\n * The top-level `intent` arg is a display label, never a data\n * channel: expanding a `{{tool<i>turn<n>}}` placeholder there would\n * dump a stored output (up to the registry cap) into a single-line\n * UI label and persist it with the message. Left verbatim instead.\n */\n next[key] =\n exemptRootIntent && key === INTENT_ARG\n ? item\n : this.transform(entries, item, unresolved);\n }\n return next;\n }\n return value;\n }\n\n private replaceInString(\n entries: ReadonlyMap<string, string>,\n input: string,\n unresolved: Set<string>\n ): string {\n if (input.indexOf('{{tool') === -1) {\n return input;\n }\n return input.replace(\n ToolOutputReferenceRegistry.PLACEHOLDER_MATCHER,\n (match, key: string) => {\n const stored = entries.get(key);\n if (stored == null) {\n unresolved.add(key);\n return match;\n }\n return stored;\n }\n );\n }\n\n private evictWithinBucket(bucket: RunStateBucket): void {\n if (bucket.totalSize <= this.maxTotalSize) {\n return;\n }\n for (const key of bucket.entries.keys()) {\n if (bucket.totalSize <= this.maxTotalSize) {\n return;\n }\n const entry = bucket.entries.get(key);\n if (entry == null) {\n continue;\n }\n bucket.totalSize -= entry.length;\n bucket.entries.delete(key);\n }\n }\n}\n\n/**\n * Cheap pre-check: returns true if any string value in `args` contains\n * the `{{tool` substring. Lets `resolve()` skip the deep tree walk (and\n * its object allocations) for the common case of plain args.\n */\nfunction hasAnyPlaceholder(value: unknown): boolean {\n if (typeof value === 'string') {\n return value.indexOf('{{tool') !== -1;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n if (hasAnyPlaceholder(item)) {\n return true;\n }\n }\n return false;\n }\n if (value !== null && typeof value === 'object') {\n for (const item of Object.values(value as Record<string, unknown>)) {\n if (hasAnyPlaceholder(item)) {\n return true;\n }\n }\n return false;\n }\n return false;\n}\n\n/**\n * Annotates `content` with a reference key and/or unresolved-ref\n * warnings so the LLM sees both alongside the tool output.\n *\n * Behavior:\n * - If `content` parses as a plain (non-array, non-null) JSON object\n * and the object does not already have a conflicting `_ref` key,\n * the reference key and (when present) `_unresolved_refs` array\n * are injected as object fields, preserving JSON validity for\n * downstream consumers that parse the output.\n * - Otherwise (string output, JSON array/primitive, parse failure,\n * or `_ref` collision), a `[ref: <key>]\\n` prefix line is\n * prepended and unresolved refs are appended as a trailing\n * `[unresolved refs: …]` line.\n *\n * The annotated string is what the LLM sees as `ToolMessage.content`.\n * The *original* (un-annotated) value is what gets stored in the\n * registry, so downstream piping remains pristine.\n *\n * @param content Raw (post-truncation) tool output.\n * @param key Reference key for this output, or undefined when\n * there is nothing to register (errors etc.).\n * @param unresolved Reference keys that failed to resolve during\n * argument substitution. Surfaced so the LLM can\n * self-correct its next tool call.\n */\nexport function annotateToolOutputWithReference(\n content: string,\n key: string | undefined,\n unresolved: string[] = []\n): string {\n const hasRefKey = key != null;\n const hasUnresolved = unresolved.length > 0;\n if (!hasRefKey && !hasUnresolved) {\n return content;\n }\n const trimmed = content.trimStart();\n if (trimmed.startsWith('{')) {\n const annotated = tryInjectRefIntoJsonObject(content, key, unresolved);\n if (annotated != null) {\n return annotated;\n }\n }\n const prefix = hasRefKey ? `${buildReferencePrefix(key!)}\\n` : '';\n const trailer = hasUnresolved\n ? `\\n[unresolved refs: ${unresolved.join(', ')}]`\n : '';\n return `${prefix}${content}${trailer}`;\n}\n\nfunction tryInjectRefIntoJsonObject(\n content: string,\n key: string | undefined,\n unresolved: string[]\n): string | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return null;\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const injectingRef = key != null;\n const injectingUnresolved = unresolved.length > 0;\n\n /**\n * Reject the JSON-injection path (fall back to prefix form) when\n * either of our keys collides with real payload data:\n * - `_ref` collision: existing value is non-null and differs from\n * the key we're about to inject.\n * - `_unresolved_refs` collision: existing value is non-null and\n * is not a deep-equal match for the array we'd inject.\n * This keeps us from silently overwriting legitimate tool output.\n */\n if (\n injectingRef &&\n TOOL_OUTPUT_REF_KEY in obj &&\n obj[TOOL_OUTPUT_REF_KEY] !== key &&\n obj[TOOL_OUTPUT_REF_KEY] != null\n ) {\n return null;\n }\n if (\n injectingUnresolved &&\n TOOL_OUTPUT_UNRESOLVED_KEY in obj &&\n obj[TOOL_OUTPUT_UNRESOLVED_KEY] != null &&\n !arraysShallowEqual(obj[TOOL_OUTPUT_UNRESOLVED_KEY], unresolved)\n ) {\n return null;\n }\n\n /**\n * Only strip the framework-owned key we're actually injecting —\n * leave everything else (including a pre-existing `_ref` on the\n * unresolved-only path, or a pre-existing `_unresolved_refs` on a\n * plain-annotation path) untouched so we annotate rather than\n * mutate downstream payload data. Our injected keys land first in\n * the serialized JSON so the LLM sees them before the body.\n */\n const omitKeys = new Set<string>();\n if (injectingRef) omitKeys.add(TOOL_OUTPUT_REF_KEY);\n if (injectingUnresolved) omitKeys.add(TOOL_OUTPUT_UNRESOLVED_KEY);\n const rest: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!omitKeys.has(k)) {\n rest[k] = v;\n }\n }\n const injected: Record<string, unknown> = {};\n if (injectingRef) {\n injected[TOOL_OUTPUT_REF_KEY] = key;\n }\n if (injectingUnresolved) {\n injected[TOOL_OUTPUT_UNRESOLVED_KEY] = unresolved;\n }\n Object.assign(injected, rest);\n\n const pretty = /^\\{\\s*\\n/.test(content);\n return pretty ? JSON.stringify(injected, null, 2) : JSON.stringify(injected);\n}\n\nfunction arraysShallowEqual(a: unknown, b: readonly string[]): boolean {\n if (!Array.isArray(a) || a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Lazy projection that, given a registry and a runId, returns a new\n * `messages` array where each `ToolMessage` carrying ref metadata is\n * projected into a transient copy with annotated content (when the ref\n * is live in the registry) and with the framework-owned `additional_\n * kwargs` keys (`_refKey`, `_refScope`, `_unresolvedRefs`) stripped\n * regardless of whether annotation applied. The original input array\n * and its messages are never mutated.\n *\n * Annotation is gated on registry presence: a stale `_refKey` from a\n * prior run (e.g. one that survived in persisted history) silently\n * no-ops on the *content* side. The strip-metadata side still runs so\n * stale framework keys never leak onto the wire under any custom or\n * future provider serializer that might transmit `additional_kwargs`.\n * `_unresolvedRefs` is always meaningful and is not gated.\n *\n * **Feature-disabled fast path:** when the host hasn't enabled the\n * tool-output-reference feature, the registry is `undefined` and this\n * function returns the input array reference-equal *without iterating\n * a single message*. The loop is exclusive to the feature-enabled\n * code path.\n */\nexport function annotateMessagesForLLM(\n messages: BaseMessage[],\n registry: ToolOutputReferenceRegistry | undefined,\n runId: string | undefined\n): BaseMessage[] {\n if (registry == null) return messages;\n\n /**\n * Lazy-allocate the output array so the common case (no ToolMessage\n * carries framework metadata) returns the input reference-equal with\n * zero allocations beyond the per-message predicate checks.\n */\n let out: BaseMessage[] | undefined;\n for (let i = 0; i < messages.length; i++) {\n const m = messages[i];\n if (m._getType() !== 'tool') continue;\n /**\n * `additional_kwargs` is untyped at the LangChain layer\n * (`Record<string, unknown>`), so persisted or client-supplied\n * ToolMessages can carry arbitrary shapes — including primitives\n * (a malformed serializer might write a string, or `null`).\n * Guard with a runtime object check before the `in` probes\n * because the `in` operator throws `TypeError` on primitives.\n * A single malformed message must never crash the provider call\n * path; skip its annotation/strip and continue.\n */\n const rawMeta = m.additional_kwargs as unknown;\n if (rawMeta == null || typeof rawMeta !== 'object') continue;\n const meta = rawMeta as Record<string, unknown>;\n const hasRefKey = '_refKey' in meta;\n const hasRefScope = '_refScope' in meta;\n const hasUnresolvedField = '_unresolvedRefs' in meta;\n if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;\n if (isComputerCallOutputMessage(m)) {\n out ??= messages.slice();\n out[i] = cloneToolMessageWithContent(m as ToolMessage, m.content);\n continue;\n }\n\n const refKey = readRefKey(meta);\n const unresolved = readUnresolvedRefs(meta);\n\n /**\n * Prefer the message-stamped `_refScope` for the registry lookup.\n * For named runs it equals the current `runId`; for anonymous\n * invocations it carries the per-batch synthetic scope minted by\n * ToolNode (`\\0anon-<n>`), which `runId` from config cannot\n * recover. Falling back to `runId` keeps backward compatibility\n * with messages stamped before this field existed.\n */\n const lookupScope = readRefScope(meta) ?? runId;\n const liveRef =\n refKey != null && registry.has(lookupScope, refKey) ? refKey : undefined;\n const annotates = liveRef != null || unresolved.length > 0;\n\n const tm = m as ToolMessage;\n let nextContent: ToolMessage['content'] = tm.content;\n\n if (annotates && typeof tm.content === 'string') {\n nextContent = annotateToolOutputWithReference(\n tm.content,\n liveRef,\n unresolved\n );\n } else if (annotates && Array.isArray(tm.content)) {\n /**\n * Array tool content. The string annotator can't run — this notably\n * includes a tail tool result that prompt caching rewrote from a string\n * into a text-block array to host its `cache_control` / `cachePoint`\n * marker (the `_refKey` survives on `additional_kwargs`). Project the\n * same markers the string path would, as leading text blocks: the live\n * `[ref: …]` prefix and/or the unresolved-refs warning. Without this the\n * common tool-result tail loses its reference marker once cached.\n *\n * `as unknown as ToolMessage['content']` is unavoidable: LangChain's\n * content union does not accept a freshly built mixed array literal even\n * though the structural shape is valid at runtime. The double-cast is\n * structurally safe — every original block is preserved and only\n * `{ type: 'text', text }` blocks (which all providers accept) are\n * prepended.\n */\n const prefixBlocks: Array<{ type: 'text'; text: string }> = [];\n if (liveRef != null) {\n prefixBlocks.push({\n type: 'text',\n text: buildReferencePrefix(liveRef),\n });\n }\n if (unresolved.length > 0) {\n prefixBlocks.push({\n type: 'text',\n text: `[unresolved refs: ${unresolved.join(', ')}]`,\n });\n }\n if (prefixBlocks.length > 0) {\n nextContent = [\n ...prefixBlocks,\n ...tm.content,\n ] as unknown as ToolMessage['content'];\n }\n }\n\n /**\n * Project unconditionally: even when no annotation applies (stale\n * `_refKey` or non-annotatable content), `cloneToolMessageWithContent`\n * runs `stripFrameworkRefMetadata` on `additional_kwargs` so the\n * framework-owned keys never reach the wire.\n */\n out ??= messages.slice();\n out[i] = cloneToolMessageWithContent(tm, nextContent);\n }\n\n return out ?? messages;\n}\n\n/**\n * Reads `_refKey` defensively from untyped `additional_kwargs`. Returns\n * undefined for non-string values so a malformed field cannot poison\n * the registry lookup or downstream string operations.\n */\nfunction readRefKey(\n meta: Record<string, unknown> | undefined\n): string | undefined {\n const v = meta?._refKey;\n return typeof v === 'string' ? v : undefined;\n}\n\n/**\n * Reads `_refScope` defensively from untyped `additional_kwargs`.\n * Mirrors {@link readRefKey} — non-string scopes are dropped (the\n * caller falls back to the run-derived scope) rather than passed into\n * the registry as a malformed key.\n */\nfunction readRefScope(\n meta: Record<string, unknown> | undefined\n): string | undefined {\n const v = meta?._refScope;\n return typeof v === 'string' ? v : undefined;\n}\n\n/**\n * Reads `_unresolvedRefs` defensively from untyped `additional_kwargs`.\n * Returns an empty array for any non-array value, and filters out\n * non-string entries from a real array. Without this guard, a hydrated\n * ToolMessage carrying e.g. `_unresolvedRefs: 'tool0turn0'` would crash\n * `attemptInvoke` on the eventual `.length` / `.join(...)` call.\n */\nfunction readUnresolvedRefs(\n meta: Record<string, unknown> | undefined\n): string[] {\n const v = meta?._unresolvedRefs;\n if (!Array.isArray(v)) return [];\n const out: string[] = [];\n for (const item of v) {\n if (typeof item === 'string') out.push(item);\n }\n return out;\n}\n\n/**\n * Builds a fresh `ToolMessage` that mirrors `tm`'s identity fields with\n * the supplied `content`. Every `ToolMessage` field but `content` is\n * carried over so the projection is structurally identical to the\n * original from a LangChain serializer's perspective.\n *\n * `additional_kwargs` is rebuilt with the framework-owned ref keys\n * stripped. Defensive: LangChain's standard provider serializers do not\n * transmit `additional_kwargs` to provider HTTP APIs, but a custom\n * adapter or future LangChain change could. Stripping keeps the\n * implementation correct under any serializer behavior at the cost of a\n * shallow object spread per annotated message.\n */\nfunction cloneToolMessageWithContent(\n tm: ToolMessage,\n content: ToolMessage['content']\n): ToolMessage {\n return new ToolMessage({\n id: tm.id,\n name: tm.name,\n status: tm.status,\n artifact: tm.artifact,\n tool_call_id: tm.tool_call_id,\n response_metadata: tm.response_metadata,\n additional_kwargs: stripFrameworkRefMetadata(tm.additional_kwargs),\n content,\n });\n}\n\n/**\n * Returns a copy of `kwargs` with `_refKey`, `_refScope`, and\n * `_unresolvedRefs` removed. Returns the input reference-equal when\n * none of those keys are present so the no-strip path stays cheap;\n * returns `undefined` when stripping leaves the object empty so the\n * caller can drop the field entirely.\n */\nfunction stripFrameworkRefMetadata(\n kwargs: Record<string, unknown> | undefined\n): Record<string, unknown> | undefined {\n if (kwargs == null) return undefined;\n if (\n !('_refKey' in kwargs) &&\n !('_refScope' in kwargs) &&\n !('_unresolvedRefs' in kwargs)\n ) {\n return kwargs;\n }\n const { _refKey, _refScope, _unresolvedRefs, ...rest } = kwargs as Record<\n string,\n unknown\n > & {\n _refKey?: unknown;\n _refScope?: unknown;\n _unresolvedRefs?: unknown;\n };\n void _refKey;\n void _refScope;\n void _unresolvedRefs;\n return Object.keys(rest).length === 0 ? undefined : rest;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"toolOutputReferences.mjs","names":[],"sources":["../../../src/tools/toolOutputReferences.ts"],"sourcesContent":["/**\n * Tool output reference registry.\n *\n * When enabled via `RunConfig.toolOutputReferences.enabled`, ToolNode\n * stores each successful tool output under a stable key\n * (`tool<idx>turn<turn>`) where `idx` is the tool's position within a\n * ToolNode batch and `turn` is the batch index within the run\n * (incremented once per ToolNode invocation).\n *\n * Subsequent tool calls can pipe a previous output into their args by\n * embedding `{{tool<idx>turn<turn>}}` inside any string argument;\n * {@link ToolOutputReferenceRegistry.resolve} walks the args and\n * substitutes the placeholders immediately before invocation.\n *\n * The registry stores the *raw, untruncated* tool output so a later\n * `{{…}}` substitution pipes the full payload into the next tool —\n * even when the LLM only saw a head+tail-truncated preview in\n * `ToolMessage.content`. Outputs are stored without any annotation\n * (the `_ref` key or the `[ref: ...]` prefix seen by the LLM is\n * strictly a UX signal attached to `ToolMessage.content`). Keeping the\n * registry pristine means downstream bash/jq piping receives the\n * complete, verbatim output with no injected fields.\n */\n\nimport { ToolMessage } from '@langchain/core/messages';\nimport type { BaseMessage } from '@langchain/core/messages';\nimport {\n calculateMaxTotalToolOutputSize,\n HARD_MAX_TOOL_RESULT_CHARS,\n HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,\n} from '@/utils/truncation';\nimport { isComputerCallOutputMessage } from '@/utils/toolContent';\nimport { INTENT_ARG } from '@/tools/intentArg';\n\n/** Parses a stringified JSON object arg; undefined for anything else. */\nfunction parseStringifiedArgsObject(\n value: string\n): Record<string, unknown> | undefined {\n if (!value.trim().startsWith('{')) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(value) as unknown;\n if (\n parsed != null &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed)\n ) {\n return parsed as Record<string, unknown>;\n }\n } catch {\n return undefined;\n }\n return undefined;\n}\n\n/**\n * Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.\n * Exported for consumers that want to detect references (e.g., syntax\n * highlighting, docs). The stateful `g` variant lives inside the\n * registry so nobody trips on `lastIndex`.\n */\nexport const TOOL_OUTPUT_REF_PATTERN = /\\{\\{(tool\\d+turn\\d+)\\}\\}/;\n\n/** Object key used when a parsed-object output has `_ref` injected. */\nexport const TOOL_OUTPUT_REF_KEY = '_ref';\n\n/**\n * Object key used to carry unresolved reference warnings on a parsed-\n * object output. Using a dedicated field instead of a trailing text\n * line keeps the annotated `ToolMessage.content` parseable as JSON for\n * downstream consumers that rely on the object shape.\n */\nexport const TOOL_OUTPUT_UNRESOLVED_KEY = '_unresolved_refs';\n\n/** Single-line prefix prepended to non-object tool outputs so the LLM sees the reference key. */\nexport function buildReferencePrefix(key: string): string {\n return `[ref: ${key}]`;\n}\n\n/** Stable registry key for a tool output. */\nexport function buildReferenceKey(toolIndex: number, turn: number): string {\n return `tool${toolIndex}turn${turn}`;\n}\n\nexport type ToolOutputReferenceRegistryOptions = {\n /** Maximum characters stored per registered output. */\n maxOutputSize?: number;\n /** Maximum total characters retained across all registered outputs. */\n maxTotalSize?: number;\n /**\n * Upper bound on the number of concurrently-tracked runs. When\n * exceeded, the oldest run bucket is evicted (FIFO). Defaults to 32.\n */\n maxActiveRuns?: number;\n};\n\nexport interface ToolOutputReferenceState {\n entries: Array<{ key: string; value: string }>;\n turnCounter: number;\n warnedNonStringTools: string[];\n}\n\n/**\n * Result of resolving placeholders in tool args.\n */\nexport type ResolveResult<T> = {\n /** Arguments with placeholders replaced. Same shape as the input. */\n resolved: T;\n /** Reference keys that were referenced but had no stored value. */\n unresolved: string[];\n};\n\n/**\n * Read-only view over a frozen registry snapshot. Returned by\n * {@link ToolOutputReferenceRegistry.snapshot} for callers that need\n * to resolve placeholders against the registry state at a specific\n * point in time, ignoring any subsequent registrations.\n */\nexport interface ToolOutputResolveView {\n resolve<T>(args: T, options?: ResolveOptions): ResolveResult<T>;\n}\n\n/**\n * Per-call resolution options. `substituteIntentKey` opts the top-level\n * `intent` key back INTO placeholder substitution: the exemption protects\n * the injected display label, but a tool whose own schema declares a\n * business parameter named `intent` (the injectors skip such tools) still\n * needs references piped into it like any other argument.\n */\nexport type ResolveOptions = {\n substituteIntentKey?: boolean;\n};\n\n/**\n * Pre-resolved arg map keyed by `toolCallId`. Used by the mixed\n * direct+event dispatch path to feed event calls' resolved args\n * (captured pre-batch) into the dispatcher without re-resolving\n * against the now-stale live registry.\n */\nexport type PreResolvedArgsMap = Map<\n string,\n { resolved: Record<string, unknown>; unresolved: string[] }\n>;\n\n/**\n * Per-call sink for resolved args, keyed by `toolCallId`. Threaded\n * as a per-batch local map so concurrent `ToolNode.run()` calls do\n * not race on shared sink state.\n */\nexport type ResolvedArgsByCallId = Map<string, Record<string, unknown>>;\n\nconst EMPTY_ENTRIES: ReadonlyMap<string, string> = new Map<string, string>();\n\n/**\n * Per-run state bucket held inside the registry. Each distinct\n * `run_id` gets its own bucket so overlapping concurrent runs on a\n * shared registry cannot leak outputs, turn counters, or warn-memos\n * into one another.\n */\nclass RunStateBucket {\n entries: Map<string, string> = new Map();\n totalSize: number = 0;\n turnCounter: number = 0;\n warnedNonStringTools: Set<string> = new Set();\n}\n\n/**\n * Anonymous (`run_id` absent) bucket key. Anonymous batches are\n * treated as fresh runs on every invocation — see `nextTurn`.\n */\nconst ANON_RUN_KEY = '\\0anon';\n\n/**\n * Default upper bound on the number of concurrently-tracked runs per\n * registry. When exceeded, the oldest run's bucket (by insertion\n * order) is evicted. Keeps memory bounded when a ToolNode is reused\n * across many runs without explicit `releaseRun` calls.\n */\nconst DEFAULT_MAX_ACTIVE_RUNS = 32;\n\n/**\n * Ordered map of reference-key → stored output, partitioned by run so\n * concurrent / interleaved runs sharing one registry cannot leak\n * outputs between each other.\n *\n * Each public method takes a `runId` which selects the run's bucket.\n * Hosts typically get one registry per run via `Graph`, in which\n * case only a single bucket is ever populated; the partitioning\n * exists so the registry also behaves correctly when a single\n * instance is reused directly.\n */\nexport class ToolOutputReferenceRegistry {\n private runStates: Map<string, RunStateBucket> = new Map();\n private readonly maxOutputSize: number;\n private readonly maxTotalSize: number;\n private readonly maxActiveRuns: number;\n /**\n * Local stateful matcher used only by `replaceInString`. Kept\n * off-module so callers of the exported `TOOL_OUTPUT_REF_PATTERN`\n * never see a stale `lastIndex`.\n */\n private static readonly PLACEHOLDER_MATCHER = /\\{\\{(tool\\d+turn\\d+)\\}\\}/g;\n\n constructor(options: ToolOutputReferenceRegistryOptions = {}) {\n /**\n * Per-output default is the same ~400 KB budget as the standard\n * tool-result truncation (`HARD_MAX_TOOL_RESULT_CHARS`). This\n * keeps a single `{{…}}` substitution at a size that is safe to\n * pass through typical shell `ARG_MAX` limits and matches what\n * the LLM would otherwise have seen. Hosts that want larger per-\n * output payloads (API consumers, long JSON streams) can raise\n * the cap explicitly up to the 5 MB total budget.\n */\n const perOutput =\n options.maxOutputSize != null && options.maxOutputSize > 0\n ? options.maxOutputSize\n : HARD_MAX_TOOL_RESULT_CHARS;\n /**\n * Clamp a caller-supplied `maxTotalSize` to\n * `HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE` (5 MB) so the documented\n * absolute cap is enforced regardless of host config —\n * `calculateMaxTotalToolOutputSize` already applies the same\n * upper bound on its computed default, but the user-provided\n * branch was bypassing it.\n */\n const totalRaw =\n options.maxTotalSize != null && options.maxTotalSize > 0\n ? Math.min(options.maxTotalSize, HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE)\n : calculateMaxTotalToolOutputSize(perOutput);\n this.maxTotalSize = totalRaw;\n /**\n * The per-output cap can never exceed the per-run aggregate cap:\n * if a single entry were allowed to be larger than `maxTotalSize`,\n * the eviction loop would either blow the cap (to keep the entry)\n * or self-evict a just-stored value. Clamping here turns\n * `maxTotalSize` into a hard upper bound on *any* state the\n * registry retains per run.\n */\n this.maxOutputSize = Math.min(perOutput, totalRaw);\n this.maxActiveRuns =\n options.maxActiveRuns != null && options.maxActiveRuns > 0\n ? options.maxActiveRuns\n : DEFAULT_MAX_ACTIVE_RUNS;\n }\n\n private keyFor(runId: string | undefined): string {\n return runId ?? ANON_RUN_KEY;\n }\n\n private getOrCreate(runId: string | undefined): RunStateBucket {\n const key = this.keyFor(runId);\n let state = this.runStates.get(key);\n if (state == null) {\n state = new RunStateBucket();\n this.runStates.set(key, state);\n if (this.runStates.size > this.maxActiveRuns) {\n const oldest = this.runStates.keys().next().value;\n if (oldest != null && oldest !== key) {\n this.runStates.delete(oldest);\n }\n }\n }\n return state;\n }\n\n /** Registers (or replaces) the output stored under `key` for `runId`. */\n set(runId: string | undefined, key: string, value: string): void {\n const bucket = this.getOrCreate(runId);\n const clipped =\n value.length > this.maxOutputSize\n ? value.slice(0, this.maxOutputSize)\n : value;\n const existing = bucket.entries.get(key);\n if (existing != null) {\n bucket.totalSize -= existing.length;\n bucket.entries.delete(key);\n }\n bucket.entries.set(key, clipped);\n bucket.totalSize += clipped.length;\n this.evictWithinBucket(bucket);\n }\n\n /** Returns the stored value for `key` in `runId`'s bucket, or `undefined`. */\n get(runId: string | undefined, key: string): string | undefined {\n return this.runStates.get(this.keyFor(runId))?.entries.get(key);\n }\n\n /**\n * Returns `true` when `key` is currently stored in `runId`'s bucket.\n * Used by {@link annotateMessagesForLLM} to gate transient annotation\n * on whether the registry still owns the referenced output (a stale\n * `_refKey` from a prior run silently no-ops here).\n */\n has(runId: string | undefined, key: string): boolean {\n return this.runStates.get(this.keyFor(runId))?.entries.has(key) ?? false;\n }\n\n /** Total number of registered outputs across every run bucket. */\n get size(): number {\n let n = 0;\n for (const bucket of this.runStates.values()) {\n n += bucket.entries.size;\n }\n return n;\n }\n\n /** Maximum characters retained per output (post-clip). */\n get perOutputLimit(): number {\n return this.maxOutputSize;\n }\n\n /** Maximum total characters retained *per run*. */\n get totalLimit(): number {\n return this.maxTotalSize;\n }\n\n /** Drops every run's state. */\n clear(): void {\n this.runStates.clear();\n }\n\n /**\n * Explicitly release `runId`'s state. Safe to call when a run has\n * finished. Hosts sharing one registry across runs should call this\n * to reclaim memory deterministically; otherwise LRU eviction kicks\n * in when `maxActiveRuns` runs accumulate.\n */\n releaseRun(runId: string | undefined): void {\n this.runStates.delete(this.keyFor(runId));\n }\n\n /**\n * Claims the next batch turn synchronously from `runId`'s bucket.\n *\n * Must be called once at the start of each ToolNode batch before\n * any `await`, so concurrent invocations within the same run see\n * distinct turn values (reads are effectively atomic by JS's\n * single-threaded execution of the sync prefix).\n *\n * If `runId` is missing the anonymous bucket is dropped and a\n * fresh one created so each anonymous call behaves as its own run.\n */\n nextTurn(runId: string | undefined): number {\n if (runId == null) {\n this.runStates.delete(ANON_RUN_KEY);\n }\n const bucket = this.getOrCreate(runId);\n return bucket.turnCounter++;\n }\n\n /**\n * Records that `toolName` has been warned about in `runId` (returns\n * `true` on the first call per run, `false` after). Used by\n * ToolNode to emit one log line per offending tool per run when a\n * `ToolMessage.content` isn't a string.\n */\n claimWarnOnce(runId: string | undefined, toolName: string): boolean {\n const bucket = this.getOrCreate(runId);\n if (bucket.warnedNonStringTools.has(toolName)) {\n return false;\n }\n bucket.warnedNonStringTools.add(toolName);\n return true;\n }\n\n /**\n * Walks `args` and replaces every `{{tool<i>turn<n>}}` placeholder in\n * string values with the stored output *from `runId`'s bucket*. Non-\n * string values and object keys are left untouched. Unresolved\n * references are left in-place and reported so the caller can\n * surface them to the LLM. When no placeholder appears anywhere in\n * the serialized args, the original input is returned without\n * walking the tree.\n */\n resolve<T>(\n runId: string | undefined,\n args: T,\n options?: ResolveOptions\n ): ResolveResult<T> {\n if (!hasAnyPlaceholder(args)) {\n return { resolved: args, unresolved: [] };\n }\n const bucket = this.runStates.get(this.keyFor(runId));\n return this.resolveAgainst(bucket?.entries ?? EMPTY_ENTRIES, args, options);\n }\n\n /**\n * Captures a frozen snapshot of `runId`'s current entries and\n * returns a view that resolves placeholders against *only* that\n * snapshot. The snapshot is decoupled from the live registry, so\n * subsequent `set()` calls (for example, same-turn direct outputs\n * registering while an event branch is still in flight) are\n * invisible to the snapshot's `resolve`. Used by the mixed\n * direct+event dispatch path to preserve same-turn isolation when\n * a `PreToolUse` hook rewrites event args after directs have\n * completed.\n */\n snapshot(runId: string | undefined): ToolOutputResolveView {\n const bucket = this.runStates.get(this.keyFor(runId));\n const entries: ReadonlyMap<string, string> = bucket\n ? new Map(bucket.entries)\n : EMPTY_ENTRIES;\n return {\n resolve: <T>(args: T, options?: ResolveOptions): ResolveResult<T> =>\n this.resolveAgainst(entries, args, options),\n };\n }\n\n /** Captures the checkpoint-safe state needed to resume one run bucket. */\n snapshotState(runId: string | undefined): ToolOutputReferenceState {\n const bucket = this.runStates.get(this.keyFor(runId));\n return {\n entries: [...(bucket?.entries ?? EMPTY_ENTRIES)].map(([key, value]) => ({\n key,\n value,\n })),\n turnCounter: bucket?.turnCounter ?? 0,\n warnedNonStringTools: [...(bucket?.warnedNonStringTools ?? [])],\n };\n }\n\n /** Restores a checkpointed run bucket under the current resume scope. */\n restoreState(\n runId: string | undefined,\n state: ToolOutputReferenceState\n ): void {\n this.releaseRun(runId);\n const bucket = this.getOrCreate(runId);\n for (const { key, value } of state.entries) {\n this.set(runId, key, value);\n }\n bucket.turnCounter = state.turnCounter;\n for (const toolName of state.warnedNonStringTools) {\n bucket.warnedNonStringTools.add(toolName);\n }\n }\n\n private resolveAgainst<T>(\n entries: ReadonlyMap<string, string>,\n args: T,\n options?: ResolveOptions\n ): ResolveResult<T> {\n if (!hasAnyPlaceholder(args)) {\n return { resolved: args, unresolved: [] };\n }\n const exemptIntentKey = options?.substituteIntentKey !== true;\n const unresolved = new Set<string>();\n /**\n * Providers may deliver the args OBJECT as a JSON string. A plain\n * string-root transform would expand placeholders inside the `intent`\n * label too, bypassing the top-level exclusion below — parse, transform\n * key-aware, and re-serialize so the label stays verbatim while every\n * other field still substitutes. Only taken when an `intent` key is\n * actually present; other strings keep the fast raw-string path.\n */\n if (exemptIntentKey && typeof args === 'string') {\n const parsedRoot = parseStringifiedArgsObject(args);\n if (parsedRoot != null && INTENT_ARG in parsedRoot) {\n const resolved = JSON.stringify(\n this.transform(entries, parsedRoot, unresolved, true)\n ) as T;\n return { resolved, unresolved: Array.from(unresolved) };\n }\n }\n const resolved = this.transform(\n entries,\n args,\n unresolved,\n exemptIntentKey\n ) as T;\n return { resolved, unresolved: Array.from(unresolved) };\n }\n\n private transform(\n entries: ReadonlyMap<string, string>,\n value: unknown,\n unresolved: Set<string>,\n exemptRootIntent = false\n ): unknown {\n if (typeof value === 'string') {\n return this.replaceInString(entries, value, unresolved);\n }\n if (Array.isArray(value)) {\n return value.map((item) => this.transform(entries, item, unresolved));\n }\n if (value !== null && typeof value === 'object') {\n const source = value as Record<string, unknown>;\n const next: Record<string, unknown> = {};\n for (const [key, item] of Object.entries(source)) {\n /**\n * The top-level `intent` arg is a display label, never a data\n * channel: expanding a `{{tool<i>turn<n>}}` placeholder there would\n * dump a stored output (up to the registry cap) into a single-line\n * UI label and persist it with the message. Left verbatim instead.\n */\n next[key] =\n exemptRootIntent && key === INTENT_ARG\n ? item\n : this.transform(entries, item, unresolved);\n }\n return next;\n }\n return value;\n }\n\n private replaceInString(\n entries: ReadonlyMap<string, string>,\n input: string,\n unresolved: Set<string>\n ): string {\n if (input.indexOf('{{tool') === -1) {\n return input;\n }\n return input.replace(\n ToolOutputReferenceRegistry.PLACEHOLDER_MATCHER,\n (match, key: string) => {\n const stored = entries.get(key);\n if (stored == null) {\n unresolved.add(key);\n return match;\n }\n return stored;\n }\n );\n }\n\n private evictWithinBucket(bucket: RunStateBucket): void {\n if (bucket.totalSize <= this.maxTotalSize) {\n return;\n }\n for (const key of bucket.entries.keys()) {\n if (bucket.totalSize <= this.maxTotalSize) {\n return;\n }\n const entry = bucket.entries.get(key);\n if (entry == null) {\n continue;\n }\n bucket.totalSize -= entry.length;\n bucket.entries.delete(key);\n }\n }\n}\n\n/**\n * Cheap pre-check: returns true if any string value in `args` contains\n * the `{{tool` substring. Lets `resolve()` skip the deep tree walk (and\n * its object allocations) for the common case of plain args.\n */\nfunction hasAnyPlaceholder(value: unknown): boolean {\n if (typeof value === 'string') {\n return value.indexOf('{{tool') !== -1;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n if (hasAnyPlaceholder(item)) {\n return true;\n }\n }\n return false;\n }\n if (value !== null && typeof value === 'object') {\n for (const item of Object.values(value as Record<string, unknown>)) {\n if (hasAnyPlaceholder(item)) {\n return true;\n }\n }\n return false;\n }\n return false;\n}\n\n/**\n * Annotates `content` with a reference key and/or unresolved-ref\n * warnings so the LLM sees both alongside the tool output.\n *\n * Behavior:\n * - If `content` parses as a plain (non-array, non-null) JSON object\n * and the object does not already have a conflicting `_ref` key,\n * the reference key and (when present) `_unresolved_refs` array\n * are injected as object fields, preserving JSON validity for\n * downstream consumers that parse the output.\n * - Otherwise (string output, JSON array/primitive, parse failure,\n * or `_ref` collision), a `[ref: <key>]\\n` prefix line is\n * prepended and unresolved refs are appended as a trailing\n * `[unresolved refs: …]` line.\n *\n * The annotated string is what the LLM sees as `ToolMessage.content`.\n * The *original* (un-annotated) value is what gets stored in the\n * registry, so downstream piping remains pristine.\n *\n * @param content Raw (post-truncation) tool output.\n * @param key Reference key for this output, or undefined when\n * there is nothing to register (errors etc.).\n * @param unresolved Reference keys that failed to resolve during\n * argument substitution. Surfaced so the LLM can\n * self-correct its next tool call.\n */\nexport function annotateToolOutputWithReference(\n content: string,\n key: string | undefined,\n unresolved: string[] = []\n): string {\n const hasRefKey = key != null;\n const hasUnresolved = unresolved.length > 0;\n if (!hasRefKey && !hasUnresolved) {\n return content;\n }\n const trimmed = content.trimStart();\n if (trimmed.startsWith('{')) {\n const annotated = tryInjectRefIntoJsonObject(content, key, unresolved);\n if (annotated != null) {\n return annotated;\n }\n }\n const prefix = hasRefKey ? `${buildReferencePrefix(key!)}\\n` : '';\n const trailer = hasUnresolved\n ? `\\n[unresolved refs: ${unresolved.join(', ')}]`\n : '';\n return `${prefix}${content}${trailer}`;\n}\n\nfunction tryInjectRefIntoJsonObject(\n content: string,\n key: string | undefined,\n unresolved: string[]\n): string | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return null;\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const injectingRef = key != null;\n const injectingUnresolved = unresolved.length > 0;\n\n /**\n * Reject the JSON-injection path (fall back to prefix form) when\n * either of our keys collides with real payload data:\n * - `_ref` collision: existing value is non-null and differs from\n * the key we're about to inject.\n * - `_unresolved_refs` collision: existing value is non-null and\n * is not a deep-equal match for the array we'd inject.\n * This keeps us from silently overwriting legitimate tool output.\n */\n if (\n injectingRef &&\n TOOL_OUTPUT_REF_KEY in obj &&\n obj[TOOL_OUTPUT_REF_KEY] !== key &&\n obj[TOOL_OUTPUT_REF_KEY] != null\n ) {\n return null;\n }\n if (\n injectingUnresolved &&\n TOOL_OUTPUT_UNRESOLVED_KEY in obj &&\n obj[TOOL_OUTPUT_UNRESOLVED_KEY] != null &&\n !arraysShallowEqual(obj[TOOL_OUTPUT_UNRESOLVED_KEY], unresolved)\n ) {\n return null;\n }\n\n /**\n * Only strip the framework-owned key we're actually injecting —\n * leave everything else (including a pre-existing `_ref` on the\n * unresolved-only path, or a pre-existing `_unresolved_refs` on a\n * plain-annotation path) untouched so we annotate rather than\n * mutate downstream payload data. Our injected keys land first in\n * the serialized JSON so the LLM sees them before the body.\n */\n const omitKeys = new Set<string>();\n if (injectingRef) omitKeys.add(TOOL_OUTPUT_REF_KEY);\n if (injectingUnresolved) omitKeys.add(TOOL_OUTPUT_UNRESOLVED_KEY);\n const rest: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (!omitKeys.has(k)) {\n rest[k] = v;\n }\n }\n const injected: Record<string, unknown> = {};\n if (injectingRef) {\n injected[TOOL_OUTPUT_REF_KEY] = key;\n }\n if (injectingUnresolved) {\n injected[TOOL_OUTPUT_UNRESOLVED_KEY] = unresolved;\n }\n Object.assign(injected, rest);\n\n const pretty = /^\\{\\s*\\n/.test(content);\n return pretty ? JSON.stringify(injected, null, 2) : JSON.stringify(injected);\n}\n\nfunction arraysShallowEqual(a: unknown, b: readonly string[]): boolean {\n if (!Array.isArray(a) || a.length !== b.length) {\n return false;\n }\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Lazy projection that, given a registry and a runId, returns a new\n * `messages` array where each `ToolMessage` carrying ref metadata is\n * projected into a transient copy with annotated content (when the ref\n * is live in the registry) and with the framework-owned `additional_\n * kwargs` keys (`_refKey`, `_refScope`, `_unresolvedRefs`) stripped\n * regardless of whether annotation applied. The original input array\n * and its messages are never mutated.\n *\n * Annotation is gated on registry presence: a stale `_refKey` from a\n * prior run (e.g. one that survived in persisted history) silently\n * no-ops on the *content* side. The strip-metadata side still runs so\n * stale framework keys never leak onto the wire under any custom or\n * future provider serializer that might transmit `additional_kwargs`.\n * `_unresolvedRefs` is always meaningful and is not gated.\n *\n * **Feature-disabled fast path:** when the host hasn't enabled the\n * tool-output-reference feature, the registry is `undefined` and this\n * function returns the input array reference-equal *without iterating\n * a single message*. The loop is exclusive to the feature-enabled\n * code path.\n */\nexport function annotateMessagesForLLM(\n messages: BaseMessage[],\n registry: ToolOutputReferenceRegistry | undefined,\n runId: string | undefined\n): BaseMessage[] {\n if (registry == null) return messages;\n\n /**\n * Lazy-allocate the output array so the common case (no ToolMessage\n * carries framework metadata) returns the input reference-equal with\n * zero allocations beyond the per-message predicate checks.\n */\n let out: BaseMessage[] | undefined;\n for (let i = 0; i < messages.length; i++) {\n const m = messages[i];\n if (m._getType() !== 'tool') continue;\n /**\n * `additional_kwargs` is untyped at the LangChain layer\n * (`Record<string, unknown>`), so persisted or client-supplied\n * ToolMessages can carry arbitrary shapes — including primitives\n * (a malformed serializer might write a string, or `null`).\n * Guard with a runtime object check before the `in` probes\n * because the `in` operator throws `TypeError` on primitives.\n * A single malformed message must never crash the provider call\n * path; skip its annotation/strip and continue.\n */\n const rawMeta = m.additional_kwargs as unknown;\n if (rawMeta == null || typeof rawMeta !== 'object') continue;\n const meta = rawMeta as Record<string, unknown>;\n const hasRefKey = '_refKey' in meta;\n const hasRefScope = '_refScope' in meta;\n const hasUnresolvedField = '_unresolvedRefs' in meta;\n if (!hasRefKey && !hasRefScope && !hasUnresolvedField) continue;\n if (isComputerCallOutputMessage(m)) {\n out ??= messages.slice();\n out[i] = cloneToolMessageWithContent(m as ToolMessage, m.content);\n continue;\n }\n\n const refKey = readRefKey(meta);\n const unresolved = readUnresolvedRefs(meta);\n\n /**\n * Prefer the message-stamped `_refScope` for the registry lookup.\n * For named runs it equals the current `runId`; for anonymous\n * invocations it carries the per-batch synthetic scope minted by\n * ToolNode (`\\0anon-<n>`), which `runId` from config cannot\n * recover. Falling back to `runId` keeps backward compatibility\n * with messages stamped before this field existed.\n */\n const lookupScope = readRefScope(meta) ?? runId;\n const liveRef =\n refKey != null && registry.has(lookupScope, refKey) ? refKey : undefined;\n const annotates = liveRef != null || unresolved.length > 0;\n\n const tm = m as ToolMessage;\n let nextContent: ToolMessage['content'] = tm.content;\n\n if (annotates && typeof tm.content === 'string') {\n nextContent = annotateToolOutputWithReference(\n tm.content,\n liveRef,\n unresolved\n );\n } else if (annotates && Array.isArray(tm.content)) {\n /**\n * Array tool content. The string annotator can't run — this notably\n * includes a tail tool result that prompt caching rewrote from a string\n * into a text-block array to host its `cache_control` / `cachePoint`\n * marker (the `_refKey` survives on `additional_kwargs`). Project the\n * same markers the string path would, as leading text blocks: the live\n * `[ref: …]` prefix and/or the unresolved-refs warning. Without this the\n * common tool-result tail loses its reference marker once cached.\n *\n * `as unknown as ToolMessage['content']` is unavoidable: LangChain's\n * content union does not accept a freshly built mixed array literal even\n * though the structural shape is valid at runtime. The double-cast is\n * structurally safe — every original block is preserved and only\n * `{ type: 'text', text }` blocks (which all providers accept) are\n * prepended.\n */\n const prefixBlocks: Array<{ type: 'text'; text: string }> = [];\n if (liveRef != null) {\n prefixBlocks.push({\n type: 'text',\n text: buildReferencePrefix(liveRef),\n });\n }\n if (unresolved.length > 0) {\n prefixBlocks.push({\n type: 'text',\n text: `[unresolved refs: ${unresolved.join(', ')}]`,\n });\n }\n if (prefixBlocks.length > 0) {\n nextContent = [\n ...prefixBlocks,\n ...tm.content,\n ] as unknown as ToolMessage['content'];\n }\n }\n\n /**\n * Project unconditionally: even when no annotation applies (stale\n * `_refKey` or non-annotatable content), `cloneToolMessageWithContent`\n * runs `stripFrameworkRefMetadata` on `additional_kwargs` so the\n * framework-owned keys never reach the wire.\n */\n out ??= messages.slice();\n out[i] = cloneToolMessageWithContent(tm, nextContent);\n }\n\n return out ?? messages;\n}\n\n/**\n * Reads `_refKey` defensively from untyped `additional_kwargs`. Returns\n * undefined for non-string values so a malformed field cannot poison\n * the registry lookup or downstream string operations.\n */\nfunction readRefKey(\n meta: Record<string, unknown> | undefined\n): string | undefined {\n const v = meta?._refKey;\n return typeof v === 'string' ? v : undefined;\n}\n\n/**\n * Reads `_refScope` defensively from untyped `additional_kwargs`.\n * Mirrors {@link readRefKey} — non-string scopes are dropped (the\n * caller falls back to the run-derived scope) rather than passed into\n * the registry as a malformed key.\n */\nfunction readRefScope(\n meta: Record<string, unknown> | undefined\n): string | undefined {\n const v = meta?._refScope;\n return typeof v === 'string' ? v : undefined;\n}\n\n/**\n * Reads `_unresolvedRefs` defensively from untyped `additional_kwargs`.\n * Returns an empty array for any non-array value, and filters out\n * non-string entries from a real array. Without this guard, a hydrated\n * ToolMessage carrying e.g. `_unresolvedRefs: 'tool0turn0'` would crash\n * `attemptInvoke` on the eventual `.length` / `.join(...)` call.\n */\nfunction readUnresolvedRefs(\n meta: Record<string, unknown> | undefined\n): string[] {\n const v = meta?._unresolvedRefs;\n if (!Array.isArray(v)) return [];\n const out: string[] = [];\n for (const item of v) {\n if (typeof item === 'string') out.push(item);\n }\n return out;\n}\n\n/**\n * Builds a fresh `ToolMessage` that mirrors `tm`'s identity fields with\n * the supplied `content`. Every `ToolMessage` field but `content` is\n * carried over so the projection is structurally identical to the\n * original from a LangChain serializer's perspective.\n *\n * `additional_kwargs` is rebuilt with the framework-owned ref keys\n * stripped. Defensive: LangChain's standard provider serializers do not\n * transmit `additional_kwargs` to provider HTTP APIs, but a custom\n * adapter or future LangChain change could. Stripping keeps the\n * implementation correct under any serializer behavior at the cost of a\n * shallow object spread per annotated message.\n */\nfunction cloneToolMessageWithContent(\n tm: ToolMessage,\n content: ToolMessage['content']\n): ToolMessage {\n return new ToolMessage({\n id: tm.id,\n name: tm.name,\n status: tm.status,\n artifact: tm.artifact,\n tool_call_id: tm.tool_call_id,\n response_metadata: tm.response_metadata,\n additional_kwargs: stripFrameworkRefMetadata(tm.additional_kwargs),\n content,\n });\n}\n\n/**\n * Returns a copy of `kwargs` with `_refKey`, `_refScope`, and\n * `_unresolvedRefs` removed. Returns the input reference-equal when\n * none of those keys are present so the no-strip path stays cheap;\n * returns `undefined` when stripping leaves the object empty so the\n * caller can drop the field entirely.\n */\nfunction stripFrameworkRefMetadata(\n kwargs: Record<string, unknown> | undefined\n): Record<string, unknown> | undefined {\n if (kwargs == null) return undefined;\n if (\n !('_refKey' in kwargs) &&\n !('_refScope' in kwargs) &&\n !('_unresolvedRefs' in kwargs)\n ) {\n return kwargs;\n }\n const { _refKey, _refScope, _unresolvedRefs, ...rest } = kwargs as Record<\n string,\n unknown\n > & {\n _refKey?: unknown;\n _refScope?: unknown;\n _unresolvedRefs?: unknown;\n };\n void _refKey;\n void _refScope;\n void _unresolvedRefs;\n return Object.keys(rest).length === 0 ? undefined : rest;\n}\n"],"mappings":"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"}
@@ -1,11 +1,11 @@
1
1
  import "./misc.mjs";
2
2
  import "./truncation.mjs";
3
3
  import "./tokens.mjs";
4
- import "./llm.mjs";
5
- import "./errors.mjs";
6
4
  import "./graph.mjs";
5
+ import "./llm.mjs";
7
6
  import "./proxy.mjs";
8
7
  import "./handlers.mjs";
9
8
  import "./run.mjs";
10
9
  import "./schema.mjs";
10
+ import "./errors.mjs";
11
11
  export {};
@@ -101,7 +101,7 @@ export declare class AgentContext {
101
101
  /** Original AgentInputs used to create this context — used for self-spawn subagent resolution. */
102
102
  _sourceInputs?: t.AgentInputs;
103
103
  /** Subagent configurations for hierarchical delegation. */
104
- subagentConfigs?: t.SubagentConfig[];
104
+ subagentConfigs?: t.SubagentConfigEntry[];
105
105
  /** Maximum subagent nesting depth. */
106
106
  maxSubagentDepth?: number;
107
107
  /** Instructions for this agent */
@@ -2,16 +2,20 @@ import { ToolNode } from '@langchain/langgraph/prebuilt';
2
2
  import { RunnableConfig } from '@langchain/core/runnables';
3
3
  import type { UsageMetadata, BaseMessage } from '@langchain/core/messages';
4
4
  import type { ToolCall } from '@langchain/core/messages/tool';
5
+ import type { SubagentGraphResumeState, SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
5
6
  import type { ResolvedStreamLimits, RunBreakerScope, StreamedToolCallArgTally, StreamDeltaEventTally } from '@/llm/streamLimits';
7
+ import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
6
8
  import type { HookRegistry } from '@/hooks';
7
9
  import type * as t from '@/types';
8
- import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
9
10
  import { StreamLimitExceededError } from '@/llm/streamLimits';
11
+ import { SubagentExecutor } from '@/tools/subagent';
12
+ import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
10
13
  import { ToolNode as CustomToolNode } from '@/tools/ToolNode';
11
14
  import { AgentContext } from '@/agents/AgentContext';
12
15
  import { HandlerRegistry } from '@/events';
13
16
  export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphState, _TNodeName extends string = string> {
14
17
  abstract resetValues(keepContent?: boolean, checkpointScope?: string): void;
18
+ restoreCheckpointMessages(_messages: BaseMessage[], _pendingMessages?: BaseMessage[]): void;
15
19
  abstract initializeTools({ currentTools, currentToolMap, }: {
16
20
  currentTools?: t.GraphTools;
17
21
  currentToolMap?: t.ToolMap;
@@ -159,7 +163,14 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
159
163
  */
160
164
  protected registerCompiledToolNode(node: {
161
165
  clearDirectPathTurns(): void;
166
+ createSubagentResumeState(): SubagentToolNodeResumeState;
167
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
162
168
  }): void;
169
+ protected registerSubagentExecutor(executor: SubagentExecutor): void;
170
+ protected resetSubagentCheckpointThreadIds(): void;
171
+ getChildCheckpointThreadIds(): string[];
172
+ createSubagentResumeState(runId: string): SubagentGraphResumeState;
173
+ restoreSubagentResumeState(state: SubagentGraphResumeState, runId: string): void;
163
174
  /**
164
175
  * Returns the shared `ToolOutputReferenceRegistry` for this run,
165
176
  * constructing it on first access. Returns `undefined` when the
@@ -194,16 +205,21 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
194
205
  * across Runs if a provider reuses call ids (Codex P2 #33).
195
206
  */
196
207
  private _compiledToolNodes;
208
+ private _subagentExecutors;
197
209
  getOrCreateFileCheckpointer(): t.LocalFileCheckpointer | undefined;
198
210
  }
199
211
  export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
200
212
  overrideModel?: t.ChatModel;
201
213
  private subagentModelOverride?;
214
+ private readonly graphFactory;
215
+ private readonly supportsMultiAgentChildren;
202
216
  /** Optional compile options passed into workflow.compile() */
203
217
  compileOptions?: t.CompileOptions | undefined;
204
218
  /** Whether the workflow was actually compiled with a checkpointer. */
205
219
  hasCompiledCheckpointer: boolean;
206
220
  messages: BaseMessage[];
221
+ /** Whether a rebuilt resume seeded the message baseline from its checkpoint. */
222
+ private hasRestoredCheckpointMessages;
207
223
  /** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
208
224
  private cachedRunMessages?;
209
225
  /** Per-agent discovery snapshots preserved before contexts are reset on cleanup. */
@@ -245,6 +261,8 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
245
261
  subagentUsageSink?: t.SubagentUsageSink;
246
262
  /** See {@link t.StandardGraphInput.subagentScope}. */
247
263
  subagentScope: boolean;
264
+ /** See {@link t.StandardGraphInput.subagentExecutionContext}. */
265
+ private readonly subagentExecutionContext?;
248
266
  /** See {@link t.StandardGraphInput.preemption}. */
249
267
  preemption?: t.StreamPreemption;
250
268
  /**
@@ -342,8 +360,10 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
342
360
  * another's turn.
343
361
  */
344
362
  pendingPreemptReturn: Set<string>;
345
- constructor({ runId, signal, agents, langfuse, tokenCounter, indexTokenCountMap, calibrationRatio, subagentUsageSink, subagentScope, preemption, streamLimits, }: t.StandardGraphInput);
363
+ constructor({ runId, signal, agents, langfuse, tokenCounter, indexTokenCountMap, calibrationRatio, subagentUsageSink, subagentScope, subagentExecutionContext, preemption, streamLimits, }: t.StandardGraphInput, dependencies?: GraphFactoryDependencies);
346
364
  resetValues(keepContent?: boolean, checkpointScope?: string): void;
365
+ /** Seeds the sidecar message view that checkpoint restoration bypasses. */
366
+ restoreCheckpointMessages(messages: BaseMessage[], pendingMessages?: BaseMessage[]): void;
347
367
  clearHeavyState(): void;
348
368
  /**
349
369
  * Per-turn seal budget and routing markers. Cleared by both reset paths so
@@ -1,3 +1,4 @@
1
+ import type { GraphFactoryDependencies } from '@/graphs/graphFactory';
1
2
  import type * as t from '@/types';
2
3
  import { StandardGraph } from './Graph';
3
4
  /**
@@ -19,6 +20,9 @@ export declare class MultiAgentGraph extends StandardGraph {
19
20
  private startingNodes;
20
21
  private directEdges;
21
22
  private handoffEdges;
23
+ private handoffSourceIds;
24
+ private readonly resultAgentId?;
25
+ private readonly memberRecursionLimit?;
22
26
  private handoffPromptLabels;
23
27
  /**
24
28
  * Map of agentId to parallel group info.
@@ -31,7 +35,7 @@ export declare class MultiAgentGraph extends StandardGraph {
31
35
  * - summarizer: undefined (sequential, order 2)
32
36
  */
33
37
  private agentParallelGroups;
34
- constructor(input: t.MultiAgentGraphInput);
38
+ constructor(input: t.MultiAgentGraphInput, dependencies?: GraphFactoryDependencies);
35
39
  /**
36
40
  * Fails fast when an edge references an agent that is not in
37
41
  * `agentContexts`. Without this check, the underlying LangGraph
@@ -48,6 +52,8 @@ export declare class MultiAgentGraph extends StandardGraph {
48
52
  * Categorize edges into handoff and direct types
49
53
  */
50
54
  private categorizeEdges;
55
+ /** Static waiting/prompt edges cannot also be driven by Command routing. */
56
+ private validateCommandRoutedDirectEdges;
51
57
  /**
52
58
  * Analyze graph structure to determine starting nodes and connections
53
59
  */
@@ -0,0 +1,3 @@
1
+ import type { StandardGraph } from './Graph';
2
+ export type GraphRuntimeConfig = Pick<StandardGraph, 'hookRegistry' | 'humanInTheLoop' | 'toolOutputReferences' | 'eagerEventToolExecution' | 'codeSessionToolNames' | 'interruptingToolNames' | 'toolExecution'>;
3
+ export declare function applyGraphRuntimeConfig(graph: StandardGraph, config: GraphRuntimeConfig): void;
@@ -0,0 +1,13 @@
1
+ import type { GraphFactoryRequest } from '@/graphs/graphFactory';
2
+ import type { MultiAgentGraphInput, StandardGraphInput } from '@/types';
3
+ import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
4
+ import { StandardGraph } from '@/graphs/Graph';
5
+ export declare function createGraph(request: {
6
+ kind: 'standard';
7
+ input: StandardGraphInput;
8
+ }): StandardGraph;
9
+ export declare function createGraph(request: {
10
+ kind: 'multi-agent';
11
+ input: MultiAgentGraphInput;
12
+ }): MultiAgentGraph;
13
+ export declare function createGraph(request: GraphFactoryRequest): StandardGraph;
@@ -0,0 +1,13 @@
1
+ import type { StandardGraph } from './Graph';
2
+ import type * as t from '@/types';
3
+ export type GraphFactoryRequest = {
4
+ kind: 'standard';
5
+ input: t.StandardGraphInput;
6
+ } | {
7
+ kind: 'multi-agent';
8
+ input: t.MultiAgentGraphInput;
9
+ };
10
+ export type GraphFactory = (request: GraphFactoryRequest) => StandardGraph;
11
+ export type GraphFactoryDependencies = {
12
+ graphFactory: GraphFactory;
13
+ };
@@ -1,2 +1,4 @@
1
1
  export * from './Graph';
2
2
  export * from './MultiAgentGraph';
3
+ export * from './createGraph';
4
+ export type * from './graphFactory';
@@ -1,4 +1,4 @@
1
- import type { HookEvent, HookMatcher } from './types';
1
+ import type { HookEvent, HookMatcher, ToolApprovalReplayKey, ToolApprovalReplaySnapshot, AggregatedHookResult } from './types';
2
2
  /**
3
3
  * Snapshot of a halt request raised by a hook returning
4
4
  * `preventContinuation: true`. The SDK's run loop polls for this between
@@ -45,6 +45,8 @@ export declare class HookRegistry {
45
45
  * O(1) insertion in hot paths, no spread-on-write.
46
46
  */
47
47
  private readonly haltSignals;
48
+ /** One-shot hook contributions retained until approval is consumed. */
49
+ private readonly pendingToolApprovals;
48
50
  /**
49
51
  * Register a matcher for the lifetime of this registry (= one Run).
50
52
  * Returns an unregister function that removes the matcher by reference.
@@ -75,6 +77,13 @@ export declare class HookRegistry {
75
77
  * cannot leak into the next session on the same registry.
76
78
  */
77
79
  clearSession(sessionId: string): void;
80
+ /** Copies session-scoped policy into a rebuilt or branched Run. */
81
+ copySession(sourceSessionId: string, targetSessionId: string): void;
82
+ getPendingToolApproval(sessionId: string, key: ToolApprovalReplayKey): AggregatedHookResult | undefined;
83
+ setPendingToolApproval(sessionId: string, key: ToolApprovalReplayKey, result: AggregatedHookResult): void;
84
+ clearPendingToolApproval(sessionId: string, key: ToolApprovalReplayKey): void;
85
+ snapshotPendingToolApprovals(sessionId: string, executionScope: string): ToolApprovalReplaySnapshot[];
86
+ restorePendingToolApprovals(sessionId: string, targetExecutionScope: string, snapshots: ReadonlyArray<ToolApprovalReplaySnapshot>): void;
78
87
  /**
79
88
  * Raise a halt signal scoped to `sessionId` (= the run id the hook
80
89
  * fired under). The SDK's run loop polls for this between stream
@@ -1,5 +1,5 @@
1
1
  import type { Logger } from 'winston';
2
- import type { HookInput, AggregatedHookResult } from './types';
2
+ import type { HookInput, ToolApprovalReplayKey, AggregatedHookResult } from './types';
3
3
  import type { HookRegistry } from './HookRegistry';
4
4
  /** Default per-hook timeout when a matcher doesn't set its own. */
5
5
  export declare const DEFAULT_HOOK_TIMEOUT_MS = 30000;
@@ -15,6 +15,10 @@ export interface ExecuteHooksOptions {
15
15
  sessionId?: string;
16
16
  /** Query string matched against each matcher's pattern (tool name, etc.). */
17
17
  matchQuery?: string;
18
+ /** Stable tool-call key for replaying consumed one-shot contributions. */
19
+ onceReplayKey?: ToolApprovalReplayKey;
20
+ /** Storage scope for replay state when it must outlive or branch from the hook session. */
21
+ onceReplaySessionId?: string;
18
22
  /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */
19
23
  signal?: AbortSignal;
20
24
  /** Default per-hook timeout; overridden by `matcher.timeout` when present. */
@@ -24,6 +24,6 @@ export { createToolPolicyHook } from './createToolPolicyHook';
24
24
  export type { ToolPolicyMode, ToolPolicyConfig } from './createToolPolicyHook';
25
25
  export { createWorkspacePolicyHook } from './createWorkspacePolicyHook';
26
26
  export type { OutsideAccessPolicy, WorkspacePolicyConfig, PathExtractor, } from './createWorkspacePolicyHook';
27
- export { HOOK_EVENTS } from './types';
28
- export type { HookEvent, HookInput, HookOutput, HookCallback, HookMatcher, HooksByEvent, HookInputByEvent, HookOutputByEvent, BaseHookInput, BaseHookOutput, ToolDecision, StopDecision, AggregatedHookResult, RunStartHookInput, UserPromptSubmitHookInput, PreToolUseHookInput, PostToolUseHookInput, PostToolUseFailureHookInput, PostToolBatchHookInput, PostToolBatchEntry, PreemptBoundaryHookInput, PermissionDeniedHookInput, SubagentStartHookInput, SubagentStopHookInput, StopHookInput, StopFailureHookInput, PreCompactHookInput, PostCompactHookInput, RunStartHookOutput, UserPromptSubmitHookOutput, PreToolUseHookOutput, PostToolUseHookOutput, PostToolUseFailureHookOutput, PostToolBatchHookOutput, PreemptBoundaryHookOutput, PermissionDeniedHookOutput, SubagentStartHookOutput, SubagentStopHookOutput, StopHookOutput, StopFailureHookOutput, PreCompactHookOutput, PostCompactHookOutput, } from './types';
27
+ export { HOOK_EVENTS, TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY } from './types';
28
+ export type { HookEvent, HookInput, HookOutput, HookCallback, HookMatcher, HooksByEvent, HookInputByEvent, HookOutputByEvent, BaseHookInput, BaseHookOutput, ToolDecision, StopDecision, ToolApprovalReplayKey, ToolApprovalReplaySnapshot, AggregatedHookResult, RunStartHookInput, UserPromptSubmitHookInput, PreToolUseHookInput, PostToolUseHookInput, PostToolUseFailureHookInput, PostToolBatchHookInput, PostToolBatchEntry, PreemptBoundaryHookInput, PermissionDeniedHookInput, SubagentStartHookInput, SubagentStopHookInput, StopHookInput, StopFailureHookInput, PreCompactHookInput, PostCompactHookInput, RunStartHookOutput, UserPromptSubmitHookOutput, PreToolUseHookOutput, PostToolUseHookOutput, PostToolUseFailureHookOutput, PostToolBatchHookOutput, PreemptBoundaryHookOutput, PermissionDeniedHookOutput, SubagentStartHookOutput, SubagentStopHookOutput, StopHookOutput, StopFailureHookOutput, PreCompactHookOutput, PostCompactHookOutput, } from './types';
29
29
  export type { ExecuteHooksOptions } from './executeHooks';
@@ -9,6 +9,7 @@ import type { InjectedMessage } from '@/types/tools';
9
9
  * `@librechat/agents` emission points.
10
10
  */
11
11
  export declare const HOOK_EVENTS: readonly ["RunStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PostToolBatch", "PreemptBoundary", "PermissionDenied", "SubagentStart", "SubagentStop", "Stop", "StopFailure", "PreCompact", "PostCompact"];
12
+ export declare const TOOL_APPROVAL_EXECUTION_SCOPE_CONFIG_KEY = "__librechat_tool_approval_execution_scope";
12
13
  export type HookEvent = (typeof HOOK_EVENTS)[number];
13
14
  /** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */
14
15
  export type ToolDecision = 'allow' | 'deny' | 'ask';
@@ -331,6 +332,17 @@ export interface PreToolUseHookOutput extends BaseHookOutput {
331
332
  */
332
333
  allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;
333
334
  }
335
+ /** Stable identity for replaying a consumed one-shot tool approval. */
336
+ export interface ToolApprovalReplayKey {
337
+ executionScope: string;
338
+ agentId: string;
339
+ toolUseId: string;
340
+ }
341
+ /** Checkpoint-safe copy of a consumed one-shot approval contribution. */
342
+ export interface ToolApprovalReplaySnapshot {
343
+ key: ToolApprovalReplayKey;
344
+ result: AggregatedHookResult;
345
+ }
334
346
  export interface PostToolUseHookOutput extends BaseHookOutput {
335
347
  /**
336
348
  * Replacement tool output. Flows through the aggregated result so the
@@ -1,8 +1,9 @@
1
+ import { BaseMessage } from '@langchain/core/messages';
1
2
  import { Command } from '@langchain/langgraph';
2
- import type { MessageContentComplex, BaseMessage } from '@langchain/core/messages';
3
+ import type { MessageContentComplex } from '@langchain/core/messages';
4
+ import type { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
5
+ import type { StandardGraph } from '@/graphs/Graph';
3
6
  import type * as t from '@/types';
4
- import { MultiAgentGraph } from '@/graphs/MultiAgentGraph';
5
- import { StandardGraph } from '@/graphs/Graph';
6
7
  export declare const defaultOmitOptions: Set<string>;
7
8
  export declare class Run<_T extends t.BaseGraphState> {
8
9
  id: string;
@@ -103,6 +104,7 @@ export declare class Run<_T extends t.BaseGraphState> {
103
104
  private runPreStreamHooks;
104
105
  static create<T extends t.BaseGraphState>(config: t.RunConfig): Promise<Run<T>>;
105
106
  getRunMessages(): BaseMessage[] | undefined;
107
+ getChildCheckpointThreadIds(): string[];
106
108
  /**
107
109
  * Returns a defensive snapshot of tools discovered by the current run.
108
110
  * Pass an agent id for that context, or omit it for the ordered union across
@@ -220,6 +222,7 @@ export declare class Run<_T extends t.BaseGraphState> {
220
222
  */
221
223
  resume<TResume = t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap>(resumeValue: TResume, callerConfig: t.RunStreamConfig, streamOptions?: t.EventStreamOptions, commandOptions?: Pick<ConstructorParameters<typeof Command>[0], 'update' | 'goto'>): Promise<MessageContentComplex[] | undefined>;
222
224
  private resolveInterruptResumeConfig;
225
+ private restoreInterruptFromCheckpoint;
223
226
  private createSystemCallback;
224
227
  getCallbacks(clientCallbacks: t.ClientCallbacks): t.SystemCallbacks;
225
228
  generateTitle({ provider, inputText, contentParts, titlePrompt, clientOptions, chainOptions, skipLanguage, titleMethod, titlePromptTemplate, }: t.RunTitleOptions): Promise<{
@@ -16,6 +16,7 @@ export declare class AgentSession {
16
16
  getLatestCheckpoint(options?: AgentSessionCheckpointLookupOptions): Promise<AgentSessionCheckpointReference | undefined>;
17
17
  private hasCheckpointState;
18
18
  private recordCheckpoint;
19
+ private recordChildCheckpointThreads;
19
20
  private getCheckpointThreadIds;
20
21
  private resetCheckpointThreads;
21
22
  private runInternal;
@@ -2,7 +2,7 @@ import type { JsonSchemaType, LCTool } from '@/types/tools';
2
2
  import type { SubagentConfig } from '@/types';
3
3
  import { Constants } from '@/common';
4
4
  export declare const SubagentToolName = Constants.SUBAGENT;
5
- export declare const SubagentToolDescription = "Delegate a task to a specialized subagent that runs in an isolated context window. The subagent executes independently and returns only its final text result \u2014 all intermediate tool calls, reasoning, and context stay isolated.\n\nWHEN TO USE:\n- The task is self-contained and can be described in a single prompt.\n- You want to offload verbose or exploratory work without bloating your own context.\n- A specialized subagent is available for the task domain.\n\nWHAT HAPPENS:\n- A fresh agent is created with the task description as its only input.\n- The subagent runs to completion using its own tools and context.\n- Only the final text response is returned to you.\n\nCONSTRAINTS:\n- subagent_type must match one of the available types listed below.\n- The subagent cannot see your conversation history.";
5
+ export declare const SubagentToolDescription = "Delegate a task to a specialized subagent or bounded agent team that runs in an isolated context window. The delegated execution returns only its designated final text result \u2014 all intermediate tool calls, reasoning, and context stay isolated.\n\nWHEN TO USE:\n- The task is self-contained and can be described in a single prompt.\n- You want to offload verbose or exploratory work without bloating your own context.\n- A specialized subagent is available for the task domain.\n\nWHAT HAPPENS:\n- A fresh agent or configured agent graph is created with the task description as its only input.\n- The delegated agent or team runs to completion using isolated tools and context.\n- Only the single agent's final response or the graph's designated result-agent response is returned to you.\n\nCONSTRAINTS:\n- subagent_type must match one of the available types listed below.\n- The subagent cannot see your conversation history.";
6
6
  export declare const SubagentToolSchema: {
7
7
  readonly type: "object";
8
8
  readonly properties: {
@@ -3,6 +3,7 @@ import { END, Command, MessagesAnnotation } from '@langchain/langgraph';
3
3
  import type { RunnableConfig } from '@langchain/core/runnables';
4
4
  import type { BaseMessage } from '@langchain/core/messages';
5
5
  import type { ToolOutputResolveView, ResolvedArgsByCallId } from '@/tools/toolOutputReferences';
6
+ import type { SubagentToolNodeResumeState } from '@/tools/subagent/SubagentReplay';
6
7
  import type * as t from '@/types';
7
8
  import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
8
9
  import { RunnableCallable } from '@/utils';
@@ -50,6 +51,8 @@ type RunToolBatchContext<T = unknown> = {
50
51
  * contract for hosts relying on it for policy / recovery guidance.
51
52
  */
52
53
  additionalContextsSink?: string[];
54
+ /** Stable identity of the assistant tool-call batch across HITL replay. */
55
+ replayBatchKey?: string;
53
56
  /**
54
57
  * Graph state the ToolNode was invoked with, threaded from `run()`
55
58
  * so `tool.invoke` can forward it as langgraph 1.4's `runtime.state`
@@ -113,6 +116,10 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
113
116
  * the Run ends.
114
117
  */
115
118
  private directPathTurns;
119
+ /** Terminal results from interrupting siblings that must survive a
120
+ * LangGraph replay of the containing ToolNode. Includes the sidecar data
121
+ * the fresh batch needs for hook-context injection and completion events. */
122
+ private settledInterruptingResults;
116
123
  /** Tool registry for filtering (lazy computation of programmatic maps) */
117
124
  private toolRegistry?;
118
125
  /** Cached programmatic tools (computed once on first PTC call) */
@@ -240,12 +247,11 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
240
247
  private shouldHandleUnknownHandoffLocally;
241
248
  private getUnknownToolErrorMessage;
242
249
  /**
243
- * Flush the per-Run direct-path turn cache. Called by the Graph at
244
- * end-of-Run via `clearHeavyState`. The map intentionally survives
245
- * `run()` re-entry so an interrupt + resume reuses the original
246
- * slot (Codex P2 #30), but it would otherwise grow linearly with
247
- * tool calls and could collide across Runs if a provider reused
248
- * call IDs (Codex P2 #33). Hosts can also call this directly if
250
+ * Flush per-Run direct replay state. Called by the Graph at end-of-Run via
251
+ * `clearHeavyState`. The state intentionally survives `run()` re-entry so
252
+ * interrupt + resume keeps both original turn slots and terminal sibling
253
+ * outputs, but it would otherwise grow linearly and could collide across
254
+ * Runs if a provider reused call IDs. Hosts can also call this directly if
249
255
  * they reuse a ToolNode across batches outside of a Graph.
250
256
  */
251
257
  clearDirectPathTurns(): void;
@@ -259,6 +265,8 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
259
265
  * @returns A ReadonlyMap where keys are tool names and values are their usage counts.
260
266
  */
261
267
  getToolUsageCounts(): ReadonlyMap<string, number>;
268
+ createSubagentResumeState(): SubagentToolNodeResumeState;
269
+ restoreSubagentResumeState(state: SubagentToolNodeResumeState): void;
262
270
  private recordToolUsageTurn;
263
271
  private recordEventToolPlanningTurn;
264
272
  /**
@@ -296,9 +304,9 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
296
304
  * `blockDirectCall`, `respond` returns the host-supplied
297
305
  * `responseText` as a synthetic success ToolMessage,
298
306
  * `edit` re-runs with edited args. LangGraph re-enters
299
- * ToolNode.run from the start on resume; the hook fires
300
- * again and the resume value distinguishes "first ask" from
301
- * "second pass with decision".
307
+ * ToolNode.run from the start on resume. Reusable hooks fire
308
+ * again; a consumed one-shot hook replays its pending approval
309
+ * result. In both cases `interrupt()` consumes the resume value.
302
310
  * • When HITL is off: collapses to a fail-closed deny (matches
303
311
  * the rest of the SDK's HITL-disabled default). One-time
304
312
  * warning logged so hosts notice the gap.
@@ -465,16 +473,16 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
465
473
  * call throws a `GraphInterrupt`, the `await` below rejects and unwinds the
466
474
  * whole ToolNode *before* any non-interrupting sibling has started — so a
467
475
  * sibling with real side effects (send_email, billing) never executes on
468
- * the first pass. On the resume pass LangGraph re-runs the batch from the
469
- * top; the interrupting tool resolves with the host's answer instead of
470
- * throwing, and the siblings execute for the FIRST time, exactly once.
476
+ * the first pass. Terminal interrupting siblings are cached by call id, so
477
+ * LangGraph replay reuses their complete lifecycle output instead of
478
+ * repeating model calls, hooks, or side effects.
471
479
  *
472
480
  * Without this ordering, a flat `Promise.all` starts every sibling
473
481
  * concurrently, so a non-idempotent sibling can complete its side effect
474
482
  * before the interrupt unwinds and then run a SECOND time on resume — the
475
- * duplicate side effect this method exists to prevent. Interrupting tools
476
- * are expected to be side-effect-free (they only suspend), so running them
477
- * as a group and re-running them on resume is harmless.
483
+ * duplicate side effect this method exists to prevent. A tool that actually
484
+ * suspends re-enters until it reaches a terminal result; siblings that
485
+ * already settled do not re-enter.
478
486
  *
479
487
  * `batchIndices[i]` is `directCalls[i]`'s position within the parent
480
488
  * ToolNode batch (used for `{{tool<i>turn<n>}}` registration); it is