@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
@@ -21,6 +21,18 @@
21
21
  class HookRegistry {
22
22
  global = {};
23
23
  sessions = new Map();
24
+ /**
25
+ * Per-session halt signals. Scoped by `sessionId` (= the run id the
26
+ * hook fired under) so a host that shares one registry across
27
+ * concurrent runs cannot leak `preventContinuation` from one run
28
+ * into another. Without scoping, a halt raised by run A's hook
29
+ * would trip run B's stream-loop poll on the next iteration —
30
+ * silently terminating an unrelated run.
31
+ *
32
+ * Map storage mirrors the reasoning above for session matchers:
33
+ * O(1) insertion in hot paths, no spread-on-write.
34
+ */
35
+ haltSignals = new Map();
24
36
  /**
25
37
  * Register a matcher for the lifetime of this registry (= one Run).
26
38
  * Returns an unregister function that removes the matcher by reference.
@@ -95,6 +107,48 @@ class HookRegistry {
95
107
  clearSession(sessionId) {
96
108
  this.sessions.delete(sessionId);
97
109
  }
110
+ /**
111
+ * Raise a halt signal scoped to `sessionId` (= the run id the hook
112
+ * fired under). The SDK's run loop polls for this between stream
113
+ * events with the run's own id. First-write-wins per session: a
114
+ * halt already raised by an earlier hook in the same run is
115
+ * preserved so the original `reason` / `source` aren't overwritten.
116
+ *
117
+ * Per-session scoping is critical when hosts share one registry
118
+ * across concurrent runs (e.g. a global policy registered once and
119
+ * reused). Without it, a `preventContinuation` from run A would
120
+ * trip run B's stream-loop poll on the next iteration and silently
121
+ * terminate an unrelated run.
122
+ *
123
+ * Called by the SDK after `executeHooks` returns an aggregate with
124
+ * `preventContinuation: true`. Hosts can also call it directly from
125
+ * inside a hook callback if they want to halt without going through
126
+ * the aggregated return value, but `preventContinuation` is the
127
+ * canonical path.
128
+ */
129
+ haltRun(sessionId, reason, source) {
130
+ if (this.haltSignals.has(sessionId)) {
131
+ return;
132
+ }
133
+ this.haltSignals.set(sessionId, { reason, source });
134
+ }
135
+ /**
136
+ * Returns the halt signal raised by hooks running under `sessionId`,
137
+ * or `undefined` if no hook in that run has halted. Polled by
138
+ * `Run.processStream` between stream events using the run's own id.
139
+ */
140
+ getHaltSignal(sessionId) {
141
+ return this.haltSignals.get(sessionId);
142
+ }
143
+ /**
144
+ * Clears the halt signal for `sessionId`. Called by
145
+ * `Run.processStream` in its `finally` block so a subsequent
146
+ * invocation of the same Run (e.g. resume) starts with a fresh
147
+ * halt state. No-op when no signal exists for that session.
148
+ */
149
+ clearHaltSignal(sessionId) {
150
+ this.haltSignals.delete(sessionId);
151
+ }
98
152
  /** True if at least one matcher exists for `event` (global + session). */
99
153
  hasHookFor(event, sessionId) {
100
154
  if (readList(this.global, event).length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"HookRegistry.cjs","sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type { HookEvent, HookMatcher } from './types';\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n private readonly global: MatcherBucket = {};\n private readonly sessions: Map<string, MatcherBucket> = new Map();\n\n /**\n * Register a matcher for the lifetime of this registry (= one Run).\n * Returns an unregister function that removes the matcher by reference.\n */\n register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n const list = ensureList(this.global, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Register a matcher for a specific session. Cleared automatically when\n * `clearSession(sessionId)` is called, or can be removed directly via the\n * returned unregister function.\n */\n registerSession<E extends HookEvent>(\n sessionId: string,\n event: E,\n matcher: HookMatcher<E>\n ): () => void {\n const bucket = this.ensureSessionBucket(sessionId);\n const list = ensureList(bucket, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Returns all matchers registered for `event`, concatenating global first\n * and then session-specific (when `sessionId` is supplied). The caller\n * receives a fresh array, so iterating it is safe even if a matcher is\n * removed mid-iteration (e.g. via `once: true`).\n */\n getMatchers<E extends HookEvent>(\n event: E,\n sessionId?: string\n ): HookMatcher<E>[] {\n const globalList = readList(this.global, event);\n if (sessionId === undefined) {\n return snapshot<E>(globalList);\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return snapshot<E>(globalList);\n }\n const sessionList = readList(bucket, event);\n if (globalList.length === 0) {\n return snapshot<E>(sessionList);\n }\n if (sessionList.length === 0) {\n return snapshot<E>(globalList);\n }\n return snapshot<E>([...globalList, ...sessionList]);\n }\n\n /**\n * Removes `matcher` by reference from global storage first, falling back\n * to the session bucket when `sessionId` is supplied. Used by\n * `executeHooks` to drop `once: true` matchers after they fire.\n */\n removeMatcher<E extends HookEvent>(\n event: E,\n matcher: HookMatcher<E>,\n sessionId?: string\n ): boolean {\n if (removeFromList(readList(this.global, event), matcher)) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return removeFromList(readList(bucket, event), matcher);\n }\n\n /**\n * Drops every session-scoped matcher for `sessionId`. Call this in the\n * `finally` block around a Run so a `once: true` hook that never fired\n * cannot leak into the next session on the same registry.\n */\n clearSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n /** True if at least one matcher exists for `event` (global + session). */\n hasHookFor(event: HookEvent, sessionId?: string): boolean {\n if (readList(this.global, event).length > 0) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return readList(bucket, event).length > 0;\n }\n\n private ensureSessionBucket(sessionId: string): MatcherBucket {\n const existing = this.sessions.get(sessionId);\n if (existing !== undefined) {\n return existing;\n }\n const fresh: MatcherBucket = {};\n this.sessions.set(sessionId, fresh);\n return fresh;\n }\n}\n\nfunction ensureList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n const existing = bucket[event];\n if (existing !== undefined) {\n return existing;\n }\n const fresh: HookMatcher<HookEvent>[] = [];\n bucket[event] = fresh;\n return fresh;\n}\n\nfunction readList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n return bucket[event] ?? [];\n}\n\nfunction removeFromList<E extends HookEvent>(\n list: HookMatcher<HookEvent>[],\n matcher: HookMatcher<E>\n): boolean {\n const idx = list.indexOf(widen(matcher));\n if (idx < 0) {\n return false;\n }\n list.splice(idx, 1);\n return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"HookRegistry.cjs","sources":["../../../src/hooks/HookRegistry.ts"],"sourcesContent":["// src/hooks/HookRegistry.ts\nimport type { HookEvent, HookMatcher } from './types';\n\n/**\n * Internal matcher storage type.\n *\n * Matchers registered via the public `register<E>` API are strictly typed\n * to a single `E`, but the storage needs one uniform slot type per event.\n * We store them as `HookMatcher<HookEvent>` and cast once at the variance\n * boundary — see `ensureList` and `snapshot` below. The invariant (every\n * matcher in `bucket[event]` was registered with that exact event) is\n * enforced by the public API; breaking it requires bypassing the types.\n */\ntype MatcherBucket = Partial<Record<HookEvent, HookMatcher<HookEvent>[]>>;\n\n/**\n * Snapshot of a halt request raised by a hook returning\n * `preventContinuation: true`. The SDK's run loop polls for this between\n * stream events and exits cleanly when set, skipping the `Stop` hook\n * (the run is being halted, not naturally completing). One per registry\n * instance — the first hook to halt wins; subsequent halts are ignored\n * so the original reason isn't clobbered.\n */\nexport interface HookHaltSignal {\n reason: string;\n /** Event of the hook that triggered the halt (for diagnostics). */\n source: HookEvent;\n}\n\n/**\n * Run-scoped storage for hook matchers with an additional layer for\n * session-scoped matchers that should be cleaned up between sessions.\n *\n * Hosts construct one registry per `Run` (mirroring how `HandlerRegistry` is\n * scoped) and register global matchers + per-session matchers against it.\n * Registration is strictly additive — nothing in this class mutates a\n * matcher's callbacks or flags after insertion.\n *\n * ## Why `Map<sessionId, MatcherBucket>` and not `Record`\n *\n * LibreChat runs thousands of parallel sessions in one Node process, and\n * hook registration happens inside hot paths (tool loading, agent spawning).\n * A `Record<sessionId, ...>` has to be spread on every insertion, which is\n * O(n) per call and O(n²) total for a batch of parallel registrations. A\n * Map mutates in place, keeping insertions O(1). This mirrors the reasoning\n * Claude Code documents at `utils/hooks/sessionHooks.ts:62`.\n */\nexport class HookRegistry {\n private readonly global: MatcherBucket = {};\n private readonly sessions: Map<string, MatcherBucket> = new Map();\n /**\n * Per-session halt signals. Scoped by `sessionId` (= the run id the\n * hook fired under) so a host that shares one registry across\n * concurrent runs cannot leak `preventContinuation` from one run\n * into another. Without scoping, a halt raised by run A's hook\n * would trip run B's stream-loop poll on the next iteration —\n * silently terminating an unrelated run.\n *\n * Map storage mirrors the reasoning above for session matchers:\n * O(1) insertion in hot paths, no spread-on-write.\n */\n private readonly haltSignals: Map<string, HookHaltSignal> = new Map();\n\n /**\n * Register a matcher for the lifetime of this registry (= one Run).\n * Returns an unregister function that removes the matcher by reference.\n */\n register<E extends HookEvent>(event: E, matcher: HookMatcher<E>): () => void {\n const list = ensureList(this.global, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Register a matcher for a specific session. Cleared automatically when\n * `clearSession(sessionId)` is called, or can be removed directly via the\n * returned unregister function.\n */\n registerSession<E extends HookEvent>(\n sessionId: string,\n event: E,\n matcher: HookMatcher<E>\n ): () => void {\n const bucket = this.ensureSessionBucket(sessionId);\n const list = ensureList(bucket, event);\n list.push(widen(matcher));\n return () => {\n removeFromList(list, matcher);\n };\n }\n\n /**\n * Returns all matchers registered for `event`, concatenating global first\n * and then session-specific (when `sessionId` is supplied). The caller\n * receives a fresh array, so iterating it is safe even if a matcher is\n * removed mid-iteration (e.g. via `once: true`).\n */\n getMatchers<E extends HookEvent>(\n event: E,\n sessionId?: string\n ): HookMatcher<E>[] {\n const globalList = readList(this.global, event);\n if (sessionId === undefined) {\n return snapshot<E>(globalList);\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return snapshot<E>(globalList);\n }\n const sessionList = readList(bucket, event);\n if (globalList.length === 0) {\n return snapshot<E>(sessionList);\n }\n if (sessionList.length === 0) {\n return snapshot<E>(globalList);\n }\n return snapshot<E>([...globalList, ...sessionList]);\n }\n\n /**\n * Removes `matcher` by reference from global storage first, falling back\n * to the session bucket when `sessionId` is supplied. Used by\n * `executeHooks` to drop `once: true` matchers after they fire.\n */\n removeMatcher<E extends HookEvent>(\n event: E,\n matcher: HookMatcher<E>,\n sessionId?: string\n ): boolean {\n if (removeFromList(readList(this.global, event), matcher)) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return removeFromList(readList(bucket, event), matcher);\n }\n\n /**\n * Drops every session-scoped matcher for `sessionId`. Call this in the\n * `finally` block around a Run so a `once: true` hook that never fired\n * cannot leak into the next session on the same registry.\n */\n clearSession(sessionId: string): void {\n this.sessions.delete(sessionId);\n }\n\n /**\n * Raise a halt signal scoped to `sessionId` (= the run id the hook\n * fired under). The SDK's run loop polls for this between stream\n * events with the run's own id. First-write-wins per session: a\n * halt already raised by an earlier hook in the same run is\n * preserved so the original `reason` / `source` aren't overwritten.\n *\n * Per-session scoping is critical when hosts share one registry\n * across concurrent runs (e.g. a global policy registered once and\n * reused). Without it, a `preventContinuation` from run A would\n * trip run B's stream-loop poll on the next iteration and silently\n * terminate an unrelated run.\n *\n * Called by the SDK after `executeHooks` returns an aggregate with\n * `preventContinuation: true`. Hosts can also call it directly from\n * inside a hook callback if they want to halt without going through\n * the aggregated return value, but `preventContinuation` is the\n * canonical path.\n */\n haltRun(sessionId: string, reason: string, source: HookEvent): void {\n if (this.haltSignals.has(sessionId)) {\n return;\n }\n this.haltSignals.set(sessionId, { reason, source });\n }\n\n /**\n * Returns the halt signal raised by hooks running under `sessionId`,\n * or `undefined` if no hook in that run has halted. Polled by\n * `Run.processStream` between stream events using the run's own id.\n */\n getHaltSignal(sessionId: string): HookHaltSignal | undefined {\n return this.haltSignals.get(sessionId);\n }\n\n /**\n * Clears the halt signal for `sessionId`. Called by\n * `Run.processStream` in its `finally` block so a subsequent\n * invocation of the same Run (e.g. resume) starts with a fresh\n * halt state. No-op when no signal exists for that session.\n */\n clearHaltSignal(sessionId: string): void {\n this.haltSignals.delete(sessionId);\n }\n\n /** True if at least one matcher exists for `event` (global + session). */\n hasHookFor(event: HookEvent, sessionId?: string): boolean {\n if (readList(this.global, event).length > 0) {\n return true;\n }\n if (sessionId === undefined) {\n return false;\n }\n const bucket = this.sessions.get(sessionId);\n if (bucket === undefined) {\n return false;\n }\n return readList(bucket, event).length > 0;\n }\n\n private ensureSessionBucket(sessionId: string): MatcherBucket {\n const existing = this.sessions.get(sessionId);\n if (existing !== undefined) {\n return existing;\n }\n const fresh: MatcherBucket = {};\n this.sessions.set(sessionId, fresh);\n return fresh;\n }\n}\n\nfunction ensureList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n const existing = bucket[event];\n if (existing !== undefined) {\n return existing;\n }\n const fresh: HookMatcher<HookEvent>[] = [];\n bucket[event] = fresh;\n return fresh;\n}\n\nfunction readList(\n bucket: MatcherBucket,\n event: HookEvent\n): HookMatcher<HookEvent>[] {\n return bucket[event] ?? [];\n}\n\nfunction removeFromList<E extends HookEvent>(\n list: HookMatcher<HookEvent>[],\n matcher: HookMatcher<E>\n): boolean {\n const idx = list.indexOf(widen(matcher));\n if (idx < 0) {\n return false;\n }\n list.splice(idx, 1);\n return true;\n}\n\n/**\n * Widen a per-event matcher to the storage's uniform slot type. Unsound at\n * the type level (function parameters are contravariant) but safe by\n * construction: `HookRegistry.register<E>` only ever puts matchers into the\n * bucket slot for their own event, and reads go through `snapshot<E>`\n * which is only called with the same `E`.\n */\nfunction widen<E extends HookEvent>(\n matcher: HookMatcher<E>\n): HookMatcher<HookEvent> {\n return matcher as unknown as HookMatcher<HookEvent>;\n}\n\n/**\n * Narrow a storage list back to a per-event matcher list on the way out.\n * Sound counterpart to `widen`: the list only contains matchers that were\n * registered against `E`, because the public API enforces it on insert.\n */\nfunction snapshot<E extends HookEvent>(\n list: readonly HookMatcher<HookEvent>[]\n): HookMatcher<E>[] {\n return list.slice() as unknown as HookMatcher<E>[];\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Declarative `PreToolUse` hook factory. Lets hosts express common
5
+ * permission policies (allow / deny / ask lists + a global mode) without
6
+ * hand-rolling matching, precedence, and decision logic per-host.
7
+ *
8
+ * Maps directly to the Claude Code Agent SDK permission vocabulary
9
+ * (`allowed_tools` / `disallowed_tools` / `permissionMode`) so users of
10
+ * either SDK can think in the same terms. See the README's HITL section
11
+ * for the cross-walk and `docs/hooks-design-report.md` for the broader
12
+ * hook system context.
13
+ */
14
+ /**
15
+ * Compile a glob string with `*` wildcards into a single anchored
16
+ * `RegExp`. Other regex metacharacters are escaped, so `read_file.md`
17
+ * matches the literal dot. Patterns are short (tool names), so we do
18
+ * not cache here — the registry's `matchesQuery` already caches its own
19
+ * regex compilations and our patterns are evaluated once per ToolNode
20
+ * batch, not once per stream chunk.
21
+ */
22
+ function globToRegex(pattern) {
23
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
24
+ return new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
25
+ }
26
+ /** Pre-compile a list of glob patterns into a single match function. */
27
+ function compileMatchers(patterns) {
28
+ if (patterns == null || patterns.length === 0) {
29
+ return () => false;
30
+ }
31
+ const regexes = patterns.map(globToRegex);
32
+ return (toolName) => {
33
+ for (const regex of regexes) {
34
+ if (regex.test(toolName)) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ };
40
+ }
41
+ function formatReason(template, toolName) {
42
+ if (template == null) {
43
+ return undefined;
44
+ }
45
+ return template.replace(/\{tool\}/g, toolName);
46
+ }
47
+ /**
48
+ * Build a `PreToolUse` hook callback that applies a declarative tool
49
+ * permission policy. Register it with a `HookRegistry` and the SDK's
50
+ * `humanInTheLoop` machinery handles the rest:
51
+ *
52
+ * ```ts
53
+ * const policyHook = createToolPolicyHook({
54
+ * mode: 'default',
55
+ * allow: ['read_*', 'grep', 'glob'],
56
+ * deny: ['delete_*'],
57
+ * ask: ['execute_*', 'mcp:*'],
58
+ * });
59
+ * registry.register('PreToolUse', { hooks: [policyHook] });
60
+ * ```
61
+ *
62
+ * Evaluation order matches Claude Code's permission flow:
63
+ *
64
+ * 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).
65
+ * 2. `mode === 'bypass'` → `'allow'`.
66
+ * 3. `allow` rule match → `'allow'`.
67
+ * 4. `ask` rule match → `'ask'`.
68
+ * 5. `mode === 'dontAsk'` → `'deny'`.
69
+ * 6. fallthrough → `'ask'`.
70
+ *
71
+ * The returned callback is a single `HookCallback`, not a `HookMatcher` —
72
+ * register it under the matcher with the pattern you want (omit the
73
+ * pattern to fire on every tool call, which is the typical case since
74
+ * the policy itself does the filtering).
75
+ */
76
+ function createToolPolicyHook(config) {
77
+ const denyMatcher = compileMatchers(config.deny);
78
+ const allowMatcher = compileMatchers(config.allow);
79
+ const askMatcher = compileMatchers(config.ask);
80
+ const mode = config.mode ?? 'default';
81
+ const reasonTemplate = config.reason;
82
+ return async (input) => {
83
+ const toolName = input.toolName;
84
+ const decision = decide(toolName, mode, denyMatcher, allowMatcher, askMatcher);
85
+ if (decision === 'allow') {
86
+ return { decision };
87
+ }
88
+ const reason = formatReason(reasonTemplate, toolName);
89
+ if (reason != null) {
90
+ return { decision, reason };
91
+ }
92
+ return { decision };
93
+ };
94
+ }
95
+ function decide(toolName, mode, denyMatch, allowMatch, askMatch) {
96
+ if (denyMatch(toolName)) {
97
+ return 'deny';
98
+ }
99
+ if (mode === 'bypass') {
100
+ return 'allow';
101
+ }
102
+ if (allowMatch(toolName)) {
103
+ return 'allow';
104
+ }
105
+ if (askMatch(toolName)) {
106
+ return 'ask';
107
+ }
108
+ if (mode === 'dontAsk') {
109
+ return 'deny';
110
+ }
111
+ return 'ask';
112
+ }
113
+
114
+ exports.createToolPolicyHook = createToolPolicyHook;
115
+ //# sourceMappingURL=createToolPolicyHook.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createToolPolicyHook.cjs","sources":["../../../src/hooks/createToolPolicyHook.ts"],"sourcesContent":["/**\n * Declarative `PreToolUse` hook factory. Lets hosts express common\n * permission policies (allow / deny / ask lists + a global mode) without\n * hand-rolling matching, precedence, and decision logic per-host.\n *\n * Maps directly to the Claude Code Agent SDK permission vocabulary\n * (`allowed_tools` / `disallowed_tools` / `permissionMode`) so users of\n * either SDK can think in the same terms. See the README's HITL section\n * for the cross-walk and `docs/hooks-design-report.md` for the broader\n * hook system context.\n */\n\nimport type { HookCallback, PreToolUseHookOutput, ToolDecision } from './types';\n\n/**\n * Permission mode controlling how tool calls that match no rule are\n * resolved. Mirrors Claude Code's `permissionMode`.\n *\n * - `default` — unmatched tools fall through to `'ask'` (interrupt).\n * - `dontAsk` — unmatched tools are denied; the human is never\n * prompted. Useful for headless / API agents where a\n * silent denial is preferable to a hung interrupt.\n * - `bypass` — every tool is approved, except those matching `deny`\n * patterns. The kill switch you flip when you trust\n * the agent and want to stop being asked. Equivalent to\n * Claude Code's `bypassPermissions`.\n */\nexport type ToolPolicyMode = 'default' | 'dontAsk' | 'bypass';\n\nexport interface ToolPolicyConfig {\n /**\n * Global mode applied to tools that don't match any rule.\n * Defaults to `'default'` (ask the human).\n */\n mode?: ToolPolicyMode;\n /**\n * Tool name patterns that are auto-approved without a prompt.\n * Patterns support glob `*` wildcards: `read_file`, `mcp:github:*`,\n * `*search*`. Match is anchored (`^pattern$`).\n */\n allow?: readonly string[];\n /**\n * Tool name patterns that are blocked outright. Wins over `allow`\n * and `ask`, and overrides `mode: 'bypass'` — a deny rule always\n * holds, matching Claude Code's \"deny rules are checked first\" guarantee.\n */\n deny?: readonly string[];\n /**\n * Tool name patterns that always trigger human approval, regardless\n * of `mode: 'default'` vs `'dontAsk'`. In `mode: 'bypass'` these are\n * still bypassed (because that's what bypass means).\n */\n ask?: readonly string[];\n /**\n * Optional reason attached to the resulting `ask` / `deny` hook\n * decision so the host UI can render why approval is required.\n * The literal token `{tool}` is replaced with the tool name.\n */\n reason?: string;\n}\n\n/**\n * Compile a glob string with `*` wildcards into a single anchored\n * `RegExp`. Other regex metacharacters are escaped, so `read_file.md`\n * matches the literal dot. Patterns are short (tool names), so we do\n * not cache here — the registry's `matchesQuery` already caches its own\n * regex compilations and our patterns are evaluated once per ToolNode\n * batch, not once per stream chunk.\n */\nfunction globToRegex(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&');\n return new RegExp('^' + escaped.replace(/\\*/g, '.*') + '$');\n}\n\n/** Pre-compile a list of glob patterns into a single match function. */\nfunction compileMatchers(\n patterns: readonly string[] | undefined\n): (toolName: string) => boolean {\n if (patterns == null || patterns.length === 0) {\n return () => false;\n }\n const regexes = patterns.map(globToRegex);\n return (toolName: string): boolean => {\n for (const regex of regexes) {\n if (regex.test(toolName)) {\n return true;\n }\n }\n return false;\n };\n}\n\nfunction formatReason(\n template: string | undefined,\n toolName: string\n): string | undefined {\n if (template == null) {\n return undefined;\n }\n return template.replace(/\\{tool\\}/g, toolName);\n}\n\n/**\n * Build a `PreToolUse` hook callback that applies a declarative tool\n * permission policy. Register it with a `HookRegistry` and the SDK's\n * `humanInTheLoop` machinery handles the rest:\n *\n * ```ts\n * const policyHook = createToolPolicyHook({\n * mode: 'default',\n * allow: ['read_*', 'grep', 'glob'],\n * deny: ['delete_*'],\n * ask: ['execute_*', 'mcp:*'],\n * });\n * registry.register('PreToolUse', { hooks: [policyHook] });\n * ```\n *\n * Evaluation order matches Claude Code's permission flow:\n *\n * 1. `deny` rule match → `'deny'` (always wins, even in `bypass`).\n * 2. `mode === 'bypass'` → `'allow'`.\n * 3. `allow` rule match → `'allow'`.\n * 4. `ask` rule match → `'ask'`.\n * 5. `mode === 'dontAsk'` → `'deny'`.\n * 6. fallthrough → `'ask'`.\n *\n * The returned callback is a single `HookCallback`, not a `HookMatcher` —\n * register it under the matcher with the pattern you want (omit the\n * pattern to fire on every tool call, which is the typical case since\n * the policy itself does the filtering).\n */\nexport function createToolPolicyHook(\n config: ToolPolicyConfig\n): HookCallback<'PreToolUse'> {\n const denyMatcher = compileMatchers(config.deny);\n const allowMatcher = compileMatchers(config.allow);\n const askMatcher = compileMatchers(config.ask);\n const mode: ToolPolicyMode = config.mode ?? 'default';\n const reasonTemplate = config.reason;\n\n return async (input): Promise<PreToolUseHookOutput> => {\n const toolName = input.toolName;\n const decision = decide(\n toolName,\n mode,\n denyMatcher,\n allowMatcher,\n askMatcher\n );\n if (decision === 'allow') {\n return { decision };\n }\n const reason = formatReason(reasonTemplate, toolName);\n if (reason != null) {\n return { decision, reason };\n }\n return { decision };\n };\n}\n\nfunction decide(\n toolName: string,\n mode: ToolPolicyMode,\n denyMatch: (n: string) => boolean,\n allowMatch: (n: string) => boolean,\n askMatch: (n: string) => boolean\n): ToolDecision {\n if (denyMatch(toolName)) {\n return 'deny';\n }\n if (mode === 'bypass') {\n return 'allow';\n }\n if (allowMatch(toolName)) {\n return 'allow';\n }\n if (askMatch(toolName)) {\n return 'ask';\n }\n if (mode === 'dontAsk') {\n return 'deny';\n }\n return 'ask';\n}\n"],"names":[],"mappings":";;AAAA;;;;;;;;;;AAUG;AAmDH;;;;;;;AAOG;AACH,SAAS,WAAW,CAAC,OAAe,EAAA;IAClC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,MAAM,CAAC;AAC7D,IAAA,OAAO,IAAI,MAAM,CAAC,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,GAAG,CAAC;AAC7D;AAEA;AACA,SAAS,eAAe,CACtB,QAAuC,EAAA;IAEvC,IAAI,QAAQ,IAAI,IAAI,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE;AAC7C,QAAA,OAAO,MAAM,KAAK;IACpB;IACA,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC;IACzC,OAAO,CAAC,QAAgB,KAAa;AACnC,QAAA,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE;AAC3B,YAAA,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;AACxB,gBAAA,OAAO,IAAI;YACb;QACF;AACA,QAAA,OAAO,KAAK;AACd,IAAA,CAAC;AACH;AAEA,SAAS,YAAY,CACnB,QAA4B,EAC5B,QAAgB,EAAA;AAEhB,IAAA,IAAI,QAAQ,IAAI,IAAI,EAAE;AACpB,QAAA,OAAO,SAAS;IAClB;IACA,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC;AAChD;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BG;AACG,SAAU,oBAAoB,CAClC,MAAwB,EAAA;IAExB,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC;IAChD,MAAM,YAAY,GAAG,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC;IAClD,MAAM,UAAU,GAAG,eAAe,CAAC,MAAM,CAAC,GAAG,CAAC;AAC9C,IAAA,MAAM,IAAI,GAAmB,MAAM,CAAC,IAAI,IAAI,SAAS;AACrD,IAAA,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM;AAEpC,IAAA,OAAO,OAAO,KAAK,KAAmC;AACpD,QAAA,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ;AAC/B,QAAA,MAAM,QAAQ,GAAG,MAAM,CACrB,QAAQ,EACR,IAAI,EACJ,WAAW,EACX,YAAY,EACZ,UAAU,CACX;AACD,QAAA,IAAI,QAAQ,KAAK,OAAO,EAAE;YACxB,OAAO,EAAE,QAAQ,EAAE;QACrB;QACA,MAAM,MAAM,GAAG,YAAY,CAAC,cAAc,EAAE,QAAQ,CAAC;AACrD,QAAA,IAAI,MAAM,IAAI,IAAI,EAAE;AAClB,YAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE;QAC7B;QACA,OAAO,EAAE,QAAQ,EAAE;AACrB,IAAA,CAAC;AACH;AAEA,SAAS,MAAM,CACb,QAAgB,EAChB,IAAoB,EACpB,SAAiC,EACjC,UAAkC,EAClC,QAAgC,EAAA;AAEhC,IAAA,IAAI,SAAS,CAAC,QAAQ,CAAC,EAAE;AACvB,QAAA,OAAO,MAAM;IACf;AACA,IAAA,IAAI,IAAI,KAAK,QAAQ,EAAE;AACrB,QAAA,OAAO,OAAO;IAChB;AACA,IAAA,IAAI,UAAU,CAAC,QAAQ,CAAC,EAAE;AACxB,QAAA,OAAO,OAAO;IAChB;AACA,IAAA,IAAI,QAAQ,CAAC,QAAQ,CAAC,EAAE;AACtB,QAAA,OAAO,KAAK;IACd;AACA,IAAA,IAAI,IAAI,KAAK,SAAS,EAAE;AACtB,QAAA,OAAO,MAAM;IACf;AACA,IAAA,OAAO,KAAK;AACd;;;;"}
@@ -165,6 +165,13 @@ function applyUpdatedOutput(agg, output) {
165
165
  }
166
166
  agg.updatedOutput = output.updatedOutput;
167
167
  }
168
+ function applyAllowedDecisions(agg, output) {
169
+ if (!('allowedDecisions' in output) ||
170
+ output.allowedDecisions === undefined) {
171
+ return;
172
+ }
173
+ agg.allowedDecisions = output.allowedDecisions;
174
+ }
168
175
  function fold(outcomes) {
169
176
  const agg = freshResult();
170
177
  for (const outcome of outcomes) {
@@ -178,11 +185,21 @@ function fold(outcomes) {
178
185
  if (output === null) {
179
186
  continue;
180
187
  }
188
+ /**
189
+ * Skip fire-and-forget outputs entirely: the agent has already
190
+ * moved on, so an async hook cannot influence the run. Background
191
+ * work inside the hook body still runs (we don't cancel it), it
192
+ * just doesn't fold into the aggregate result.
193
+ */
194
+ if (output.async === true) {
195
+ continue;
196
+ }
181
197
  applyContext(agg, output);
182
198
  applyStopFlag(agg, output);
183
199
  applyDecision(agg, output);
184
200
  applyUpdatedInput(agg, output);
185
201
  applyUpdatedOutput(agg, output);
202
+ applyAllowedDecisions(agg, output);
186
203
  }
187
204
  return agg;
188
205
  }
@@ -268,7 +285,29 @@ async function executeHooks(opts) {
268
285
  }
269
286
  const outcomes = await Promise.all(tasks);
270
287
  reportErrors(outcomes, event, logger);
271
- return fold(outcomes);
288
+ const aggregated = fold(outcomes);
289
+ /**
290
+ * Centralized `preventContinuation` propagation: when any hook (across
291
+ * any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,
292
+ * etc.) returns `preventContinuation: true`, raise a halt signal on
293
+ * the registry scoped to the run's `sessionId`. `Run.processStream`
294
+ * polls the signal between stream events using its own id and exits
295
+ * cleanly, skipping the `Stop` hook (since the run is being halted,
296
+ * not naturally completing).
297
+ *
298
+ * First-write-wins per session inside the registry — a halt already
299
+ * raised by an earlier hook in the same run is preserved so the
300
+ * original `reason` / `source` are not clobbered. Hooks fired
301
+ * without a `sessionId` cannot raise a halt (there's no run for the
302
+ * loop to poll under), which is fine: every in-tree callsite passes
303
+ * `sessionId: runId`. Pre-stream callsites in `Run.processStream`
304
+ * still read `preventContinuation` directly off the result for an
305
+ * early return because they have not yet entered the stream loop.
306
+ */
307
+ if (aggregated.preventContinuation === true && sessionId !== undefined) {
308
+ registry.haltRun(sessionId, aggregated.stopReason ?? 'preventContinuation', event);
309
+ }
310
+ return aggregated;
272
311
  }
273
312
 
274
313
  exports.DEFAULT_HOOK_TIMEOUT_MS = DEFAULT_HOOK_TIMEOUT_MS;
@@ -1 +1 @@
1
- {"version":3,"file":"executeHooks.cjs","sources":["../../../src/hooks/executeHooks.ts"],"sourcesContent":["// src/hooks/executeHooks.ts\nimport type { Logger } from 'winston';\nimport type { HookRegistry } from './HookRegistry';\nimport type {\n HookInput,\n HookEvent,\n HookOutput,\n HookMatcher,\n ToolDecision,\n StopDecision,\n HookCallback,\n AggregatedHookResult,\n} from './types';\nimport { matchesQuery } from './matchers';\n\n/** Default per-hook timeout when a matcher doesn't set its own. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Options for a single `executeHooks` call. The `input` drives everything —\n * the event name is read from `input.hook_event_name`, matchers are looked\n * up against that event, and each hook receives `input` directly.\n */\nexport interface ExecuteHooksOptions {\n registry: HookRegistry;\n input: HookInput;\n /** Scope lookup to this session (in addition to global matchers). */\n sessionId?: string;\n /** Query string matched against each matcher's pattern (tool name, etc.). */\n matchQuery?: string;\n /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */\n signal?: AbortSignal;\n /** Default per-hook timeout; overridden by `matcher.timeout` when present. */\n timeoutMs?: number;\n /** Optional winston logger for non-internal hook errors. */\n logger?: Logger;\n}\n\ntype WideMatcher = HookMatcher<HookEvent>;\ntype WideCallback = HookCallback<HookEvent>;\n\ninterface HookOutcome {\n matcher: WideMatcher;\n output: HookOutput | null;\n error: string | null;\n timedOut: boolean;\n}\n\nfunction freshResult(): AggregatedHookResult {\n return {\n additionalContexts: [],\n errors: [],\n };\n}\n\nfunction combineSignals(\n parent: AbortSignal | undefined,\n timeoutMs: number\n): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (parent === undefined) {\n return timeoutSignal;\n }\n return AbortSignal.any([parent, timeoutSignal]);\n}\n\nfunction isTimeout(err: unknown): boolean {\n if (err instanceof Error) {\n return err.name === 'TimeoutError' || err.name === 'AbortError';\n }\n return false;\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message !== '' ? err.message : err.name;\n }\n return String(err);\n}\n\nfunction makeAbortPromise(signal: AbortSignal): {\n promise: Promise<never>;\n cleanup: () => void;\n} {\n let onAbort: (() => void) | undefined;\n const promise = new Promise<never>((_resolve, reject) => {\n if (signal.aborted) {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n return;\n }\n onAbort = (): void => {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n const cleanup = (): void => {\n if (onAbort !== undefined) {\n signal.removeEventListener('abort', onAbort);\n onAbort = undefined;\n }\n };\n return { promise, cleanup };\n}\n\nasync function runHook(\n hook: WideCallback,\n input: HookInput,\n signal: AbortSignal,\n matcher: WideMatcher\n): Promise<HookOutcome> {\n const hookPromise = Promise.resolve().then(() => hook(input, signal));\n const { promise: abortPromise, cleanup } = makeAbortPromise(signal);\n try {\n const output = await Promise.race([hookPromise, abortPromise]);\n return { matcher, output, error: null, timedOut: false };\n } catch (err) {\n return {\n matcher,\n output: null,\n error: describeError(err),\n timedOut: isTimeout(err),\n };\n } finally {\n cleanup();\n }\n}\n\nfunction reportErrors(\n outcomes: readonly HookOutcome[],\n event: HookEvent,\n logger: Logger | undefined\n): void {\n for (const outcome of outcomes) {\n if (outcome.error === null) {\n continue;\n }\n if (outcome.matcher.internal === true) {\n continue;\n }\n const label = outcome.timedOut ? 'timed out' : 'threw an error';\n const message = `Hook for ${event} ${label}: ${outcome.error}`;\n if (logger !== undefined) {\n logger.warn(message);\n continue;\n }\n // eslint-disable-next-line no-console\n console.warn(message);\n }\n}\n\nfunction applyToolDecision(\n agg: AggregatedHookResult,\n decision: ToolDecision,\n reason: string | undefined\n): void {\n if (decision === 'deny') {\n if (agg.decision === 'deny') {\n return;\n }\n agg.decision = 'deny';\n agg.reason = reason;\n return;\n }\n if (decision === 'ask') {\n if (agg.decision === 'deny' || agg.decision === 'ask') {\n return;\n }\n agg.decision = 'ask';\n agg.reason = reason;\n return;\n }\n if (agg.decision === undefined) {\n agg.decision = 'allow';\n agg.reason = reason;\n }\n}\n\nfunction applyStopDecision(\n agg: AggregatedHookResult,\n decision: StopDecision,\n reason: string | undefined\n): void {\n if (decision === 'block') {\n if (agg.stopDecision === 'block') {\n return;\n }\n agg.stopDecision = 'block';\n agg.reason = reason;\n return;\n }\n if (agg.stopDecision === undefined) {\n agg.stopDecision = 'continue';\n if (agg.reason === undefined) {\n agg.reason = reason;\n }\n }\n}\n\nfunction applyDecision(agg: AggregatedHookResult, output: HookOutput): void {\n if (!('decision' in output) || output.decision === undefined) {\n return;\n }\n const decision = output.decision;\n const reason =\n 'reason' in output && typeof output.reason === 'string'\n ? output.reason\n : undefined;\n if (decision === 'deny' || decision === 'ask' || decision === 'allow') {\n applyToolDecision(agg, decision, reason);\n return;\n }\n applyStopDecision(agg, decision, reason);\n}\n\nfunction applyContext(agg: AggregatedHookResult, output: HookOutput): void {\n if (\n typeof output.additionalContext === 'string' &&\n output.additionalContext.length > 0\n ) {\n agg.additionalContexts.push(output.additionalContext);\n }\n}\n\nfunction applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {\n if (output.preventContinuation !== true) {\n return;\n }\n agg.preventContinuation = true;\n if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {\n agg.stopReason = output.stopReason;\n }\n}\n\nfunction applyUpdatedInput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedInput' in output) || output.updatedInput === undefined) {\n return;\n }\n agg.updatedInput = output.updatedInput;\n}\n\nfunction applyUpdatedOutput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedOutput' in output) || output.updatedOutput === undefined) {\n return;\n }\n agg.updatedOutput = output.updatedOutput;\n}\n\nfunction fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {\n const agg = freshResult();\n for (const outcome of outcomes) {\n if (outcome.error !== null) {\n if (outcome.matcher.internal !== true) {\n agg.errors.push(outcome.error);\n }\n continue;\n }\n const output = outcome.output;\n if (output === null) {\n continue;\n }\n applyContext(agg, output);\n applyStopFlag(agg, output);\n applyDecision(agg, output);\n applyUpdatedInput(agg, output);\n applyUpdatedOutput(agg, output);\n }\n return agg;\n}\n\n/**\n * Fires every matcher registered against `input.hook_event_name`, folding\n * their results per `deny > ask > allow` precedence and accumulating\n * context/errors.\n *\n * ## Parallelism and determinism\n *\n * All matching hooks fire simultaneously and are awaited via `Promise.all`,\n * which preserves input-array order in its returned results. The fold\n * therefore iterates outcomes in **registration order** — outer loop over\n * matchers as they sit in the registry (global first, then session), inner\n * loop over each matcher's `hooks` array. Last-writer-wins fields\n * (`updatedInput`, `updatedOutput`) are deterministic in that order, even\n * though hooks may complete in arbitrary wall-clock order.\n *\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle\n * precedence bugs when matchers are added in a different order than\n * expected.\n *\n * ## Timeouts and cancellation\n *\n * Each matcher receives **one shared `AbortSignal`** derived from the\n * caller's parent signal combined with `matcher.timeout` (falling back to\n * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the\n * signal across hooks in a matcher collapses N timer allocations into\n * one, which matters on the PreToolUse hot path where a matcher with\n * several hooks fires on every tool call. Each hook call is raced\n * against the shared signal, so even a hook that ignores the signal is\n * force-unblocked when the timeout fires. Timeout/abort errors are\n * swallowed into the aggregated result's `errors` array (non-fatal by\n * default).\n *\n * ## Internal matchers\n *\n * A matcher with `internal: true` is excluded from both the `errors` array\n * and the logger output. Use it for infrastructure hooks whose failures\n * should not pollute user-visible diagnostics.\n *\n * ## Once semantics — atomic at-most-once\n *\n * A matcher with `once: true` is removed from the registry **before any\n * hook runs**, inside the synchronous prefix of `executeHooks` (between\n * `getMatchers` and the first `await`). Because Node's event loop serialises\n * sync work, two concurrent `executeHooks` calls can never both observe\n * and dispatch the same `once` matcher — whichever call runs its sync\n * prefix first consumes it, and the loser sees an empty bucket.\n *\n * Trade-off: if every hook in a `once` matcher throws, the matcher is\n * still gone. \"Once\" here means \"at most one dispatch, ever\", not \"at\n * most one successful execution with retry on failure\". Hosts that need\n * retry semantics should register a normal matcher and self-unregister\n * via the `unregister` callback returned from `registry.register`.\n */\nexport async function executeHooks(\n opts: ExecuteHooksOptions\n): Promise<AggregatedHookResult> {\n const {\n registry,\n input,\n sessionId,\n matchQuery,\n signal,\n timeoutMs = DEFAULT_HOOK_TIMEOUT_MS,\n logger,\n } = opts;\n const event = input.hook_event_name;\n const matchers = registry.getMatchers(event, sessionId);\n if (matchers.length === 0) {\n return freshResult();\n }\n\n // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---\n const tasks: Promise<HookOutcome>[] = [];\n for (const matcher of matchers) {\n if (!matchesQuery(matcher.pattern, matchQuery)) {\n continue;\n }\n if (matcher.once === true) {\n registry.removeMatcher(event, matcher, sessionId);\n }\n const perHookTimeout = matcher.timeout ?? timeoutMs;\n const matcherSignal = combineSignals(signal, perHookTimeout);\n for (const hook of matcher.hooks) {\n tasks.push(runHook(hook, input, matcherSignal, matcher));\n }\n }\n // --- END SYNC CRITICAL SECTION ---\n if (tasks.length === 0) {\n return freshResult();\n }\n\n const outcomes = await Promise.all(tasks);\n reportErrors(outcomes, event, logger);\n return fold(outcomes);\n}\n"],"names":["matchers","matchesQuery"],"mappings":"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"}
1
+ {"version":3,"file":"executeHooks.cjs","sources":["../../../src/hooks/executeHooks.ts"],"sourcesContent":["// src/hooks/executeHooks.ts\nimport type { Logger } from 'winston';\nimport type { HookRegistry } from './HookRegistry';\nimport type {\n HookInput,\n HookEvent,\n HookOutput,\n HookMatcher,\n ToolDecision,\n StopDecision,\n HookCallback,\n AggregatedHookResult,\n} from './types';\nimport { matchesQuery } from './matchers';\n\n/** Default per-hook timeout when a matcher doesn't set its own. */\nexport const DEFAULT_HOOK_TIMEOUT_MS = 30_000;\n\n/**\n * Options for a single `executeHooks` call. The `input` drives everything —\n * the event name is read from `input.hook_event_name`, matchers are looked\n * up against that event, and each hook receives `input` directly.\n */\nexport interface ExecuteHooksOptions {\n registry: HookRegistry;\n input: HookInput;\n /** Scope lookup to this session (in addition to global matchers). */\n sessionId?: string;\n /** Query string matched against each matcher's pattern (tool name, etc.). */\n matchQuery?: string;\n /** Parent AbortSignal — combined with per-hook timeout into the hook signal. */\n signal?: AbortSignal;\n /** Default per-hook timeout; overridden by `matcher.timeout` when present. */\n timeoutMs?: number;\n /** Optional winston logger for non-internal hook errors. */\n logger?: Logger;\n}\n\ntype WideMatcher = HookMatcher<HookEvent>;\ntype WideCallback = HookCallback<HookEvent>;\n\ninterface HookOutcome {\n matcher: WideMatcher;\n output: HookOutput | null;\n error: string | null;\n timedOut: boolean;\n}\n\nfunction freshResult(): AggregatedHookResult {\n return {\n additionalContexts: [],\n errors: [],\n };\n}\n\nfunction combineSignals(\n parent: AbortSignal | undefined,\n timeoutMs: number\n): AbortSignal {\n const timeoutSignal = AbortSignal.timeout(timeoutMs);\n if (parent === undefined) {\n return timeoutSignal;\n }\n return AbortSignal.any([parent, timeoutSignal]);\n}\n\nfunction isTimeout(err: unknown): boolean {\n if (err instanceof Error) {\n return err.name === 'TimeoutError' || err.name === 'AbortError';\n }\n return false;\n}\n\nfunction describeError(err: unknown): string {\n if (err instanceof Error) {\n return err.message !== '' ? err.message : err.name;\n }\n return String(err);\n}\n\nfunction makeAbortPromise(signal: AbortSignal): {\n promise: Promise<never>;\n cleanup: () => void;\n} {\n let onAbort: (() => void) | undefined;\n const promise = new Promise<never>((_resolve, reject) => {\n if (signal.aborted) {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n return;\n }\n onAbort = (): void => {\n reject(\n signal.reason instanceof Error ? signal.reason : new Error('aborted')\n );\n };\n signal.addEventListener('abort', onAbort, { once: true });\n });\n const cleanup = (): void => {\n if (onAbort !== undefined) {\n signal.removeEventListener('abort', onAbort);\n onAbort = undefined;\n }\n };\n return { promise, cleanup };\n}\n\nasync function runHook(\n hook: WideCallback,\n input: HookInput,\n signal: AbortSignal,\n matcher: WideMatcher\n): Promise<HookOutcome> {\n const hookPromise = Promise.resolve().then(() => hook(input, signal));\n const { promise: abortPromise, cleanup } = makeAbortPromise(signal);\n try {\n const output = await Promise.race([hookPromise, abortPromise]);\n return { matcher, output, error: null, timedOut: false };\n } catch (err) {\n return {\n matcher,\n output: null,\n error: describeError(err),\n timedOut: isTimeout(err),\n };\n } finally {\n cleanup();\n }\n}\n\nfunction reportErrors(\n outcomes: readonly HookOutcome[],\n event: HookEvent,\n logger: Logger | undefined\n): void {\n for (const outcome of outcomes) {\n if (outcome.error === null) {\n continue;\n }\n if (outcome.matcher.internal === true) {\n continue;\n }\n const label = outcome.timedOut ? 'timed out' : 'threw an error';\n const message = `Hook for ${event} ${label}: ${outcome.error}`;\n if (logger !== undefined) {\n logger.warn(message);\n continue;\n }\n // eslint-disable-next-line no-console\n console.warn(message);\n }\n}\n\nfunction applyToolDecision(\n agg: AggregatedHookResult,\n decision: ToolDecision,\n reason: string | undefined\n): void {\n if (decision === 'deny') {\n if (agg.decision === 'deny') {\n return;\n }\n agg.decision = 'deny';\n agg.reason = reason;\n return;\n }\n if (decision === 'ask') {\n if (agg.decision === 'deny' || agg.decision === 'ask') {\n return;\n }\n agg.decision = 'ask';\n agg.reason = reason;\n return;\n }\n if (agg.decision === undefined) {\n agg.decision = 'allow';\n agg.reason = reason;\n }\n}\n\nfunction applyStopDecision(\n agg: AggregatedHookResult,\n decision: StopDecision,\n reason: string | undefined\n): void {\n if (decision === 'block') {\n if (agg.stopDecision === 'block') {\n return;\n }\n agg.stopDecision = 'block';\n agg.reason = reason;\n return;\n }\n if (agg.stopDecision === undefined) {\n agg.stopDecision = 'continue';\n if (agg.reason === undefined) {\n agg.reason = reason;\n }\n }\n}\n\nfunction applyDecision(agg: AggregatedHookResult, output: HookOutput): void {\n if (!('decision' in output) || output.decision === undefined) {\n return;\n }\n const decision = output.decision;\n const reason =\n 'reason' in output && typeof output.reason === 'string'\n ? output.reason\n : undefined;\n if (decision === 'deny' || decision === 'ask' || decision === 'allow') {\n applyToolDecision(agg, decision, reason);\n return;\n }\n applyStopDecision(agg, decision, reason);\n}\n\nfunction applyContext(agg: AggregatedHookResult, output: HookOutput): void {\n if (\n typeof output.additionalContext === 'string' &&\n output.additionalContext.length > 0\n ) {\n agg.additionalContexts.push(output.additionalContext);\n }\n}\n\nfunction applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {\n if (output.preventContinuation !== true) {\n return;\n }\n agg.preventContinuation = true;\n if (typeof output.stopReason === 'string' && agg.stopReason === undefined) {\n agg.stopReason = output.stopReason;\n }\n}\n\nfunction applyUpdatedInput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedInput' in output) || output.updatedInput === undefined) {\n return;\n }\n agg.updatedInput = output.updatedInput;\n}\n\nfunction applyUpdatedOutput(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (!('updatedOutput' in output) || output.updatedOutput === undefined) {\n return;\n }\n agg.updatedOutput = output.updatedOutput;\n}\n\nfunction applyAllowedDecisions(\n agg: AggregatedHookResult,\n output: HookOutput\n): void {\n if (\n !('allowedDecisions' in output) ||\n output.allowedDecisions === undefined\n ) {\n return;\n }\n agg.allowedDecisions = output.allowedDecisions;\n}\n\nfunction fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {\n const agg = freshResult();\n for (const outcome of outcomes) {\n if (outcome.error !== null) {\n if (outcome.matcher.internal !== true) {\n agg.errors.push(outcome.error);\n }\n continue;\n }\n const output = outcome.output;\n if (output === null) {\n continue;\n }\n /**\n * Skip fire-and-forget outputs entirely: the agent has already\n * moved on, so an async hook cannot influence the run. Background\n * work inside the hook body still runs (we don't cancel it), it\n * just doesn't fold into the aggregate result.\n */\n if (output.async === true) {\n continue;\n }\n applyContext(agg, output);\n applyStopFlag(agg, output);\n applyDecision(agg, output);\n applyUpdatedInput(agg, output);\n applyUpdatedOutput(agg, output);\n applyAllowedDecisions(agg, output);\n }\n return agg;\n}\n\n/**\n * Fires every matcher registered against `input.hook_event_name`, folding\n * their results per `deny > ask > allow` precedence and accumulating\n * context/errors.\n *\n * ## Parallelism and determinism\n *\n * All matching hooks fire simultaneously and are awaited via `Promise.all`,\n * which preserves input-array order in its returned results. The fold\n * therefore iterates outcomes in **registration order** — outer loop over\n * matchers as they sit in the registry (global first, then session), inner\n * loop over each matcher's `hooks` array. Last-writer-wins fields\n * (`updatedInput`, `updatedOutput`) are deterministic in that order, even\n * though hooks may complete in arbitrary wall-clock order.\n *\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput`/`updatedOutput` to one hook per matcher to avoid subtle\n * precedence bugs when matchers are added in a different order than\n * expected.\n *\n * ## Timeouts and cancellation\n *\n * Each matcher receives **one shared `AbortSignal`** derived from the\n * caller's parent signal combined with `matcher.timeout` (falling back to\n * `opts.timeoutMs`, default {@link DEFAULT_HOOK_TIMEOUT_MS}). Sharing the\n * signal across hooks in a matcher collapses N timer allocations into\n * one, which matters on the PreToolUse hot path where a matcher with\n * several hooks fires on every tool call. Each hook call is raced\n * against the shared signal, so even a hook that ignores the signal is\n * force-unblocked when the timeout fires. Timeout/abort errors are\n * swallowed into the aggregated result's `errors` array (non-fatal by\n * default).\n *\n * ## Internal matchers\n *\n * A matcher with `internal: true` is excluded from both the `errors` array\n * and the logger output. Use it for infrastructure hooks whose failures\n * should not pollute user-visible diagnostics.\n *\n * ## Once semantics — atomic at-most-once\n *\n * A matcher with `once: true` is removed from the registry **before any\n * hook runs**, inside the synchronous prefix of `executeHooks` (between\n * `getMatchers` and the first `await`). Because Node's event loop serialises\n * sync work, two concurrent `executeHooks` calls can never both observe\n * and dispatch the same `once` matcher — whichever call runs its sync\n * prefix first consumes it, and the loser sees an empty bucket.\n *\n * Trade-off: if every hook in a `once` matcher throws, the matcher is\n * still gone. \"Once\" here means \"at most one dispatch, ever\", not \"at\n * most one successful execution with retry on failure\". Hosts that need\n * retry semantics should register a normal matcher and self-unregister\n * via the `unregister` callback returned from `registry.register`.\n */\nexport async function executeHooks(\n opts: ExecuteHooksOptions\n): Promise<AggregatedHookResult> {\n const {\n registry,\n input,\n sessionId,\n matchQuery,\n signal,\n timeoutMs = DEFAULT_HOOK_TIMEOUT_MS,\n logger,\n } = opts;\n const event = input.hook_event_name;\n const matchers = registry.getMatchers(event, sessionId);\n if (matchers.length === 0) {\n return freshResult();\n }\n\n // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---\n const tasks: Promise<HookOutcome>[] = [];\n for (const matcher of matchers) {\n if (!matchesQuery(matcher.pattern, matchQuery)) {\n continue;\n }\n if (matcher.once === true) {\n registry.removeMatcher(event, matcher, sessionId);\n }\n const perHookTimeout = matcher.timeout ?? timeoutMs;\n const matcherSignal = combineSignals(signal, perHookTimeout);\n for (const hook of matcher.hooks) {\n tasks.push(runHook(hook, input, matcherSignal, matcher));\n }\n }\n // --- END SYNC CRITICAL SECTION ---\n if (tasks.length === 0) {\n return freshResult();\n }\n\n const outcomes = await Promise.all(tasks);\n reportErrors(outcomes, event, logger);\n const aggregated = fold(outcomes);\n /**\n * Centralized `preventContinuation` propagation: when any hook (across\n * any callsite — RunStart, PreToolUse, PostToolBatch, SubagentStop,\n * etc.) returns `preventContinuation: true`, raise a halt signal on\n * the registry scoped to the run's `sessionId`. `Run.processStream`\n * polls the signal between stream events using its own id and exits\n * cleanly, skipping the `Stop` hook (since the run is being halted,\n * not naturally completing).\n *\n * First-write-wins per session inside the registry — a halt already\n * raised by an earlier hook in the same run is preserved so the\n * original `reason` / `source` are not clobbered. Hooks fired\n * without a `sessionId` cannot raise a halt (there's no run for the\n * loop to poll under), which is fine: every in-tree callsite passes\n * `sessionId: runId`. Pre-stream callsites in `Run.processStream`\n * still read `preventContinuation` directly off the result for an\n * early return because they have not yet entered the stream loop.\n */\n if (aggregated.preventContinuation === true && sessionId !== undefined) {\n registry.haltRun(\n sessionId,\n aggregated.stopReason ?? 'preventContinuation',\n event\n );\n }\n return aggregated;\n}\n"],"names":["matchers","matchesQuery"],"mappings":"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"}
@@ -14,6 +14,7 @@ const HOOK_EVENTS = [
14
14
  'PreToolUse',
15
15
  'PostToolUse',
16
16
  'PostToolUseFailure',
17
+ 'PostToolBatch',
17
18
  'PermissionDenied',
18
19
  'SubagentStart',
19
20
  'SubagentStop',
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs","sources":["../../../src/hooks/types.ts"],"sourcesContent":["// src/hooks/types.ts\nimport type { BaseMessage } from '@langchain/core/messages';\n\n/**\n * Closed set of hook lifecycle events supported by the hooks system.\n *\n * These mirror the subset of Claude Code's event surface that makes sense\n * for a library context (no filesystem/CLI-specific events). See\n * `docs/hooks-design-report.md` §3.2 for the mapping to existing\n * `@librechat/agents` emission points.\n */\nexport const HOOK_EVENTS = [\n 'RunStart',\n 'UserPromptSubmit',\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n 'PermissionDenied',\n 'SubagentStart',\n 'SubagentStop',\n 'Stop',\n 'StopFailure',\n 'PreCompact',\n 'PostCompact',\n] as const;\n\nexport type HookEvent = (typeof HOOK_EVENTS)[number];\n\n/** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */\nexport type ToolDecision = 'allow' | 'deny' | 'ask';\n\n/** Stop-loop decision; `block` means \"do not stop, run another turn\". Any `block` wins. */\nexport type StopDecision = 'continue' | 'block';\n\n/**\n * Fields shared by every `HookInput`. Discriminated by `hook_event_name`.\n *\n * - `runId` identifies the current agent run and is always present.\n * - `threadId` identifies the conversation thread when the host has one.\n * - `agentId` is only set when the hook fires inside a subagent scope.\n */\nexport interface BaseHookInput {\n runId: string;\n threadId?: string;\n agentId?: string;\n}\n\nexport interface RunStartHookInput extends BaseHookInput {\n hook_event_name: 'RunStart';\n messages: BaseMessage[];\n}\n\nexport interface UserPromptSubmitHookInput extends BaseHookInput {\n hook_event_name: 'UserPromptSubmit';\n prompt: string;\n attachments?: BaseMessage[];\n}\n\n/**\n * Fires before a tool is invoked. Hook may return `deny`/`ask`/`allow` and/or\n * an `updatedInput` that replaces the tool arguments before invocation.\n *\n * `toolInput` is intentionally typed as `Record<string, unknown>` because the\n * SDK is tool-agnostic — concrete tool argument shapes are only known at the\n * call site and are narrowed by the host. This is the one escape hatch in\n * the hook type system.\n */\nexport interface PreToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PreToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n /**\n * Number of times this tool has been invoked in prior batches within the\n * current run. Within a single batch of parallel calls, all calls to the\n * same tool share the same turn value — per-call discrimination within a\n * batch is not supported in v1.\n */\n turn?: number;\n}\n\nexport interface PostToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolOutput: unknown;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n}\n\nexport interface PostToolUseFailureHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUseFailure';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n error: string;\n stepId?: string;\n turn?: number;\n}\n\nexport interface PermissionDeniedHookInput extends BaseHookInput {\n hook_event_name: 'PermissionDenied';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n reason: string;\n}\n\nexport interface SubagentStartHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStart';\n parentAgentId?: string;\n agentId: string;\n agentType: string;\n inputs: BaseMessage[];\n}\n\nexport interface SubagentStopHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStop';\n agentId: string;\n agentType: string;\n messages: BaseMessage[];\n}\n\nexport interface StopHookInput extends BaseHookInput {\n hook_event_name: 'Stop';\n messages: BaseMessage[];\n stopReason?: string;\n stopHookActive: boolean;\n}\n\nexport interface StopFailureHookInput extends BaseHookInput {\n hook_event_name: 'StopFailure';\n error: string;\n lastAssistantMessage?: BaseMessage;\n}\n\nexport interface PreCompactHookInput extends BaseHookInput {\n hook_event_name: 'PreCompact';\n messagesBeforeCount: number;\n /**\n * What triggered compaction. Matches `SummarizationTrigger.type` from the\n * agent's summarization config. `'default'` means no trigger was\n * configured and compaction fired because messages were pruned.\n */\n trigger:\n | 'token_ratio'\n | 'remaining_tokens'\n | 'messages_to_refine'\n | 'default'\n | (string & {});\n}\n\nexport interface PostCompactHookInput extends BaseHookInput {\n hook_event_name: 'PostCompact';\n summary: string;\n /**\n * Number of messages remaining after compaction. The summarize node\n * returns a `removeAll` signal that clears all messages from state;\n * the summary itself is injected into the system prompt, not as a\n * message. This is `0` at the point of hook dispatch.\n */\n messagesAfterCount: number;\n}\n\n/** Discriminated union of every hook input shape. */\nexport type HookInput =\n | RunStartHookInput\n | UserPromptSubmitHookInput\n | PreToolUseHookInput\n | PostToolUseHookInput\n | PostToolUseFailureHookInput\n | PermissionDeniedHookInput\n | SubagentStartHookInput\n | SubagentStopHookInput\n | StopHookInput\n | StopFailureHookInput\n | PreCompactHookInput\n | PostCompactHookInput;\n\n/** Compile-time map from event name to its input shape. */\nexport type HookInputByEvent = {\n RunStart: RunStartHookInput;\n UserPromptSubmit: UserPromptSubmitHookInput;\n PreToolUse: PreToolUseHookInput;\n PostToolUse: PostToolUseHookInput;\n PostToolUseFailure: PostToolUseFailureHookInput;\n PermissionDenied: PermissionDeniedHookInput;\n SubagentStart: SubagentStartHookInput;\n SubagentStop: SubagentStopHookInput;\n Stop: StopHookInput;\n StopFailure: StopFailureHookInput;\n PreCompact: PreCompactHookInput;\n PostCompact: PostCompactHookInput;\n};\n\n/**\n * Fields common to every hook output. Hooks that have nothing to say simply\n * return `{}` (or omit the fields below).\n */\nexport interface BaseHookOutput {\n /** Context string to inject into the conversation. Accumulated across hooks. */\n additionalContext?: string;\n /** True to prevent the next model turn. Any hook can set this. */\n preventContinuation?: boolean;\n /** Reason reported alongside `preventContinuation`. */\n stopReason?: string;\n}\n\nexport type RunStartHookOutput = BaseHookOutput;\n\nexport interface UserPromptSubmitHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport interface PreToolUseHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n /**\n * Replacement tool input. Merged into the pending tool call by the host.\n *\n * When multiple hooks set `updatedInput` within a single `executeHooks`\n * call, the last writer in registration order wins (outer loop: matcher\n * registration order; inner loop: hook position within the matcher). The\n * winner is deterministic — `Promise.all` preserves input-array order.\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput` to one hook per matcher to avoid confusing precedence.\n */\n updatedInput?: Record<string, unknown>;\n}\n\nexport interface PostToolUseHookOutput extends BaseHookOutput {\n /**\n * Replacement tool output. Flows through the aggregated result so the\n * host can substitute it before appending the tool result message.\n * Ordering semantics match `PreToolUseHookOutput.updatedInput`:\n * last-writer-wins in registration order.\n */\n updatedOutput?: unknown;\n}\n\nexport type PostToolUseFailureHookOutput = BaseHookOutput;\n\nexport type PermissionDeniedHookOutput = BaseHookOutput;\n\nexport interface SubagentStartHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport type SubagentStopHookOutput = BaseHookOutput;\n\nexport interface StopHookOutput extends BaseHookOutput {\n decision?: StopDecision;\n reason?: string;\n}\n\nexport type StopFailureHookOutput = BaseHookOutput;\n\nexport type PreCompactHookOutput = BaseHookOutput;\n\nexport type PostCompactHookOutput = BaseHookOutput;\n\n/** Compile-time map from event name to its output shape. */\nexport type HookOutputByEvent = {\n RunStart: RunStartHookOutput;\n UserPromptSubmit: UserPromptSubmitHookOutput;\n PreToolUse: PreToolUseHookOutput;\n PostToolUse: PostToolUseHookOutput;\n PostToolUseFailure: PostToolUseFailureHookOutput;\n PermissionDenied: PermissionDeniedHookOutput;\n SubagentStart: SubagentStartHookOutput;\n SubagentStop: SubagentStopHookOutput;\n Stop: StopHookOutput;\n StopFailure: StopFailureHookOutput;\n PreCompact: PreCompactHookOutput;\n PostCompact: PostCompactHookOutput;\n};\n\n/** Superset output shape used by the executor's fold loop. */\nexport type HookOutput =\n | RunStartHookOutput\n | UserPromptSubmitHookOutput\n | PreToolUseHookOutput\n | PostToolUseHookOutput\n | PostToolUseFailureHookOutput\n | PermissionDeniedHookOutput\n | SubagentStartHookOutput\n | SubagentStopHookOutput\n | StopHookOutput\n | StopFailureHookOutput\n | PreCompactHookOutput\n | PostCompactHookOutput;\n\n/**\n * A hook callback is a plain async function registered against a specific\n * event. The `signal` is always supplied by `executeHooks` and combines the\n * batch's parent signal with the per-hook timeout — callbacks that perform\n * long-running work should observe it.\n */\nexport type HookCallback<E extends HookEvent = HookEvent> = (\n input: HookInputByEvent[E],\n signal: AbortSignal\n) => HookOutputByEvent[E] | Promise<HookOutputByEvent[E]>;\n\n/**\n * A matcher groups one or more callbacks under a shared regex filter and\n * shared timeout/once/internal flags. The generic `E` ties the callback\n * types to the event the matcher is registered against.\n */\nexport interface HookMatcher<E extends HookEvent = HookEvent> {\n /**\n * Regex pattern matched against the event's primary query string (e.g.\n * the tool name for `PreToolUse`, the agent type for `SubagentStart`).\n *\n * Omitted or empty means \"always match\". For events that do not supply a\n * query string (`RunStart`, `Stop`, etc.), only wildcard matchers fire —\n * a non-empty pattern on such events will never match.\n *\n * Patterns are treated as trusted input: `executeHooks` compiles them\n * with `new RegExp(pattern)` without any sandbox, and a pathological\n * pattern can block the event loop. Host registration code is expected\n * to validate or length-bound patterns that originate from user input.\n */\n pattern?: string;\n /** Callbacks that fire when the matcher hits. Executed in parallel. */\n hooks: HookCallback<E>[];\n /** Per-matcher timeout in ms. Defaults to the executor's batch timeout. */\n timeout?: number;\n /**\n * Atomically remove the matcher before its first dispatch.\n *\n * `executeHooks` claims `once: true` matchers synchronously — between\n * `getMatchers` and its first `await` — so two concurrent calls cannot\n * both dispatch the same matcher. Whichever call runs its sync prefix\n * first wins the matcher; the other sees an empty bucket.\n *\n * Semantics are \"at most one dispatch, ever\" — if every hook in the\n * matcher throws, the matcher is still gone. Use `once` for\n * fire-and-forget bootstrapping (registration, telemetry, setup). Hosts\n * that need retry semantics should register a normal matcher and\n * self-unregister via the callback returned from `registry.register`.\n */\n once?: boolean;\n /** Internal hooks are excluded from telemetry and non-fatal error logging. */\n internal?: boolean;\n}\n\n/**\n * Storage shape for matchers keyed by event. Each event's matcher list is\n * a generic array parameterized by that event type, so lookup via\n * `HooksByEvent[E]` preserves type-safe callback signatures.\n */\nexport type HooksByEvent = {\n [E in HookEvent]?: HookMatcher<E>[];\n};\n\n/**\n * Aggregated result of a single `executeHooks` call. Fields are populated\n * according to the fold rules in `executeHooks.ts`.\n */\nexport interface AggregatedHookResult {\n /** Folded tool-gating decision; `deny > ask > allow`. */\n decision?: ToolDecision;\n /** Folded stop decision; any `block` wins. */\n stopDecision?: StopDecision;\n /** Reason from the hook that set the winning decision. */\n reason?: string;\n /**\n * Replacement tool input from a `PreToolUse` hook.\n *\n * Last-writer-wins in **registration order**: `executeHooks` uses\n * `Promise.all`, which preserves input-array order, so the fold iterates\n * outcomes in the same order they were pushed — outer loop over matchers\n * as they sit in the registry, inner loop over each matcher's `hooks`\n * array. The winner is therefore deterministic but may not match the\n * order in which hooks actually completed. Consumers that want a single\n * authoritative rewrite should still register one `updatedInput`-setting\n * hook per matcher to avoid subtle precedence bugs.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Replacement tool output from a `PostToolUse` hook.\n *\n * Same last-writer-wins-in-registration-order semantics as\n * `updatedInput`. Present only when at least one hook set it; `undefined`\n * means \"use the original tool output\".\n */\n updatedOutput?: unknown;\n /** Accumulated `additionalContext` strings from every hook, in order. */\n additionalContexts: string[];\n /** True if any hook returned `preventContinuation`. */\n preventContinuation?: boolean;\n /**\n * Reason recorded alongside `preventContinuation`. First winner wins:\n * once a hook sets both flags, later hooks that also set\n * `preventContinuation` do not overwrite the reason.\n */\n stopReason?: string;\n /** Error messages from hooks that threw; always present (possibly empty). */\n errors: string[];\n}\n"],"names":[],"mappings":";;AAGA;;;;;;;AAOG;AACI,MAAM,WAAW,GAAG;IACzB,UAAU;IACV,kBAAkB;IAClB,YAAY;IACZ,aAAa;IACb,oBAAoB;IACpB,kBAAkB;IAClB,eAAe;IACf,cAAc;IACd,MAAM;IACN,aAAa;IACb,YAAY;IACZ,aAAa;;;;;"}
1
+ {"version":3,"file":"types.cjs","sources":["../../../src/hooks/types.ts"],"sourcesContent":["// src/hooks/types.ts\nimport type { BaseMessage } from '@langchain/core/messages';\n\n/**\n * Closed set of hook lifecycle events supported by the hooks system.\n *\n * These mirror the subset of Claude Code's event surface that makes sense\n * for a library context (no filesystem/CLI-specific events). See\n * `docs/hooks-design-report.md` §3.2 for the mapping to existing\n * `@librechat/agents` emission points.\n */\nexport const HOOK_EVENTS = [\n 'RunStart',\n 'UserPromptSubmit',\n 'PreToolUse',\n 'PostToolUse',\n 'PostToolUseFailure',\n 'PostToolBatch',\n 'PermissionDenied',\n 'SubagentStart',\n 'SubagentStop',\n 'Stop',\n 'StopFailure',\n 'PreCompact',\n 'PostCompact',\n] as const;\n\nexport type HookEvent = (typeof HOOK_EVENTS)[number];\n\n/** Tool-gating decision; executeHooks folds with `deny > ask > allow` precedence. */\nexport type ToolDecision = 'allow' | 'deny' | 'ask';\n\n/** Stop-loop decision; `block` means \"do not stop, run another turn\". Any `block` wins. */\nexport type StopDecision = 'continue' | 'block';\n\n/**\n * Fields shared by every `HookInput`. Discriminated by `hook_event_name`.\n *\n * - `runId` identifies the current agent run and is always present.\n * - `threadId` identifies the conversation thread when the host has one.\n * - `agentId` is only set when the hook fires inside a subagent scope.\n */\nexport interface BaseHookInput {\n runId: string;\n threadId?: string;\n agentId?: string;\n}\n\nexport interface RunStartHookInput extends BaseHookInput {\n hook_event_name: 'RunStart';\n messages: BaseMessage[];\n}\n\nexport interface UserPromptSubmitHookInput extends BaseHookInput {\n hook_event_name: 'UserPromptSubmit';\n prompt: string;\n attachments?: BaseMessage[];\n}\n\n/**\n * Fires before a tool is invoked. Hook may return `deny`/`ask`/`allow` and/or\n * an `updatedInput` that replaces the tool arguments before invocation.\n *\n * `toolInput` is intentionally typed as `Record<string, unknown>` because the\n * SDK is tool-agnostic — concrete tool argument shapes are only known at the\n * call site and are narrowed by the host. This is the one escape hatch in\n * the hook type system.\n */\nexport interface PreToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PreToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n /**\n * Number of times this tool has been invoked in prior batches within the\n * current run. Within a single batch of parallel calls, all calls to the\n * same tool share the same turn value — per-call discrimination within a\n * batch is not supported in v1.\n */\n turn?: number;\n}\n\nexport interface PostToolUseHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUse';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolOutput: unknown;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n}\n\nexport interface PostToolUseFailureHookInput extends BaseHookInput {\n hook_event_name: 'PostToolUseFailure';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n error: string;\n stepId?: string;\n turn?: number;\n}\n\n/**\n * Per-tool result snapshot included in a `PostToolBatch` event. Mirrors\n * the data PostToolUse / PostToolUseFailure get individually, but the\n * batch view lets a single hook see the whole set so it can inject one\n * consolidated convention/audit message rather than N per-tool ones.\n */\nexport interface PostToolBatchEntry {\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n stepId?: string;\n turn?: number;\n /** Successful tool output, present only when `status === 'success'`. */\n toolOutput?: unknown;\n /** Error message, present only when `status === 'error'`. */\n error?: string;\n status: 'success' | 'error';\n}\n\n/**\n * Fires once after every tool call in a single batch finishes (including\n * any that were rejected via HITL). Lets a hook react to the batch as a\n * whole — useful for \"inject conventions once for the whole batch\", batch\n * audit logging, or coordinating cleanup that depends on knowing the full\n * result set rather than streaming each tool's result independently.\n *\n * Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks\n * for the same batch have completed, BEFORE the next model call. Pass an\n * `additionalContext` to inject context for that next model turn.\n */\nexport interface PostToolBatchHookInput extends BaseHookInput {\n hook_event_name: 'PostToolBatch';\n /** All tool calls (and their outcomes) from this batch, in batch order. */\n entries: PostToolBatchEntry[];\n}\n\nexport interface PermissionDeniedHookInput extends BaseHookInput {\n hook_event_name: 'PermissionDenied';\n toolName: string;\n toolInput: Record<string, unknown>;\n toolUseId: string;\n reason: string;\n}\n\nexport interface SubagentStartHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStart';\n parentAgentId?: string;\n agentId: string;\n agentType: string;\n inputs: BaseMessage[];\n}\n\nexport interface SubagentStopHookInput extends BaseHookInput {\n hook_event_name: 'SubagentStop';\n agentId: string;\n agentType: string;\n messages: BaseMessage[];\n}\n\nexport interface StopHookInput extends BaseHookInput {\n hook_event_name: 'Stop';\n messages: BaseMessage[];\n stopReason?: string;\n stopHookActive: boolean;\n}\n\nexport interface StopFailureHookInput extends BaseHookInput {\n hook_event_name: 'StopFailure';\n error: string;\n lastAssistantMessage?: BaseMessage;\n}\n\nexport interface PreCompactHookInput extends BaseHookInput {\n hook_event_name: 'PreCompact';\n messagesBeforeCount: number;\n /**\n * What triggered compaction. Matches `SummarizationTrigger.type` from the\n * agent's summarization config. `'default'` means no trigger was\n * configured and compaction fired because messages were pruned.\n */\n trigger:\n | 'token_ratio'\n | 'remaining_tokens'\n | 'messages_to_refine'\n | 'default'\n | (string & {});\n}\n\nexport interface PostCompactHookInput extends BaseHookInput {\n hook_event_name: 'PostCompact';\n summary: string;\n /**\n * Number of messages remaining after compaction. The summarize node\n * returns a `removeAll` signal that clears all messages from state;\n * the summary itself is injected into the system prompt, not as a\n * message. This is `0` at the point of hook dispatch.\n */\n messagesAfterCount: number;\n}\n\n/** Discriminated union of every hook input shape. */\nexport type HookInput =\n | RunStartHookInput\n | UserPromptSubmitHookInput\n | PreToolUseHookInput\n | PostToolUseHookInput\n | PostToolUseFailureHookInput\n | PostToolBatchHookInput\n | PermissionDeniedHookInput\n | SubagentStartHookInput\n | SubagentStopHookInput\n | StopHookInput\n | StopFailureHookInput\n | PreCompactHookInput\n | PostCompactHookInput;\n\n/** Compile-time map from event name to its input shape. */\nexport type HookInputByEvent = {\n RunStart: RunStartHookInput;\n UserPromptSubmit: UserPromptSubmitHookInput;\n PreToolUse: PreToolUseHookInput;\n PostToolUse: PostToolUseHookInput;\n PostToolUseFailure: PostToolUseFailureHookInput;\n PostToolBatch: PostToolBatchHookInput;\n PermissionDenied: PermissionDeniedHookInput;\n SubagentStart: SubagentStartHookInput;\n SubagentStop: SubagentStopHookInput;\n Stop: StopHookInput;\n StopFailure: StopFailureHookInput;\n PreCompact: PreCompactHookInput;\n PostCompact: PostCompactHookInput;\n};\n\n/**\n * Fields common to every hook output. Hooks that have nothing to say simply\n * return `{}` (or omit the fields below).\n */\nexport interface BaseHookOutput {\n /** Context string to inject into the conversation. Accumulated across hooks. */\n additionalContext?: string;\n /** True to prevent the next model turn. Any hook can set this. */\n preventContinuation?: boolean;\n /** Reason reported alongside `preventContinuation`. */\n stopReason?: string;\n /**\n * Marks this hook output as fire-and-forget for INFLUENCE only.\n * When `true`, the SDK skips every other field on this output —\n * `decision`, `additionalContext`, `updatedInput`,\n * `preventContinuation`, `allowedDecisions`, `updatedOutput` are\n * all ignored. The hook's return value cannot block, modify, or\n * inject context, so it's safe to use for pure side effects\n * (logging, metrics, webhooks).\n *\n * Important caveat: the hook's CALLBACK promise is still awaited\n * by `executeHooks` (subject to the matcher's timeout and the\n * default `DEFAULT_HOOK_TIMEOUT_MS`). The SDK does not\n * speculatively detach hooks based on output shape, because the\n * shape is only known after the promise resolves. For TRUE\n * fire-and-forget where the agent doesn't wait at all, the hook\n * body should detach its side effect itself and return\n * immediately:\n *\n * @example\n * ```ts\n * async (input) => {\n * // Detach the slow work — the SDK awaits this hook's\n * // returned promise, which resolves immediately because we\n * // don't `await` the side effect.\n * void sendToLoggingService(input).catch(console.error);\n * return { async: true };\n * };\n * ```\n *\n * @example WRONG — the agent will block on the webhook\n * ```ts\n * async (input) => {\n * await sendToLoggingService(input); // ← awaited, blocks\n * return { async: true }; // returning async:true doesn't undo the await\n * };\n * ```\n *\n * Mirrors Claude Code Agent SDK's `async` output, with the same\n * \"detach inside the hook body\" pattern.\n */\n async?: boolean;\n /**\n * Optional advisory timeout in milliseconds for the background work\n * a host has detached inside an `async: true` hook body. The SDK\n * does not enforce this (the hook's own AbortSignal handling does)\n * but the field is preserved on the wire so downstream\n * observability can surface long-running side effects. Ignored\n * unless `async` is true.\n */\n asyncTimeout?: number;\n}\n\nexport type RunStartHookOutput = BaseHookOutput;\n\nexport interface UserPromptSubmitHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport interface PreToolUseHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n /**\n * Replacement tool input. Merged into the pending tool call by the host.\n *\n * When multiple hooks set `updatedInput` within a single `executeHooks`\n * call, the last writer in registration order wins (outer loop: matcher\n * registration order; inner loop: hook position within the matcher). The\n * winner is deterministic — `Promise.all` preserves input-array order.\n * Consumers that need a single authoritative rewrite should still scope\n * `updatedInput` to one hook per matcher to avoid confusing precedence.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricts which decisions the host UI is allowed to surface for this\n * tool call when the hook returns `decision: 'ask'`. Pass to lock a\n * tool down to a subset of `'approve' | 'reject' | 'edit' | 'respond'`\n * — for example, `['approve', 'reject']` to forbid the user from\n * editing the tool's args or substituting a custom response.\n *\n * The values flow into the resulting interrupt's\n * `review_configs[i].allowed_decisions`. Omitting the field keeps the\n * SDK default (all four decisions advertised). Last-writer-wins in\n * registration order, same precedence rules as `updatedInput`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n}\n\nexport interface PostToolUseHookOutput extends BaseHookOutput {\n /**\n * Replacement tool output. Flows through the aggregated result so the\n * host can substitute it before appending the tool result message.\n * Ordering semantics match `PreToolUseHookOutput.updatedInput`:\n * last-writer-wins in registration order.\n */\n updatedOutput?: unknown;\n}\n\nexport type PostToolUseFailureHookOutput = BaseHookOutput;\n\nexport type PostToolBatchHookOutput = BaseHookOutput;\n\nexport type PermissionDeniedHookOutput = BaseHookOutput;\n\nexport interface SubagentStartHookOutput extends BaseHookOutput {\n decision?: ToolDecision;\n reason?: string;\n}\n\nexport type SubagentStopHookOutput = BaseHookOutput;\n\nexport interface StopHookOutput extends BaseHookOutput {\n decision?: StopDecision;\n reason?: string;\n}\n\nexport type StopFailureHookOutput = BaseHookOutput;\n\nexport type PreCompactHookOutput = BaseHookOutput;\n\nexport type PostCompactHookOutput = BaseHookOutput;\n\n/** Compile-time map from event name to its output shape. */\nexport type HookOutputByEvent = {\n RunStart: RunStartHookOutput;\n UserPromptSubmit: UserPromptSubmitHookOutput;\n PreToolUse: PreToolUseHookOutput;\n PostToolUse: PostToolUseHookOutput;\n PostToolUseFailure: PostToolUseFailureHookOutput;\n PostToolBatch: PostToolBatchHookOutput;\n PermissionDenied: PermissionDeniedHookOutput;\n SubagentStart: SubagentStartHookOutput;\n SubagentStop: SubagentStopHookOutput;\n Stop: StopHookOutput;\n StopFailure: StopFailureHookOutput;\n PreCompact: PreCompactHookOutput;\n PostCompact: PostCompactHookOutput;\n};\n\n/** Superset output shape used by the executor's fold loop. */\nexport type HookOutput =\n | RunStartHookOutput\n | UserPromptSubmitHookOutput\n | PreToolUseHookOutput\n | PostToolUseHookOutput\n | PostToolUseFailureHookOutput\n | PostToolBatchHookOutput\n | PermissionDeniedHookOutput\n | SubagentStartHookOutput\n | SubagentStopHookOutput\n | StopHookOutput\n | StopFailureHookOutput\n | PreCompactHookOutput\n | PostCompactHookOutput;\n\n/**\n * A hook callback is a plain async function registered against a specific\n * event. The `signal` is always supplied by `executeHooks` and combines the\n * batch's parent signal with the per-hook timeout — callbacks that perform\n * long-running work should observe it.\n */\nexport type HookCallback<E extends HookEvent = HookEvent> = (\n input: HookInputByEvent[E],\n signal: AbortSignal\n) => HookOutputByEvent[E] | Promise<HookOutputByEvent[E]>;\n\n/**\n * A matcher groups one or more callbacks under a shared regex filter and\n * shared timeout/once/internal flags. The generic `E` ties the callback\n * types to the event the matcher is registered against.\n */\nexport interface HookMatcher<E extends HookEvent = HookEvent> {\n /**\n * Regex pattern matched against the event's primary query string (e.g.\n * the tool name for `PreToolUse`, the agent type for `SubagentStart`).\n *\n * Omitted or empty means \"always match\". For events that do not supply a\n * query string (`RunStart`, `Stop`, etc.), only wildcard matchers fire —\n * a non-empty pattern on such events will never match.\n *\n * Patterns are treated as trusted input: `executeHooks` compiles them\n * with `new RegExp(pattern)` without any sandbox, and a pathological\n * pattern can block the event loop. Host registration code is expected\n * to validate or length-bound patterns that originate from user input.\n */\n pattern?: string;\n /** Callbacks that fire when the matcher hits. Executed in parallel. */\n hooks: HookCallback<E>[];\n /** Per-matcher timeout in ms. Defaults to the executor's batch timeout. */\n timeout?: number;\n /**\n * Atomically remove the matcher before its first dispatch.\n *\n * `executeHooks` claims `once: true` matchers synchronously — between\n * `getMatchers` and its first `await` — so two concurrent calls cannot\n * both dispatch the same matcher. Whichever call runs its sync prefix\n * first wins the matcher; the other sees an empty bucket.\n *\n * Semantics are \"at most one dispatch, ever\" — if every hook in the\n * matcher throws, the matcher is still gone. Use `once` for\n * fire-and-forget bootstrapping (registration, telemetry, setup). Hosts\n * that need retry semantics should register a normal matcher and\n * self-unregister via the callback returned from `registry.register`.\n */\n once?: boolean;\n /** Internal hooks are excluded from telemetry and non-fatal error logging. */\n internal?: boolean;\n}\n\n/**\n * Storage shape for matchers keyed by event. Each event's matcher list is\n * a generic array parameterized by that event type, so lookup via\n * `HooksByEvent[E]` preserves type-safe callback signatures.\n */\nexport type HooksByEvent = {\n [E in HookEvent]?: HookMatcher<E>[];\n};\n\n/**\n * Aggregated result of a single `executeHooks` call. Fields are populated\n * according to the fold rules in `executeHooks.ts`.\n */\nexport interface AggregatedHookResult {\n /** Folded tool-gating decision; `deny > ask > allow`. */\n decision?: ToolDecision;\n /** Folded stop decision; any `block` wins. */\n stopDecision?: StopDecision;\n /** Reason from the hook that set the winning decision. */\n reason?: string;\n /**\n * Replacement tool input from a `PreToolUse` hook.\n *\n * Last-writer-wins in **registration order**: `executeHooks` uses\n * `Promise.all`, which preserves input-array order, so the fold iterates\n * outcomes in the same order they were pushed — outer loop over matchers\n * as they sit in the registry, inner loop over each matcher's `hooks`\n * array. The winner is therefore deterministic but may not match the\n * order in which hooks actually completed. Consumers that want a single\n * authoritative rewrite should still register one `updatedInput`-setting\n * hook per matcher to avoid subtle precedence bugs.\n */\n updatedInput?: Record<string, unknown>;\n /**\n * Restricted decision set from a `PreToolUse` hook. Same last-writer-wins\n * semantics as `updatedInput`. Surfaces to the interrupt payload's\n * `review_configs[i].allowed_decisions`.\n */\n allowedDecisions?: ReadonlyArray<'approve' | 'reject' | 'edit' | 'respond'>;\n /**\n * Replacement tool output from a `PostToolUse` hook.\n *\n * Same last-writer-wins-in-registration-order semantics as\n * `updatedInput`. Present only when at least one hook set it; `undefined`\n * means \"use the original tool output\".\n */\n updatedOutput?: unknown;\n /** Accumulated `additionalContext` strings from every hook, in order. */\n additionalContexts: string[];\n /** True if any hook returned `preventContinuation`. */\n preventContinuation?: boolean;\n /**\n * Reason recorded alongside `preventContinuation`. First winner wins:\n * once a hook sets both flags, later hooks that also set\n * `preventContinuation` do not overwrite the reason.\n */\n stopReason?: string;\n /** Error messages from hooks that threw; always present (possibly empty). */\n errors: string[];\n}\n"],"names":[],"mappings":";;AAGA;;;;;;;AAOG;AACI,MAAM,WAAW,GAAG;IACzB,UAAU;IACV,kBAAkB;IAClB,YAAY;IACZ,aAAa;IACb,oBAAoB;IACpB,eAAe;IACf,kBAAkB;IAClB,eAAe;IACf,cAAc;IACd,MAAM;IACN,aAAa;IACb,YAAY;IACZ,aAAa;;;;;"}
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=google-common.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"google-common.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ var messages = require('@langchain/core/messages');
4
+ var prompts = require('@langchain/core/prompts');
5
+ var runnables = require('@langchain/core/runnables');
6
+ var tools = require('@langchain/core/tools');
7
+
8
+
9
+
10
+ Object.defineProperty(exports, "AIMessage", {
11
+ enumerable: true,
12
+ get: function () { return messages.AIMessage; }
13
+ });
14
+ Object.defineProperty(exports, "AIMessageChunk", {
15
+ enumerable: true,
16
+ get: function () { return messages.AIMessageChunk; }
17
+ });
18
+ Object.defineProperty(exports, "BaseMessage", {
19
+ enumerable: true,
20
+ get: function () { return messages.BaseMessage; }
21
+ });
22
+ Object.defineProperty(exports, "BaseMessageChunk", {
23
+ enumerable: true,
24
+ get: function () { return messages.BaseMessageChunk; }
25
+ });
26
+ Object.defineProperty(exports, "HumanMessage", {
27
+ enumerable: true,
28
+ get: function () { return messages.HumanMessage; }
29
+ });
30
+ Object.defineProperty(exports, "SystemMessage", {
31
+ enumerable: true,
32
+ get: function () { return messages.SystemMessage; }
33
+ });
34
+ Object.defineProperty(exports, "ToolMessage", {
35
+ enumerable: true,
36
+ get: function () { return messages.ToolMessage; }
37
+ });
38
+ Object.defineProperty(exports, "getBufferString", {
39
+ enumerable: true,
40
+ get: function () { return messages.getBufferString; }
41
+ });
42
+ Object.defineProperty(exports, "isAIMessage", {
43
+ enumerable: true,
44
+ get: function () { return messages.isAIMessage; }
45
+ });
46
+ Object.defineProperty(exports, "isBaseMessage", {
47
+ enumerable: true,
48
+ get: function () { return messages.isBaseMessage; }
49
+ });
50
+ Object.defineProperty(exports, "isToolMessage", {
51
+ enumerable: true,
52
+ get: function () { return messages.isToolMessage; }
53
+ });
54
+ Object.defineProperty(exports, "PromptTemplate", {
55
+ enumerable: true,
56
+ get: function () { return prompts.PromptTemplate; }
57
+ });
58
+ Object.defineProperty(exports, "Runnable", {
59
+ enumerable: true,
60
+ get: function () { return runnables.Runnable; }
61
+ });
62
+ Object.defineProperty(exports, "RunnableLambda", {
63
+ enumerable: true,
64
+ get: function () { return runnables.RunnableLambda; }
65
+ });
66
+ Object.defineProperty(exports, "RunnableSequence", {
67
+ enumerable: true,
68
+ get: function () { return runnables.RunnableSequence; }
69
+ });
70
+ Object.defineProperty(exports, "DynamicStructuredTool", {
71
+ enumerable: true,
72
+ get: function () { return tools.DynamicStructuredTool; }
73
+ });
74
+ Object.defineProperty(exports, "StructuredTool", {
75
+ enumerable: true,
76
+ get: function () { return tools.StructuredTool; }
77
+ });
78
+ Object.defineProperty(exports, "Tool", {
79
+ enumerable: true,
80
+ get: function () { return tools.Tool; }
81
+ });
82
+ Object.defineProperty(exports, "tool", {
83
+ enumerable: true,
84
+ get: function () { return tools.tool; }
85
+ });
86
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=chat_models.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat_models.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ //# sourceMappingURL=tool.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;"}