@strands-agents/sdk 1.0.0 → 1.2.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 (284) hide show
  1. package/README.md +6 -0
  2. package/dist/src/__fixtures__/agent-helpers.d.ts +16 -1
  3. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
  4. package/dist/src/__fixtures__/agent-helpers.js +42 -0
  5. package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
  6. package/dist/src/__fixtures__/tool-helpers.d.ts +2 -1
  7. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
  8. package/dist/src/__fixtures__/tool-helpers.js +20 -3
  9. package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
  10. package/dist/src/__tests__/interrupt.test.d.ts +2 -0
  11. package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
  12. package/dist/src/__tests__/interrupt.test.js +264 -0
  13. package/dist/src/__tests__/interrupt.test.js.map +1 -0
  14. package/dist/src/__tests__/mcp.test.js +447 -7
  15. package/dist/src/__tests__/mcp.test.js.map +1 -1
  16. package/dist/src/agent/__tests__/agent.hook.test.js +551 -1
  17. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
  18. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
  19. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
  20. package/dist/src/agent/__tests__/agent.interrupt.test.js +779 -0
  21. package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
  22. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
  23. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
  24. package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
  25. package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
  26. package/dist/src/agent/__tests__/agent.test.js +174 -0
  27. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  28. package/dist/src/agent/__tests__/snapshot.test.js +148 -4
  29. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
  30. package/dist/src/agent/agent-as-tool.d.ts.map +1 -1
  31. package/dist/src/agent/agent-as-tool.js +2 -3
  32. package/dist/src/agent/agent-as-tool.js.map +1 -1
  33. package/dist/src/agent/agent.d.ts +94 -4
  34. package/dist/src/agent/agent.d.ts.map +1 -1
  35. package/dist/src/agent/agent.js +625 -223
  36. package/dist/src/agent/agent.js.map +1 -1
  37. package/dist/src/agent/snapshot.d.ts +11 -19
  38. package/dist/src/agent/snapshot.d.ts.map +1 -1
  39. package/dist/src/agent/snapshot.js +23 -19
  40. package/dist/src/agent/snapshot.js.map +1 -1
  41. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
  42. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
  43. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
  44. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
  45. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +422 -41
  46. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
  47. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +75 -1
  48. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
  49. package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
  50. package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
  51. package/dist/src/conversation-manager/conversation-manager.js +65 -13
  52. package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
  53. package/dist/src/conversation-manager/index.d.ts +1 -1
  54. package/dist/src/conversation-manager/index.d.ts.map +1 -1
  55. package/dist/src/conversation-manager/index.js +1 -1
  56. package/dist/src/conversation-manager/index.js.map +1 -1
  57. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +43 -10
  58. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
  59. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +202 -45
  60. package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
  61. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
  62. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
  63. package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
  64. package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
  65. package/dist/src/hooks/__tests__/events.test.js +99 -12
  66. package/dist/src/hooks/__tests__/events.test.js.map +1 -1
  67. package/dist/src/hooks/__tests__/registry.test.js +166 -2
  68. package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
  69. package/dist/src/hooks/events.d.ts +125 -32
  70. package/dist/src/hooks/events.d.ts.map +1 -1
  71. package/dist/src/hooks/events.js +111 -8
  72. package/dist/src/hooks/events.js.map +1 -1
  73. package/dist/src/hooks/index.d.ts +4 -3
  74. package/dist/src/hooks/index.d.ts.map +1 -1
  75. package/dist/src/hooks/index.js +2 -1
  76. package/dist/src/hooks/index.js.map +1 -1
  77. package/dist/src/hooks/registry.d.ts +12 -12
  78. package/dist/src/hooks/registry.d.ts.map +1 -1
  79. package/dist/src/hooks/registry.js +55 -15
  80. package/dist/src/hooks/registry.js.map +1 -1
  81. package/dist/src/hooks/types.d.ts +23 -0
  82. package/dist/src/hooks/types.d.ts.map +1 -1
  83. package/dist/src/hooks/types.js +17 -1
  84. package/dist/src/hooks/types.js.map +1 -1
  85. package/dist/src/index.d.ts +12 -6
  86. package/dist/src/index.d.ts.map +1 -1
  87. package/dist/src/index.js +7 -2
  88. package/dist/src/index.js.map +1 -1
  89. package/dist/src/interrupt.d.ts +247 -0
  90. package/dist/src/interrupt.d.ts.map +1 -0
  91. package/dist/src/interrupt.js +316 -0
  92. package/dist/src/interrupt.js.map +1 -0
  93. package/dist/src/mcp.d.ts +61 -4
  94. package/dist/src/mcp.d.ts.map +1 -1
  95. package/dist/src/mcp.js +161 -25
  96. package/dist/src/mcp.js.map +1 -1
  97. package/dist/src/models/__tests__/anthropic.test.js +78 -8
  98. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  99. package/dist/src/models/__tests__/bedrock.test.js +156 -18
  100. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  101. package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
  102. package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
  103. package/dist/src/models/__tests__/defaults.test.js +36 -0
  104. package/dist/src/models/__tests__/defaults.test.js.map +1 -0
  105. package/dist/src/models/__tests__/google.test.js +72 -6
  106. package/dist/src/models/__tests__/google.test.js.map +1 -1
  107. package/dist/src/models/anthropic.d.ts +10 -0
  108. package/dist/src/models/anthropic.d.ts.map +1 -1
  109. package/dist/src/models/anthropic.js +14 -4
  110. package/dist/src/models/anthropic.js.map +1 -1
  111. package/dist/src/models/bedrock.d.ts +17 -3
  112. package/dist/src/models/bedrock.d.ts.map +1 -1
  113. package/dist/src/models/bedrock.js +80 -13
  114. package/dist/src/models/bedrock.js.map +1 -1
  115. package/dist/src/models/defaults.d.ts +10 -0
  116. package/dist/src/models/defaults.d.ts.map +1 -1
  117. package/dist/src/models/defaults.js +129 -0
  118. package/dist/src/models/defaults.js.map +1 -1
  119. package/dist/src/models/google/model.d.ts.map +1 -1
  120. package/dist/src/models/google/model.js +4 -2
  121. package/dist/src/models/google/model.js.map +1 -1
  122. package/dist/src/models/google/types.d.ts +10 -0
  123. package/dist/src/models/google/types.d.ts.map +1 -1
  124. package/dist/src/models/model.d.ts +15 -0
  125. package/dist/src/models/model.d.ts.map +1 -1
  126. package/dist/src/models/model.js +18 -0
  127. package/dist/src/models/model.js.map +1 -1
  128. package/dist/src/models/openai/__tests__/chat.test.js +55 -2
  129. package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
  130. package/dist/src/models/openai/__tests__/responses.test.js +19 -0
  131. package/dist/src/models/openai/__tests__/responses.test.js.map +1 -1
  132. package/dist/src/models/openai/errors.d.ts.map +1 -1
  133. package/dist/src/models/openai/errors.js +7 -4
  134. package/dist/src/models/openai/errors.js.map +1 -1
  135. package/dist/src/models/openai/model.d.ts.map +1 -1
  136. package/dist/src/models/openai/model.js +2 -2
  137. package/dist/src/models/openai/model.js.map +1 -1
  138. package/dist/src/multiagent/__tests__/graph.test.js +69 -0
  139. package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
  140. package/dist/src/multiagent/__tests__/graph.tracer.test.js +14 -0
  141. package/dist/src/multiagent/__tests__/graph.tracer.test.js.map +1 -1
  142. package/dist/src/multiagent/__tests__/interrupts.test.d.ts +2 -0
  143. package/dist/src/multiagent/__tests__/interrupts.test.d.ts.map +1 -0
  144. package/dist/src/multiagent/__tests__/interrupts.test.js +390 -0
  145. package/dist/src/multiagent/__tests__/interrupts.test.js.map +1 -0
  146. package/dist/src/multiagent/__tests__/nodes.test.js +13 -0
  147. package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
  148. package/dist/src/multiagent/__tests__/state.test.js +139 -1
  149. package/dist/src/multiagent/__tests__/state.test.js.map +1 -1
  150. package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
  151. package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
  152. package/dist/src/multiagent/events.d.ts +15 -1
  153. package/dist/src/multiagent/events.d.ts.map +1 -1
  154. package/dist/src/multiagent/events.js +18 -0
  155. package/dist/src/multiagent/events.js.map +1 -1
  156. package/dist/src/multiagent/graph.d.ts +59 -3
  157. package/dist/src/multiagent/graph.d.ts.map +1 -1
  158. package/dist/src/multiagent/graph.js +201 -34
  159. package/dist/src/multiagent/graph.js.map +1 -1
  160. package/dist/src/multiagent/multiagent.d.ts +77 -3
  161. package/dist/src/multiagent/multiagent.d.ts.map +1 -1
  162. package/dist/src/multiagent/multiagent.js +115 -1
  163. package/dist/src/multiagent/multiagent.js.map +1 -1
  164. package/dist/src/multiagent/nodes.d.ts +18 -0
  165. package/dist/src/multiagent/nodes.d.ts.map +1 -1
  166. package/dist/src/multiagent/nodes.js +69 -22
  167. package/dist/src/multiagent/nodes.js.map +1 -1
  168. package/dist/src/multiagent/state.d.ts +39 -3
  169. package/dist/src/multiagent/state.d.ts.map +1 -1
  170. package/dist/src/multiagent/state.js +80 -1
  171. package/dist/src/multiagent/state.js.map +1 -1
  172. package/dist/src/multiagent/swarm.d.ts +30 -1
  173. package/dist/src/multiagent/swarm.d.ts.map +1 -1
  174. package/dist/src/multiagent/swarm.js +166 -33
  175. package/dist/src/multiagent/swarm.js.map +1 -1
  176. package/dist/src/registry/__tests__/tool-registry.test.js +37 -0
  177. package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
  178. package/dist/src/registry/tool-registry.d.ts +13 -7
  179. package/dist/src/registry/tool-registry.d.ts.map +1 -1
  180. package/dist/src/registry/tool-registry.js +35 -10
  181. package/dist/src/registry/tool-registry.js.map +1 -1
  182. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
  183. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
  184. package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
  185. package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
  186. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
  187. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
  188. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
  189. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
  190. package/dist/src/retry/backoff-strategy.d.ts +108 -0
  191. package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
  192. package/dist/src/retry/backoff-strategy.js +86 -0
  193. package/dist/src/retry/backoff-strategy.js.map +1 -0
  194. package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
  195. package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
  196. package/dist/src/retry/default-model-retry-strategy.js +104 -0
  197. package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
  198. package/dist/src/retry/index.d.ts +8 -0
  199. package/dist/src/retry/index.d.ts.map +1 -0
  200. package/dist/src/retry/index.js +7 -0
  201. package/dist/src/retry/index.js.map +1 -0
  202. package/dist/src/retry/model-retry-strategy.d.ts +80 -0
  203. package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
  204. package/dist/src/retry/model-retry-strategy.js +85 -0
  205. package/dist/src/retry/model-retry-strategy.js.map +1 -0
  206. package/dist/src/retry/retry-strategy.d.ts +34 -0
  207. package/dist/src/retry/retry-strategy.d.ts.map +1 -0
  208. package/dist/src/retry/retry-strategy.js +25 -0
  209. package/dist/src/retry/retry-strategy.js.map +1 -0
  210. package/dist/src/session/__tests__/session-manager.test.js +84 -3
  211. package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
  212. package/dist/src/session/session-manager.d.ts +11 -2
  213. package/dist/src/session/session-manager.d.ts.map +1 -1
  214. package/dist/src/session/session-manager.js +17 -6
  215. package/dist/src/session/session-manager.js.map +1 -1
  216. package/dist/src/telemetry/__tests__/meter.test.js +5 -27
  217. package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
  218. package/dist/src/telemetry/meter.d.ts +12 -4
  219. package/dist/src/telemetry/meter.d.ts.map +1 -1
  220. package/dist/src/telemetry/meter.js +13 -8
  221. package/dist/src/telemetry/meter.js.map +1 -1
  222. package/dist/src/tools/__tests__/tool.test.js +24 -1
  223. package/dist/src/tools/__tests__/tool.test.js.map +1 -1
  224. package/dist/src/tools/function-tool.d.ts.map +1 -1
  225. package/dist/src/tools/function-tool.js +6 -1
  226. package/dist/src/tools/function-tool.js.map +1 -1
  227. package/dist/src/tools/mcp-tool.d.ts.map +1 -1
  228. package/dist/src/tools/mcp-tool.js +3 -2
  229. package/dist/src/tools/mcp-tool.js.map +1 -1
  230. package/dist/src/tools/tool.d.ts +10 -1
  231. package/dist/src/tools/tool.d.ts.map +1 -1
  232. package/dist/src/tools/tool.js +12 -0
  233. package/dist/src/tools/tool.js.map +1 -1
  234. package/dist/src/tsconfig.tsbuildinfo +1 -1
  235. package/dist/src/types/__tests__/agent.test.js +97 -0
  236. package/dist/src/types/__tests__/agent.test.js.map +1 -1
  237. package/dist/src/types/agent.d.ts +48 -8
  238. package/dist/src/types/agent.d.ts.map +1 -1
  239. package/dist/src/types/agent.js +28 -3
  240. package/dist/src/types/agent.js.map +1 -1
  241. package/dist/src/types/interrupt.d.ts +103 -0
  242. package/dist/src/types/interrupt.d.ts.map +1 -0
  243. package/dist/src/types/interrupt.js +63 -0
  244. package/dist/src/types/interrupt.js.map +1 -0
  245. package/dist/src/types/messages.d.ts +2 -1
  246. package/dist/src/types/messages.d.ts.map +1 -1
  247. package/dist/src/types/messages.js.map +1 -1
  248. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
  249. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
  250. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
  251. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
  252. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
  253. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
  254. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
  255. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
  256. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
  257. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
  258. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
  259. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
  260. package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
  261. package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
  262. package/dist/src/vended-plugins/context-offloader/index.js +21 -0
  263. package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
  264. package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
  265. package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
  266. package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
  267. package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
  268. package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
  269. package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
  270. package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
  271. package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
  272. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +12 -0
  273. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
  274. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +3 -0
  275. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
  276. package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
  277. package/dist/src/vended-tools/bash/bash.js +0 -3
  278. package/dist/src/vended-tools/bash/bash.js.map +1 -1
  279. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +3 -0
  280. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
  281. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +3 -0
  282. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
  283. package/dist/src/vended-tools/notebook/notebook.d.ts +1 -1
  284. package/package.json +9 -5
@@ -54,7 +54,7 @@ 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';
@@ -68,7 +68,7 @@ import { SlidingWindowConversationManager } from '../conversation-manager/slidin
68
68
  import { NullConversationManager } from '../conversation-manager/null-conversation-manager.js';
69
69
  import { ConversationManager } from '../conversation-manager/conversation-manager.js';
70
70
  import { HookRegistryImplementation } from '../hooks/registry.js';
71
- import { InitializedEvent, AfterInvocationEvent, AfterModelCallEvent, AfterToolCallEvent, AfterToolsEvent, BeforeInvocationEvent, BeforeModelCallEvent, BeforeToolCallEvent, BeforeToolsEvent, HookableEvent, MessageAddedEvent, ModelStreamUpdateEvent, ContentBlockEvent, ModelMessageEvent, ToolResultEvent, AgentResultEvent, ToolStreamUpdateEvent, } from '../hooks/events.js';
71
+ import { InitializedEvent, AfterInvocationEvent, AfterModelCallEvent, AfterToolCallEvent, AfterToolsEvent, BeforeInvocationEvent, BeforeModelCallEvent, BeforeToolCallEvent, BeforeToolsEvent, HookableEvent, MessageAddedEvent, ModelStreamUpdateEvent, ContentBlockEvent, ModelMessageEvent, ToolResultEvent, AgentResultEvent, ToolStreamUpdateEvent, InterruptEvent, } from '../hooks/events.js';
72
72
  import { StructuredOutputTool, STRUCTURED_OUTPUT_TOOL_NAME } from '../tools/structured-output-tool.js';
73
73
  import { AgentAsTool } from './agent-as-tool.js';
74
74
  import { SessionManager } from '../session/session-manager.js';
@@ -76,6 +76,11 @@ import { Tracer } from '../telemetry/tracer.js';
76
76
  import { Meter } from '../telemetry/meter.js';
77
77
  import { logger } from '../logging/logger.js';
78
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';
83
+ import { takeSnapshot as takeSnapshotInternal, loadSnapshot as loadSnapshotInternal } from './snapshot.js';
79
84
  /** Default name assigned to agents when none is provided. */
80
85
  const DEFAULT_AGENT_NAME = 'Strands Agent';
81
86
  /** Default identifier assigned to agents when none is provided. */
@@ -140,6 +145,8 @@ export class Agent {
140
145
  _tracer;
141
146
  /** Meter instance for accumulating loop metrics during invocation. */
142
147
  _meter;
148
+ /** Interrupt state for human-in-the-loop workflows. */
149
+ _interruptState;
143
150
  /** Strategy for executing tool calls from a single assistant turn. */
144
151
  _toolExecutor;
145
152
  /**
@@ -178,11 +185,26 @@ export class Agent {
178
185
  this._mcpClients = mcpClients;
179
186
  // Initialize hooks registry
180
187
  this._hooksRegistry = new HookRegistryImplementation();
181
- // Initialize plugin registry with all plugins to be initialized during initialize()
182
- // ModelPlugin is registered last so that on AfterInvocationEvent (which uses reverse
183
- // callback ordering), it runs first — clearing messages before SessionManager saves.
188
+ // `undefined` (omitted) install the default; `null`/`[]` explicit opt-out.
189
+ const retryStrategies = config?.retryStrategy === null
190
+ ? []
191
+ : config?.retryStrategy === undefined
192
+ ? [new DefaultModelRetryStrategy()]
193
+ : Array.isArray(config.retryStrategy)
194
+ ? config.retryStrategy
195
+ : [config.retryStrategy];
196
+ warnOnDuplicateRetryStrategyTypes(retryStrategies);
197
+ // Initialize plugin registry with all plugins to be initialized during initialize().
198
+ // Ordering notes:
199
+ // - ModelPlugin is registered last so that on AfterInvocationEvent (which uses
200
+ // reverse callback ordering), it runs first — clearing messages before
201
+ // SessionManager saves.
202
+ // - Retry-strategy ordering is not load-bearing for correctness: `DefaultModelRetryStrategy`
203
+ // guards on `event.retry`, so a user hook that already set it short-circuits
204
+ // the strategy regardless of registration order.
184
205
  this._pluginRegistry = new PluginRegistry([
185
206
  this._conversationManager,
207
+ ...retryStrategies,
186
208
  ...(config?.plugins ?? []),
187
209
  ...(config?.sessionManager ? [config.sessionManager] : []),
188
210
  new ModelPlugin(this.model),
@@ -201,6 +223,8 @@ export class Agent {
201
223
  this._tracer = new Tracer(config?.traceAttributes);
202
224
  // Initialize meter for local metrics accumulation
203
225
  this._meter = new Meter();
226
+ // Initialize interrupt state for human-in-the-loop workflows
227
+ this._interruptState = new InterruptState();
204
228
  this._toolExecutor = config?.toolExecutor ?? 'concurrent';
205
229
  this._initialized = false;
206
230
  }
@@ -209,6 +233,7 @@ export class Agent {
209
233
  *
210
234
  * @param eventType - The event class constructor to register the callback for
211
235
  * @param callback - The callback function to invoke when the event occurs
236
+ * @param options - Optional configuration including execution order
212
237
  * @returns Cleanup function that removes the callback when invoked
213
238
  *
214
239
  * @example
@@ -223,8 +248,8 @@ export class Agent {
223
248
  * cleanup()
224
249
  * ```
225
250
  */
226
- addHook(eventType, callback) {
227
- return this._hooksRegistry.addCallback(eventType, callback);
251
+ addHook(eventType, callback, options) {
252
+ return this._hooksRegistry.addCallback(eventType, callback, options);
228
253
  }
229
254
  async initialize() {
230
255
  if (this._initialized) {
@@ -234,6 +259,10 @@ export class Agent {
234
259
  await Promise.all(this._mcpClients.map(async (client) => {
235
260
  const tools = await client.listTools();
236
261
  this._toolRegistry.add(tools);
262
+ client.onToolsChanged = (oldTools, newTools) => {
263
+ oldTools.forEach((name) => this._toolRegistry.remove(name));
264
+ this._toolRegistry.addOrReplace(newTools);
265
+ };
237
266
  }));
238
267
  await this._pluginRegistry.initialize(this);
239
268
  await this._hooksRegistry.invokeCallbacks(new InitializedEvent({ agent: this }));
@@ -383,53 +412,86 @@ export class Agent {
383
412
  async *stream(args, options) {
384
413
  const env_1 = { stack: [], error: void 0, hasError: false };
385
414
  try {
386
- const _lock = __addDisposableResource(env_1, this.acquireLock()
387
- // Create AbortController for this invocation and compose with external signal
388
- , false);
389
- // Create AbortController for this invocation and compose with external signal
390
- this._abortController = new AbortController();
391
- this._abortSignal = options?.cancelSignal
392
- ? AbortSignal.any([this._abortController.signal, options.cancelSignal])
393
- : this._abortController.signal;
415
+ const _lock = __addDisposableResource(env_1, this.acquireLock(), false);
394
416
  await this.initialize();
395
- // Delegate to _stream and process events through printer and hooks
396
- const streamGenerator = this._stream(args, options);
397
- let caughtError;
398
- try {
399
- let result = await streamGenerator.next();
400
- while (!result.done) {
401
- yield await this._invokeCallbacks(result.value);
402
- result = await streamGenerator.next();
403
- }
404
- yield await this._invokeCallbacks(new AgentResultEvent({ agent: this, result: result.value, invocationState: result.value.invocationState }));
405
- return result.value;
406
- }
407
- catch (error) {
408
- caughtError = error;
409
- throw error;
410
- }
411
- finally {
412
- // Drain _stream() so cleanup hooks and printer still fire.
413
- // Yield only on error (consumer may still be iterating); on a consumer
414
- // break, yielding would suspend the generator and leak the lock.
415
- let result = await streamGenerator.return(undefined);
416
- while (!result.done) {
417
- try {
418
- if (caughtError) {
419
- yield await this._invokeCallbacks(result.value);
417
+ let currentArgs = args;
418
+ // Outer loop: re-enters _stream when a hook sets AfterInvocationEvent.resume.
419
+ // One invocation lock spans the whole resume chain.
420
+ while (true) {
421
+ // Fresh AbortController per invocation iteration, composed with any external signal.
422
+ this._abortController = new AbortController();
423
+ this._abortSignal = options?.cancelSignal
424
+ ? AbortSignal.any([this._abortController.signal, options.cancelSignal])
425
+ : this._abortController.signal;
426
+ const streamGenerator = this._stream(currentArgs, options);
427
+ let caughtError;
428
+ let lastAfterInvocation;
429
+ let iterationResult;
430
+ try {
431
+ iterationResult = await streamGenerator.next();
432
+ while (!iterationResult.done) {
433
+ try {
434
+ const processed = await this._invokeCallbacks(iterationResult.value);
435
+ if (processed instanceof AfterInvocationEvent) {
436
+ lastAfterInvocation = processed;
437
+ }
438
+ yield processed;
439
+ iterationResult = await streamGenerator.next();
420
440
  }
421
- else {
422
- await this._invokeCallbacks(result.value);
441
+ catch (error) {
442
+ // Throw interrupt errors back into _stream so executeTools can store the
443
+ // assistant message as pending execution state for resume.
444
+ if (error instanceof InterruptError) {
445
+ iterationResult = await streamGenerator.throw(error);
446
+ }
447
+ else {
448
+ throw error;
449
+ }
423
450
  }
424
451
  }
425
- catch (error) {
426
- logger.warn(`event_type=<${result.value.type}>, error=<${error}> | error invoking callbacks during cleanup`);
452
+ // Suppress AgentResultEvent for resumed iterations — only the final
453
+ // invocation in a resume chain reports an agent result.
454
+ if (lastAfterInvocation?.resume === undefined) {
455
+ yield await this._invokeCallbacks(new AgentResultEvent({
456
+ agent: this,
457
+ result: iterationResult.value,
458
+ invocationState: iterationResult.value.invocationState,
459
+ }));
427
460
  }
428
- result = await streamGenerator.next();
429
461
  }
430
- // Reset controller and signal for next invocation
431
- this._abortController = new AbortController();
432
- this._abortSignal = this._abortController.signal;
462
+ catch (error) {
463
+ caughtError = error;
464
+ throw error;
465
+ }
466
+ finally {
467
+ // Drain _stream() so cleanup hooks and printer still fire.
468
+ // Yield only on error (consumer may still be iterating); on a consumer
469
+ // break, yielding would suspend the generator and leak the lock.
470
+ let drainResult = await streamGenerator.return(undefined);
471
+ while (!drainResult.done) {
472
+ try {
473
+ if (caughtError) {
474
+ yield await this._invokeCallbacks(drainResult.value);
475
+ }
476
+ else {
477
+ await this._invokeCallbacks(drainResult.value);
478
+ }
479
+ }
480
+ catch (error) {
481
+ logger.warn(`event_type=<${drainResult.value.type}>, error=<${error}> | error invoking callbacks during cleanup`);
482
+ }
483
+ drainResult = await streamGenerator.next();
484
+ }
485
+ // Reset controller and signal for next iteration / invocation
486
+ this._abortController = new AbortController();
487
+ this._abortSignal = this._abortController.signal;
488
+ }
489
+ // Resume only on a clean invocation — errors propagate above.
490
+ if (lastAfterInvocation?.resume !== undefined) {
491
+ currentArgs = lastAfterInvocation.resume;
492
+ continue;
493
+ }
494
+ return iterationResult.value;
433
495
  }
434
496
  }
435
497
  catch (e_1) {
@@ -468,6 +530,67 @@ export class Agent {
468
530
  asTool(options) {
469
531
  return new AgentAsTool({ agent: this, ...options });
470
532
  }
533
+ /**
534
+ * Captures a point-in-time snapshot of the agent's current state.
535
+ *
536
+ * Use snapshots to checkpoint agent state for later restoration, enabling
537
+ * use cases like undo/redo, branching conversations, and session persistence.
538
+ *
539
+ * Fields are selected via a preset/include/exclude model:
540
+ * 1. Start with preset fields (e.g. `'session'` captures all fields)
541
+ * 2. Add any `include` fields
542
+ * 3. Remove any `exclude` fields
543
+ *
544
+ * @param options - Controls which fields to capture and optional app data to store
545
+ * @returns A {@link Snapshot} containing the captured agent state
546
+ * @throws Error if no fields would be included after applying options
547
+ *
548
+ * @example
549
+ * ```typescript
550
+ * // Capture all session-relevant state
551
+ * const snapshot = agent.takeSnapshot({ preset: 'session' })
552
+ *
553
+ * // Capture only messages and state
554
+ * const partial = agent.takeSnapshot({ include: ['messages', 'state'] })
555
+ *
556
+ * // Capture session state but exclude interrupts
557
+ * const noInterrupts = agent.takeSnapshot({ preset: 'session', exclude: ['interrupts'] })
558
+ *
559
+ * // Attach application-owned metadata
560
+ * const withMeta = agent.takeSnapshot({ preset: 'session', appData: { userId: 'u-123' } })
561
+ * ```
562
+ */
563
+ takeSnapshot(options) {
564
+ return takeSnapshotInternal(this, options);
565
+ }
566
+ /**
567
+ * Restores agent state from a previously captured snapshot.
568
+ *
569
+ * Only fields present in `snapshot.data` are restored; absent fields are left
570
+ * unchanged. This allows partial snapshots to update specific aspects of state
571
+ * without affecting others.
572
+ *
573
+ * @param snapshot - The snapshot to restore from
574
+ * @throws Error if `snapshot.schemaVersion` is incompatible or scope is wrong
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * // Save and restore a conversation checkpoint
579
+ * const checkpoint = agent.takeSnapshot({ preset: 'session' })
580
+ *
581
+ * // ... agent continues processing ...
582
+ *
583
+ * // Restore to the checkpoint
584
+ * agent.loadSnapshot(checkpoint)
585
+ *
586
+ * // Restore from a JSON-serialized snapshot (e.g. from storage)
587
+ * const stored = JSON.parse(savedSnapshotJson)
588
+ * agent.loadSnapshot(stored)
589
+ * ```
590
+ */
591
+ loadSnapshot(snapshot) {
592
+ loadSnapshotInternal(this, snapshot);
593
+ }
471
594
  /**
472
595
  * Invokes hook callbacks and printer for a stream event.
473
596
  *
@@ -501,6 +624,15 @@ export class Agent {
501
624
  // AgentResult. Mutations by hooks/tools are visible across all recursive
502
625
  // agent loop cycles within this invocation.
503
626
  const invocationState = options?.invocationState ?? {};
627
+ // Handle interrupt responses if present in input
628
+ const interruptResponses = this._extractInterruptResponses(args);
629
+ if (interruptResponses.length > 0) {
630
+ this._interruptState.resume(interruptResponses);
631
+ }
632
+ // Reject non-interrupt input while in interrupted state
633
+ if (this._interruptState.activated && interruptResponses.length === 0) {
634
+ throw new TypeError('Agent is in an interrupted state. Resume by invoking with interruptResponse content blocks.');
635
+ }
504
636
  const beforeInvocationEvent = new BeforeInvocationEvent({ agent: this, invocationState });
505
637
  yield beforeInvocationEvent;
506
638
  if (beforeInvocationEvent.cancel) {
@@ -557,72 +689,119 @@ export class Agent {
557
689
  }
558
690
  currentArgs = undefined;
559
691
  }
560
- const modelResult = yield* this._invokeModel(invocationState, structuredOutputChoice);
561
- if (modelResult.stopReason !== 'toolUse') {
562
- // If structured output is required, force it
563
- if (structuredOutputTool) {
564
- if (structuredOutputChoice) {
692
+ // Check if we're resuming from a tool interrupt
693
+ const pendingExecution = this._interruptState.getPendingExecution();
694
+ let assistantMessage;
695
+ let completedToolResults;
696
+ if (pendingExecution) {
697
+ // Resume from stored state - skip model call
698
+ assistantMessage = pendingExecution.assistantMessage;
699
+ completedToolResults = pendingExecution.completedToolResults;
700
+ this._interruptState.clearPendingToolExecution();
701
+ }
702
+ else {
703
+ const modelResult = yield* this._invokeModel(invocationState, structuredOutputChoice);
704
+ if (modelResult.stopReason !== 'toolUse') {
705
+ // Schema set, we already forced, and the model still refused.
706
+ // Throw before closing the span so the cycle span records the error.
707
+ if (structuredOutputTool && structuredOutputChoice) {
565
708
  throw new StructuredOutputError('The model failed to invoke the structured output tool even after it was forced.');
566
709
  }
567
- structuredOutputChoice = { tool: { name: STRUCTURED_OUTPUT_TOOL_NAME } };
710
+ this._meter.endCycle(cycleStartTime);
711
+ this._tracer.endAgentLoopSpan(cycleSpan);
712
+ // Schema set, model ignored the tool — drop the response and force the tool next cycle.
713
+ // Appending the plain-text turn here would leave the conversation ending on an
714
+ // assistant message, which providers like Bedrock reject as assistant prefill.
715
+ if (structuredOutputTool) {
716
+ structuredOutputChoice = { tool: { name: STRUCTURED_OUTPUT_TOOL_NAME } };
717
+ logger.debug('structured output schema set but model responded with plain text; forcing tool use on next cycle');
718
+ continue;
719
+ }
720
+ // Normal end of turn.
721
+ yield this._appendMessage(modelResult.message, invocationState);
722
+ result = new AgentResult({
723
+ stopReason: modelResult.stopReason,
724
+ lastMessage: modelResult.message,
725
+ traces: this._tracer.localTraces,
726
+ metrics: this._meter.metrics,
727
+ invocationState,
728
+ });
729
+ return result;
568
730
  }
569
- this._meter.endCycle(cycleStartTime);
570
- this._tracer.endAgentLoopSpan(cycleSpan);
571
- yield this._appendMessage(modelResult.message, invocationState);
572
- if (structuredOutputChoice) {
573
- continue;
731
+ // Cancel before tool execution: create error results for all pending tools
732
+ if (this.isCancelled) {
733
+ const toolUseBlocks = modelResult.message.content.filter((block) => block.type === 'toolUseBlock');
734
+ const cancelBlocks = toolUseBlocks.map((block) => new ToolResultBlock({
735
+ toolUseId: block.toolUseId,
736
+ status: 'error',
737
+ content: [new TextBlock('Tool execution cancelled')],
738
+ }));
739
+ const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
740
+ yield this._appendMessage(modelResult.message, invocationState);
741
+ yield this._appendMessage(toolResultMessage, invocationState);
742
+ this._meter.endCycle(cycleStartTime);
743
+ this._tracer.endAgentLoopSpan(cycleSpan);
744
+ result = new AgentResult({
745
+ stopReason: 'cancelled',
746
+ lastMessage: modelResult.message,
747
+ traces: this._tracer.localTraces,
748
+ metrics: this._meter.metrics,
749
+ invocationState,
750
+ });
751
+ return result;
574
752
  }
575
- result = new AgentResult({
576
- stopReason: modelResult.stopReason,
577
- lastMessage: modelResult.message,
578
- traces: this._tracer.localTraces,
579
- metrics: this._meter.metrics,
580
- invocationState,
581
- });
582
- return result;
753
+ assistantMessage = modelResult.message;
583
754
  }
584
- // Cancel before tool execution: create error results for all pending tools
585
- if (this.isCancelled) {
586
- const toolUseBlocks = modelResult.message.content.filter((block) => block.type === 'toolUseBlock');
587
- const cancelBlocks = toolUseBlocks.map((block) => new ToolResultBlock({
588
- toolUseId: block.toolUseId,
589
- status: 'error',
590
- content: [new TextBlock('Tool execution cancelled')],
591
- }));
592
- const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
593
- yield this._appendMessage(modelResult.message, invocationState);
594
- yield this._appendMessage(toolResultMessage, invocationState);
755
+ // Execute tools
756
+ const toolsResult = yield* this.executeTools(assistantMessage, this._toolRegistry, invocationState, completedToolResults);
757
+ // When the consumer breaks the stream (e.g. agent.cancel() + break),
758
+ // yield* returns undefined because the inner generator was closed.
759
+ if (!toolsResult) {
595
760
  this._meter.endCycle(cycleStartTime);
596
761
  this._tracer.endAgentLoopSpan(cycleSpan);
597
- result = new AgentResult({
598
- stopReason: 'cancelled',
599
- lastMessage: modelResult.message,
600
- traces: this._tracer.localTraces,
601
- metrics: this._meter.metrics,
602
- invocationState,
603
- });
604
- return result;
762
+ continue;
605
763
  }
606
- // Execute tools
607
- const toolResultMessage = yield* this.executeTools(modelResult.message, this._toolRegistry, invocationState);
764
+ const toolResultMessage = toolsResult.message;
608
765
  /**
609
766
  * Deferred append: both messages are added AFTER tool execution completes.
610
767
  * This keeps agent.messages in a valid, reinvokable state at all times.
611
768
  * If interrupted during tool execution, messages has no dangling toolUse
612
769
  * without a matching toolResult, so the agent can be reinvoked cleanly.
613
770
  */
614
- yield this._appendMessage(modelResult.message, invocationState);
771
+ yield this._appendMessage(assistantMessage, invocationState);
615
772
  yield this._appendMessage(toolResultMessage, invocationState);
773
+ // Deactivate interrupt state after successful tool execution so the next
774
+ // cycle starts with a clean slate (new interrupts can be raised again).
775
+ if (this._interruptState.activated) {
776
+ this._interruptState.deactivate();
777
+ }
616
778
  this._meter.endCycle(cycleStartTime);
617
779
  this._tracer.endAgentLoopSpan(cycleSpan);
780
+ // Hook requested halt: exit without calling the model again
781
+ const { afterToolsEvent } = toolsResult;
782
+ if (afterToolsEvent.endTurn) {
783
+ const endTurnText = typeof afterToolsEvent.endTurn === 'string'
784
+ ? afterToolsEvent.endTurn
785
+ : 'Turn ended early by hook after tool execution';
786
+ const lastMessage = new Message({ role: 'assistant', content: [new TextBlock(endTurnText)] });
787
+ yield this._appendMessage(lastMessage, invocationState);
788
+ result = new AgentResult({
789
+ stopReason: 'endTurn',
790
+ lastMessage,
791
+ traces: this._tracer.localTraces,
792
+ metrics: this._meter.metrics,
793
+ invocationState,
794
+ });
795
+ return result;
796
+ }
618
797
  // Structured output captured: exit
619
798
  const structuredOutput = structuredOutputTool
620
- ? this._extractStructuredOutput(modelResult.message, toolResultMessage)
799
+ ? this._extractStructuredOutput(assistantMessage, toolResultMessage)
621
800
  : undefined;
622
801
  if (structuredOutput !== undefined) {
623
802
  result = new AgentResult({
624
- stopReason: modelResult.stopReason,
625
- lastMessage: modelResult.message,
803
+ stopReason: 'toolUse',
804
+ lastMessage: assistantMessage,
626
805
  traces: this._tracer.localTraces,
627
806
  structuredOutput,
628
807
  metrics: this._meter.metrics,
@@ -656,6 +835,16 @@ export class Agent {
656
835
  });
657
836
  return result;
658
837
  }
838
+ if (error instanceof InterruptError) {
839
+ // Fan out one event per interrupt. Each event exposes `interrupt.source` so
840
+ // consumers can filter by origin (tool callback vs hook callback) without
841
+ // subscribing to separate event types.
842
+ for (const interrupt of error.interrupts) {
843
+ yield new InterruptEvent({ agent: this, interrupt, invocationState });
844
+ }
845
+ result = this._createInterruptResult(invocationState);
846
+ return result;
847
+ }
659
848
  caughtError = error;
660
849
  throw error;
661
850
  }
@@ -701,6 +890,51 @@ export class Agent {
701
890
  const firstContent = toolResult.content[0];
702
891
  return firstContent?.type === 'jsonBlock' ? firstContent.json : undefined;
703
892
  }
893
+ /**
894
+ * Creates an AgentResult for an interrupt stop.
895
+ *
896
+ * @param invocationState - The current invocation state
897
+ * @returns AgentResult with stopReason 'interrupt'
898
+ */
899
+ _createInterruptResult(invocationState) {
900
+ this._interruptState.activate();
901
+ return new AgentResult({
902
+ stopReason: 'interrupt',
903
+ lastMessage: this.messages.length > 0
904
+ ? this.messages[this.messages.length - 1]
905
+ : new Message({ role: 'assistant', content: [new TextBlock('Interrupted')] }),
906
+ traces: this._tracer.localTraces,
907
+ metrics: this._meter.metrics,
908
+ interrupts: this._interruptState.getUnansweredInterrupts(),
909
+ invocationState,
910
+ });
911
+ }
912
+ /**
913
+ * Extracts interrupt response content blocks from invocation args.
914
+ *
915
+ * @param args - The invocation arguments
916
+ * @returns Array of InterruptResponseContent blocks, empty if none found
917
+ * @throws TypeError if args mix interrupt responses with other content
918
+ */
919
+ _extractInterruptResponses(args) {
920
+ if (!Array.isArray(args) || args.length === 0) {
921
+ return [];
922
+ }
923
+ const responses = [];
924
+ let hasNonInterrupt = false;
925
+ for (const item of args) {
926
+ if (isInterruptResponseContent(item)) {
927
+ responses.push(item);
928
+ }
929
+ else {
930
+ hasNonInterrupt = true;
931
+ }
932
+ }
933
+ if (responses.length > 0 && hasNonInterrupt) {
934
+ throw new TypeError('Must resume from interrupt with a list of interruptResponse content blocks only');
935
+ }
936
+ return responses;
937
+ }
704
938
  /**
705
939
  * Normalizes agent invocation input into an array of messages to append.
706
940
  *
@@ -720,6 +954,11 @@ export class Agent {
720
954
  }
721
955
  else if (Array.isArray(args) && args.length > 0) {
722
956
  const firstElement = args[0];
957
+ // Check if it's interrupt responses - skip creating messages for these
958
+ if (isInterruptResponseContent(firstElement)) {
959
+ // Pure interrupt responses: no messages to add
960
+ return [];
961
+ }
723
962
  // Check if it's Message[] or MessageData[]
724
963
  if ('role' in firstElement && typeof firstElement.role === 'string') {
725
964
  // Check if it's a Message instance or MessageData
@@ -773,108 +1012,117 @@ export class Agent {
773
1012
  if (toolChoice) {
774
1013
  streamOptions.toolChoice = toolChoice;
775
1014
  }
776
- // Estimate input tokens for the upcoming model call (non-fatal if estimation fails)
777
- let projectedInputTokens;
778
- try {
779
- projectedInputTokens = await this._estimateInputTokens(streamOptions);
780
- }
781
- catch (e) {
782
- logger.debug(`error=<${e}> | token estimation failed, proceeding without estimate`);
783
- }
784
- const beforeModelCallEvent = new BeforeModelCallEvent({
785
- agent: this,
786
- model: this.model,
787
- invocationState,
788
- ...(projectedInputTokens !== undefined && { projectedInputTokens }),
789
- });
790
- yield beforeModelCallEvent;
791
- if (beforeModelCallEvent.cancel) {
792
- const cancelText = typeof beforeModelCallEvent.cancel === 'string' ? beforeModelCallEvent.cancel : 'model call denied by hook';
793
- const message = new Message({ role: 'assistant', content: [new TextBlock(cancelText)] });
794
- const stopData = { message, stopReason: 'endTurn' };
795
- const afterModelCallEvent = new AfterModelCallEvent({
796
- agent: this,
797
- model: this.model,
798
- stopData,
799
- invocationState,
800
- });
801
- yield afterModelCallEvent;
802
- if (afterModelCallEvent.retry) {
803
- return yield* this._invokeModel(invocationState, toolChoice);
1015
+ let attemptCount = 1;
1016
+ while (true) {
1017
+ // Estimate input tokens for the upcoming model call (non-fatal if estimation fails)
1018
+ let projectedInputTokens;
1019
+ try {
1020
+ projectedInputTokens = await this._estimateInputTokens(streamOptions);
804
1021
  }
805
- return { message, stopReason: 'endTurn' };
806
- }
807
- // Start model span within loop span context
808
- const modelId = this.model.modelId;
809
- const modelSpan = this._tracer.startModelInvokeSpan({
810
- messages: this.messages,
811
- ...(modelId && { modelId }),
812
- ...(this.systemPrompt !== undefined && { systemPrompt: this.systemPrompt }),
813
- });
814
- try {
815
- const result = yield* this._streamFromModel(this.messages, streamOptions, invocationState);
816
- // Accumulate token usage and model latency metrics
817
- this._meter.updateCycle(result.metadata);
818
- // End model span with usage
819
- const usage = result.metadata?.usage;
820
- const metrics = result.metadata?.metrics;
821
- this._tracer.endModelInvokeSpan(modelSpan, {
822
- output: result.message,
823
- stopReason: result.stopReason,
824
- ...(usage && { usage }),
825
- ...(metrics && { metrics }),
826
- });
827
- yield new ModelMessageEvent({
828
- agent: this,
829
- message: result.message,
830
- stopReason: result.stopReason,
831
- invocationState,
832
- });
833
- // Handle user content redaction if guardrails blocked input
834
- if (result.redaction?.userMessage) {
835
- this._redactLastMessage(result.redaction.userMessage);
1022
+ catch (e) {
1023
+ logger.debug(`error=<${e}> | token estimation failed, proceeding without estimate`);
836
1024
  }
837
- const stopData = {
838
- message: result.message,
839
- stopReason: result.stopReason,
840
- ...(result.redaction && { redaction: result.redaction }),
841
- };
842
- const afterModelCallEvent = new AfterModelCallEvent({
1025
+ const beforeModelCallEvent = new BeforeModelCallEvent({
843
1026
  agent: this,
844
1027
  model: this.model,
845
- stopData,
846
1028
  invocationState,
1029
+ ...(projectedInputTokens !== undefined && { projectedInputTokens }),
847
1030
  });
848
- yield afterModelCallEvent;
849
- if (afterModelCallEvent.retry) {
850
- return yield* this._invokeModel(invocationState, toolChoice);
1031
+ yield beforeModelCallEvent;
1032
+ if (beforeModelCallEvent.cancel) {
1033
+ const cancelText = typeof beforeModelCallEvent.cancel === 'string' ? beforeModelCallEvent.cancel : 'model call denied by hook';
1034
+ const message = new Message({ role: 'assistant', content: [new TextBlock(cancelText)] });
1035
+ const stopData = { message, stopReason: 'endTurn' };
1036
+ const afterModelCallEvent = new AfterModelCallEvent({
1037
+ agent: this,
1038
+ model: this.model,
1039
+ attemptCount,
1040
+ stopData,
1041
+ invocationState,
1042
+ });
1043
+ yield afterModelCallEvent;
1044
+ if (afterModelCallEvent.retry) {
1045
+ attemptCount += 1;
1046
+ continue;
1047
+ }
1048
+ return { message, stopReason: 'endTurn' };
851
1049
  }
852
- return result;
853
- }
854
- catch (error) {
855
- const modelError = normalizeError(error);
856
- // End model span with error
857
- this._tracer.endModelInvokeSpan(modelSpan, { error: modelError });
858
- // Create error event
859
- const errorEvent = new AfterModelCallEvent({
860
- agent: this,
861
- model: this.model,
862
- error: modelError,
863
- invocationState,
1050
+ // Start model span within loop span context
1051
+ const modelId = this.model.modelId;
1052
+ const modelSpan = this._tracer.startModelInvokeSpan({
1053
+ messages: this.messages,
1054
+ ...(modelId && { modelId }),
1055
+ ...(this.systemPrompt !== undefined && { systemPrompt: this.systemPrompt }),
864
1056
  });
865
- // Yield error event - stream will invoke hooks
866
- yield errorEvent;
867
- // Let CancelledError propagate directly no retry
868
- // (we emit the AfterModelCall because we already emitted Before and we guarentee the pair)
869
- if (error instanceof CancelledError) {
870
- throw error;
1057
+ try {
1058
+ const result = yield* this._streamFromModel(this.messages, streamOptions, invocationState);
1059
+ // Accumulate token usage and model latency metrics
1060
+ this._meter.updateCycle(result.metadata);
1061
+ // End model span with usage
1062
+ const usage = result.metadata?.usage;
1063
+ const metrics = result.metadata?.metrics;
1064
+ this._tracer.endModelInvokeSpan(modelSpan, {
1065
+ output: result.message,
1066
+ stopReason: result.stopReason,
1067
+ ...(usage && { usage }),
1068
+ ...(metrics && { metrics }),
1069
+ });
1070
+ yield new ModelMessageEvent({
1071
+ agent: this,
1072
+ message: result.message,
1073
+ stopReason: result.stopReason,
1074
+ invocationState,
1075
+ });
1076
+ // Handle user content redaction if guardrails blocked input
1077
+ if (result.redaction?.userMessage) {
1078
+ this._redactLastMessage(result.redaction.userMessage);
1079
+ }
1080
+ const stopData = {
1081
+ message: result.message,
1082
+ stopReason: result.stopReason,
1083
+ ...(result.redaction && { redaction: result.redaction }),
1084
+ };
1085
+ const afterModelCallEvent = new AfterModelCallEvent({
1086
+ agent: this,
1087
+ model: this.model,
1088
+ attemptCount,
1089
+ stopData,
1090
+ invocationState,
1091
+ });
1092
+ yield afterModelCallEvent;
1093
+ if (afterModelCallEvent.retry) {
1094
+ attemptCount += 1;
1095
+ continue;
1096
+ }
1097
+ return result;
871
1098
  }
872
- // After yielding, hooks have been invoked and may have set retry
873
- if (errorEvent.retry) {
874
- return yield* this._invokeModel(invocationState, toolChoice);
1099
+ catch (error) {
1100
+ const modelError = normalizeError(error);
1101
+ // End model span with error
1102
+ this._tracer.endModelInvokeSpan(modelSpan, { error: modelError });
1103
+ // Create error event
1104
+ const errorEvent = new AfterModelCallEvent({
1105
+ agent: this,
1106
+ model: this.model,
1107
+ attemptCount,
1108
+ error: modelError,
1109
+ invocationState,
1110
+ });
1111
+ // Yield error event - stream will invoke hooks
1112
+ yield errorEvent;
1113
+ // Let CancelledError propagate directly — no retry
1114
+ // (we emit the AfterModelCall because we already emitted Before and we guarentee the pair)
1115
+ if (error instanceof CancelledError) {
1116
+ throw error;
1117
+ }
1118
+ // After yielding, hooks have been invoked and may have set retry
1119
+ if (errorEvent.retry) {
1120
+ attemptCount += 1;
1121
+ continue;
1122
+ }
1123
+ // Re-throw error
1124
+ throw error;
875
1125
  }
876
- // Re-throw error
877
- throw error;
878
1126
  }
879
1127
  }
880
1128
  /**
@@ -894,6 +1142,7 @@ export class Agent {
894
1142
  * @returns StreamAggregatedResult containing message, stop reason, and optional redaction message
895
1143
  */
896
1144
  async *_streamFromModel(messages, streamOptions, invocationState) {
1145
+ messages = normalizeToolUseNames(messages);
897
1146
  const streamGenerator = this.model.streamAggregated(messages, streamOptions);
898
1147
  let result = await streamGenerator.next();
899
1148
  while (!result.done) {
@@ -920,11 +1169,24 @@ export class Agent {
920
1169
  *
921
1170
  * @param assistantMessage - The assistant message containing tool use blocks
922
1171
  * @param toolRegistry - Registry containing available tools
923
- * @returns User message containing tool results
1172
+ * @returns Tool-result message and the dispatched AfterToolsEvent
924
1173
  */
925
- async *executeTools(assistantMessage, toolRegistry, invocationState) {
1174
+ async *executeTools(assistantMessage, toolRegistry, invocationState, completedToolResults) {
926
1175
  const beforeToolsEvent = new BeforeToolsEvent({ agent: this, message: assistantMessage, invocationState });
927
- yield beforeToolsEvent;
1176
+ try {
1177
+ yield beforeToolsEvent;
1178
+ }
1179
+ catch (error) {
1180
+ // Store pending state before re-throwing so the agent can resume from this point.
1181
+ // The error must still propagate to _stream which handles the interrupt stop.
1182
+ if (error instanceof InterruptError) {
1183
+ this._interruptState.setPendingToolExecution({
1184
+ assistantMessageData: assistantMessage.toJSON(),
1185
+ completedToolResults: {},
1186
+ });
1187
+ }
1188
+ throw error;
1189
+ }
928
1190
  const toolUseBlocks = assistantMessage.content.filter((block) => block.type === 'toolUseBlock');
929
1191
  if (toolUseBlocks.length === 0) {
930
1192
  // Preserve BeforeToolsEvent/AfterToolsEvent bracket symmetry even on
@@ -946,9 +1208,9 @@ export class Agent {
946
1208
  }
947
1209
  switch (this._toolExecutor) {
948
1210
  case 'sequential':
949
- return yield* this._executeToolsSequential(toolUseBlocks, toolRegistry, invocationState);
1211
+ return yield* this._executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
950
1212
  case 'concurrent':
951
- return yield* this._executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState);
1213
+ return yield* this._executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
952
1214
  default: {
953
1215
  const _exhaustive = this._toolExecutor;
954
1216
  throw new Error(`Unknown toolExecutor: ${_exhaustive}`);
@@ -957,7 +1219,7 @@ export class Agent {
957
1219
  }
958
1220
  /**
959
1221
  * Emits a `ToolResultEvent` for every block plus an `AfterToolsEvent`, and
960
- * returns the resulting tool-result message. Used by the pre-launch cancel
1222
+ * returns the resulting tool-result message and dispatched event. Used by the pre-launch cancel
961
1223
  * paths shared across executors.
962
1224
  */
963
1225
  async *_yieldCancelledToolResults(toolUseBlocks, message, invocationState) {
@@ -966,18 +1228,28 @@ export class Agent {
966
1228
  yield new ToolResultEvent({ agent: this, result, invocationState });
967
1229
  }
968
1230
  const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
969
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
970
- return toolResultMessage;
1231
+ const afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1232
+ yield afterToolsEvent;
1233
+ return { message: toolResultMessage, afterToolsEvent };
971
1234
  }
972
1235
  /**
973
1236
  * Executes tools one at a time, honoring `agent.cancelSignal` between
974
1237
  * iterations to short-circuit not-yet-started tools.
975
1238
  */
976
- async *_executeToolsSequential(toolUseBlocks, toolRegistry, invocationState) {
1239
+ async *_executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
977
1240
  const toolResultBlocks = [];
978
1241
  let toolResultMessage;
1242
+ let afterToolsEvent;
979
1243
  try {
980
1244
  for (const toolUseBlock of toolUseBlocks) {
1245
+ // Skip tools that were already completed before the interrupt
1246
+ if (completedToolResults?.has(toolUseBlock.toolUseId)) {
1247
+ const completedResult = completedToolResults.get(toolUseBlock.toolUseId);
1248
+ // No events emitted for already-completed tools.
1249
+ // The result is included in the final tool result message.
1250
+ toolResultBlocks.push(completedResult);
1251
+ continue;
1252
+ }
981
1253
  if (this.isCancelled) {
982
1254
  const cancelBlock = new ToolResultBlock({
983
1255
  toolUseId: toolUseBlock.toolUseId,
@@ -988,16 +1260,40 @@ export class Agent {
988
1260
  yield new ToolResultEvent({ agent: this, result: cancelBlock, invocationState });
989
1261
  continue;
990
1262
  }
991
- const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry, invocationState);
992
- toolResultBlocks.push(toolResultBlock);
993
- yield new ToolResultEvent({ agent: this, result: toolResultBlock, invocationState });
1263
+ try {
1264
+ const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry, invocationState);
1265
+ toolResultBlocks.push(toolResultBlock);
1266
+ yield new ToolResultEvent({ agent: this, result: toolResultBlock, invocationState });
1267
+ }
1268
+ catch (error) {
1269
+ if (error instanceof InterruptError) {
1270
+ // Store pending state with completed results so far
1271
+ const completedSoFar = {};
1272
+ for (const block of toolResultBlocks) {
1273
+ completedSoFar[block.toolUseId] = block.toJSON();
1274
+ }
1275
+ // Also include any previously completed results
1276
+ if (completedToolResults) {
1277
+ for (const [id, block] of completedToolResults) {
1278
+ completedSoFar[id] = block.toJSON();
1279
+ }
1280
+ }
1281
+ this._interruptState.setPendingToolExecution({
1282
+ assistantMessageData: assistantMessage.toJSON(),
1283
+ completedToolResults: completedSoFar,
1284
+ });
1285
+ throw error;
1286
+ }
1287
+ throw error;
1288
+ }
994
1289
  }
995
1290
  }
996
1291
  finally {
997
1292
  toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
998
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1293
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1294
+ yield afterToolsEvent;
999
1295
  }
1000
- return toolResultMessage;
1296
+ return { message: toolResultMessage, afterToolsEvent };
1001
1297
  }
1002
1298
  /**
1003
1299
  * Produces one error ToolResultBlock per tool use block, each carrying
@@ -1020,15 +1316,32 @@ export class Agent {
1020
1316
  * `executeTool`'s own `while(true)` loop, so one tool retrying does not
1021
1317
  * disturb its siblings.
1022
1318
  */
1023
- async *_executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState) {
1319
+ async *_executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
1024
1320
  let toolResultMessage;
1321
+ let afterToolsEvent;
1025
1322
  const gens = toolUseBlocks.map((block) => ({
1026
1323
  block,
1027
- gen: this.executeTool(block, toolRegistry, invocationState),
1324
+ gen: completedToolResults?.has(block.toolUseId)
1325
+ ? undefined // Skip already-completed tools
1326
+ : this.executeTool(block, toolRegistry, invocationState),
1028
1327
  }));
1029
1328
  const step = (idx) => gens[idx].gen.next().then((res) => ({ idx, kind: 'next', res }), (error) => ({ idx, kind: 'throw', error }));
1030
- const pendingNext = new Map(gens.map((_, idx) => [idx, step(idx)]));
1329
+ // Seed completed results from resume state
1031
1330
  const resultsByToolUseId = new Map();
1331
+ if (completedToolResults) {
1332
+ for (const [id, result] of completedToolResults) {
1333
+ resultsByToolUseId.set(id, result);
1334
+ }
1335
+ }
1336
+ // Only race tools that need execution
1337
+ const pendingNext = new Map();
1338
+ for (let idx = 0; idx < gens.length; idx++) {
1339
+ if (gens[idx].gen) {
1340
+ pendingNext.set(idx, step(idx));
1341
+ }
1342
+ }
1343
+ // Track interrupts — let all other tools finish before propagating
1344
+ let interruptError;
1032
1345
  try {
1033
1346
  while (pendingNext.size > 0) {
1034
1347
  const winner = await Promise.race(pendingNext.values());
@@ -1036,6 +1349,11 @@ export class Agent {
1036
1349
  const block = gens[idx].block;
1037
1350
  if (winner.kind === 'throw') {
1038
1351
  pendingNext.delete(idx);
1352
+ // Detect InterruptError — don't convert to error result, track it
1353
+ if (winner.error instanceof InterruptError) {
1354
+ interruptError = winner.error;
1355
+ continue;
1356
+ }
1039
1357
  const err = normalizeError(winner.error);
1040
1358
  const result = new ToolResultBlock({
1041
1359
  toolUseId: block.toolUseId,
@@ -1053,10 +1371,33 @@ export class Agent {
1053
1371
  yield new ToolResultEvent({ agent: this, result: winner.res.value, invocationState });
1054
1372
  }
1055
1373
  else {
1056
- yield winner.res.value;
1374
+ try {
1375
+ yield winner.res.value;
1376
+ }
1377
+ catch (e) {
1378
+ // InterruptError thrown back into generator from stream() error injection
1379
+ if (e instanceof InterruptError) {
1380
+ interruptError = e;
1381
+ pendingNext.delete(idx);
1382
+ continue;
1383
+ }
1384
+ throw e;
1385
+ }
1057
1386
  pendingNext.set(idx, step(idx));
1058
1387
  }
1059
1388
  }
1389
+ // After all tools finish, propagate interrupt if one was raised
1390
+ if (interruptError) {
1391
+ const completedSoFar = {};
1392
+ for (const [id, result] of resultsByToolUseId) {
1393
+ completedSoFar[id] = result.toJSON();
1394
+ }
1395
+ this._interruptState.setPendingToolExecution({
1396
+ assistantMessageData: assistantMessage.toJSON(),
1397
+ completedToolResults: completedSoFar,
1398
+ });
1399
+ throw interruptError;
1400
+ }
1060
1401
  }
1061
1402
  finally {
1062
1403
  // Close any generators still in-flight (e.g. consumer broke out of stream).
@@ -1079,9 +1420,10 @@ export class Agent {
1079
1420
  }
1080
1421
  }
1081
1422
  toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
1082
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1423
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1424
+ yield afterToolsEvent;
1083
1425
  }
1084
- return toolResultMessage;
1426
+ return { message: toolResultMessage, afterToolsEvent };
1085
1427
  }
1086
1428
  /**
1087
1429
  * Executes a single tool and returns the result.
@@ -1094,8 +1436,9 @@ export class Agent {
1094
1436
  * @returns Tool result block
1095
1437
  */
1096
1438
  async *executeTool(toolUseBlock, toolRegistry, invocationState) {
1097
- const tool = toolRegistry.get(toolUseBlock.name);
1098
- // Create toolUse object for hook events and telemetry
1439
+ const registryTool = toolRegistry.get(toolUseBlock.name);
1440
+ // Create toolUse object for hook events and telemetry. Callbacks may mutate
1441
+ // this object's fields (input/name/toolUseId) inside BeforeToolCallEvent.
1099
1442
  const toolUse = {
1100
1443
  name: toolUseBlock.name,
1101
1444
  toolUseId: toolUseBlock.toolUseId,
@@ -1103,21 +1446,33 @@ export class Agent {
1103
1446
  };
1104
1447
  // Retry loop for tool execution
1105
1448
  while (true) {
1106
- const beforeToolCallEvent = new BeforeToolCallEvent({ agent: this, toolUse, tool, invocationState });
1449
+ const beforeToolCallEvent = new BeforeToolCallEvent({
1450
+ agent: this,
1451
+ toolUse,
1452
+ tool: registryTool,
1453
+ invocationState,
1454
+ });
1107
1455
  yield beforeToolCallEvent;
1456
+ // Resolve the tool that would actually execute. selectedTool wins;
1457
+ // otherwise if the hook renamed toolUse.name, re-resolve from the
1458
+ // registry under the new name; otherwise use the original registry
1459
+ // lookup. Resolved before the cancel check so AfterToolCallEvent.tool
1460
+ // is consistent whether the cancel or execution branch runs.
1461
+ const effectiveTool = beforeToolCallEvent.selectedTool ??
1462
+ (toolUse.name !== toolUseBlock.name ? toolRegistry.get(toolUse.name) : registryTool);
1108
1463
  // Cancel individual tool if hook requested it
1109
1464
  if (beforeToolCallEvent.cancel) {
1110
1465
  const cancelMessage = typeof beforeToolCallEvent.cancel === 'string' ? beforeToolCallEvent.cancel : 'Tool cancelled by hook';
1111
- const toolResult = new ToolResultBlock({
1112
- toolUseId: toolUseBlock.toolUseId,
1466
+ const cancelResult = new ToolResultBlock({
1467
+ toolUseId: toolUse.toolUseId,
1113
1468
  status: 'error',
1114
1469
  content: [new TextBlock(cancelMessage)],
1115
1470
  });
1116
1471
  const afterToolCallEvent = new AfterToolCallEvent({
1117
1472
  agent: this,
1118
1473
  toolUse,
1119
- tool,
1120
- result: toolResult,
1474
+ tool: effectiveTool,
1475
+ result: cancelResult,
1121
1476
  invocationState,
1122
1477
  });
1123
1478
  yield afterToolCallEvent;
@@ -1134,24 +1489,27 @@ export class Agent {
1134
1489
  const toolStartTime = Date.now();
1135
1490
  let toolResult;
1136
1491
  let error;
1137
- if (!tool) {
1492
+ if (!effectiveTool) {
1138
1493
  // Tool not found
1139
1494
  toolResult = new ToolResultBlock({
1140
- toolUseId: toolUseBlock.toolUseId,
1495
+ toolUseId: toolUse.toolUseId,
1141
1496
  status: 'error',
1142
- content: [new TextBlock(`Tool '${toolUseBlock.name}' not found in registry`)],
1497
+ content: [new TextBlock(`Tool '${toolUse.name}' not found in registry`)],
1143
1498
  });
1144
1499
  }
1145
1500
  else {
1146
1501
  // Execute tool within the tool span context
1147
1502
  const toolContext = {
1148
1503
  toolUse: {
1149
- name: toolUseBlock.name,
1150
- toolUseId: toolUseBlock.toolUseId,
1151
- input: toolUseBlock.input,
1504
+ name: toolUse.name,
1505
+ toolUseId: toolUse.toolUseId,
1506
+ input: toolUse.input,
1152
1507
  },
1153
1508
  agent: this,
1154
1509
  invocationState,
1510
+ interrupt: (params) => {
1511
+ return interruptFromAgent(this, `tool:${toolUseBlock.toolUseId}:${params.name}`, params, 'tool');
1512
+ },
1155
1513
  };
1156
1514
  try {
1157
1515
  // Manually iterate tool stream to wrap each ToolStreamEvent in ToolStreamUpdateEvent.
@@ -1159,7 +1517,7 @@ export class Agent {
1159
1517
  // without knowledge of agents or hooks, and we wrap at the boundary.
1160
1518
  // Tool execution is ran within the tool span's context so that
1161
1519
  // downstream calls (e.g., MCP clients) can propagate trace context
1162
- const toolGenerator = this._tracer.withSpanContext(toolSpan, () => tool.stream(toolContext));
1520
+ const toolGenerator = this._tracer.withSpanContext(toolSpan, () => effectiveTool.stream(toolContext));
1163
1521
  let toolNext = await this._tracer.withSpanContext(toolSpan, () => toolGenerator.next());
1164
1522
  while (!toolNext.done) {
1165
1523
  yield new ToolStreamUpdateEvent({ agent: this, event: toolNext.value, invocationState });
@@ -1169,9 +1527,9 @@ export class Agent {
1169
1527
  if (!result) {
1170
1528
  // Tool didn't return a result
1171
1529
  toolResult = new ToolResultBlock({
1172
- toolUseId: toolUseBlock.toolUseId,
1530
+ toolUseId: toolUse.toolUseId,
1173
1531
  status: 'error',
1174
- content: [new TextBlock(`Tool '${toolUseBlock.name}' did not return a result`)],
1532
+ content: [new TextBlock(`Tool '${toolUse.name}' did not return a result`)],
1175
1533
  });
1176
1534
  }
1177
1535
  else {
@@ -1180,17 +1538,22 @@ export class Agent {
1180
1538
  }
1181
1539
  }
1182
1540
  catch (e) {
1541
+ // Re-throw InterruptError to allow interrupt handling
1542
+ if (e instanceof InterruptError) {
1543
+ throw e;
1544
+ }
1183
1545
  // Tool execution failed with error
1184
1546
  error = normalizeError(e);
1185
1547
  toolResult = new ToolResultBlock({
1186
- toolUseId: toolUseBlock.toolUseId,
1548
+ toolUseId: toolUse.toolUseId,
1187
1549
  status: 'error',
1188
1550
  content: [new TextBlock(error.message)],
1189
1551
  error,
1190
1552
  });
1191
1553
  }
1192
1554
  }
1193
- // End tool span
1555
+ // End tool span with the raw tool result — telemetry reflects what the
1556
+ // tool actually returned, independent of AfterToolCallEvent mutations.
1194
1557
  this._tracer.endToolCallSpan(toolSpan, { toolResult, ...(error && { error }) });
1195
1558
  // End tool metrics tracking
1196
1559
  this._meter.endToolCall({
@@ -1202,7 +1565,7 @@ export class Agent {
1202
1565
  const afterToolCallEvent = new AfterToolCallEvent({
1203
1566
  agent: this,
1204
1567
  toolUse,
1205
- tool,
1568
+ tool: effectiveTool,
1206
1569
  result: toolResult,
1207
1570
  invocationState,
1208
1571
  ...(error !== undefined && { error }),
@@ -1211,6 +1574,8 @@ export class Agent {
1211
1574
  if (afterToolCallEvent.retry) {
1212
1575
  continue;
1213
1576
  }
1577
+ // Return the (possibly mutated) result so hook transformations propagate
1578
+ // to ToolResultEvent and the conversation message the model will see.
1214
1579
  return afterToolCallEvent.result;
1215
1580
  }
1216
1581
  }
@@ -1311,6 +1676,43 @@ export class Agent {
1311
1676
  return new MessageAddedEvent({ agent: this, message, invocationState });
1312
1677
  }
1313
1678
  }
1679
+ const INVALID_TOOL_NAME_PLACEHOLDER = 'INVALID_TOOL_NAME';
1680
+ /**
1681
+ * Replaces invalid tool-use names on assistant messages with `INVALID_TOOL_NAME`
1682
+ * so providers that reject malformed names don't fail the whole request.
1683
+ * Returns the input unchanged (same reference) when nothing needs replacing.
1684
+ */
1685
+ function normalizeToolUseNames(messages) {
1686
+ let replaced = false;
1687
+ const next = messages.map((message) => {
1688
+ if (!message || message.role !== 'assistant')
1689
+ return message;
1690
+ let messageReplaced = false;
1691
+ const content = message.content.map((block) => {
1692
+ if (block.type !== 'toolUseBlock')
1693
+ return block;
1694
+ if (isValidToolName(block.name))
1695
+ return block;
1696
+ messageReplaced = true;
1697
+ logger.debug(`tool_name=<${block.name}> | replacing invalid tool name with ${INVALID_TOOL_NAME_PLACEHOLDER}`);
1698
+ return new ToolUseBlock({
1699
+ name: INVALID_TOOL_NAME_PLACEHOLDER,
1700
+ toolUseId: block.toolUseId,
1701
+ input: block.input,
1702
+ ...(block.reasoningSignature !== undefined && { reasoningSignature: block.reasoningSignature }),
1703
+ });
1704
+ });
1705
+ if (!messageReplaced)
1706
+ return message;
1707
+ replaced = true;
1708
+ return new Message({
1709
+ role: message.role,
1710
+ content,
1711
+ ...(message.metadata !== undefined && { metadata: message.metadata }),
1712
+ });
1713
+ });
1714
+ return replaced ? next : messages;
1715
+ }
1314
1716
  /**
1315
1717
  * Recursively flattens nested arrays of tools into a single flat array.
1316
1718
  * @param tools - Tools or nested arrays of tools