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