@librechat/agents 3.1.75 → 3.1.77-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/dist/cjs/graphs/Graph.cjs +22 -3
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hitl/askUserQuestion.cjs +67 -0
  4. package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -0
  5. package/dist/cjs/hooks/HookRegistry.cjs +54 -0
  6. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  7. package/dist/cjs/hooks/createToolPolicyHook.cjs +115 -0
  8. package/dist/cjs/hooks/createToolPolicyHook.cjs.map +1 -0
  9. package/dist/cjs/hooks/executeHooks.cjs +40 -1
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  11. package/dist/cjs/hooks/types.cjs +1 -0
  12. package/dist/cjs/hooks/types.cjs.map +1 -1
  13. package/dist/cjs/langchain/google-common.cjs +3 -0
  14. package/dist/cjs/langchain/google-common.cjs.map +1 -0
  15. package/dist/cjs/langchain/index.cjs +86 -0
  16. package/dist/cjs/langchain/index.cjs.map +1 -0
  17. package/dist/cjs/langchain/language_models/chat_models.cjs +3 -0
  18. package/dist/cjs/langchain/language_models/chat_models.cjs.map +1 -0
  19. package/dist/cjs/langchain/messages/tool.cjs +3 -0
  20. package/dist/cjs/langchain/messages/tool.cjs.map +1 -0
  21. package/dist/cjs/langchain/messages.cjs +51 -0
  22. package/dist/cjs/langchain/messages.cjs.map +1 -0
  23. package/dist/cjs/langchain/openai.cjs +3 -0
  24. package/dist/cjs/langchain/openai.cjs.map +1 -0
  25. package/dist/cjs/langchain/prompts.cjs +11 -0
  26. package/dist/cjs/langchain/prompts.cjs.map +1 -0
  27. package/dist/cjs/langchain/runnables.cjs +19 -0
  28. package/dist/cjs/langchain/runnables.cjs.map +1 -0
  29. package/dist/cjs/langchain/tools.cjs +23 -0
  30. package/dist/cjs/langchain/tools.cjs.map +1 -0
  31. package/dist/cjs/langchain/utils/env.cjs +11 -0
  32. package/dist/cjs/langchain/utils/env.cjs.map +1 -0
  33. package/dist/cjs/llm/anthropic/index.cjs +145 -52
  34. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  35. package/dist/cjs/llm/anthropic/types.cjs.map +1 -1
  36. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +21 -14
  37. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  38. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs +84 -70
  39. package/dist/cjs/llm/anthropic/utils/message_outputs.cjs.map +1 -1
  40. package/dist/cjs/llm/bedrock/index.cjs +1 -1
  41. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  42. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +213 -3
  43. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  44. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs +2 -1
  45. package/dist/cjs/llm/bedrock/utils/message_outputs.cjs.map +1 -1
  46. package/dist/cjs/llm/google/utils/common.cjs +5 -4
  47. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  48. package/dist/cjs/llm/openai/index.cjs +519 -655
  49. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  50. package/dist/cjs/llm/openai/utils/index.cjs +20 -458
  51. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  52. package/dist/cjs/llm/openrouter/index.cjs +57 -175
  53. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  54. package/dist/cjs/llm/vertexai/index.cjs +5 -3
  55. package/dist/cjs/llm/vertexai/index.cjs.map +1 -1
  56. package/dist/cjs/main.cjs +112 -3
  57. package/dist/cjs/main.cjs.map +1 -1
  58. package/dist/cjs/messages/cache.cjs +2 -1
  59. package/dist/cjs/messages/cache.cjs.map +1 -1
  60. package/dist/cjs/messages/core.cjs +7 -6
  61. package/dist/cjs/messages/core.cjs.map +1 -1
  62. package/dist/cjs/messages/format.cjs +73 -15
  63. package/dist/cjs/messages/format.cjs.map +1 -1
  64. package/dist/cjs/messages/langchain.cjs +26 -0
  65. package/dist/cjs/messages/langchain.cjs.map +1 -0
  66. package/dist/cjs/messages/prune.cjs +7 -6
  67. package/dist/cjs/messages/prune.cjs.map +1 -1
  68. package/dist/cjs/run.cjs +400 -42
  69. package/dist/cjs/run.cjs.map +1 -1
  70. package/dist/cjs/tools/ToolNode.cjs +556 -56
  71. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  72. package/dist/cjs/tools/search/search.cjs +55 -66
  73. package/dist/cjs/tools/search/search.cjs.map +1 -1
  74. package/dist/cjs/tools/search/tavily-scraper.cjs +189 -0
  75. package/dist/cjs/tools/search/tavily-scraper.cjs.map +1 -0
  76. package/dist/cjs/tools/search/tavily-search.cjs +372 -0
  77. package/dist/cjs/tools/search/tavily-search.cjs.map +1 -0
  78. package/dist/cjs/tools/search/tool.cjs +26 -4
  79. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  80. package/dist/cjs/tools/search/utils.cjs +10 -3
  81. package/dist/cjs/tools/search/utils.cjs.map +1 -1
  82. package/dist/esm/graphs/Graph.mjs +22 -3
  83. package/dist/esm/graphs/Graph.mjs.map +1 -1
  84. package/dist/esm/hitl/askUserQuestion.mjs +65 -0
  85. package/dist/esm/hitl/askUserQuestion.mjs.map +1 -0
  86. package/dist/esm/hooks/HookRegistry.mjs +54 -0
  87. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  88. package/dist/esm/hooks/createToolPolicyHook.mjs +113 -0
  89. package/dist/esm/hooks/createToolPolicyHook.mjs.map +1 -0
  90. package/dist/esm/hooks/executeHooks.mjs +40 -1
  91. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  92. package/dist/esm/hooks/types.mjs +1 -0
  93. package/dist/esm/hooks/types.mjs.map +1 -1
  94. package/dist/esm/langchain/google-common.mjs +2 -0
  95. package/dist/esm/langchain/google-common.mjs.map +1 -0
  96. package/dist/esm/langchain/index.mjs +5 -0
  97. package/dist/esm/langchain/index.mjs.map +1 -0
  98. package/dist/esm/langchain/language_models/chat_models.mjs +2 -0
  99. package/dist/esm/langchain/language_models/chat_models.mjs.map +1 -0
  100. package/dist/esm/langchain/messages/tool.mjs +2 -0
  101. package/dist/esm/langchain/messages/tool.mjs.map +1 -0
  102. package/dist/esm/langchain/messages.mjs +2 -0
  103. package/dist/esm/langchain/messages.mjs.map +1 -0
  104. package/dist/esm/langchain/openai.mjs +2 -0
  105. package/dist/esm/langchain/openai.mjs.map +1 -0
  106. package/dist/esm/langchain/prompts.mjs +2 -0
  107. package/dist/esm/langchain/prompts.mjs.map +1 -0
  108. package/dist/esm/langchain/runnables.mjs +2 -0
  109. package/dist/esm/langchain/runnables.mjs.map +1 -0
  110. package/dist/esm/langchain/tools.mjs +2 -0
  111. package/dist/esm/langchain/tools.mjs.map +1 -0
  112. package/dist/esm/langchain/utils/env.mjs +2 -0
  113. package/dist/esm/langchain/utils/env.mjs.map +1 -0
  114. package/dist/esm/llm/anthropic/index.mjs +146 -54
  115. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  116. package/dist/esm/llm/anthropic/types.mjs.map +1 -1
  117. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +21 -14
  118. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  119. package/dist/esm/llm/anthropic/utils/message_outputs.mjs +84 -71
  120. package/dist/esm/llm/anthropic/utils/message_outputs.mjs.map +1 -1
  121. package/dist/esm/llm/bedrock/index.mjs +1 -1
  122. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  123. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +214 -4
  124. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  125. package/dist/esm/llm/bedrock/utils/message_outputs.mjs +2 -1
  126. package/dist/esm/llm/bedrock/utils/message_outputs.mjs.map +1 -1
  127. package/dist/esm/llm/google/utils/common.mjs +5 -4
  128. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  129. package/dist/esm/llm/openai/index.mjs +520 -656
  130. package/dist/esm/llm/openai/index.mjs.map +1 -1
  131. package/dist/esm/llm/openai/utils/index.mjs +23 -459
  132. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  133. package/dist/esm/llm/openrouter/index.mjs +57 -175
  134. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  135. package/dist/esm/llm/vertexai/index.mjs +5 -3
  136. package/dist/esm/llm/vertexai/index.mjs.map +1 -1
  137. package/dist/esm/main.mjs +7 -0
  138. package/dist/esm/main.mjs.map +1 -1
  139. package/dist/esm/messages/cache.mjs +2 -1
  140. package/dist/esm/messages/cache.mjs.map +1 -1
  141. package/dist/esm/messages/core.mjs +7 -6
  142. package/dist/esm/messages/core.mjs.map +1 -1
  143. package/dist/esm/messages/format.mjs +73 -15
  144. package/dist/esm/messages/format.mjs.map +1 -1
  145. package/dist/esm/messages/langchain.mjs +23 -0
  146. package/dist/esm/messages/langchain.mjs.map +1 -0
  147. package/dist/esm/messages/prune.mjs +7 -6
  148. package/dist/esm/messages/prune.mjs.map +1 -1
  149. package/dist/esm/run.mjs +400 -42
  150. package/dist/esm/run.mjs.map +1 -1
  151. package/dist/esm/tools/ToolNode.mjs +557 -57
  152. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  153. package/dist/esm/tools/search/search.mjs +55 -66
  154. package/dist/esm/tools/search/search.mjs.map +1 -1
  155. package/dist/esm/tools/search/tavily-scraper.mjs +186 -0
  156. package/dist/esm/tools/search/tavily-scraper.mjs.map +1 -0
  157. package/dist/esm/tools/search/tavily-search.mjs +370 -0
  158. package/dist/esm/tools/search/tavily-search.mjs.map +1 -0
  159. package/dist/esm/tools/search/tool.mjs +26 -4
  160. package/dist/esm/tools/search/tool.mjs.map +1 -1
  161. package/dist/esm/tools/search/utils.mjs +10 -3
  162. package/dist/esm/tools/search/utils.mjs.map +1 -1
  163. package/dist/types/graphs/Graph.d.ts +7 -0
  164. package/dist/types/hitl/askUserQuestion.d.ts +55 -0
  165. package/dist/types/hitl/index.d.ts +6 -0
  166. package/dist/types/hooks/HookRegistry.d.ts +58 -0
  167. package/dist/types/hooks/createToolPolicyHook.d.ts +87 -0
  168. package/dist/types/hooks/index.d.ts +4 -1
  169. package/dist/types/hooks/types.d.ts +109 -3
  170. package/dist/types/index.d.ts +10 -0
  171. package/dist/types/langchain/google-common.d.ts +1 -0
  172. package/dist/types/langchain/index.d.ts +8 -0
  173. package/dist/types/langchain/language_models/chat_models.d.ts +1 -0
  174. package/dist/types/langchain/messages/tool.d.ts +1 -0
  175. package/dist/types/langchain/messages.d.ts +2 -0
  176. package/dist/types/langchain/openai.d.ts +1 -0
  177. package/dist/types/langchain/prompts.d.ts +1 -0
  178. package/dist/types/langchain/runnables.d.ts +2 -0
  179. package/dist/types/langchain/tools.d.ts +2 -0
  180. package/dist/types/langchain/utils/env.d.ts +1 -0
  181. package/dist/types/llm/anthropic/index.d.ts +22 -9
  182. package/dist/types/llm/anthropic/types.d.ts +5 -1
  183. package/dist/types/llm/anthropic/utils/message_outputs.d.ts +13 -6
  184. package/dist/types/llm/anthropic/utils/output_parsers.d.ts +1 -1
  185. package/dist/types/llm/openai/index.d.ts +21 -24
  186. package/dist/types/llm/openrouter/index.d.ts +11 -9
  187. package/dist/types/llm/vertexai/index.d.ts +1 -0
  188. package/dist/types/messages/cache.d.ts +4 -1
  189. package/dist/types/messages/format.d.ts +4 -1
  190. package/dist/types/messages/langchain.d.ts +27 -0
  191. package/dist/types/run.d.ts +117 -1
  192. package/dist/types/tools/ToolNode.d.ts +26 -1
  193. package/dist/types/tools/search/tavily-scraper.d.ts +19 -0
  194. package/dist/types/tools/search/tavily-search.d.ts +4 -0
  195. package/dist/types/tools/search/types.d.ts +99 -5
  196. package/dist/types/tools/search/utils.d.ts +2 -2
  197. package/dist/types/types/graph.d.ts +23 -37
  198. package/dist/types/types/hitl.d.ts +272 -0
  199. package/dist/types/types/index.d.ts +1 -0
  200. package/dist/types/types/llm.d.ts +3 -3
  201. package/dist/types/types/run.d.ts +33 -0
  202. package/dist/types/types/stream.d.ts +1 -1
  203. package/dist/types/types/tools.d.ts +19 -0
  204. package/package.json +80 -17
  205. package/src/graphs/Graph.ts +33 -4
  206. package/src/graphs/__tests__/composition.smoke.test.ts +188 -0
  207. package/src/hitl/askUserQuestion.ts +72 -0
  208. package/src/hitl/index.ts +7 -0
  209. package/src/hooks/HookRegistry.ts +71 -0
  210. package/src/hooks/__tests__/createToolPolicyHook.test.ts +259 -0
  211. package/src/hooks/createToolPolicyHook.ts +184 -0
  212. package/src/hooks/executeHooks.ts +50 -1
  213. package/src/hooks/index.ts +6 -0
  214. package/src/hooks/types.ts +112 -0
  215. package/src/index.ts +22 -0
  216. package/src/langchain/google-common.ts +1 -0
  217. package/src/langchain/index.ts +8 -0
  218. package/src/langchain/language_models/chat_models.ts +1 -0
  219. package/src/langchain/messages/tool.ts +5 -0
  220. package/src/langchain/messages.ts +21 -0
  221. package/src/langchain/openai.ts +1 -0
  222. package/src/langchain/prompts.ts +1 -0
  223. package/src/langchain/runnables.ts +7 -0
  224. package/src/langchain/tools.ts +8 -0
  225. package/src/langchain/utils/env.ts +1 -0
  226. package/src/llm/anthropic/index.ts +252 -84
  227. package/src/llm/anthropic/llm.spec.ts +751 -102
  228. package/src/llm/anthropic/types.ts +9 -1
  229. package/src/llm/anthropic/utils/message_inputs.ts +37 -19
  230. package/src/llm/anthropic/utils/message_outputs.ts +119 -101
  231. package/src/llm/bedrock/index.ts +2 -2
  232. package/src/llm/bedrock/llm.spec.ts +341 -0
  233. package/src/llm/bedrock/utils/message_inputs.ts +303 -4
  234. package/src/llm/bedrock/utils/message_outputs.ts +2 -1
  235. package/src/llm/custom-chat-models.smoke.test.ts +836 -0
  236. package/src/llm/google/llm.spec.ts +339 -57
  237. package/src/llm/google/utils/common.ts +53 -48
  238. package/src/llm/openai/contentBlocks.test.ts +346 -0
  239. package/src/llm/openai/index.ts +856 -833
  240. package/src/llm/openai/utils/index.ts +107 -78
  241. package/src/llm/openai/utils/messages.test.ts +159 -0
  242. package/src/llm/openrouter/index.ts +124 -247
  243. package/src/llm/openrouter/reasoning.test.ts +8 -1
  244. package/src/llm/vertexai/index.ts +11 -5
  245. package/src/llm/vertexai/llm.spec.ts +28 -1
  246. package/src/messages/cache.test.ts +4 -3
  247. package/src/messages/cache.ts +3 -2
  248. package/src/messages/core.ts +16 -9
  249. package/src/messages/format.ts +96 -16
  250. package/src/messages/formatAgentMessages.test.ts +166 -1
  251. package/src/messages/langchain.ts +39 -0
  252. package/src/messages/prune.ts +12 -8
  253. package/src/run.ts +456 -47
  254. package/src/scripts/caching.ts +2 -3
  255. package/src/specs/summarization.test.ts +51 -58
  256. package/src/tools/ToolNode.ts +706 -63
  257. package/src/tools/__tests__/hitl.test.ts +3593 -0
  258. package/src/tools/search/search.ts +83 -73
  259. package/src/tools/search/tavily-scraper.ts +235 -0
  260. package/src/tools/search/tavily-search.ts +424 -0
  261. package/src/tools/search/tavily.test.ts +965 -0
  262. package/src/tools/search/tool.ts +36 -26
  263. package/src/tools/search/types.ts +133 -8
  264. package/src/tools/search/utils.ts +13 -5
  265. package/src/types/graph.ts +32 -87
  266. package/src/types/hitl.ts +303 -0
  267. package/src/types/index.ts +1 -0
  268. package/src/types/llm.ts +3 -3
  269. package/src/types/run.ts +33 -0
  270. package/src/types/stream.ts +1 -1
  271. package/src/types/tools.ts +19 -0
  272. package/src/utils/llmConfig.ts +1 -6
package/dist/cjs/run.cjs CHANGED
@@ -5,6 +5,8 @@ var langchain = require('@langfuse/langchain');
5
5
  var prompts = require('@langchain/core/prompts');
6
6
  var runnables = require('@langchain/core/runnables');
7
7
  var openai = require('@langchain/openai');
8
+ var langgraph = require('@langchain/langgraph');
9
+ var messages = require('@langchain/core/messages');
8
10
  var base = require('@langchain/core/callbacks/base');
9
11
  var title = require('./utils/title.cjs');
10
12
  var tokens = require('./utils/tokens.cjs');
@@ -35,6 +37,7 @@ class Run {
35
37
  tokenCounter;
36
38
  handlerRegistry;
37
39
  hookRegistry;
40
+ humanInTheLoop;
38
41
  toolOutputReferences;
39
42
  indexTokenCountMap;
40
43
  calibrationRatio = 1;
@@ -43,6 +46,15 @@ class Run {
43
46
  returnContent = false;
44
47
  skipCleanup = false;
45
48
  _streamResult;
49
+ /**
50
+ * Captured interrupt payload typed as `unknown` because the SDK
51
+ * does not validate the runtime shape — custom graph nodes can
52
+ * raise interrupts with arbitrary payloads (not just the SDK's
53
+ * `HumanInterruptPayload` union). The public `getInterrupt<T>()`
54
+ * lets callers assert the type they expect.
55
+ */
56
+ _interrupt;
57
+ _haltedReason;
46
58
  constructor(config) {
47
59
  const runId = config.runId ?? '';
48
60
  if (!runId) {
@@ -62,6 +74,7 @@ class Run {
62
74
  }
63
75
  this.handlerRegistry = handlerRegistry;
64
76
  this.hookRegistry = config.hooks;
77
+ this.humanInTheLoop = config.humanInTheLoop;
65
78
  this.toolOutputReferences = config.toolOutputReferences;
66
79
  if (!config.graphConfig) {
67
80
  throw new Error('Graph config not provided');
@@ -123,8 +136,9 @@ class Run {
123
136
  calibrationRatio: this.calibrationRatio,
124
137
  });
125
138
  /** Propagate compile options from graph config */
126
- standardGraph.compileOptions = config.compileOptions;
139
+ standardGraph.compileOptions = this.applyHITLCheckpointerFallback(config.compileOptions);
127
140
  standardGraph.hookRegistry = this.hookRegistry;
141
+ standardGraph.humanInTheLoop = this.humanInTheLoop;
128
142
  standardGraph.toolOutputReferences = this.toolOutputReferences;
129
143
  this.Graph = standardGraph;
130
144
  return standardGraph.createWorkflow();
@@ -139,14 +153,180 @@ class Run {
139
153
  indexTokenCountMap: this.indexTokenCountMap,
140
154
  calibrationRatio: this.calibrationRatio,
141
155
  });
142
- if (compileOptions != null) {
143
- multiAgentGraph.compileOptions = compileOptions;
144
- }
156
+ multiAgentGraph.compileOptions =
157
+ this.applyHITLCheckpointerFallback(compileOptions);
145
158
  multiAgentGraph.hookRegistry = this.hookRegistry;
159
+ multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
146
160
  multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
147
161
  this.Graph = multiAgentGraph;
148
162
  return multiAgentGraph.createWorkflow();
149
163
  }
164
+ /**
165
+ * When the host opted into HITL via `humanInTheLoop: { enabled: true }`
166
+ * and did not supply a checkpointer, install an in-memory `MemorySaver`
167
+ * so `interrupt()` can persist checkpoints and `Command({ resume })`
168
+ * can rebuild state. The fallback is intentionally process-local:
169
+ * production hosts that need durable resumption across processes /
170
+ * restarts must provide their own checkpointer (Redis, Postgres, etc.)
171
+ * on `compileOptions.checkpointer`.
172
+ *
173
+ * No-op when HITL is off (the default — omitted, or
174
+ * `{ enabled: false }`) or the host already supplied a checkpointer
175
+ * of their own. See `HumanInTheLoopConfig` JSDoc for the rationale
176
+ * behind the default-off stance.
177
+ */
178
+ applyHITLCheckpointerFallback(compileOptions) {
179
+ if (this.humanInTheLoop?.enabled !== true) {
180
+ return compileOptions;
181
+ }
182
+ if (compileOptions?.checkpointer != null) {
183
+ return compileOptions;
184
+ }
185
+ return {
186
+ ...(compileOptions ?? {}),
187
+ checkpointer: new langgraph.MemorySaver(),
188
+ };
189
+ }
190
+ /**
191
+ * Run RunStart + UserPromptSubmit hooks before the graph stream
192
+ * begins, accumulate any `additionalContext` strings into the input
193
+ * messages, and short-circuit when a hook signals the run should not
194
+ * proceed (deny / ask decision on the prompt, or `preventContinuation`
195
+ * on either hook).
196
+ *
197
+ * Returns `true` when the caller should bail with `undefined` (run
198
+ * was halted before any model call); returns `false` to proceed
199
+ * into the stream loop.
200
+ *
201
+ * ## Side effects
202
+ *
203
+ * On the success path:
204
+ * - Mutates `stateInputs.messages` in place to append a
205
+ * consolidated `HumanMessage` carrying any hook
206
+ * `additionalContext` strings. Safe because the host owns the
207
+ * array and `processStream` is the only consumer until LangGraph
208
+ * reads it.
209
+ *
210
+ * On the halt path (returning `true`):
211
+ * - Sets `this._haltedReason` so callers (and the eventual host)
212
+ * can distinguish a hook-driven halt from a natural completion.
213
+ * - Calls `registry.clearSession(this.id)` and
214
+ * `registry.clearHaltSignal(this.id)` because no resume is
215
+ * expected from a pre-stream halt — the run never entered the
216
+ * graph, so the session/halt state for this run would otherwise
217
+ * leak to the next `processStream` invocation on the same
218
+ * registry. Other concurrent runs on the same registry are
219
+ * untouched (halt signals are scoped per session id).
220
+ * - Sets `config.callbacks = undefined` to drop the callback
221
+ * references the caller built (langfuse handler, custom event
222
+ * handler, etc.) since they won't be exercised. Mirrors the
223
+ * equivalent cleanup the `processStream` `finally` block does
224
+ * on the natural-completion path.
225
+ */
226
+ async runPreStreamHooks(stateInputs, threadId, config) {
227
+ const registry = this.hookRegistry;
228
+ /**
229
+ * Defensive guard: `processStream` already validated `this.Graph`
230
+ * before calling this helper, but TypeScript can't propagate that
231
+ * narrowing across method boundaries. The check keeps the body
232
+ * free of `this.Graph!` non-null assertions.
233
+ */
234
+ if (registry == null || this.Graph == null) {
235
+ return false;
236
+ }
237
+ const preStreamContexts = [];
238
+ const runStartResult = await executeHooks.executeHooks({
239
+ registry,
240
+ input: {
241
+ hook_event_name: 'RunStart',
242
+ runId: this.id,
243
+ threadId,
244
+ agentId: this.Graph.defaultAgentId,
245
+ messages: stateInputs.messages,
246
+ },
247
+ sessionId: this.id,
248
+ });
249
+ for (const ctx of runStartResult.additionalContexts) {
250
+ preStreamContexts.push(ctx);
251
+ }
252
+ /**
253
+ * Honor `preventContinuation` from RunStart before the stream
254
+ * starts. Mid-flight halts (from tool/compact/subagent hooks)
255
+ * route through `HookRegistry.haltRun` and are polled by the
256
+ * stream loop in `processStream` — different mechanism, same
257
+ * intent.
258
+ */
259
+ if (runStartResult.preventContinuation === true) {
260
+ this._haltedReason = runStartResult.stopReason ?? 'preventContinuation';
261
+ registry.clearSession(this.id);
262
+ registry.clearHaltSignal(this.id);
263
+ config.callbacks = undefined;
264
+ return true;
265
+ }
266
+ const lastHuman = findLastMessageOfType(stateInputs.messages, 'human');
267
+ if (lastHuman != null) {
268
+ const promptResult = await executeHooks.executeHooks({
269
+ registry,
270
+ input: {
271
+ hook_event_name: 'UserPromptSubmit',
272
+ runId: this.id,
273
+ threadId,
274
+ agentId: this.Graph.defaultAgentId,
275
+ prompt: extractPromptText(lastHuman),
276
+ // attachments: not yet wired — Phase 2 will extract
277
+ // non-text content blocks (images, files) from messages
278
+ },
279
+ sessionId: this.id,
280
+ });
281
+ if (promptResult.decision === 'deny' ||
282
+ promptResult.decision === 'ask' ||
283
+ promptResult.preventContinuation === true) {
284
+ /**
285
+ * Always set `_haltedReason` so the host can call
286
+ * `getHaltReason()` and distinguish a hook-blocked prompt
287
+ * from a natural empty-output completion. Three signals can
288
+ * land here, each with its own canonical reason string when
289
+ * the hook didn't supply one.
290
+ */
291
+ if (promptResult.preventContinuation === true) {
292
+ this._haltedReason = promptResult.stopReason ?? 'preventContinuation';
293
+ }
294
+ else if (promptResult.decision === 'deny') {
295
+ this._haltedReason = promptResult.reason ?? 'prompt_denied';
296
+ }
297
+ else {
298
+ this._haltedReason =
299
+ promptResult.reason ?? 'prompt_requires_approval';
300
+ }
301
+ registry.clearSession(this.id);
302
+ registry.clearHaltSignal(this.id);
303
+ config.callbacks = undefined;
304
+ return true;
305
+ }
306
+ for (const ctx of promptResult.additionalContexts) {
307
+ preStreamContexts.push(ctx);
308
+ }
309
+ }
310
+ if (preStreamContexts.length > 0) {
311
+ /**
312
+ * Wraps the joined hook contexts as a `HumanMessage` even though
313
+ * the intent is system-level guidance. Using a `SystemMessage`
314
+ * mid-conversation is rejected by Anthropic and Google providers
315
+ * (system messages must be the leading entry), so the LangChain
316
+ * convention — also used by `ToolNode.convertInjectedMessages`
317
+ * — is `HumanMessage` carrying `additional_kwargs.role` as a
318
+ * marker for hosts inspecting state. The model still sees a
319
+ * user-role message; the `role: 'system'` field is metadata
320
+ * only. Hosts that want a true system message should compose
321
+ * it into the agent's `instructions` config instead.
322
+ */
323
+ stateInputs.messages.push(new messages.HumanMessage({
324
+ content: preStreamContexts.join('\n\n'),
325
+ additional_kwargs: { role: 'system', source: 'hook' },
326
+ }));
327
+ }
328
+ return false;
329
+ }
150
330
  static async create(config) {
151
331
  /** Create tokenCounter if indexTokenCountMap is provided but tokenCounter is not */
152
332
  if (config.indexTokenCountMap && !config.tokenCounter) {
@@ -205,12 +385,37 @@ class Run {
205
385
  if (!this.Graph) {
206
386
  throw new Error('Graph not initialized. Make sure to use Run.create() to instantiate the Run.');
207
387
  }
388
+ /**
389
+ * `Command` inputs (currently only `Command({ resume })`) are
390
+ * resume-mode invocations: LangGraph rebuilds graph state from the
391
+ * checkpointer, so we skip RunStart / UserPromptSubmit hooks (no
392
+ * new prompt to evaluate) and read run-state from the Graph wrapper
393
+ * instead of `inputs.messages`.
394
+ */
395
+ const isResume = inputs instanceof langgraph.Command;
396
+ const stateInputs = isResume ? undefined : inputs;
208
397
  const config = {
209
398
  recursionLimit: 50,
210
399
  ...callerConfig,
211
400
  configurable: { ...callerConfig.configurable },
212
401
  };
213
- this.Graph.resetValues(streamOptions?.keepContent);
402
+ /**
403
+ * Skip `resetValues` on resume — we're continuing an in-flight
404
+ * run, not starting a fresh one. Resetting would wipe the
405
+ * sidecars (`toolCallStepIds`, `stepKeyIds`, accumulated
406
+ * `messages`, etc.) the resumed `ToolNode` needs to dispatch
407
+ * tool completions with the correct step ids and re-resolve
408
+ * `{{tool<i>turn<n>}}` references. Pairs with the
409
+ * `awaitingResume` gate on `clearHeavyState` in the `finally`
410
+ * block so the sidecars survive both ends of the interrupt
411
+ * boundary.
412
+ */
413
+ if (!isResume) {
414
+ this.Graph.resetValues(streamOptions?.keepContent);
415
+ }
416
+ this._interrupt = undefined;
417
+ this._haltedReason = undefined;
418
+ this.hookRegistry?.clearHaltSignal(this.id);
214
419
  /** Custom event callback to intercept and handle custom events */
215
420
  const customEventCallback = this.createCustomEventCallback();
216
421
  const baseCallbacks = config.callbacks ?? [];
@@ -250,41 +455,18 @@ class Run {
250
455
  run_id: this.id,
251
456
  });
252
457
  const threadId = config.configurable.thread_id;
253
- if (this.hookRegistry != null) {
254
- await executeHooks.executeHooks({
255
- registry: this.hookRegistry,
256
- input: {
257
- hook_event_name: 'RunStart',
258
- runId: this.id,
259
- threadId,
260
- agentId: this.Graph.defaultAgentId,
261
- messages: inputs.messages,
262
- },
263
- sessionId: this.id,
264
- });
265
- const lastHuman = findLastMessageOfType(inputs.messages, 'human');
266
- if (lastHuman != null) {
267
- const promptResult = await executeHooks.executeHooks({
268
- registry: this.hookRegistry,
269
- input: {
270
- hook_event_name: 'UserPromptSubmit',
271
- runId: this.id,
272
- threadId,
273
- agentId: this.Graph.defaultAgentId,
274
- prompt: extractPromptText(lastHuman),
275
- // attachments: not yet wired — Phase 2 will extract
276
- // non-text content blocks (images, files) from messages
277
- },
278
- sessionId: this.id,
279
- });
280
- if (promptResult.decision === 'deny' ||
281
- promptResult.decision === 'ask') {
282
- this.hookRegistry.clearSession(this.id);
283
- config.callbacks = undefined;
284
- return undefined;
285
- }
458
+ if (this.hookRegistry != null && stateInputs != null) {
459
+ const shouldHalt = await this.runPreStreamHooks(stateInputs, threadId, config);
460
+ if (shouldHalt) {
461
+ return undefined;
286
462
  }
287
463
  }
464
+ /**
465
+ * `streamEvents` accepts both state inputs and `Command` (resume) at
466
+ * runtime, but our `CompiledStateWorkflow` type narrows the first
467
+ * arg to `BaseGraphState`. Cast on the call so the resume path
468
+ * type-checks without widening the wrapper for every caller.
469
+ */
288
470
  const stream = this.graphRunnable.streamEvents(inputs, config, {
289
471
  raiseError: true,
290
472
  /**
@@ -297,6 +479,16 @@ class Run {
297
479
  */
298
480
  ignoreCustomEvent: true,
299
481
  });
482
+ /**
483
+ * Tracks whether the stream loop threw. Used by the `finally`
484
+ * block to decide whether to honor the interrupt-preservation
485
+ * guard for session hooks: a captured `_interrupt` is only
486
+ * meaningful if the stream completed cleanly. If the loop errored
487
+ * after stashing an interrupt (e.g. a downstream handler throws
488
+ * after the interrupt event landed), the interrupt is stale —
489
+ * preserving session hooks would leak them into the next run.
490
+ */
491
+ let streamThrew = false;
300
492
  try {
301
493
  for await (const event of stream) {
302
494
  const { data, metadata, ...info } = event;
@@ -305,12 +497,74 @@ class Run {
305
497
  if (eventName === _enum.GraphEvents.ON_CUSTOM_EVENT) {
306
498
  continue;
307
499
  }
500
+ /**
501
+ * Detect interrupts surfaced by LangGraph as a synthetic
502
+ * `__interrupt__` field on the streamed chunk and stash the
503
+ * first one for the host to read via `run.getInterrupt()`
504
+ * once the stream drains. Captured as `unknown` because the
505
+ * SDK does not validate the runtime payload shape — the
506
+ * built-in ToolNode raises a `HumanInterruptPayload`
507
+ * (`tool_approval` / `ask_user_question`), but custom nodes
508
+ * can pass any payload to `interrupt()`. Callers narrow with
509
+ * the `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt`
510
+ * guards or assert via `getInterrupt<T>()`.
511
+ */
512
+ if (this._interrupt == null &&
513
+ data.chunk != null &&
514
+ langgraph.isInterrupted(data.chunk)) {
515
+ const interrupts = data.chunk[langgraph.INTERRUPT];
516
+ if (interrupts.length > 0) {
517
+ const first = interrupts[0];
518
+ /**
519
+ * Capture the interrupt unconditionally — `interrupt(null)`
520
+ * and `interrupt(undefined)` are valid pauses (a custom
521
+ * node may want to pause without metadata) and the host
522
+ * still needs to know the run is awaiting resume. Gating
523
+ * on `payload != null` would silently downgrade a paused
524
+ * run to "completed" and let the `Stop` hook fire,
525
+ * breaking host resume handling.
526
+ */
527
+ this._interrupt = {
528
+ interruptId: first.id ?? '',
529
+ threadId,
530
+ payload: first.value,
531
+ };
532
+ }
533
+ }
308
534
  const handler = this.handlerRegistry?.getHandler(eventName);
309
535
  if (handler) {
310
536
  await handler.handle(eventName, data, metadata, this.Graph);
311
537
  }
538
+ /**
539
+ * Mid-flight halt: any hook (PreToolUse, PostToolUse,
540
+ * PostToolBatch, SubagentStart/Stop, PreCompact, PostCompact)
541
+ * that returned `preventContinuation: true` raises a halt
542
+ * signal on the registry via `executeHooks`. We poll between
543
+ * stream events and break out as soon as one is set so the
544
+ * graph doesn't take another model turn after the halting
545
+ * operation completes.
546
+ *
547
+ * Limitation: the current step (in-flight model call, ongoing
548
+ * tool batch) is not aborted — only the next step is skipped.
549
+ * This matches Claude Code's `continue: false` semantic where
550
+ * the active operation finishes before halting takes effect.
551
+ */
552
+ const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
553
+ if (haltSignal != null) {
554
+ this._haltedReason = haltSignal.reason;
555
+ break;
556
+ }
312
557
  }
313
- if (this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
558
+ /**
559
+ * Skip the Stop hook when the run paused on a HITL interrupt
560
+ * (still pending human input) or was halted by a hook (the host
561
+ * already chose to stop, so a Stop hook firing now would be
562
+ * misleading). The host fires Stop on the resumed-and-completed
563
+ * run instead.
564
+ */
565
+ if (this._interrupt == null &&
566
+ this._haltedReason == null &&
567
+ this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
314
568
  await executeHooks.executeHooks({
315
569
  registry: this.hookRegistry,
316
570
  input: {
@@ -318,7 +572,7 @@ class Run {
318
572
  runId: this.id,
319
573
  threadId,
320
574
  agentId: this.Graph.defaultAgentId,
321
- messages: this.Graph.getRunMessages() ?? inputs.messages,
575
+ messages: this.Graph.getRunMessages() ?? stateInputs?.messages ?? [],
322
576
  stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
323
577
  },
324
578
  sessionId: this.id,
@@ -328,6 +582,7 @@ class Run {
328
582
  }
329
583
  }
330
584
  catch (err) {
585
+ streamThrew = true;
331
586
  if (this.hookRegistry?.hasHookFor('StopFailure', this.id) === true) {
332
587
  const runMessages = this.Graph.getRunMessages() ?? [];
333
588
  await executeHooks.executeHooks({
@@ -348,7 +603,34 @@ class Run {
348
603
  throw err;
349
604
  }
350
605
  finally {
351
- this.hookRegistry?.clearSession(this.id);
606
+ /**
607
+ * Preserve session-scoped hooks when the run paused on a HITL
608
+ * interrupt — the very next call will be `Run.resume()`, which
609
+ * needs the same policy hooks (e.g., the `PreToolUse` matcher
610
+ * that triggered the interrupt) to fire on the re-executed node
611
+ * and uphold the approval flow. Clearing here would leak the
612
+ * approval gate on resume. The session is cleared instead at
613
+ * natural completion, error (including errors that happen AFTER
614
+ * an interrupt was captured — those interrupts are stale), or
615
+ * hook-driven halt (including hooks that returned BOTH `ask`
616
+ * and `preventContinuation` — the halt wins, no resume is
617
+ * expected, sessions must drop). Every state where no resume
618
+ * is expected clears.
619
+ */
620
+ if (this._interrupt == null ||
621
+ this._haltedReason != null ||
622
+ streamThrew) {
623
+ this.hookRegistry?.clearSession(this.id);
624
+ }
625
+ /**
626
+ * Drop any halt signal raised mid-stream for this run so a
627
+ * subsequent `processStream` / `resume` starts with clean state.
628
+ * The Run captured `_haltedReason` already; the registry entry
629
+ * for this `sessionId` would otherwise spuriously trip the next
630
+ * loop. Other concurrent runs sharing this registry are
631
+ * unaffected — their entries live under their own session ids.
632
+ */
633
+ this.hookRegistry?.clearHaltSignal(this.id);
352
634
  /**
353
635
  * Break the reference chain that keeps heavy data alive via
354
636
  * LangGraph's internal `__pregel_scratchpad.currentTaskInput` →
@@ -377,13 +659,89 @@ class Run {
377
659
  ? this.Graph.getContentParts()
378
660
  : undefined;
379
661
  this.calibrationRatio = this.Graph.getCalibrationRatio();
380
- if (!this.skipCleanup) {
662
+ /**
663
+ * Skip `clearHeavyState()` when the run paused on a clean HITL
664
+ * interrupt awaiting resume — `Run.resume()` re-enters the same
665
+ * `ToolNode` instance and needs the sidecars `clearHeavyState`
666
+ * would wipe (`toolCallStepIds` for completion-event step ids,
667
+ * the `_toolOutputRegistry` for `{{tool<i>turn<n>}}`
668
+ * substitutions, `sessions` for code-env continuity, plus the
669
+ * `hookRegistry` and `humanInTheLoop` config the interrupt
670
+ * branch itself relies on). Without preservation, the resumed
671
+ * tool completion would dispatch `ON_RUN_STEP_COMPLETED` with
672
+ * an empty step id and downstream stream consumers would drop
673
+ * the result.
674
+ *
675
+ * The natural-completion / error / hook-driven-halt paths still
676
+ * clean up — `_haltedReason != null` or `streamThrew` mean no
677
+ * resume is expected. Cross-process resume (host rebuilds the
678
+ * Run from scratch) is a separate concern; see
679
+ * `HumanInTheLoopConfig` JSDoc.
680
+ */
681
+ const awaitingResume = this._interrupt != null && this._haltedReason == null && !streamThrew;
682
+ if (!this.skipCleanup && !awaitingResume) {
381
683
  this.Graph.clearHeavyState();
382
684
  }
383
685
  this._streamResult = result;
384
686
  }
385
687
  return this._streamResult;
386
688
  }
689
+ /**
690
+ * Returns the pending interrupt captured during the most recent
691
+ * `processStream` (or `resume`) invocation. `undefined` when the run
692
+ * either has not been streamed yet or completed without pausing.
693
+ *
694
+ * Hosts call this immediately after `processStream` returns to decide
695
+ * whether the run is awaiting human input. Persist the returned
696
+ * descriptor (alongside `thread_id` and the agent run config) so a
697
+ * later `resume(decisions)` can rebuild the run.
698
+ *
699
+ * The default `TPayload` is the SDK's `HumanInterruptPayload` union
700
+ * (`tool_approval` / `ask_user_question`), suitable for the common
701
+ * case where interrupts come from the built-in ToolNode or
702
+ * `askUserQuestion()` helper. Hosts that raise custom interrupts
703
+ * from custom graph nodes pass their own type — the SDK does not
704
+ * validate the runtime shape, it just transports whatever the
705
+ * `interrupt()` call carried. When in doubt, narrow with the
706
+ * `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt` type
707
+ * guards (which accept `unknown`) before reading variant-specific
708
+ * fields.
709
+ */
710
+ getInterrupt() {
711
+ return this._interrupt;
712
+ }
713
+ /**
714
+ * Returns the reason a hook halted the run via
715
+ * `preventContinuation: true`, or `undefined` if no hook halted.
716
+ *
717
+ * Hosts inspect this after `processStream` returns to distinguish a
718
+ * natural completion (`undefined`) from a hook-driven halt (a
719
+ * truthy string). Independent from `getInterrupt()` — a halted run
720
+ * has no interrupt; an interrupted run has no halt reason.
721
+ */
722
+ getHaltReason() {
723
+ return this._haltedReason;
724
+ }
725
+ /**
726
+ * Resume a paused HITL run with the value the user (or whatever
727
+ * decided the interrupt) supplied. The default `TResume` covers the
728
+ * `tool_approval` interrupt (the common case): an array of decisions
729
+ * in `action_requests` order, or a record keyed by `tool_call_id`.
730
+ *
731
+ * For other interrupt types (e.g., `ask_user_question` →
732
+ * `AskUserQuestionResolution`, or any custom interrupt a host raises
733
+ * from a custom node), pass the type parameter and the SDK forwards
734
+ * the value through unchanged. LangGraph delivers it as the return
735
+ * value of the original `interrupt()` call inside the paused node.
736
+ *
737
+ * The host MUST construct this Run with the same `thread_id` and the
738
+ * same checkpointer as the original paused run; LangGraph rebuilds
739
+ * graph state from the checkpoint and re-enters the interrupted node
740
+ * from the start.
741
+ */
742
+ async resume(resumeValue, callerConfig, streamOptions) {
743
+ return this.processStream(new langgraph.Command({ resume: resumeValue }), callerConfig, streamOptions);
744
+ }
387
745
  createSystemCallback(clientCallbacks, key) {
388
746
  return ((...args) => {
389
747
  const clientCallback = clientCallbacks[key];