@strands-agents/sdk 1.0.0-rc.5 → 1.1.0

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 (380) hide show
  1. package/LICENSE +175 -0
  2. package/README.md +340 -0
  3. package/dist/src/__fixtures__/agent-helpers.d.ts +22 -1
  4. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
  5. package/dist/src/__fixtures__/agent-helpers.js +45 -1
  6. package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
  7. package/dist/src/__fixtures__/mock-plugin.d.ts.map +1 -1
  8. package/dist/src/__fixtures__/mock-plugin.js +3 -1
  9. package/dist/src/__fixtures__/mock-plugin.js.map +1 -1
  10. package/dist/src/__fixtures__/tool-helpers.d.ts +5 -2
  11. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
  12. package/dist/src/__fixtures__/tool-helpers.js +23 -4
  13. package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
  14. package/dist/src/__tests__/interrupt.test.d.ts +2 -0
  15. package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
  16. package/dist/src/__tests__/interrupt.test.js +259 -0
  17. package/dist/src/__tests__/interrupt.test.js.map +1 -0
  18. package/dist/src/__tests__/mcp.test.js +448 -2
  19. package/dist/src/__tests__/mcp.test.js.map +1 -1
  20. package/dist/src/a2a/__tests__/events.test.js +2 -0
  21. package/dist/src/a2a/__tests__/events.test.js.map +1 -1
  22. package/dist/src/a2a/__tests__/executor.test.js +16 -5
  23. package/dist/src/a2a/__tests__/executor.test.js.map +1 -1
  24. package/dist/src/a2a/a2a-agent.d.ts +8 -3
  25. package/dist/src/a2a/a2a-agent.d.ts.map +1 -1
  26. package/dist/src/a2a/a2a-agent.js +12 -6
  27. package/dist/src/a2a/a2a-agent.js.map +1 -1
  28. package/dist/src/a2a/executor.d.ts +13 -0
  29. package/dist/src/a2a/executor.d.ts.map +1 -1
  30. package/dist/src/a2a/executor.js +19 -1
  31. package/dist/src/a2a/executor.js.map +1 -1
  32. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts +2 -0
  33. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts.map +1 -0
  34. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js +23 -0
  35. package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js.map +1 -0
  36. package/dist/src/agent/__tests__/agent.cancel.test.js +1 -1
  37. package/dist/src/agent/__tests__/agent.cancel.test.js.map +1 -1
  38. package/dist/src/agent/__tests__/agent.concurrent.test.d.ts +2 -0
  39. package/dist/src/agent/__tests__/agent.concurrent.test.d.ts.map +1 -0
  40. package/dist/src/agent/__tests__/agent.concurrent.test.js +488 -0
  41. package/dist/src/agent/__tests__/agent.concurrent.test.js.map +1 -0
  42. package/dist/src/agent/__tests__/agent.hook.test.js +724 -12
  43. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
  44. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
  45. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
  46. package/dist/src/agent/__tests__/agent.interrupt.test.js +730 -0
  47. package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
  48. package/dist/src/agent/__tests__/agent.invocation-state.test.d.ts +2 -0
  49. package/dist/src/agent/__tests__/agent.invocation-state.test.d.ts.map +1 -0
  50. package/dist/src/agent/__tests__/agent.invocation-state.test.js +219 -0
  51. package/dist/src/agent/__tests__/agent.invocation-state.test.js.map +1 -0
  52. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
  53. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
  54. package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
  55. package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
  56. package/dist/src/agent/__tests__/agent.stateful-model.test.d.ts +2 -0
  57. package/dist/src/agent/__tests__/agent.stateful-model.test.d.ts.map +1 -0
  58. package/dist/src/agent/__tests__/agent.stateful-model.test.js +169 -0
  59. package/dist/src/agent/__tests__/agent.stateful-model.test.js.map +1 -0
  60. package/dist/src/agent/__tests__/agent.test.js +217 -2
  61. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  62. package/dist/src/agent/__tests__/agent.tracer.test.node.js +39 -0
  63. package/dist/src/agent/__tests__/agent.tracer.test.node.js.map +1 -1
  64. package/dist/src/agent/__tests__/snapshot.test.js +51 -4
  65. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
  66. package/dist/src/agent/agent-as-tool.d.ts.map +1 -1
  67. package/dist/src/agent/agent-as-tool.js +4 -2
  68. package/dist/src/agent/agent-as-tool.js.map +1 -1
  69. package/dist/src/agent/agent.d.ts +109 -4
  70. package/dist/src/agent/agent.d.ts.map +1 -1
  71. package/dist/src/agent/agent.js +790 -224
  72. package/dist/src/agent/agent.js.map +1 -1
  73. package/dist/src/agent/snapshot.d.ts +2 -2
  74. package/dist/src/agent/snapshot.d.ts.map +1 -1
  75. package/dist/src/agent/snapshot.js +20 -2
  76. package/dist/src/agent/snapshot.js.map +1 -1
  77. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
  78. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
  79. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
  80. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
  81. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +58 -4
  82. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
  83. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +76 -1
  84. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
  85. package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
  86. package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
  87. package/dist/src/conversation-manager/conversation-manager.js +65 -13
  88. package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
  89. package/dist/src/conversation-manager/index.d.ts +1 -1
  90. package/dist/src/conversation-manager/index.d.ts.map +1 -1
  91. package/dist/src/conversation-manager/index.js +1 -1
  92. package/dist/src/conversation-manager/index.js.map +1 -1
  93. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +17 -3
  94. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
  95. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +10 -4
  96. package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
  97. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
  98. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
  99. package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
  100. package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
  101. package/dist/src/errors.d.ts +11 -0
  102. package/dist/src/errors.d.ts.map +1 -1
  103. package/dist/src/errors.js +12 -0
  104. package/dist/src/errors.js.map +1 -1
  105. package/dist/src/hooks/__tests__/events.test.js +267 -73
  106. package/dist/src/hooks/__tests__/events.test.js.map +1 -1
  107. package/dist/src/hooks/__tests__/registry.test.js +182 -18
  108. package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
  109. package/dist/src/hooks/events.d.ts +193 -51
  110. package/dist/src/hooks/events.d.ts.map +1 -1
  111. package/dist/src/hooks/events.js +182 -26
  112. package/dist/src/hooks/events.js.map +1 -1
  113. package/dist/src/hooks/index.d.ts +3 -2
  114. package/dist/src/hooks/index.d.ts.map +1 -1
  115. package/dist/src/hooks/index.js +1 -0
  116. package/dist/src/hooks/index.js.map +1 -1
  117. package/dist/src/hooks/registry.d.ts +12 -12
  118. package/dist/src/hooks/registry.d.ts.map +1 -1
  119. package/dist/src/hooks/registry.js +55 -15
  120. package/dist/src/hooks/registry.js.map +1 -1
  121. package/dist/src/hooks/types.d.ts +23 -0
  122. package/dist/src/hooks/types.d.ts.map +1 -1
  123. package/dist/src/hooks/types.js +17 -1
  124. package/dist/src/hooks/types.js.map +1 -1
  125. package/dist/src/index.d.ts +12 -7
  126. package/dist/src/index.d.ts.map +1 -1
  127. package/dist/src/index.js +4 -1
  128. package/dist/src/index.js.map +1 -1
  129. package/dist/src/interrupt.d.ts +220 -0
  130. package/dist/src/interrupt.d.ts.map +1 -0
  131. package/dist/src/interrupt.js +274 -0
  132. package/dist/src/interrupt.js.map +1 -0
  133. package/dist/src/logging/__tests__/warn-once.test.d.ts +2 -0
  134. package/dist/src/logging/__tests__/warn-once.test.d.ts.map +1 -0
  135. package/dist/src/logging/__tests__/warn-once.test.js +30 -0
  136. package/dist/src/logging/__tests__/warn-once.test.js.map +1 -0
  137. package/dist/src/logging/warn-once.d.ts +13 -0
  138. package/dist/src/logging/warn-once.d.ts.map +1 -0
  139. package/dist/src/logging/warn-once.js +18 -0
  140. package/dist/src/logging/warn-once.js.map +1 -0
  141. package/dist/src/mcp.d.ts +43 -3
  142. package/dist/src/mcp.d.ts.map +1 -1
  143. package/dist/src/mcp.js +85 -17
  144. package/dist/src/mcp.js.map +1 -1
  145. package/dist/src/mime.d.ts +2 -1
  146. package/dist/src/mime.d.ts.map +1 -1
  147. package/dist/src/mime.js +1 -0
  148. package/dist/src/mime.js.map +1 -1
  149. package/dist/src/models/__tests__/anthropic.test.js +147 -3
  150. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  151. package/dist/src/models/__tests__/bedrock.test.js +228 -2
  152. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  153. package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
  154. package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
  155. package/dist/src/models/__tests__/defaults.test.js +36 -0
  156. package/dist/src/models/__tests__/defaults.test.js.map +1 -0
  157. package/dist/src/models/__tests__/google.test.js +135 -0
  158. package/dist/src/models/__tests__/google.test.js.map +1 -1
  159. package/dist/src/models/__tests__/model.test.js +149 -1
  160. package/dist/src/models/__tests__/model.test.js.map +1 -1
  161. package/dist/src/models/anthropic.d.ts +20 -1
  162. package/dist/src/models/anthropic.d.ts.map +1 -1
  163. package/dist/src/models/anthropic.js +42 -8
  164. package/dist/src/models/anthropic.js.map +1 -1
  165. package/dist/src/models/bedrock.d.ts +27 -1
  166. package/dist/src/models/bedrock.d.ts.map +1 -1
  167. package/dist/src/models/bedrock.js +100 -12
  168. package/dist/src/models/bedrock.js.map +1 -1
  169. package/dist/src/models/defaults.d.ts +47 -0
  170. package/dist/src/models/defaults.d.ts.map +1 -0
  171. package/dist/src/models/defaults.js +170 -0
  172. package/dist/src/models/defaults.js.map +1 -0
  173. package/dist/src/models/google/model.d.ts +14 -1
  174. package/dist/src/models/google/model.d.ts.map +1 -1
  175. package/dist/src/models/google/model.js +54 -8
  176. package/dist/src/models/google/model.js.map +1 -1
  177. package/dist/src/models/google/types.d.ts +8 -0
  178. package/dist/src/models/google/types.d.ts.map +1 -1
  179. package/dist/src/models/model.d.ts +65 -0
  180. package/dist/src/models/model.d.ts.map +1 -1
  181. package/dist/src/models/model.js +138 -0
  182. package/dist/src/models/model.js.map +1 -1
  183. package/dist/src/models/openai/__tests__/chat.test.d.ts +2 -0
  184. package/dist/src/models/openai/__tests__/chat.test.d.ts.map +1 -0
  185. package/dist/src/models/{__tests__/openai.test.js → openai/__tests__/chat.test.js} +117 -7
  186. package/dist/src/models/openai/__tests__/chat.test.js.map +1 -0
  187. package/dist/src/models/openai/__tests__/responses.test.d.ts +2 -0
  188. package/dist/src/models/openai/__tests__/responses.test.d.ts.map +1 -0
  189. package/dist/src/models/openai/__tests__/responses.test.js +668 -0
  190. package/dist/src/models/openai/__tests__/responses.test.js.map +1 -0
  191. package/dist/src/models/openai/chat-adapter.d.ts +33 -0
  192. package/dist/src/models/openai/chat-adapter.d.ts.map +1 -0
  193. package/dist/src/models/openai/chat-adapter.js +383 -0
  194. package/dist/src/models/openai/chat-adapter.js.map +1 -0
  195. package/dist/src/models/openai/errors.d.ts +16 -0
  196. package/dist/src/models/openai/errors.d.ts.map +1 -0
  197. package/dist/src/models/openai/errors.js +40 -0
  198. package/dist/src/models/openai/errors.js.map +1 -0
  199. package/dist/src/models/openai/formatting.d.ts +18 -0
  200. package/dist/src/models/openai/formatting.d.ts.map +1 -0
  201. package/dist/src/models/openai/formatting.js +38 -0
  202. package/dist/src/models/openai/formatting.js.map +1 -0
  203. package/dist/src/models/openai/index.d.ts +19 -0
  204. package/dist/src/models/openai/index.d.ts.map +1 -0
  205. package/dist/src/models/openai/index.js +18 -0
  206. package/dist/src/models/openai/index.js.map +1 -0
  207. package/dist/src/models/openai/model.d.ts +77 -0
  208. package/dist/src/models/openai/model.d.ts.map +1 -0
  209. package/dist/src/models/openai/model.js +211 -0
  210. package/dist/src/models/openai/model.js.map +1 -0
  211. package/dist/src/models/openai/responses-adapter.d.ts +78 -0
  212. package/dist/src/models/openai/responses-adapter.d.ts.map +1 -0
  213. package/dist/src/models/openai/responses-adapter.js +467 -0
  214. package/dist/src/models/openai/responses-adapter.js.map +1 -0
  215. package/dist/src/models/openai/types.d.ts +131 -0
  216. package/dist/src/models/openai/types.d.ts.map +1 -0
  217. package/dist/src/models/openai/types.js +5 -0
  218. package/dist/src/models/openai/types.js.map +1 -0
  219. package/dist/src/multiagent/__tests__/events.test.js +122 -28
  220. package/dist/src/multiagent/__tests__/events.test.js.map +1 -1
  221. package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts +2 -0
  222. package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts.map +1 -0
  223. package/dist/src/multiagent/__tests__/graph.invocation-state.test.js +95 -0
  224. package/dist/src/multiagent/__tests__/graph.invocation-state.test.js.map +1 -0
  225. package/dist/src/multiagent/__tests__/graph.test.js +69 -0
  226. package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
  227. package/dist/src/multiagent/__tests__/nodes.test.js +18 -2
  228. package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
  229. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts +2 -0
  230. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts.map +1 -0
  231. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js +56 -0
  232. package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js.map +1 -0
  233. package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
  234. package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
  235. package/dist/src/multiagent/events.d.ts +19 -1
  236. package/dist/src/multiagent/events.d.ts.map +1 -1
  237. package/dist/src/multiagent/events.js +18 -0
  238. package/dist/src/multiagent/events.js.map +1 -1
  239. package/dist/src/multiagent/graph.d.ts +27 -5
  240. package/dist/src/multiagent/graph.d.ts.map +1 -1
  241. package/dist/src/multiagent/graph.js +61 -15
  242. package/dist/src/multiagent/graph.js.map +1 -1
  243. package/dist/src/multiagent/index.d.ts +1 -1
  244. package/dist/src/multiagent/index.d.ts.map +1 -1
  245. package/dist/src/multiagent/multiagent.d.ts +21 -6
  246. package/dist/src/multiagent/multiagent.d.ts.map +1 -1
  247. package/dist/src/multiagent/nodes.d.ts +28 -3
  248. package/dist/src/multiagent/nodes.d.ts.map +1 -1
  249. package/dist/src/multiagent/nodes.js +42 -7
  250. package/dist/src/multiagent/nodes.js.map +1 -1
  251. package/dist/src/multiagent/swarm.d.ts +20 -4
  252. package/dist/src/multiagent/swarm.d.ts.map +1 -1
  253. package/dist/src/multiagent/swarm.js +65 -16
  254. package/dist/src/multiagent/swarm.js.map +1 -1
  255. package/dist/src/plugins/__tests__/registry.test.js +1 -1
  256. package/dist/src/plugins/__tests__/registry.test.js.map +1 -1
  257. package/dist/src/plugins/model-plugin.d.ts +20 -0
  258. package/dist/src/plugins/model-plugin.d.ts.map +1 -0
  259. package/dist/src/plugins/model-plugin.js +29 -0
  260. package/dist/src/plugins/model-plugin.js.map +1 -0
  261. package/dist/src/registry/__tests__/tool-registry.test.js +11 -0
  262. package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
  263. package/dist/src/registry/tool-registry.d.ts +4 -0
  264. package/dist/src/registry/tool-registry.d.ts.map +1 -1
  265. package/dist/src/registry/tool-registry.js +6 -0
  266. package/dist/src/registry/tool-registry.js.map +1 -1
  267. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
  268. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
  269. package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
  270. package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
  271. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
  272. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
  273. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
  274. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
  275. package/dist/src/retry/backoff-strategy.d.ts +108 -0
  276. package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
  277. package/dist/src/retry/backoff-strategy.js +86 -0
  278. package/dist/src/retry/backoff-strategy.js.map +1 -0
  279. package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
  280. package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
  281. package/dist/src/retry/default-model-retry-strategy.js +104 -0
  282. package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
  283. package/dist/src/retry/index.d.ts +8 -0
  284. package/dist/src/retry/index.d.ts.map +1 -0
  285. package/dist/src/retry/index.js +7 -0
  286. package/dist/src/retry/index.js.map +1 -0
  287. package/dist/src/retry/model-retry-strategy.d.ts +80 -0
  288. package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
  289. package/dist/src/retry/model-retry-strategy.js +85 -0
  290. package/dist/src/retry/model-retry-strategy.js.map +1 -0
  291. package/dist/src/retry/retry-strategy.d.ts +34 -0
  292. package/dist/src/retry/retry-strategy.d.ts.map +1 -0
  293. package/dist/src/retry/retry-strategy.js +25 -0
  294. package/dist/src/retry/retry-strategy.js.map +1 -0
  295. package/dist/src/session/__tests__/session-manager.test.js +52 -11
  296. package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
  297. package/dist/src/session/session-manager.d.ts +6 -0
  298. package/dist/src/session/session-manager.d.ts.map +1 -1
  299. package/dist/src/session/session-manager.js +17 -0
  300. package/dist/src/session/session-manager.js.map +1 -1
  301. package/dist/src/telemetry/__tests__/meter.test.js +23 -0
  302. package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
  303. package/dist/src/telemetry/meter.d.ts +15 -0
  304. package/dist/src/telemetry/meter.d.ts.map +1 -1
  305. package/dist/src/telemetry/meter.js +14 -0
  306. package/dist/src/telemetry/meter.js.map +1 -1
  307. package/dist/src/tools/__tests__/tool.test.js +24 -1
  308. package/dist/src/tools/__tests__/tool.test.js.map +1 -1
  309. package/dist/src/tools/function-tool.d.ts.map +1 -1
  310. package/dist/src/tools/function-tool.js +6 -1
  311. package/dist/src/tools/function-tool.js.map +1 -1
  312. package/dist/src/tools/mcp-tool.d.ts +24 -3
  313. package/dist/src/tools/mcp-tool.d.ts.map +1 -1
  314. package/dist/src/tools/mcp-tool.js +103 -31
  315. package/dist/src/tools/mcp-tool.js.map +1 -1
  316. package/dist/src/tools/tool.d.ts +21 -2
  317. package/dist/src/tools/tool.d.ts.map +1 -1
  318. package/dist/src/tools/tool.js +12 -0
  319. package/dist/src/tools/tool.js.map +1 -1
  320. package/dist/src/tsconfig.tsbuildinfo +1 -1
  321. package/dist/src/types/__tests__/agent.test.js +48 -0
  322. package/dist/src/types/__tests__/agent.test.js.map +1 -1
  323. package/dist/src/types/agent.d.ts +77 -9
  324. package/dist/src/types/agent.d.ts.map +1 -1
  325. package/dist/src/types/agent.js +30 -6
  326. package/dist/src/types/agent.js.map +1 -1
  327. package/dist/src/types/elicitation.d.ts +15 -0
  328. package/dist/src/types/elicitation.d.ts.map +1 -0
  329. package/dist/src/types/elicitation.js +2 -0
  330. package/dist/src/types/elicitation.js.map +1 -0
  331. package/dist/src/types/interrupt.d.ts +103 -0
  332. package/dist/src/types/interrupt.d.ts.map +1 -0
  333. package/dist/src/types/interrupt.js +63 -0
  334. package/dist/src/types/interrupt.js.map +1 -0
  335. package/dist/src/types/messages.d.ts +2 -1
  336. package/dist/src/types/messages.d.ts.map +1 -1
  337. package/dist/src/types/messages.js.map +1 -1
  338. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
  339. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
  340. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
  341. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
  342. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
  343. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
  344. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
  345. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
  346. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
  347. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
  348. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
  349. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
  350. package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
  351. package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
  352. package/dist/src/vended-plugins/context-offloader/index.js +21 -0
  353. package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
  354. package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
  355. package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
  356. package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
  357. package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
  358. package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
  359. package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
  360. package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
  361. package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
  362. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +21 -5
  363. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
  364. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +4 -0
  365. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
  366. package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
  367. package/dist/src/vended-tools/bash/bash.js +0 -3
  368. package/dist/src/vended-tools/bash/bash.js.map +1 -1
  369. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +4 -0
  370. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
  371. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +4 -0
  372. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
  373. package/package.json +17 -9
  374. package/dist/src/models/__tests__/openai.test.d.ts +0 -2
  375. package/dist/src/models/__tests__/openai.test.d.ts.map +0 -1
  376. package/dist/src/models/__tests__/openai.test.js.map +0 -1
  377. package/dist/src/models/openai.d.ts +0 -312
  378. package/dist/src/models/openai.d.ts.map +0 -1
  379. package/dist/src/models/openai.js +0 -789
  380. package/dist/src/models/openai.js.map +0 -1
@@ -54,16 +54,18 @@ import { AgentResult, } from '../types/agent.js';
54
54
  import { BedrockModel } from '../models/bedrock.js';
55
55
  import { contentBlockFromData, Message, TextBlock, ToolResultBlock, ToolUseBlock, } from '../types/messages.js';
56
56
  import { McpClient } from '../mcp.js';
57
- import {} from '../tools/tool.js';
57
+ import { isValidToolName } from '../tools/tool.js';
58
58
  import { systemPromptFromData } from '../types/messages.js';
59
59
  import { normalizeError, ConcurrentInvocationError, StructuredOutputError } from '../errors.js';
60
60
  import { Model } from '../models/model.js';
61
+ import { ModelPlugin } from '../plugins/model-plugin.js';
61
62
  import { isModelStreamEvent } from '../models/streaming.js';
62
63
  import { ToolRegistry } from '../registry/tool-registry.js';
63
64
  import { StateStore } from '../state-store.js';
64
65
  import { AgentPrinter, getDefaultAppender } from './printer.js';
65
66
  import { PluginRegistry } from '../plugins/registry.js';
66
67
  import { SlidingWindowConversationManager } from '../conversation-manager/sliding-window-conversation-manager.js';
68
+ import { NullConversationManager } from '../conversation-manager/null-conversation-manager.js';
67
69
  import { ConversationManager } from '../conversation-manager/conversation-manager.js';
68
70
  import { HookRegistryImplementation } from '../hooks/registry.js';
69
71
  import { InitializedEvent, AfterInvocationEvent, AfterModelCallEvent, AfterToolCallEvent, AfterToolsEvent, BeforeInvocationEvent, BeforeModelCallEvent, BeforeToolCallEvent, BeforeToolsEvent, HookableEvent, MessageAddedEvent, ModelStreamUpdateEvent, ContentBlockEvent, ModelMessageEvent, ToolResultEvent, AgentResultEvent, ToolStreamUpdateEvent, } from '../hooks/events.js';
@@ -74,6 +76,10 @@ import { Tracer } from '../telemetry/tracer.js';
74
76
  import { Meter } from '../telemetry/meter.js';
75
77
  import { logger } from '../logging/logger.js';
76
78
  import { CancelledError } from '../errors.js';
79
+ import { DefaultModelRetryStrategy } from '../retry/default-model-retry-strategy.js';
80
+ import { warnOnDuplicateRetryStrategyTypes } from '../retry/retry-strategy.js';
81
+ import { InterruptError, InterruptState, interruptFromAgent } from '../interrupt.js';
82
+ import { isInterruptResponseContent } from '../types/interrupt.js';
77
83
  /** Default name assigned to agents when none is provided. */
78
84
  const DEFAULT_AGENT_NAME = 'Strands Agent';
79
85
  /** Default identifier assigned to agents when none is provided. */
@@ -93,6 +99,12 @@ export class Agent {
93
99
  * State is not passed to the model during inference.
94
100
  */
95
101
  appState;
102
+ /**
103
+ * Runtime state for the model provider. Used by stateful models to persist
104
+ * provider-specific data (e.g., response IDs for conversation chaining)
105
+ * across invocations.
106
+ */
107
+ modelState;
96
108
  _conversationManager;
97
109
  /**
98
110
  * The model provider used by the agent for inference.
@@ -132,6 +144,10 @@ export class Agent {
132
144
  _tracer;
133
145
  /** Meter instance for accumulating loop metrics during invocation. */
134
146
  _meter;
147
+ /** Interrupt state for human-in-the-loop workflows. */
148
+ _interruptState;
149
+ /** Strategy for executing tool calls from a single assistant turn. */
150
+ _toolExecutor;
135
151
  /**
136
152
  * Creates an instance of the Agent.
137
153
  * @param config - The configuration for the agent.
@@ -140,7 +156,7 @@ export class Agent {
140
156
  // Initialize public fields
141
157
  this.messages = (config?.messages ?? []).map((msg) => (msg instanceof Message ? msg : Message.fromMessageData(msg)));
142
158
  this.appState = new StateStore(config?.appState);
143
- this._conversationManager = config?.conversationManager ?? new SlidingWindowConversationManager({ windowSize: 40 });
159
+ this.modelState = new StateStore(config?.modelState);
144
160
  this.name = config?.name ?? DEFAULT_AGENT_NAME;
145
161
  this.id = config?.id ?? DEFAULT_AGENT_ID;
146
162
  if (config?.description !== undefined)
@@ -152,16 +168,45 @@ export class Agent {
152
168
  else {
153
169
  this.model = config?.model ?? new BedrockModel();
154
170
  }
171
+ // Validate and assign conversation manager
172
+ if (this.model.stateful) {
173
+ if (config?.conversationManager) {
174
+ throw new Error('Cannot use a conversationManager with a stateful model. The model manages conversation state server-side.');
175
+ }
176
+ this._conversationManager = new NullConversationManager();
177
+ }
178
+ else {
179
+ this._conversationManager =
180
+ config?.conversationManager ?? new SlidingWindowConversationManager({ windowSize: 40 });
181
+ }
155
182
  const { tools, mcpClients } = flattenTools(config?.tools ?? []);
156
183
  this._toolRegistry = new ToolRegistry(tools);
157
184
  this._mcpClients = mcpClients;
158
185
  // Initialize hooks registry
159
186
  this._hooksRegistry = new HookRegistryImplementation();
160
- // Initialize plugin registry with all plugins to be initialized during initialize()
187
+ // `undefined` (omitted) install the default; `null`/`[]` explicit opt-out.
188
+ const retryStrategies = config?.retryStrategy === null
189
+ ? []
190
+ : config?.retryStrategy === undefined
191
+ ? [new DefaultModelRetryStrategy()]
192
+ : Array.isArray(config.retryStrategy)
193
+ ? config.retryStrategy
194
+ : [config.retryStrategy];
195
+ warnOnDuplicateRetryStrategyTypes(retryStrategies);
196
+ // Initialize plugin registry with all plugins to be initialized during initialize().
197
+ // Ordering notes:
198
+ // - ModelPlugin is registered last so that on AfterInvocationEvent (which uses
199
+ // reverse callback ordering), it runs first — clearing messages before
200
+ // SessionManager saves.
201
+ // - Retry-strategy ordering is not load-bearing for correctness: `DefaultModelRetryStrategy`
202
+ // guards on `event.retry`, so a user hook that already set it short-circuits
203
+ // the strategy regardless of registration order.
161
204
  this._pluginRegistry = new PluginRegistry([
162
205
  this._conversationManager,
206
+ ...retryStrategies,
163
207
  ...(config?.plugins ?? []),
164
208
  ...(config?.sessionManager ? [config.sessionManager] : []),
209
+ new ModelPlugin(this.model),
165
210
  ]);
166
211
  if (config?.systemPrompt !== undefined) {
167
212
  this.systemPrompt = systemPromptFromData(config.systemPrompt);
@@ -177,6 +222,9 @@ export class Agent {
177
222
  this._tracer = new Tracer(config?.traceAttributes);
178
223
  // Initialize meter for local metrics accumulation
179
224
  this._meter = new Meter();
225
+ // Initialize interrupt state for human-in-the-loop workflows
226
+ this._interruptState = new InterruptState();
227
+ this._toolExecutor = config?.toolExecutor ?? 'concurrent';
180
228
  this._initialized = false;
181
229
  }
182
230
  /**
@@ -184,6 +232,7 @@ export class Agent {
184
232
  *
185
233
  * @param eventType - The event class constructor to register the callback for
186
234
  * @param callback - The callback function to invoke when the event occurs
235
+ * @param options - Optional configuration including execution order
187
236
  * @returns Cleanup function that removes the callback when invoked
188
237
  *
189
238
  * @example
@@ -198,8 +247,8 @@ export class Agent {
198
247
  * cleanup()
199
248
  * ```
200
249
  */
201
- addHook(eventType, callback) {
202
- return this._hooksRegistry.addCallback(eventType, callback);
250
+ addHook(eventType, callback, options) {
251
+ return this._hooksRegistry.addCallback(eventType, callback, options);
203
252
  }
204
253
  async initialize() {
205
254
  if (this._initialized) {
@@ -358,53 +407,86 @@ export class Agent {
358
407
  async *stream(args, options) {
359
408
  const env_1 = { stack: [], error: void 0, hasError: false };
360
409
  try {
361
- const _lock = __addDisposableResource(env_1, this.acquireLock()
362
- // Create AbortController for this invocation and compose with external signal
363
- , false);
364
- // Create AbortController for this invocation and compose with external signal
365
- this._abortController = new AbortController();
366
- this._abortSignal = options?.cancelSignal
367
- ? AbortSignal.any([this._abortController.signal, options.cancelSignal])
368
- : this._abortController.signal;
410
+ const _lock = __addDisposableResource(env_1, this.acquireLock(), false);
369
411
  await this.initialize();
370
- // Delegate to _stream and process events through printer and hooks
371
- const streamGenerator = this._stream(args, options);
372
- let caughtError;
373
- try {
374
- let result = await streamGenerator.next();
375
- while (!result.done) {
376
- yield await this._invokeCallbacks(result.value);
377
- result = await streamGenerator.next();
378
- }
379
- yield await this._invokeCallbacks(new AgentResultEvent({ agent: this, result: result.value }));
380
- return result.value;
381
- }
382
- catch (error) {
383
- caughtError = error;
384
- throw error;
385
- }
386
- finally {
387
- // Drain _stream() so cleanup hooks and printer still fire.
388
- // Yield only on error (consumer may still be iterating); on a consumer
389
- // break, yielding would suspend the generator and leak the lock.
390
- let result = await streamGenerator.return(undefined);
391
- while (!result.done) {
392
- try {
393
- if (caughtError) {
394
- yield await this._invokeCallbacks(result.value);
412
+ let currentArgs = args;
413
+ // Outer loop: re-enters _stream when a hook sets AfterInvocationEvent.resume.
414
+ // One invocation lock spans the whole resume chain.
415
+ while (true) {
416
+ // Fresh AbortController per invocation iteration, composed with any external signal.
417
+ this._abortController = new AbortController();
418
+ this._abortSignal = options?.cancelSignal
419
+ ? AbortSignal.any([this._abortController.signal, options.cancelSignal])
420
+ : this._abortController.signal;
421
+ const streamGenerator = this._stream(currentArgs, options);
422
+ let caughtError;
423
+ let lastAfterInvocation;
424
+ let iterationResult;
425
+ try {
426
+ iterationResult = await streamGenerator.next();
427
+ while (!iterationResult.done) {
428
+ try {
429
+ const processed = await this._invokeCallbacks(iterationResult.value);
430
+ if (processed instanceof AfterInvocationEvent) {
431
+ lastAfterInvocation = processed;
432
+ }
433
+ yield processed;
434
+ iterationResult = await streamGenerator.next();
395
435
  }
396
- else {
397
- await this._invokeCallbacks(result.value);
436
+ catch (error) {
437
+ // Throw interrupt errors back into _stream so executeTools can store the
438
+ // assistant message as pending execution state for resume.
439
+ if (error instanceof InterruptError) {
440
+ iterationResult = await streamGenerator.throw(error);
441
+ }
442
+ else {
443
+ throw error;
444
+ }
398
445
  }
399
446
  }
400
- catch (error) {
401
- logger.warn(`event_type=<${result.value.type}>, error=<${error}> | error invoking callbacks during cleanup`);
447
+ // Suppress AgentResultEvent for resumed iterations — only the final
448
+ // invocation in a resume chain reports an agent result.
449
+ if (lastAfterInvocation?.resume === undefined) {
450
+ yield await this._invokeCallbacks(new AgentResultEvent({
451
+ agent: this,
452
+ result: iterationResult.value,
453
+ invocationState: iterationResult.value.invocationState,
454
+ }));
402
455
  }
403
- result = await streamGenerator.next();
404
456
  }
405
- // Reset controller and signal for next invocation
406
- this._abortController = new AbortController();
407
- this._abortSignal = this._abortController.signal;
457
+ catch (error) {
458
+ caughtError = error;
459
+ throw error;
460
+ }
461
+ finally {
462
+ // Drain _stream() so cleanup hooks and printer still fire.
463
+ // Yield only on error (consumer may still be iterating); on a consumer
464
+ // break, yielding would suspend the generator and leak the lock.
465
+ let drainResult = await streamGenerator.return(undefined);
466
+ while (!drainResult.done) {
467
+ try {
468
+ if (caughtError) {
469
+ yield await this._invokeCallbacks(drainResult.value);
470
+ }
471
+ else {
472
+ await this._invokeCallbacks(drainResult.value);
473
+ }
474
+ }
475
+ catch (error) {
476
+ logger.warn(`event_type=<${drainResult.value.type}>, error=<${error}> | error invoking callbacks during cleanup`);
477
+ }
478
+ drainResult = await streamGenerator.next();
479
+ }
480
+ // Reset controller and signal for next iteration / invocation
481
+ this._abortController = new AbortController();
482
+ this._abortSignal = this._abortController.signal;
483
+ }
484
+ // Resume only on a clean invocation — errors propagate above.
485
+ if (lastAfterInvocation?.resume !== undefined) {
486
+ currentArgs = lastAfterInvocation.resume;
487
+ continue;
488
+ }
489
+ return iterationResult.value;
408
490
  }
409
491
  }
410
492
  catch (e_1) {
@@ -471,8 +553,35 @@ export class Agent {
471
553
  const structuredOutputSchema = options?.structuredOutputSchema ?? this._structuredOutputSchema;
472
554
  const structuredOutputTool = structuredOutputSchema ? new StructuredOutputTool(structuredOutputSchema) : undefined;
473
555
  let structuredOutputChoice;
474
- // Emit event before the try block
475
- yield new BeforeInvocationEvent({ agent: this });
556
+ // Resolve per-invocation state once. The same object is threaded through
557
+ // every lifecycle hook event, every tool context, and is surfaced on the
558
+ // AgentResult. Mutations by hooks/tools are visible across all recursive
559
+ // agent loop cycles within this invocation.
560
+ const invocationState = options?.invocationState ?? {};
561
+ // Handle interrupt responses if present in input
562
+ const interruptResponses = this._extractInterruptResponses(args);
563
+ if (interruptResponses.length > 0) {
564
+ this._interruptState.resume(interruptResponses);
565
+ }
566
+ // Reject non-interrupt input while in interrupted state
567
+ if (this._interruptState.activated && interruptResponses.length === 0) {
568
+ throw new TypeError('Agent is in an interrupted state. Resume by invoking with interruptResponse content blocks.');
569
+ }
570
+ const beforeInvocationEvent = new BeforeInvocationEvent({ agent: this, invocationState });
571
+ yield beforeInvocationEvent;
572
+ if (beforeInvocationEvent.cancel) {
573
+ const cancelText = typeof beforeInvocationEvent.cancel === 'string' ? beforeInvocationEvent.cancel : 'invocation denied by hook';
574
+ const message = new Message({ role: 'assistant', content: [new TextBlock(cancelText)] });
575
+ yield this._appendMessage(message, invocationState);
576
+ yield new AfterInvocationEvent({ agent: this, invocationState });
577
+ return new AgentResult({
578
+ stopReason: 'endTurn',
579
+ lastMessage: message,
580
+ traces: this._tracer.localTraces,
581
+ metrics: this._meter.metrics,
582
+ invocationState,
583
+ });
584
+ }
476
585
  // Normalize input to get the user messages for telemetry
477
586
  const inputMessages = this._normalizeInput(args);
478
587
  // Start agent trace span
@@ -510,77 +619,123 @@ export class Agent {
510
619
  if (currentArgs !== undefined) {
511
620
  const messagesToAppend = this._normalizeInput(currentArgs);
512
621
  for (const message of messagesToAppend) {
513
- yield this._appendMessage(message);
622
+ yield this._appendMessage(message, invocationState);
514
623
  }
515
624
  currentArgs = undefined;
516
625
  }
517
- const modelResult = yield* this._invokeModel(structuredOutputChoice);
518
- if (modelResult.stopReason !== 'toolUse') {
519
- // If structured output is required, force it
520
- if (structuredOutputTool) {
626
+ // Check if we're resuming from a tool interrupt
627
+ const pendingExecution = this._interruptState.getPendingExecution();
628
+ let assistantMessage;
629
+ let completedToolResults;
630
+ if (pendingExecution) {
631
+ // Resume from stored state - skip model call
632
+ assistantMessage = pendingExecution.assistantMessage;
633
+ completedToolResults = pendingExecution.completedToolResults;
634
+ this._interruptState.clearPendingToolExecution();
635
+ }
636
+ else {
637
+ const modelResult = yield* this._invokeModel(invocationState, structuredOutputChoice);
638
+ if (modelResult.stopReason !== 'toolUse') {
639
+ // If structured output is required, force it
640
+ if (structuredOutputTool) {
641
+ if (structuredOutputChoice) {
642
+ throw new StructuredOutputError('The model failed to invoke the structured output tool even after it was forced.');
643
+ }
644
+ structuredOutputChoice = { tool: { name: STRUCTURED_OUTPUT_TOOL_NAME } };
645
+ }
646
+ this._meter.endCycle(cycleStartTime);
647
+ this._tracer.endAgentLoopSpan(cycleSpan);
648
+ yield this._appendMessage(modelResult.message, invocationState);
521
649
  if (structuredOutputChoice) {
522
- throw new StructuredOutputError('The model failed to invoke the structured output tool even after it was forced.');
650
+ continue;
523
651
  }
524
- structuredOutputChoice = { tool: { name: STRUCTURED_OUTPUT_TOOL_NAME } };
652
+ result = new AgentResult({
653
+ stopReason: modelResult.stopReason,
654
+ lastMessage: modelResult.message,
655
+ traces: this._tracer.localTraces,
656
+ metrics: this._meter.metrics,
657
+ invocationState,
658
+ });
659
+ return result;
525
660
  }
526
- this._meter.endCycle(cycleStartTime);
527
- this._tracer.endAgentLoopSpan(cycleSpan);
528
- yield this._appendMessage(modelResult.message);
529
- if (structuredOutputChoice) {
530
- continue;
661
+ // Cancel before tool execution: create error results for all pending tools
662
+ if (this.isCancelled) {
663
+ const toolUseBlocks = modelResult.message.content.filter((block) => block.type === 'toolUseBlock');
664
+ const cancelBlocks = toolUseBlocks.map((block) => new ToolResultBlock({
665
+ toolUseId: block.toolUseId,
666
+ status: 'error',
667
+ content: [new TextBlock('Tool execution cancelled')],
668
+ }));
669
+ const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
670
+ yield this._appendMessage(modelResult.message, invocationState);
671
+ yield this._appendMessage(toolResultMessage, invocationState);
672
+ this._meter.endCycle(cycleStartTime);
673
+ this._tracer.endAgentLoopSpan(cycleSpan);
674
+ result = new AgentResult({
675
+ stopReason: 'cancelled',
676
+ lastMessage: modelResult.message,
677
+ traces: this._tracer.localTraces,
678
+ metrics: this._meter.metrics,
679
+ invocationState,
680
+ });
681
+ return result;
531
682
  }
532
- result = new AgentResult({
533
- stopReason: modelResult.stopReason,
534
- lastMessage: modelResult.message,
535
- traces: this._tracer.localTraces,
536
- metrics: this._meter.metrics,
537
- });
538
- return result;
683
+ assistantMessage = modelResult.message;
539
684
  }
540
- // Cancel before tool execution: create error results for all pending tools
541
- if (this.isCancelled) {
542
- const toolUseBlocks = modelResult.message.content.filter((block) => block.type === 'toolUseBlock');
543
- const cancelBlocks = toolUseBlocks.map((block) => new ToolResultBlock({
544
- toolUseId: block.toolUseId,
545
- status: 'error',
546
- content: [new TextBlock('Tool execution cancelled')],
547
- }));
548
- const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
549
- yield this._appendMessage(modelResult.message);
550
- yield this._appendMessage(toolResultMessage);
685
+ // Execute tools
686
+ const toolsResult = yield* this.executeTools(assistantMessage, this._toolRegistry, invocationState, completedToolResults);
687
+ // When the consumer breaks the stream (e.g. agent.cancel() + break),
688
+ // yield* returns undefined because the inner generator was closed.
689
+ if (!toolsResult) {
551
690
  this._meter.endCycle(cycleStartTime);
552
691
  this._tracer.endAgentLoopSpan(cycleSpan);
553
- result = new AgentResult({
554
- stopReason: 'cancelled',
555
- lastMessage: modelResult.message,
556
- traces: this._tracer.localTraces,
557
- metrics: this._meter.metrics,
558
- });
559
- return result;
692
+ continue;
560
693
  }
561
- // Execute tools
562
- const toolResultMessage = yield* this.executeTools(modelResult.message, this._toolRegistry);
694
+ const toolResultMessage = toolsResult.message;
563
695
  /**
564
696
  * Deferred append: both messages are added AFTER tool execution completes.
565
697
  * This keeps agent.messages in a valid, reinvokable state at all times.
566
698
  * If interrupted during tool execution, messages has no dangling toolUse
567
699
  * without a matching toolResult, so the agent can be reinvoked cleanly.
568
700
  */
569
- yield this._appendMessage(modelResult.message);
570
- yield this._appendMessage(toolResultMessage);
701
+ yield this._appendMessage(assistantMessage, invocationState);
702
+ yield this._appendMessage(toolResultMessage, invocationState);
703
+ // Deactivate interrupt state after successful tool execution so the next
704
+ // cycle starts with a clean slate (new interrupts can be raised again).
705
+ if (this._interruptState.activated) {
706
+ this._interruptState.deactivate();
707
+ }
571
708
  this._meter.endCycle(cycleStartTime);
572
709
  this._tracer.endAgentLoopSpan(cycleSpan);
710
+ // Hook requested halt: exit without calling the model again
711
+ const { afterToolsEvent } = toolsResult;
712
+ if (afterToolsEvent.endTurn) {
713
+ const endTurnText = typeof afterToolsEvent.endTurn === 'string'
714
+ ? afterToolsEvent.endTurn
715
+ : 'Turn ended early by hook after tool execution';
716
+ const lastMessage = new Message({ role: 'assistant', content: [new TextBlock(endTurnText)] });
717
+ yield this._appendMessage(lastMessage, invocationState);
718
+ result = new AgentResult({
719
+ stopReason: 'endTurn',
720
+ lastMessage,
721
+ traces: this._tracer.localTraces,
722
+ metrics: this._meter.metrics,
723
+ invocationState,
724
+ });
725
+ return result;
726
+ }
573
727
  // Structured output captured: exit
574
728
  const structuredOutput = structuredOutputTool
575
- ? this._extractStructuredOutput(modelResult.message, toolResultMessage)
729
+ ? this._extractStructuredOutput(assistantMessage, toolResultMessage)
576
730
  : undefined;
577
731
  if (structuredOutput !== undefined) {
578
732
  result = new AgentResult({
579
- stopReason: modelResult.stopReason,
580
- lastMessage: modelResult.message,
733
+ stopReason: 'toolUse',
734
+ lastMessage: assistantMessage,
581
735
  traces: this._tracer.localTraces,
582
736
  structuredOutput,
583
737
  metrics: this._meter.metrics,
738
+ invocationState,
584
739
  });
585
740
  return result;
586
741
  }
@@ -600,15 +755,20 @@ export class Agent {
600
755
  role: 'assistant',
601
756
  content: [new TextBlock('Cancelled by user')],
602
757
  });
603
- yield this._appendMessage(cancelMessage);
758
+ yield this._appendMessage(cancelMessage, invocationState);
604
759
  result = new AgentResult({
605
760
  stopReason: 'cancelled',
606
761
  lastMessage: cancelMessage,
607
762
  traces: this._tracer.localTraces,
608
763
  metrics: this._meter.metrics,
764
+ invocationState,
609
765
  });
610
766
  return result;
611
767
  }
768
+ if (error instanceof InterruptError) {
769
+ result = this._createInterruptResult(invocationState);
770
+ return result;
771
+ }
612
772
  caughtError = error;
613
773
  throw error;
614
774
  }
@@ -621,7 +781,7 @@ export class Agent {
621
781
  role: 'assistant',
622
782
  content: [new TextBlock('Cancelled by user')],
623
783
  });
624
- yield this._appendMessage(cancelMessage);
784
+ yield this._appendMessage(cancelMessage, invocationState);
625
785
  }
626
786
  this._tracer.endAgentSpan(agentSpan, {
627
787
  ...(caughtError && { error: caughtError }),
@@ -634,7 +794,7 @@ export class Agent {
634
794
  this._toolRegistry.remove(STRUCTURED_OUTPUT_TOOL_NAME);
635
795
  }
636
796
  // Always emit final event
637
- yield new AfterInvocationEvent({ agent: this });
797
+ yield new AfterInvocationEvent({ agent: this, invocationState });
638
798
  }
639
799
  }
640
800
  /**
@@ -654,6 +814,51 @@ export class Agent {
654
814
  const firstContent = toolResult.content[0];
655
815
  return firstContent?.type === 'jsonBlock' ? firstContent.json : undefined;
656
816
  }
817
+ /**
818
+ * Creates an AgentResult for an interrupt stop.
819
+ *
820
+ * @param invocationState - The current invocation state
821
+ * @returns AgentResult with stopReason 'interrupt'
822
+ */
823
+ _createInterruptResult(invocationState) {
824
+ this._interruptState.activate();
825
+ return new AgentResult({
826
+ stopReason: 'interrupt',
827
+ lastMessage: this.messages.length > 0
828
+ ? this.messages[this.messages.length - 1]
829
+ : new Message({ role: 'assistant', content: [new TextBlock('Interrupted')] }),
830
+ traces: this._tracer.localTraces,
831
+ metrics: this._meter.metrics,
832
+ interrupts: this._interruptState.getUnansweredInterrupts(),
833
+ invocationState,
834
+ });
835
+ }
836
+ /**
837
+ * Extracts interrupt response content blocks from invocation args.
838
+ *
839
+ * @param args - The invocation arguments
840
+ * @returns Array of InterruptResponseContent blocks, empty if none found
841
+ * @throws TypeError if args mix interrupt responses with other content
842
+ */
843
+ _extractInterruptResponses(args) {
844
+ if (!Array.isArray(args) || args.length === 0) {
845
+ return [];
846
+ }
847
+ const responses = [];
848
+ let hasNonInterrupt = false;
849
+ for (const item of args) {
850
+ if (isInterruptResponseContent(item)) {
851
+ responses.push(item);
852
+ }
853
+ else {
854
+ hasNonInterrupt = true;
855
+ }
856
+ }
857
+ if (responses.length > 0 && hasNonInterrupt) {
858
+ throw new TypeError('Must resume from interrupt with a list of interruptResponse content blocks only');
859
+ }
860
+ return responses;
861
+ }
657
862
  /**
658
863
  * Normalizes agent invocation input into an array of messages to append.
659
864
  *
@@ -673,6 +878,11 @@ export class Agent {
673
878
  }
674
879
  else if (Array.isArray(args) && args.length > 0) {
675
880
  const firstElement = args[0];
881
+ // Check if it's interrupt responses - skip creating messages for these
882
+ if (isInterruptResponseContent(firstElement)) {
883
+ // Pure interrupt responses: no messages to add
884
+ return [];
885
+ }
676
886
  // Check if it's Message[] or MessageData[]
677
887
  if ('role' in firstElement && typeof firstElement.role === 'string') {
678
888
  // Check if it's a Message instance or MessageData
@@ -716,9 +926,9 @@ export class Agent {
716
926
  * @param toolChoice - Optional tool choice to force specific tool usage
717
927
  * @returns Object containing the assistant message, stop reason, and optional redaction message
718
928
  */
719
- async *_invokeModel(toolChoice) {
929
+ async *_invokeModel(invocationState, toolChoice) {
720
930
  const toolSpecs = this._toolRegistry.list().map((tool) => tool.toolSpec);
721
- const streamOptions = { toolSpecs };
931
+ const streamOptions = { toolSpecs, modelState: this.modelState };
722
932
  if (this.systemPrompt !== undefined) {
723
933
  streamOptions.systemPrompt = this.systemPrompt;
724
934
  }
@@ -726,63 +936,117 @@ export class Agent {
726
936
  if (toolChoice) {
727
937
  streamOptions.toolChoice = toolChoice;
728
938
  }
729
- yield new BeforeModelCallEvent({ agent: this, model: this.model });
730
- // Start model span within loop span context
731
- const modelId = this.model.modelId;
732
- const modelSpan = this._tracer.startModelInvokeSpan({
733
- messages: this.messages,
734
- ...(modelId && { modelId }),
735
- ...(this.systemPrompt !== undefined && { systemPrompt: this.systemPrompt }),
736
- });
737
- try {
738
- const result = yield* this._streamFromModel(this.messages, streamOptions);
739
- // Accumulate token usage and model latency metrics
740
- this._meter.updateCycle(result.metadata);
741
- // End model span with usage
742
- const usage = result.metadata?.usage;
743
- const metrics = result.metadata?.metrics;
744
- this._tracer.endModelInvokeSpan(modelSpan, {
745
- output: result.message,
746
- stopReason: result.stopReason,
747
- ...(usage && { usage }),
748
- ...(metrics && { metrics }),
939
+ let attemptCount = 1;
940
+ while (true) {
941
+ // Estimate input tokens for the upcoming model call (non-fatal if estimation fails)
942
+ let projectedInputTokens;
943
+ try {
944
+ projectedInputTokens = await this._estimateInputTokens(streamOptions);
945
+ }
946
+ catch (e) {
947
+ logger.debug(`error=<${e}> | token estimation failed, proceeding without estimate`);
948
+ }
949
+ const beforeModelCallEvent = new BeforeModelCallEvent({
950
+ agent: this,
951
+ model: this.model,
952
+ invocationState,
953
+ ...(projectedInputTokens !== undefined && { projectedInputTokens }),
749
954
  });
750
- yield new ModelMessageEvent({ agent: this, message: result.message, stopReason: result.stopReason });
751
- // Handle user content redaction if guardrails blocked input
752
- if (result.redaction?.userMessage) {
753
- this._redactLastMessage(result.redaction.userMessage);
955
+ yield beforeModelCallEvent;
956
+ if (beforeModelCallEvent.cancel) {
957
+ const cancelText = typeof beforeModelCallEvent.cancel === 'string' ? beforeModelCallEvent.cancel : 'model call denied by hook';
958
+ const message = new Message({ role: 'assistant', content: [new TextBlock(cancelText)] });
959
+ const stopData = { message, stopReason: 'endTurn' };
960
+ const afterModelCallEvent = new AfterModelCallEvent({
961
+ agent: this,
962
+ model: this.model,
963
+ attemptCount,
964
+ stopData,
965
+ invocationState,
966
+ });
967
+ yield afterModelCallEvent;
968
+ if (afterModelCallEvent.retry) {
969
+ attemptCount += 1;
970
+ continue;
971
+ }
972
+ return { message, stopReason: 'endTurn' };
754
973
  }
755
- const stopData = {
756
- message: result.message,
757
- stopReason: result.stopReason,
758
- ...(result.redaction && { redaction: result.redaction }),
759
- };
760
- const afterModelCallEvent = new AfterModelCallEvent({ agent: this, model: this.model, stopData });
761
- yield afterModelCallEvent;
762
- if (afterModelCallEvent.retry) {
763
- return yield* this._invokeModel(toolChoice);
974
+ // Start model span within loop span context
975
+ const modelId = this.model.modelId;
976
+ const modelSpan = this._tracer.startModelInvokeSpan({
977
+ messages: this.messages,
978
+ ...(modelId && { modelId }),
979
+ ...(this.systemPrompt !== undefined && { systemPrompt: this.systemPrompt }),
980
+ });
981
+ try {
982
+ const result = yield* this._streamFromModel(this.messages, streamOptions, invocationState);
983
+ // Accumulate token usage and model latency metrics
984
+ this._meter.updateCycle(result.metadata);
985
+ // End model span with usage
986
+ const usage = result.metadata?.usage;
987
+ const metrics = result.metadata?.metrics;
988
+ this._tracer.endModelInvokeSpan(modelSpan, {
989
+ output: result.message,
990
+ stopReason: result.stopReason,
991
+ ...(usage && { usage }),
992
+ ...(metrics && { metrics }),
993
+ });
994
+ yield new ModelMessageEvent({
995
+ agent: this,
996
+ message: result.message,
997
+ stopReason: result.stopReason,
998
+ invocationState,
999
+ });
1000
+ // Handle user content redaction if guardrails blocked input
1001
+ if (result.redaction?.userMessage) {
1002
+ this._redactLastMessage(result.redaction.userMessage);
1003
+ }
1004
+ const stopData = {
1005
+ message: result.message,
1006
+ stopReason: result.stopReason,
1007
+ ...(result.redaction && { redaction: result.redaction }),
1008
+ };
1009
+ const afterModelCallEvent = new AfterModelCallEvent({
1010
+ agent: this,
1011
+ model: this.model,
1012
+ attemptCount,
1013
+ stopData,
1014
+ invocationState,
1015
+ });
1016
+ yield afterModelCallEvent;
1017
+ if (afterModelCallEvent.retry) {
1018
+ attemptCount += 1;
1019
+ continue;
1020
+ }
1021
+ return result;
764
1022
  }
765
- return result;
766
- }
767
- catch (error) {
768
- const modelError = normalizeError(error);
769
- // End model span with error
770
- this._tracer.endModelInvokeSpan(modelSpan, { error: modelError });
771
- // Create error event
772
- const errorEvent = new AfterModelCallEvent({ agent: this, model: this.model, error: modelError });
773
- // Yield error event - stream will invoke hooks
774
- yield errorEvent;
775
- // Let CancelledError propagate directly — no retry
776
- // (we emit the AfterModelCall because we already emitted Before and we guarentee the pair)
777
- if (error instanceof CancelledError) {
1023
+ catch (error) {
1024
+ const modelError = normalizeError(error);
1025
+ // End model span with error
1026
+ this._tracer.endModelInvokeSpan(modelSpan, { error: modelError });
1027
+ // Create error event
1028
+ const errorEvent = new AfterModelCallEvent({
1029
+ agent: this,
1030
+ model: this.model,
1031
+ attemptCount,
1032
+ error: modelError,
1033
+ invocationState,
1034
+ });
1035
+ // Yield error event - stream will invoke hooks
1036
+ yield errorEvent;
1037
+ // Let CancelledError propagate directly — no retry
1038
+ // (we emit the AfterModelCall because we already emitted Before and we guarentee the pair)
1039
+ if (error instanceof CancelledError) {
1040
+ throw error;
1041
+ }
1042
+ // After yielding, hooks have been invoked and may have set retry
1043
+ if (errorEvent.retry) {
1044
+ attemptCount += 1;
1045
+ continue;
1046
+ }
1047
+ // Re-throw error
778
1048
  throw error;
779
1049
  }
780
- // After yielding, hooks have been invoked and may have set retry
781
- if (errorEvent.retry) {
782
- return yield* this._invokeModel(toolChoice);
783
- }
784
- // Re-throw error
785
- throw error;
786
1050
  }
787
1051
  }
788
1052
  /**
@@ -801,7 +1065,8 @@ export class Agent {
801
1065
  * @param streamOptions - Options for streaming
802
1066
  * @returns StreamAggregatedResult containing message, stop reason, and optional redaction message
803
1067
  */
804
- async *_streamFromModel(messages, streamOptions) {
1068
+ async *_streamFromModel(messages, streamOptions, invocationState) {
1069
+ messages = normalizeToolUseNames(messages);
805
1070
  const streamGenerator = this.model.streamAggregated(messages, streamOptions);
806
1071
  let result = await streamGenerator.next();
807
1072
  while (!result.done) {
@@ -809,11 +1074,11 @@ export class Agent {
809
1074
  const event = result.value;
810
1075
  if (isModelStreamEvent(event)) {
811
1076
  // ModelStreamEvent: wrap in ModelStreamUpdateEvent
812
- yield new ModelStreamUpdateEvent({ agent: this, event });
1077
+ yield new ModelStreamUpdateEvent({ agent: this, event, invocationState });
813
1078
  }
814
1079
  else {
815
1080
  // ContentBlock: wrap in ContentBlockEvent
816
- yield new ContentBlockEvent({ agent: this, contentBlock: event });
1081
+ yield new ContentBlockEvent({ agent: this, contentBlock: event, invocationState });
817
1082
  }
818
1083
  result = await streamGenerator.next();
819
1084
  }
@@ -821,63 +1086,268 @@ export class Agent {
821
1086
  return result.value;
822
1087
  }
823
1088
  /**
824
- * Executes tools sequentially and streams all tool events.
1089
+ * Emits `BeforeToolsEvent`, handles the pre-launch cancel paths, then
1090
+ * delegates per-tool execution to the configured {@link ToolExecutorStrategy}.
1091
+ * Always pairs `BeforeToolsEvent` with a terminal `AfterToolsEvent`, even on
1092
+ * the invariant-violation throw path.
825
1093
  *
826
1094
  * @param assistantMessage - The assistant message containing tool use blocks
827
1095
  * @param toolRegistry - Registry containing available tools
828
- * @returns User message containing tool results
1096
+ * @returns Tool-result message and the dispatched AfterToolsEvent
829
1097
  */
830
- async *executeTools(assistantMessage, toolRegistry) {
831
- const beforeToolsEvent = new BeforeToolsEvent({ agent: this, message: assistantMessage });
832
- yield beforeToolsEvent;
1098
+ async *executeTools(assistantMessage, toolRegistry, invocationState, completedToolResults) {
1099
+ const beforeToolsEvent = new BeforeToolsEvent({ agent: this, message: assistantMessage, invocationState });
1100
+ try {
1101
+ yield beforeToolsEvent;
1102
+ }
1103
+ catch (error) {
1104
+ // Store pending state before re-throwing so the agent can resume from this point.
1105
+ // The error must still propagate to _stream which handles the interrupt stop.
1106
+ if (error instanceof InterruptError) {
1107
+ this._interruptState.setPendingToolExecution({
1108
+ assistantMessageData: assistantMessage.toJSON(),
1109
+ completedToolResults: {},
1110
+ });
1111
+ }
1112
+ throw error;
1113
+ }
1114
+ const toolUseBlocks = assistantMessage.content.filter((block) => block.type === 'toolUseBlock');
1115
+ if (toolUseBlocks.length === 0) {
1116
+ // Preserve BeforeToolsEvent/AfterToolsEvent bracket symmetry even on
1117
+ // this invariant-violation branch.
1118
+ yield new AfterToolsEvent({
1119
+ agent: this,
1120
+ message: new Message({ role: 'user', content: [] }),
1121
+ invocationState,
1122
+ });
1123
+ throw new Error('Model indicated toolUse but no tool use blocks found in message');
1124
+ }
1125
+ // Pre-launch cancel paths are strategy-independent.
1126
+ if (beforeToolsEvent.cancel) {
1127
+ const message = typeof beforeToolsEvent.cancel === 'string' ? beforeToolsEvent.cancel : 'Tool cancelled by hook';
1128
+ return yield* this._yieldCancelledToolResults(toolUseBlocks, message, invocationState);
1129
+ }
1130
+ if (this.isCancelled) {
1131
+ return yield* this._yieldCancelledToolResults(toolUseBlocks, 'Tool execution cancelled', invocationState);
1132
+ }
1133
+ switch (this._toolExecutor) {
1134
+ case 'sequential':
1135
+ return yield* this._executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
1136
+ case 'concurrent':
1137
+ return yield* this._executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
1138
+ default: {
1139
+ const _exhaustive = this._toolExecutor;
1140
+ throw new Error(`Unknown toolExecutor: ${_exhaustive}`);
1141
+ }
1142
+ }
1143
+ }
1144
+ /**
1145
+ * Emits a `ToolResultEvent` for every block plus an `AfterToolsEvent`, and
1146
+ * returns the resulting tool-result message and dispatched event. Used by the pre-launch cancel
1147
+ * paths shared across executors.
1148
+ */
1149
+ async *_yieldCancelledToolResults(toolUseBlocks, message, invocationState) {
1150
+ const cancelBlocks = this._cancelAllAsResults(toolUseBlocks, message);
1151
+ for (const result of cancelBlocks) {
1152
+ yield new ToolResultEvent({ agent: this, result, invocationState });
1153
+ }
1154
+ const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
1155
+ const afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1156
+ yield afterToolsEvent;
1157
+ return { message: toolResultMessage, afterToolsEvent };
1158
+ }
1159
+ /**
1160
+ * Executes tools one at a time, honoring `agent.cancelSignal` between
1161
+ * iterations to short-circuit not-yet-started tools.
1162
+ */
1163
+ async *_executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
833
1164
  const toolResultBlocks = [];
834
1165
  let toolResultMessage;
1166
+ let afterToolsEvent;
835
1167
  try {
836
- // Extract tool use blocks from assistant message
837
- const toolUseBlocks = assistantMessage.content.filter((block) => block.type === 'toolUseBlock');
838
- if (toolUseBlocks.length === 0) {
839
- // No tool use blocks found even though stopReason is toolUse
840
- throw new Error('Model indicated toolUse but no tool use blocks found in message');
841
- }
842
- // Cancel all tools if hook requested it
843
- if (beforeToolsEvent.cancel) {
844
- const cancelMessage = cancelToolMessage(beforeToolsEvent.cancel);
845
- const cancelBlocks = toolUseBlocks.map((block) => new ToolResultBlock({
846
- toolUseId: block.toolUseId,
847
- status: 'error',
848
- content: [new TextBlock(cancelMessage)],
849
- }));
850
- for (const result of cancelBlocks) {
851
- yield new ToolResultEvent({ agent: this, result });
1168
+ for (const toolUseBlock of toolUseBlocks) {
1169
+ // Skip tools that were already completed before the interrupt
1170
+ if (completedToolResults?.has(toolUseBlock.toolUseId)) {
1171
+ const completedResult = completedToolResults.get(toolUseBlock.toolUseId);
1172
+ // No events emitted for already-completed tools.
1173
+ // The result is included in the final tool result message.
1174
+ toolResultBlocks.push(completedResult);
1175
+ continue;
852
1176
  }
853
- toolResultBlocks.push(...cancelBlocks);
854
- }
855
- else {
856
- for (const toolUseBlock of toolUseBlocks) {
857
- if (this.isCancelled) {
858
- const cancelBlock = new ToolResultBlock({
859
- toolUseId: toolUseBlock.toolUseId,
860
- status: 'error',
861
- content: [new TextBlock('Tool execution cancelled')],
1177
+ if (this.isCancelled) {
1178
+ const cancelBlock = new ToolResultBlock({
1179
+ toolUseId: toolUseBlock.toolUseId,
1180
+ status: 'error',
1181
+ content: [new TextBlock('Tool execution cancelled')],
1182
+ });
1183
+ toolResultBlocks.push(cancelBlock);
1184
+ yield new ToolResultEvent({ agent: this, result: cancelBlock, invocationState });
1185
+ continue;
1186
+ }
1187
+ try {
1188
+ const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry, invocationState);
1189
+ toolResultBlocks.push(toolResultBlock);
1190
+ yield new ToolResultEvent({ agent: this, result: toolResultBlock, invocationState });
1191
+ }
1192
+ catch (error) {
1193
+ if (error instanceof InterruptError) {
1194
+ // Store pending state with completed results so far
1195
+ const completedSoFar = {};
1196
+ for (const block of toolResultBlocks) {
1197
+ completedSoFar[block.toolUseId] = block.toJSON();
1198
+ }
1199
+ // Also include any previously completed results
1200
+ if (completedToolResults) {
1201
+ for (const [id, block] of completedToolResults) {
1202
+ completedSoFar[id] = block.toJSON();
1203
+ }
1204
+ }
1205
+ this._interruptState.setPendingToolExecution({
1206
+ assistantMessageData: assistantMessage.toJSON(),
1207
+ completedToolResults: completedSoFar,
862
1208
  });
863
- toolResultBlocks.push(cancelBlock);
864
- yield new ToolResultEvent({ agent: this, result: cancelBlock });
1209
+ throw error;
1210
+ }
1211
+ throw error;
1212
+ }
1213
+ }
1214
+ }
1215
+ finally {
1216
+ toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
1217
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1218
+ yield afterToolsEvent;
1219
+ }
1220
+ return { message: toolResultMessage, afterToolsEvent };
1221
+ }
1222
+ /**
1223
+ * Produces one error ToolResultBlock per tool use block, each carrying
1224
+ * `message` as its error text. Shared by pre-launch cancel paths.
1225
+ */
1226
+ _cancelAllAsResults(toolUseBlocks, message) {
1227
+ return toolUseBlocks.map((block) => new ToolResultBlock({
1228
+ toolUseId: block.toolUseId,
1229
+ status: 'error',
1230
+ content: [new TextBlock(message)],
1231
+ }));
1232
+ }
1233
+ /**
1234
+ * Executes tools concurrently by merging N per-tool {@link executeTool}
1235
+ * async generators via `Promise.race`. Per-tool event order is preserved
1236
+ * (because each generator is iterated serially); cross-tool events may
1237
+ * interleave at race resolution boundaries.
1238
+ *
1239
+ * Per-tool retry (`AfterToolCallEvent.retry`) is isolated — it lives inside
1240
+ * `executeTool`'s own `while(true)` loop, so one tool retrying does not
1241
+ * disturb its siblings.
1242
+ */
1243
+ async *_executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
1244
+ let toolResultMessage;
1245
+ let afterToolsEvent;
1246
+ const gens = toolUseBlocks.map((block) => ({
1247
+ block,
1248
+ gen: completedToolResults?.has(block.toolUseId)
1249
+ ? undefined // Skip already-completed tools
1250
+ : this.executeTool(block, toolRegistry, invocationState),
1251
+ }));
1252
+ const step = (idx) => gens[idx].gen.next().then((res) => ({ idx, kind: 'next', res }), (error) => ({ idx, kind: 'throw', error }));
1253
+ // Seed completed results from resume state
1254
+ const resultsByToolUseId = new Map();
1255
+ if (completedToolResults) {
1256
+ for (const [id, result] of completedToolResults) {
1257
+ resultsByToolUseId.set(id, result);
1258
+ }
1259
+ }
1260
+ // Only race tools that need execution
1261
+ const pendingNext = new Map();
1262
+ for (let idx = 0; idx < gens.length; idx++) {
1263
+ if (gens[idx].gen) {
1264
+ pendingNext.set(idx, step(idx));
1265
+ }
1266
+ }
1267
+ // Track interrupts — let all other tools finish before propagating
1268
+ let interruptError;
1269
+ try {
1270
+ while (pendingNext.size > 0) {
1271
+ const winner = await Promise.race(pendingNext.values());
1272
+ const { idx } = winner;
1273
+ const block = gens[idx].block;
1274
+ if (winner.kind === 'throw') {
1275
+ pendingNext.delete(idx);
1276
+ // Detect InterruptError — don't convert to error result, track it
1277
+ if (winner.error instanceof InterruptError) {
1278
+ interruptError = winner.error;
865
1279
  continue;
866
1280
  }
867
- const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry);
868
- toolResultBlocks.push(toolResultBlock);
869
- yield new ToolResultEvent({ agent: this, result: toolResultBlock });
1281
+ const err = normalizeError(winner.error);
1282
+ const result = new ToolResultBlock({
1283
+ toolUseId: block.toolUseId,
1284
+ status: 'error',
1285
+ content: [new TextBlock(err.message)],
1286
+ error: err,
1287
+ });
1288
+ resultsByToolUseId.set(block.toolUseId, result);
1289
+ yield new ToolResultEvent({ agent: this, result, invocationState });
1290
+ continue;
1291
+ }
1292
+ if (winner.res.done) {
1293
+ pendingNext.delete(idx);
1294
+ resultsByToolUseId.set(block.toolUseId, winner.res.value);
1295
+ yield new ToolResultEvent({ agent: this, result: winner.res.value, invocationState });
1296
+ }
1297
+ else {
1298
+ try {
1299
+ yield winner.res.value;
1300
+ }
1301
+ catch (e) {
1302
+ // InterruptError thrown back into generator from stream() error injection
1303
+ if (e instanceof InterruptError) {
1304
+ interruptError = e;
1305
+ pendingNext.delete(idx);
1306
+ continue;
1307
+ }
1308
+ throw e;
1309
+ }
1310
+ pendingNext.set(idx, step(idx));
870
1311
  }
871
1312
  }
1313
+ // After all tools finish, propagate interrupt if one was raised
1314
+ if (interruptError) {
1315
+ const completedSoFar = {};
1316
+ for (const [id, result] of resultsByToolUseId) {
1317
+ completedSoFar[id] = result.toJSON();
1318
+ }
1319
+ this._interruptState.setPendingToolExecution({
1320
+ assistantMessageData: assistantMessage.toJSON(),
1321
+ completedToolResults: completedSoFar,
1322
+ });
1323
+ throw interruptError;
1324
+ }
872
1325
  }
873
1326
  finally {
874
- toolResultMessage = new Message({
875
- role: 'user',
876
- content: toolResultBlocks,
877
- });
878
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage });
1327
+ // Close any generators still in-flight (e.g. consumer broke out of stream).
1328
+ await Promise.allSettled(Array.from(pendingNext.keys(), (idx) => gens[idx].gen.return(undefined)));
1329
+ // Build the result message from whatever completed, in source order.
1330
+ // Missing entries get a fallback error block so the message always
1331
+ // accounts for every toolUseBlock the model emitted.
1332
+ const toolResultBlocks = [];
1333
+ for (const block of toolUseBlocks) {
1334
+ const result = resultsByToolUseId.get(block.toolUseId);
1335
+ if (result) {
1336
+ toolResultBlocks.push(result);
1337
+ }
1338
+ else {
1339
+ toolResultBlocks.push(new ToolResultBlock({
1340
+ toolUseId: block.toolUseId,
1341
+ status: 'error',
1342
+ content: [new TextBlock('Tool execution interrupted')],
1343
+ }));
1344
+ }
1345
+ }
1346
+ toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
1347
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1348
+ yield afterToolsEvent;
879
1349
  }
880
- return toolResultMessage;
1350
+ return { message: toolResultMessage, afterToolsEvent };
881
1351
  }
882
1352
  /**
883
1353
  * Executes a single tool and returns the result.
@@ -889,9 +1359,10 @@ export class Agent {
889
1359
  * @param toolRegistry - Registry containing available tools
890
1360
  * @returns Tool result block
891
1361
  */
892
- async *executeTool(toolUseBlock, toolRegistry) {
893
- const tool = toolRegistry.get(toolUseBlock.name);
894
- // Create toolUse object for hook events and telemetry
1362
+ async *executeTool(toolUseBlock, toolRegistry, invocationState) {
1363
+ const registryTool = toolRegistry.get(toolUseBlock.name);
1364
+ // Create toolUse object for hook events and telemetry. Callbacks may mutate
1365
+ // this object's fields (input/name/toolUseId) inside BeforeToolCallEvent.
895
1366
  const toolUse = {
896
1367
  name: toolUseBlock.name,
897
1368
  toolUseId: toolUseBlock.toolUseId,
@@ -899,27 +1370,40 @@ export class Agent {
899
1370
  };
900
1371
  // Retry loop for tool execution
901
1372
  while (true) {
902
- const beforeToolCallEvent = new BeforeToolCallEvent({ agent: this, toolUse, tool });
1373
+ const beforeToolCallEvent = new BeforeToolCallEvent({
1374
+ agent: this,
1375
+ toolUse,
1376
+ tool: registryTool,
1377
+ invocationState,
1378
+ });
903
1379
  yield beforeToolCallEvent;
1380
+ // Resolve the tool that would actually execute. selectedTool wins;
1381
+ // otherwise if the hook renamed toolUse.name, re-resolve from the
1382
+ // registry under the new name; otherwise use the original registry
1383
+ // lookup. Resolved before the cancel check so AfterToolCallEvent.tool
1384
+ // is consistent whether the cancel or execution branch runs.
1385
+ const effectiveTool = beforeToolCallEvent.selectedTool ??
1386
+ (toolUse.name !== toolUseBlock.name ? toolRegistry.get(toolUse.name) : registryTool);
904
1387
  // Cancel individual tool if hook requested it
905
1388
  if (beforeToolCallEvent.cancel) {
906
- const cancelMessage = cancelToolMessage(beforeToolCallEvent.cancel);
907
- const toolResult = new ToolResultBlock({
908
- toolUseId: toolUseBlock.toolUseId,
1389
+ const cancelMessage = typeof beforeToolCallEvent.cancel === 'string' ? beforeToolCallEvent.cancel : 'Tool cancelled by hook';
1390
+ const cancelResult = new ToolResultBlock({
1391
+ toolUseId: toolUse.toolUseId,
909
1392
  status: 'error',
910
1393
  content: [new TextBlock(cancelMessage)],
911
1394
  });
912
1395
  const afterToolCallEvent = new AfterToolCallEvent({
913
1396
  agent: this,
914
1397
  toolUse,
915
- tool,
916
- result: toolResult,
1398
+ tool: effectiveTool,
1399
+ result: cancelResult,
1400
+ invocationState,
917
1401
  });
918
1402
  yield afterToolCallEvent;
919
1403
  if (afterToolCallEvent.retry) {
920
1404
  continue;
921
1405
  }
922
- return toolResult;
1406
+ return afterToolCallEvent.result;
923
1407
  }
924
1408
  // Start tool span within loop span context
925
1409
  const toolSpan = this._tracer.startToolCallSpan({
@@ -929,23 +1413,27 @@ export class Agent {
929
1413
  const toolStartTime = Date.now();
930
1414
  let toolResult;
931
1415
  let error;
932
- if (!tool) {
1416
+ if (!effectiveTool) {
933
1417
  // Tool not found
934
1418
  toolResult = new ToolResultBlock({
935
- toolUseId: toolUseBlock.toolUseId,
1419
+ toolUseId: toolUse.toolUseId,
936
1420
  status: 'error',
937
- content: [new TextBlock(`Tool '${toolUseBlock.name}' not found in registry`)],
1421
+ content: [new TextBlock(`Tool '${toolUse.name}' not found in registry`)],
938
1422
  });
939
1423
  }
940
1424
  else {
941
1425
  // Execute tool within the tool span context
942
1426
  const toolContext = {
943
1427
  toolUse: {
944
- name: toolUseBlock.name,
945
- toolUseId: toolUseBlock.toolUseId,
946
- input: toolUseBlock.input,
1428
+ name: toolUse.name,
1429
+ toolUseId: toolUse.toolUseId,
1430
+ input: toolUse.input,
947
1431
  },
948
1432
  agent: this,
1433
+ invocationState,
1434
+ interrupt: (params) => {
1435
+ return interruptFromAgent(this, `tool:${toolUseBlock.toolUseId}:${params.name}`, params);
1436
+ },
949
1437
  };
950
1438
  try {
951
1439
  // Manually iterate tool stream to wrap each ToolStreamEvent in ToolStreamUpdateEvent.
@@ -953,19 +1441,19 @@ export class Agent {
953
1441
  // without knowledge of agents or hooks, and we wrap at the boundary.
954
1442
  // Tool execution is ran within the tool span's context so that
955
1443
  // downstream calls (e.g., MCP clients) can propagate trace context
956
- const toolGenerator = this._tracer.withSpanContext(toolSpan, () => tool.stream(toolContext));
1444
+ const toolGenerator = this._tracer.withSpanContext(toolSpan, () => effectiveTool.stream(toolContext));
957
1445
  let toolNext = await this._tracer.withSpanContext(toolSpan, () => toolGenerator.next());
958
1446
  while (!toolNext.done) {
959
- yield new ToolStreamUpdateEvent({ agent: this, event: toolNext.value });
1447
+ yield new ToolStreamUpdateEvent({ agent: this, event: toolNext.value, invocationState });
960
1448
  toolNext = await this._tracer.withSpanContext(toolSpan, () => toolGenerator.next());
961
1449
  }
962
1450
  const result = toolNext.value;
963
1451
  if (!result) {
964
1452
  // Tool didn't return a result
965
1453
  toolResult = new ToolResultBlock({
966
- toolUseId: toolUseBlock.toolUseId,
1454
+ toolUseId: toolUse.toolUseId,
967
1455
  status: 'error',
968
- content: [new TextBlock(`Tool '${toolUseBlock.name}' did not return a result`)],
1456
+ content: [new TextBlock(`Tool '${toolUse.name}' did not return a result`)],
969
1457
  });
970
1458
  }
971
1459
  else {
@@ -974,17 +1462,22 @@ export class Agent {
974
1462
  }
975
1463
  }
976
1464
  catch (e) {
1465
+ // Re-throw InterruptError to allow interrupt handling
1466
+ if (e instanceof InterruptError) {
1467
+ throw e;
1468
+ }
977
1469
  // Tool execution failed with error
978
1470
  error = normalizeError(e);
979
1471
  toolResult = new ToolResultBlock({
980
- toolUseId: toolUseBlock.toolUseId,
1472
+ toolUseId: toolUse.toolUseId,
981
1473
  status: 'error',
982
1474
  content: [new TextBlock(error.message)],
983
1475
  error,
984
1476
  });
985
1477
  }
986
1478
  }
987
- // End tool span
1479
+ // End tool span with the raw tool result — telemetry reflects what the
1480
+ // tool actually returned, independent of AfterToolCallEvent mutations.
988
1481
  this._tracer.endToolCallSpan(toolSpan, { toolResult, ...(error && { error }) });
989
1482
  // End tool metrics tracking
990
1483
  this._meter.endToolCall({
@@ -996,15 +1489,18 @@ export class Agent {
996
1489
  const afterToolCallEvent = new AfterToolCallEvent({
997
1490
  agent: this,
998
1491
  toolUse,
999
- tool,
1492
+ tool: effectiveTool,
1000
1493
  result: toolResult,
1494
+ invocationState,
1001
1495
  ...(error !== undefined && { error }),
1002
1496
  });
1003
1497
  yield afterToolCallEvent;
1004
1498
  if (afterToolCallEvent.retry) {
1005
1499
  continue;
1006
1500
  }
1007
- return toolResult;
1501
+ // Return the (possibly mutated) result so hook transformations propagate
1502
+ // to ToolResultEvent and the conversation message the model will see.
1503
+ return afterToolCallEvent.result;
1008
1504
  }
1009
1505
  }
1010
1506
  /**
@@ -1052,24 +1548,94 @@ export class Agent {
1052
1548
  }
1053
1549
  }
1054
1550
  }
1551
+ /**
1552
+ * Estimate the input token count for the next model call.
1553
+ *
1554
+ * Uses the token counting strategy: reads inputTokens + outputTokens
1555
+ * from the last assistant message's metadata as a known baseline, then estimates
1556
+ * only new messages added after it. Falls back to full estimation when no metadata
1557
+ * is available (cold start or first call).
1558
+ *
1559
+ * @param streamOptions - The stream options containing system prompt and tool specs
1560
+ * @returns Estimated input token count
1561
+ */
1562
+ async _estimateInputTokens(streamOptions) {
1563
+ // Find the last assistant message with usage metadata
1564
+ let lastAssistantIdx = -1;
1565
+ for (let i = this.messages.length - 1; i >= 0; i--) {
1566
+ if (this.messages[i].role === 'assistant' && this.messages[i].metadata?.usage) {
1567
+ lastAssistantIdx = i;
1568
+ break;
1569
+ }
1570
+ }
1571
+ let estimate;
1572
+ if (lastAssistantIdx >= 0) {
1573
+ const usage = this.messages[lastAssistantIdx].metadata.usage;
1574
+ const knownBaseline = usage.inputTokens + usage.outputTokens;
1575
+ const newMessages = this.messages.slice(lastAssistantIdx + 1);
1576
+ if (newMessages.length === 0) {
1577
+ estimate = knownBaseline;
1578
+ }
1579
+ else {
1580
+ // System prompt and tool spec tokens are already included in the baseline from the prior model call
1581
+ estimate = knownBaseline + (await this.model.countTokens(newMessages));
1582
+ }
1583
+ }
1584
+ else {
1585
+ estimate = await this.model.countTokens(this.messages, {
1586
+ ...(streamOptions.systemPrompt !== undefined && { systemPrompt: streamOptions.systemPrompt }),
1587
+ ...(streamOptions.toolSpecs !== undefined && { toolSpecs: streamOptions.toolSpecs }),
1588
+ });
1589
+ }
1590
+ return estimate;
1591
+ }
1055
1592
  /**
1056
1593
  * Appends a message to the conversation history and returns the event for yielding.
1057
1594
  *
1058
1595
  * @param message - The message to append
1059
1596
  * @returns MessageAddedEvent to be yielded
1060
1597
  */
1061
- _appendMessage(message) {
1598
+ _appendMessage(message, invocationState) {
1062
1599
  this.messages.push(message);
1063
- return new MessageAddedEvent({ agent: this, message });
1600
+ return new MessageAddedEvent({ agent: this, message, invocationState });
1064
1601
  }
1065
1602
  }
1603
+ const INVALID_TOOL_NAME_PLACEHOLDER = 'INVALID_TOOL_NAME';
1066
1604
  /**
1067
- * Returns the cancel message for a cancelled tool.
1068
- * @param cancelTool - The cancel value (true or custom message)
1069
- * @returns The cancel message string
1605
+ * Replaces invalid tool-use names on assistant messages with `INVALID_TOOL_NAME`
1606
+ * so providers that reject malformed names don't fail the whole request.
1607
+ * Returns the input unchanged (same reference) when nothing needs replacing.
1070
1608
  */
1071
- function cancelToolMessage(cancelTool) {
1072
- return typeof cancelTool === 'string' ? cancelTool : 'tool cancelled by hook';
1609
+ function normalizeToolUseNames(messages) {
1610
+ let replaced = false;
1611
+ const next = messages.map((message) => {
1612
+ if (!message || message.role !== 'assistant')
1613
+ return message;
1614
+ let messageReplaced = false;
1615
+ const content = message.content.map((block) => {
1616
+ if (block.type !== 'toolUseBlock')
1617
+ return block;
1618
+ if (isValidToolName(block.name))
1619
+ return block;
1620
+ messageReplaced = true;
1621
+ logger.debug(`tool_name=<${block.name}> | replacing invalid tool name with ${INVALID_TOOL_NAME_PLACEHOLDER}`);
1622
+ return new ToolUseBlock({
1623
+ name: INVALID_TOOL_NAME_PLACEHOLDER,
1624
+ toolUseId: block.toolUseId,
1625
+ input: block.input,
1626
+ ...(block.reasoningSignature !== undefined && { reasoningSignature: block.reasoningSignature }),
1627
+ });
1628
+ });
1629
+ if (!messageReplaced)
1630
+ return message;
1631
+ replaced = true;
1632
+ return new Message({
1633
+ role: message.role,
1634
+ content,
1635
+ ...(message.metadata !== undefined && { metadata: message.metadata }),
1636
+ });
1637
+ });
1638
+ return replaced ? next : messages;
1073
1639
  }
1074
1640
  /**
1075
1641
  * Recursively flattens nested arrays of tools into a single flat array.