@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/src/run.ts CHANGED
@@ -4,6 +4,13 @@ import { CallbackHandler } from '@langfuse/langchain';
4
4
  import { PromptTemplate } from '@langchain/core/prompts';
5
5
  import { RunnableLambda } from '@langchain/core/runnables';
6
6
  import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
7
+ import {
8
+ Command,
9
+ INTERRUPT,
10
+ MemorySaver,
11
+ isInterrupted,
12
+ } from '@langchain/langgraph';
13
+ import { HumanMessage } from '@langchain/core/messages';
7
14
  import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
15
  import type {
9
16
  MessageContentComplex,
@@ -45,6 +52,7 @@ export class Run<_T extends t.BaseGraphState> {
45
52
  private tokenCounter?: t.TokenCounter;
46
53
  private handlerRegistry?: HandlerRegistry;
47
54
  private hookRegistry?: HookRegistry;
55
+ private humanInTheLoop?: t.HumanInTheLoopConfig;
48
56
  private toolOutputReferences?: t.ToolOutputReferencesConfig;
49
57
  private indexTokenCountMap?: Record<string, number>;
50
58
  calibrationRatio: number = 1;
@@ -53,6 +61,15 @@ export class Run<_T extends t.BaseGraphState> {
53
61
  returnContent: boolean = false;
54
62
  private skipCleanup: boolean = false;
55
63
  private _streamResult: t.MessageContentComplex[] | undefined;
64
+ /**
65
+ * Captured interrupt payload typed as `unknown` because the SDK
66
+ * does not validate the runtime shape — custom graph nodes can
67
+ * raise interrupts with arbitrary payloads (not just the SDK's
68
+ * `HumanInterruptPayload` union). The public `getInterrupt<T>()`
69
+ * lets callers assert the type they expect.
70
+ */
71
+ private _interrupt: t.RunInterruptResult<unknown> | undefined;
72
+ private _haltedReason: string | undefined;
56
73
 
57
74
  private constructor(config: Partial<t.RunConfig>) {
58
75
  const runId = config.runId ?? '';
@@ -79,6 +96,7 @@ export class Run<_T extends t.BaseGraphState> {
79
96
 
80
97
  this.handlerRegistry = handlerRegistry;
81
98
  this.hookRegistry = config.hooks;
99
+ this.humanInTheLoop = config.humanInTheLoop;
82
100
  this.toolOutputReferences = config.toolOutputReferences;
83
101
 
84
102
  if (!config.graphConfig) {
@@ -154,8 +172,11 @@ export class Run<_T extends t.BaseGraphState> {
154
172
  calibrationRatio: this.calibrationRatio,
155
173
  });
156
174
  /** Propagate compile options from graph config */
157
- standardGraph.compileOptions = config.compileOptions;
175
+ standardGraph.compileOptions = this.applyHITLCheckpointerFallback(
176
+ config.compileOptions
177
+ );
158
178
  standardGraph.hookRegistry = this.hookRegistry;
179
+ standardGraph.humanInTheLoop = this.humanInTheLoop;
159
180
  standardGraph.toolOutputReferences = this.toolOutputReferences;
160
181
  this.Graph = standardGraph;
161
182
  return standardGraph.createWorkflow();
@@ -175,16 +196,197 @@ export class Run<_T extends t.BaseGraphState> {
175
196
  calibrationRatio: this.calibrationRatio,
176
197
  });
177
198
 
178
- if (compileOptions != null) {
179
- multiAgentGraph.compileOptions = compileOptions;
180
- }
199
+ multiAgentGraph.compileOptions =
200
+ this.applyHITLCheckpointerFallback(compileOptions);
181
201
 
182
202
  multiAgentGraph.hookRegistry = this.hookRegistry;
203
+ multiAgentGraph.humanInTheLoop = this.humanInTheLoop;
183
204
  multiAgentGraph.toolOutputReferences = this.toolOutputReferences;
184
205
  this.Graph = multiAgentGraph;
185
206
  return multiAgentGraph.createWorkflow();
186
207
  }
187
208
 
209
+ /**
210
+ * When the host opted into HITL via `humanInTheLoop: { enabled: true }`
211
+ * and did not supply a checkpointer, install an in-memory `MemorySaver`
212
+ * so `interrupt()` can persist checkpoints and `Command({ resume })`
213
+ * can rebuild state. The fallback is intentionally process-local:
214
+ * production hosts that need durable resumption across processes /
215
+ * restarts must provide their own checkpointer (Redis, Postgres, etc.)
216
+ * on `compileOptions.checkpointer`.
217
+ *
218
+ * No-op when HITL is off (the default — omitted, or
219
+ * `{ enabled: false }`) or the host already supplied a checkpointer
220
+ * of their own. See `HumanInTheLoopConfig` JSDoc for the rationale
221
+ * behind the default-off stance.
222
+ */
223
+ private applyHITLCheckpointerFallback(
224
+ compileOptions: t.CompileOptions | undefined
225
+ ): t.CompileOptions | undefined {
226
+ if (this.humanInTheLoop?.enabled !== true) {
227
+ return compileOptions;
228
+ }
229
+ if (compileOptions?.checkpointer != null) {
230
+ return compileOptions;
231
+ }
232
+ return {
233
+ ...(compileOptions ?? {}),
234
+ checkpointer: new MemorySaver(),
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Run RunStart + UserPromptSubmit hooks before the graph stream
240
+ * begins, accumulate any `additionalContext` strings into the input
241
+ * messages, and short-circuit when a hook signals the run should not
242
+ * proceed (deny / ask decision on the prompt, or `preventContinuation`
243
+ * on either hook).
244
+ *
245
+ * Returns `true` when the caller should bail with `undefined` (run
246
+ * was halted before any model call); returns `false` to proceed
247
+ * into the stream loop.
248
+ *
249
+ * ## Side effects
250
+ *
251
+ * On the success path:
252
+ * - Mutates `stateInputs.messages` in place to append a
253
+ * consolidated `HumanMessage` carrying any hook
254
+ * `additionalContext` strings. Safe because the host owns the
255
+ * array and `processStream` is the only consumer until LangGraph
256
+ * reads it.
257
+ *
258
+ * On the halt path (returning `true`):
259
+ * - Sets `this._haltedReason` so callers (and the eventual host)
260
+ * can distinguish a hook-driven halt from a natural completion.
261
+ * - Calls `registry.clearSession(this.id)` and
262
+ * `registry.clearHaltSignal(this.id)` because no resume is
263
+ * expected from a pre-stream halt — the run never entered the
264
+ * graph, so the session/halt state for this run would otherwise
265
+ * leak to the next `processStream` invocation on the same
266
+ * registry. Other concurrent runs on the same registry are
267
+ * untouched (halt signals are scoped per session id).
268
+ * - Sets `config.callbacks = undefined` to drop the callback
269
+ * references the caller built (langfuse handler, custom event
270
+ * handler, etc.) since they won't be exercised. Mirrors the
271
+ * equivalent cleanup the `processStream` `finally` block does
272
+ * on the natural-completion path.
273
+ */
274
+ private async runPreStreamHooks(
275
+ stateInputs: t.IState,
276
+ threadId: string | undefined,
277
+ config: Partial<RunnableConfig>
278
+ ): Promise<boolean> {
279
+ const registry = this.hookRegistry;
280
+ /**
281
+ * Defensive guard: `processStream` already validated `this.Graph`
282
+ * before calling this helper, but TypeScript can't propagate that
283
+ * narrowing across method boundaries. The check keeps the body
284
+ * free of `this.Graph!` non-null assertions.
285
+ */
286
+ if (registry == null || this.Graph == null) {
287
+ return false;
288
+ }
289
+
290
+ const preStreamContexts: string[] = [];
291
+
292
+ const runStartResult = await executeHooks({
293
+ registry,
294
+ input: {
295
+ hook_event_name: 'RunStart',
296
+ runId: this.id,
297
+ threadId,
298
+ agentId: this.Graph.defaultAgentId,
299
+ messages: stateInputs.messages,
300
+ },
301
+ sessionId: this.id,
302
+ });
303
+ for (const ctx of runStartResult.additionalContexts) {
304
+ preStreamContexts.push(ctx);
305
+ }
306
+ /**
307
+ * Honor `preventContinuation` from RunStart before the stream
308
+ * starts. Mid-flight halts (from tool/compact/subagent hooks)
309
+ * route through `HookRegistry.haltRun` and are polled by the
310
+ * stream loop in `processStream` — different mechanism, same
311
+ * intent.
312
+ */
313
+ if (runStartResult.preventContinuation === true) {
314
+ this._haltedReason = runStartResult.stopReason ?? 'preventContinuation';
315
+ registry.clearSession(this.id);
316
+ registry.clearHaltSignal(this.id);
317
+ config.callbacks = undefined;
318
+ return true;
319
+ }
320
+
321
+ const lastHuman = findLastMessageOfType(stateInputs.messages, 'human');
322
+ if (lastHuman != null) {
323
+ const promptResult = await executeHooks({
324
+ registry,
325
+ input: {
326
+ hook_event_name: 'UserPromptSubmit',
327
+ runId: this.id,
328
+ threadId,
329
+ agentId: this.Graph.defaultAgentId,
330
+ prompt: extractPromptText(lastHuman),
331
+ // attachments: not yet wired — Phase 2 will extract
332
+ // non-text content blocks (images, files) from messages
333
+ },
334
+ sessionId: this.id,
335
+ });
336
+ if (
337
+ promptResult.decision === 'deny' ||
338
+ promptResult.decision === 'ask' ||
339
+ promptResult.preventContinuation === true
340
+ ) {
341
+ /**
342
+ * Always set `_haltedReason` so the host can call
343
+ * `getHaltReason()` and distinguish a hook-blocked prompt
344
+ * from a natural empty-output completion. Three signals can
345
+ * land here, each with its own canonical reason string when
346
+ * the hook didn't supply one.
347
+ */
348
+ if (promptResult.preventContinuation === true) {
349
+ this._haltedReason = promptResult.stopReason ?? 'preventContinuation';
350
+ } else if (promptResult.decision === 'deny') {
351
+ this._haltedReason = promptResult.reason ?? 'prompt_denied';
352
+ } else {
353
+ this._haltedReason =
354
+ promptResult.reason ?? 'prompt_requires_approval';
355
+ }
356
+ registry.clearSession(this.id);
357
+ registry.clearHaltSignal(this.id);
358
+ config.callbacks = undefined;
359
+ return true;
360
+ }
361
+ for (const ctx of promptResult.additionalContexts) {
362
+ preStreamContexts.push(ctx);
363
+ }
364
+ }
365
+
366
+ if (preStreamContexts.length > 0) {
367
+ /**
368
+ * Wraps the joined hook contexts as a `HumanMessage` even though
369
+ * the intent is system-level guidance. Using a `SystemMessage`
370
+ * mid-conversation is rejected by Anthropic and Google providers
371
+ * (system messages must be the leading entry), so the LangChain
372
+ * convention — also used by `ToolNode.convertInjectedMessages`
373
+ * — is `HumanMessage` carrying `additional_kwargs.role` as a
374
+ * marker for hosts inspecting state. The model still sees a
375
+ * user-role message; the `role: 'system'` field is metadata
376
+ * only. Hosts that want a true system message should compose
377
+ * it into the agent's `instructions` config instead.
378
+ */
379
+ stateInputs.messages.push(
380
+ new HumanMessage({
381
+ content: preStreamContexts.join('\n\n'),
382
+ additional_kwargs: { role: 'system', source: 'hook' },
383
+ })
384
+ );
385
+ }
386
+
387
+ return false;
388
+ }
389
+
188
390
  static async create<T extends t.BaseGraphState>(
189
391
  config: t.RunConfig
190
392
  ): Promise<Run<T>> {
@@ -268,7 +470,7 @@ export class Run<_T extends t.BaseGraphState> {
268
470
  }
269
471
 
270
472
  async processStream(
271
- inputs: t.IState,
473
+ inputs: t.IState | Command,
272
474
  callerConfig: Partial<RunnableConfig> & {
273
475
  version: 'v1' | 'v2';
274
476
  run_id?: string;
@@ -286,6 +488,16 @@ export class Run<_T extends t.BaseGraphState> {
286
488
  );
287
489
  }
288
490
 
491
+ /**
492
+ * `Command` inputs (currently only `Command({ resume })`) are
493
+ * resume-mode invocations: LangGraph rebuilds graph state from the
494
+ * checkpointer, so we skip RunStart / UserPromptSubmit hooks (no
495
+ * new prompt to evaluate) and read run-state from the Graph wrapper
496
+ * instead of `inputs.messages`.
497
+ */
498
+ const isResume = inputs instanceof Command;
499
+ const stateInputs = isResume ? undefined : (inputs as t.IState);
500
+
289
501
  const config: Partial<RunnableConfig> & {
290
502
  version: 'v1' | 'v2';
291
503
  run_id?: string;
@@ -295,7 +507,23 @@ export class Run<_T extends t.BaseGraphState> {
295
507
  configurable: { ...callerConfig.configurable },
296
508
  };
297
509
 
298
- this.Graph.resetValues(streamOptions?.keepContent);
510
+ /**
511
+ * Skip `resetValues` on resume — we're continuing an in-flight
512
+ * run, not starting a fresh one. Resetting would wipe the
513
+ * sidecars (`toolCallStepIds`, `stepKeyIds`, accumulated
514
+ * `messages`, etc.) the resumed `ToolNode` needs to dispatch
515
+ * tool completions with the correct step ids and re-resolve
516
+ * `{{tool<i>turn<n>}}` references. Pairs with the
517
+ * `awaitingResume` gate on `clearHeavyState` in the `finally`
518
+ * block so the sidecars survive both ends of the interrupt
519
+ * boundary.
520
+ */
521
+ if (!isResume) {
522
+ this.Graph.resetValues(streamOptions?.keepContent);
523
+ }
524
+ this._interrupt = undefined;
525
+ this._haltedReason = undefined;
526
+ this.hookRegistry?.clearHaltSignal(this.id);
299
527
 
300
528
  /** Custom event callback to intercept and handle custom events */
301
529
  const customEventCallback = this.createCustomEventCallback();
@@ -350,46 +578,24 @@ export class Run<_T extends t.BaseGraphState> {
350
578
 
351
579
  const threadId = config.configurable.thread_id as string | undefined;
352
580
 
353
- if (this.hookRegistry != null) {
354
- await executeHooks({
355
- registry: this.hookRegistry,
356
- input: {
357
- hook_event_name: 'RunStart',
358
- runId: this.id,
359
- threadId,
360
- agentId: this.Graph.defaultAgentId,
361
- messages: inputs.messages,
362
- },
363
- sessionId: this.id,
364
- });
365
-
366
- const lastHuman = findLastMessageOfType(inputs.messages, 'human');
367
- if (lastHuman != null) {
368
- const promptResult = await executeHooks({
369
- registry: this.hookRegistry,
370
- input: {
371
- hook_event_name: 'UserPromptSubmit',
372
- runId: this.id,
373
- threadId,
374
- agentId: this.Graph.defaultAgentId,
375
- prompt: extractPromptText(lastHuman),
376
- // attachments: not yet wired — Phase 2 will extract
377
- // non-text content blocks (images, files) from messages
378
- },
379
- sessionId: this.id,
380
- });
381
- if (
382
- promptResult.decision === 'deny' ||
383
- promptResult.decision === 'ask'
384
- ) {
385
- this.hookRegistry.clearSession(this.id);
386
- config.callbacks = undefined;
387
- return undefined;
388
- }
581
+ if (this.hookRegistry != null && stateInputs != null) {
582
+ const shouldHalt = await this.runPreStreamHooks(
583
+ stateInputs,
584
+ threadId,
585
+ config
586
+ );
587
+ if (shouldHalt) {
588
+ return undefined;
389
589
  }
390
590
  }
391
591
 
392
- const stream = this.graphRunnable.streamEvents(inputs, config, {
592
+ /**
593
+ * `streamEvents` accepts both state inputs and `Command` (resume) at
594
+ * runtime, but our `CompiledStateWorkflow` type narrows the first
595
+ * arg to `BaseGraphState`. Cast on the call so the resume path
596
+ * type-checks without widening the wrapper for every caller.
597
+ */
598
+ const stream = this.graphRunnable.streamEvents(inputs as t.IState, config, {
393
599
  raiseError: true,
394
600
  /**
395
601
  * Prevent EventStreamCallbackHandler from processing custom events.
@@ -402,6 +608,17 @@ export class Run<_T extends t.BaseGraphState> {
402
608
  ignoreCustomEvent: true,
403
609
  });
404
610
 
611
+ /**
612
+ * Tracks whether the stream loop threw. Used by the `finally`
613
+ * block to decide whether to honor the interrupt-preservation
614
+ * guard for session hooks: a captured `_interrupt` is only
615
+ * meaningful if the stream completed cleanly. If the loop errored
616
+ * after stashing an interrupt (e.g. a downstream handler throws
617
+ * after the interrupt event landed), the interrupt is stale —
618
+ * preserving session hooks would leak them into the next run.
619
+ */
620
+ let streamThrew = false;
621
+
405
622
  try {
406
623
  for await (const event of stream) {
407
624
  const { data, metadata, ...info } = event;
@@ -413,13 +630,81 @@ export class Run<_T extends t.BaseGraphState> {
413
630
  continue;
414
631
  }
415
632
 
633
+ /**
634
+ * Detect interrupts surfaced by LangGraph as a synthetic
635
+ * `__interrupt__` field on the streamed chunk and stash the
636
+ * first one for the host to read via `run.getInterrupt()`
637
+ * once the stream drains. Captured as `unknown` because the
638
+ * SDK does not validate the runtime payload shape — the
639
+ * built-in ToolNode raises a `HumanInterruptPayload`
640
+ * (`tool_approval` / `ask_user_question`), but custom nodes
641
+ * can pass any payload to `interrupt()`. Callers narrow with
642
+ * the `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt`
643
+ * guards or assert via `getInterrupt<T>()`.
644
+ */
645
+ if (
646
+ this._interrupt == null &&
647
+ data.chunk != null &&
648
+ isInterrupted<unknown>(data.chunk)
649
+ ) {
650
+ const interrupts = data.chunk[INTERRUPT];
651
+ if (interrupts.length > 0) {
652
+ const first = interrupts[0];
653
+ /**
654
+ * Capture the interrupt unconditionally — `interrupt(null)`
655
+ * and `interrupt(undefined)` are valid pauses (a custom
656
+ * node may want to pause without metadata) and the host
657
+ * still needs to know the run is awaiting resume. Gating
658
+ * on `payload != null` would silently downgrade a paused
659
+ * run to "completed" and let the `Stop` hook fire,
660
+ * breaking host resume handling.
661
+ */
662
+ this._interrupt = {
663
+ interruptId: first.id ?? '',
664
+ threadId,
665
+ payload: first.value,
666
+ };
667
+ }
668
+ }
669
+
416
670
  const handler = this.handlerRegistry?.getHandler(eventName);
417
671
  if (handler) {
418
672
  await handler.handle(eventName, data, metadata, this.Graph);
419
673
  }
674
+
675
+ /**
676
+ * Mid-flight halt: any hook (PreToolUse, PostToolUse,
677
+ * PostToolBatch, SubagentStart/Stop, PreCompact, PostCompact)
678
+ * that returned `preventContinuation: true` raises a halt
679
+ * signal on the registry via `executeHooks`. We poll between
680
+ * stream events and break out as soon as one is set so the
681
+ * graph doesn't take another model turn after the halting
682
+ * operation completes.
683
+ *
684
+ * Limitation: the current step (in-flight model call, ongoing
685
+ * tool batch) is not aborted — only the next step is skipped.
686
+ * This matches Claude Code's `continue: false` semantic where
687
+ * the active operation finishes before halting takes effect.
688
+ */
689
+ const haltSignal = this.hookRegistry?.getHaltSignal(this.id);
690
+ if (haltSignal != null) {
691
+ this._haltedReason = haltSignal.reason;
692
+ break;
693
+ }
420
694
  }
421
695
 
422
- if (this.hookRegistry?.hasHookFor('Stop', this.id) === true) {
696
+ /**
697
+ * Skip the Stop hook when the run paused on a HITL interrupt
698
+ * (still pending human input) or was halted by a hook (the host
699
+ * already chose to stop, so a Stop hook firing now would be
700
+ * misleading). The host fires Stop on the resumed-and-completed
701
+ * run instead.
702
+ */
703
+ if (
704
+ this._interrupt == null &&
705
+ this._haltedReason == null &&
706
+ this.hookRegistry?.hasHookFor('Stop', this.id) === true
707
+ ) {
423
708
  await executeHooks({
424
709
  registry: this.hookRegistry,
425
710
  input: {
@@ -427,7 +712,8 @@ export class Run<_T extends t.BaseGraphState> {
427
712
  runId: this.id,
428
713
  threadId,
429
714
  agentId: this.Graph.defaultAgentId,
430
- messages: this.Graph.getRunMessages() ?? inputs.messages,
715
+ messages:
716
+ this.Graph.getRunMessages() ?? stateInputs?.messages ?? [],
431
717
  stopHookActive: false, // will be true when stop is triggered by a hook (Phase 2)
432
718
  },
433
719
  sessionId: this.id,
@@ -436,6 +722,7 @@ export class Run<_T extends t.BaseGraphState> {
436
722
  });
437
723
  }
438
724
  } catch (err) {
725
+ streamThrew = true;
439
726
  if (this.hookRegistry?.hasHookFor('StopFailure', this.id) === true) {
440
727
  const runMessages = this.Graph.getRunMessages() ?? [];
441
728
  await executeHooks({
@@ -455,7 +742,36 @@ export class Run<_T extends t.BaseGraphState> {
455
742
  }
456
743
  throw err;
457
744
  } finally {
458
- this.hookRegistry?.clearSession(this.id);
745
+ /**
746
+ * Preserve session-scoped hooks when the run paused on a HITL
747
+ * interrupt — the very next call will be `Run.resume()`, which
748
+ * needs the same policy hooks (e.g., the `PreToolUse` matcher
749
+ * that triggered the interrupt) to fire on the re-executed node
750
+ * and uphold the approval flow. Clearing here would leak the
751
+ * approval gate on resume. The session is cleared instead at
752
+ * natural completion, error (including errors that happen AFTER
753
+ * an interrupt was captured — those interrupts are stale), or
754
+ * hook-driven halt (including hooks that returned BOTH `ask`
755
+ * and `preventContinuation` — the halt wins, no resume is
756
+ * expected, sessions must drop). Every state where no resume
757
+ * is expected clears.
758
+ */
759
+ if (
760
+ this._interrupt == null ||
761
+ this._haltedReason != null ||
762
+ streamThrew
763
+ ) {
764
+ this.hookRegistry?.clearSession(this.id);
765
+ }
766
+ /**
767
+ * Drop any halt signal raised mid-stream for this run so a
768
+ * subsequent `processStream` / `resume` starts with clean state.
769
+ * The Run captured `_haltedReason` already; the registry entry
770
+ * for this `sessionId` would otherwise spuriously trip the next
771
+ * loop. Other concurrent runs sharing this registry are
772
+ * unaffected — their entries live under their own session ids.
773
+ */
774
+ this.hookRegistry?.clearHaltSignal(this.id);
459
775
 
460
776
  /**
461
777
  * Break the reference chain that keeps heavy data alive via
@@ -494,7 +810,28 @@ export class Run<_T extends t.BaseGraphState> {
494
810
 
495
811
  this.calibrationRatio = this.Graph.getCalibrationRatio();
496
812
 
497
- if (!this.skipCleanup) {
813
+ /**
814
+ * Skip `clearHeavyState()` when the run paused on a clean HITL
815
+ * interrupt awaiting resume — `Run.resume()` re-enters the same
816
+ * `ToolNode` instance and needs the sidecars `clearHeavyState`
817
+ * would wipe (`toolCallStepIds` for completion-event step ids,
818
+ * the `_toolOutputRegistry` for `{{tool<i>turn<n>}}`
819
+ * substitutions, `sessions` for code-env continuity, plus the
820
+ * `hookRegistry` and `humanInTheLoop` config the interrupt
821
+ * branch itself relies on). Without preservation, the resumed
822
+ * tool completion would dispatch `ON_RUN_STEP_COMPLETED` with
823
+ * an empty step id and downstream stream consumers would drop
824
+ * the result.
825
+ *
826
+ * The natural-completion / error / hook-driven-halt paths still
827
+ * clean up — `_haltedReason != null` or `streamThrew` mean no
828
+ * resume is expected. Cross-process resume (host rebuilds the
829
+ * Run from scratch) is a separate concern; see
830
+ * `HumanInTheLoopConfig` JSDoc.
831
+ */
832
+ const awaitingResume =
833
+ this._interrupt != null && this._haltedReason == null && !streamThrew;
834
+ if (!this.skipCleanup && !awaitingResume) {
498
835
  this.Graph.clearHeavyState();
499
836
  }
500
837
 
@@ -504,6 +841,78 @@ export class Run<_T extends t.BaseGraphState> {
504
841
  return this._streamResult;
505
842
  }
506
843
 
844
+ /**
845
+ * Returns the pending interrupt captured during the most recent
846
+ * `processStream` (or `resume`) invocation. `undefined` when the run
847
+ * either has not been streamed yet or completed without pausing.
848
+ *
849
+ * Hosts call this immediately after `processStream` returns to decide
850
+ * whether the run is awaiting human input. Persist the returned
851
+ * descriptor (alongside `thread_id` and the agent run config) so a
852
+ * later `resume(decisions)` can rebuild the run.
853
+ *
854
+ * The default `TPayload` is the SDK's `HumanInterruptPayload` union
855
+ * (`tool_approval` / `ask_user_question`), suitable for the common
856
+ * case where interrupts come from the built-in ToolNode or
857
+ * `askUserQuestion()` helper. Hosts that raise custom interrupts
858
+ * from custom graph nodes pass their own type — the SDK does not
859
+ * validate the runtime shape, it just transports whatever the
860
+ * `interrupt()` call carried. When in doubt, narrow with the
861
+ * `isToolApprovalInterrupt` / `isAskUserQuestionInterrupt` type
862
+ * guards (which accept `unknown`) before reading variant-specific
863
+ * fields.
864
+ */
865
+ getInterrupt<TPayload = t.HumanInterruptPayload>():
866
+ | t.RunInterruptResult<TPayload>
867
+ | undefined {
868
+ return this._interrupt as t.RunInterruptResult<TPayload> | undefined;
869
+ }
870
+
871
+ /**
872
+ * Returns the reason a hook halted the run via
873
+ * `preventContinuation: true`, or `undefined` if no hook halted.
874
+ *
875
+ * Hosts inspect this after `processStream` returns to distinguish a
876
+ * natural completion (`undefined`) from a hook-driven halt (a
877
+ * truthy string). Independent from `getInterrupt()` — a halted run
878
+ * has no interrupt; an interrupted run has no halt reason.
879
+ */
880
+ getHaltReason(): string | undefined {
881
+ return this._haltedReason;
882
+ }
883
+
884
+ /**
885
+ * Resume a paused HITL run with the value the user (or whatever
886
+ * decided the interrupt) supplied. The default `TResume` covers the
887
+ * `tool_approval` interrupt (the common case): an array of decisions
888
+ * in `action_requests` order, or a record keyed by `tool_call_id`.
889
+ *
890
+ * For other interrupt types (e.g., `ask_user_question` →
891
+ * `AskUserQuestionResolution`, or any custom interrupt a host raises
892
+ * from a custom node), pass the type parameter and the SDK forwards
893
+ * the value through unchanged. LangGraph delivers it as the return
894
+ * value of the original `interrupt()` call inside the paused node.
895
+ *
896
+ * The host MUST construct this Run with the same `thread_id` and the
897
+ * same checkpointer as the original paused run; LangGraph rebuilds
898
+ * graph state from the checkpoint and re-enters the interrupted node
899
+ * from the start.
900
+ */
901
+ async resume<TResume = t.ToolApprovalDecision[] | t.ToolApprovalDecisionMap>(
902
+ resumeValue: TResume,
903
+ callerConfig: Partial<RunnableConfig> & {
904
+ version: 'v1' | 'v2';
905
+ run_id?: string;
906
+ },
907
+ streamOptions?: t.EventStreamOptions
908
+ ): Promise<MessageContentComplex[] | undefined> {
909
+ return this.processStream(
910
+ new Command({ resume: resumeValue }),
911
+ callerConfig,
912
+ streamOptions
913
+ );
914
+ }
915
+
507
916
  private createSystemCallback<K extends keyof t.ClientCallbacks>(
508
917
  clientCallbacks: t.ClientCallbacks,
509
918
  key: K
@@ -50,9 +50,8 @@ ${CACHED_TEXT}`;
50
50
  },
51
51
  };
52
52
 
53
- const baseLlmConfig: t.LLMConfig & t.AnthropicClientOptions = getLLMConfig(
54
- Providers.ANTHROPIC
55
- );
53
+ const baseLlmConfig = getLLMConfig(Providers.ANTHROPIC) as t.LLMConfig &
54
+ t.AnthropicClientOptions;
56
55
 
57
56
  if (baseLlmConfig.provider !== 'anthropic') {
58
57
  console.error(