@strands-agents/sdk 1.0.0 → 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 (245) hide show
  1. package/dist/src/__fixtures__/agent-helpers.d.ts +16 -1
  2. package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
  3. package/dist/src/__fixtures__/agent-helpers.js +42 -0
  4. package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
  5. package/dist/src/__fixtures__/tool-helpers.d.ts +2 -1
  6. package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
  7. package/dist/src/__fixtures__/tool-helpers.js +20 -3
  8. package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
  9. package/dist/src/__tests__/interrupt.test.d.ts +2 -0
  10. package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
  11. package/dist/src/__tests__/interrupt.test.js +259 -0
  12. package/dist/src/__tests__/interrupt.test.js.map +1 -0
  13. package/dist/src/__tests__/mcp.test.js +226 -0
  14. package/dist/src/__tests__/mcp.test.js.map +1 -1
  15. package/dist/src/agent/__tests__/agent.hook.test.js +551 -1
  16. package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
  17. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
  18. package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
  19. package/dist/src/agent/__tests__/agent.interrupt.test.js +730 -0
  20. package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
  21. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
  22. package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
  23. package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
  24. package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
  25. package/dist/src/agent/__tests__/agent.test.js +118 -0
  26. package/dist/src/agent/__tests__/agent.test.js.map +1 -1
  27. package/dist/src/agent/__tests__/snapshot.test.js +50 -4
  28. package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
  29. package/dist/src/agent/agent.d.ts +35 -4
  30. package/dist/src/agent/agent.d.ts.map +1 -1
  31. package/dist/src/agent/agent.js +548 -222
  32. package/dist/src/agent/agent.js.map +1 -1
  33. package/dist/src/agent/snapshot.d.ts +2 -2
  34. package/dist/src/agent/snapshot.d.ts.map +1 -1
  35. package/dist/src/agent/snapshot.js +14 -2
  36. package/dist/src/agent/snapshot.js.map +1 -1
  37. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
  38. package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
  39. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
  40. package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
  41. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +51 -2
  42. package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
  43. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +75 -1
  44. package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
  45. package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
  46. package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
  47. package/dist/src/conversation-manager/conversation-manager.js +65 -13
  48. package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
  49. package/dist/src/conversation-manager/index.d.ts +1 -1
  50. package/dist/src/conversation-manager/index.d.ts.map +1 -1
  51. package/dist/src/conversation-manager/index.js +1 -1
  52. package/dist/src/conversation-manager/index.js.map +1 -1
  53. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +17 -3
  54. package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
  55. package/dist/src/conversation-manager/sliding-window-conversation-manager.js +10 -4
  56. package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
  57. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
  58. package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
  59. package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
  60. package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
  61. package/dist/src/hooks/__tests__/events.test.js +99 -12
  62. package/dist/src/hooks/__tests__/events.test.js.map +1 -1
  63. package/dist/src/hooks/__tests__/registry.test.js +166 -2
  64. package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
  65. package/dist/src/hooks/events.d.ts +102 -30
  66. package/dist/src/hooks/events.d.ts.map +1 -1
  67. package/dist/src/hooks/events.js +87 -6
  68. package/dist/src/hooks/events.js.map +1 -1
  69. package/dist/src/hooks/index.d.ts +3 -2
  70. package/dist/src/hooks/index.d.ts.map +1 -1
  71. package/dist/src/hooks/index.js +1 -0
  72. package/dist/src/hooks/index.js.map +1 -1
  73. package/dist/src/hooks/registry.d.ts +12 -12
  74. package/dist/src/hooks/registry.d.ts.map +1 -1
  75. package/dist/src/hooks/registry.js +55 -15
  76. package/dist/src/hooks/registry.js.map +1 -1
  77. package/dist/src/hooks/types.d.ts +23 -0
  78. package/dist/src/hooks/types.d.ts.map +1 -1
  79. package/dist/src/hooks/types.js +17 -1
  80. package/dist/src/hooks/types.js.map +1 -1
  81. package/dist/src/index.d.ts +9 -5
  82. package/dist/src/index.d.ts.map +1 -1
  83. package/dist/src/index.js +4 -1
  84. package/dist/src/index.js.map +1 -1
  85. package/dist/src/interrupt.d.ts +220 -0
  86. package/dist/src/interrupt.d.ts.map +1 -0
  87. package/dist/src/interrupt.js +274 -0
  88. package/dist/src/interrupt.js.map +1 -0
  89. package/dist/src/mcp.d.ts +23 -2
  90. package/dist/src/mcp.d.ts.map +1 -1
  91. package/dist/src/mcp.js +77 -18
  92. package/dist/src/mcp.js.map +1 -1
  93. package/dist/src/models/__tests__/anthropic.test.js +55 -0
  94. package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
  95. package/dist/src/models/__tests__/bedrock.test.js +115 -0
  96. package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
  97. package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
  98. package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
  99. package/dist/src/models/__tests__/defaults.test.js +36 -0
  100. package/dist/src/models/__tests__/defaults.test.js.map +1 -0
  101. package/dist/src/models/__tests__/google.test.js +58 -0
  102. package/dist/src/models/__tests__/google.test.js.map +1 -1
  103. package/dist/src/models/anthropic.d.ts +8 -0
  104. package/dist/src/models/anthropic.d.ts.map +1 -1
  105. package/dist/src/models/anthropic.js +4 -2
  106. package/dist/src/models/anthropic.js.map +1 -1
  107. package/dist/src/models/bedrock.d.ts +15 -0
  108. package/dist/src/models/bedrock.d.ts.map +1 -1
  109. package/dist/src/models/bedrock.js +58 -4
  110. package/dist/src/models/bedrock.js.map +1 -1
  111. package/dist/src/models/defaults.d.ts +10 -0
  112. package/dist/src/models/defaults.d.ts.map +1 -1
  113. package/dist/src/models/defaults.js +129 -0
  114. package/dist/src/models/defaults.js.map +1 -1
  115. package/dist/src/models/google/model.d.ts.map +1 -1
  116. package/dist/src/models/google/model.js +4 -2
  117. package/dist/src/models/google/model.js.map +1 -1
  118. package/dist/src/models/google/types.d.ts +8 -0
  119. package/dist/src/models/google/types.d.ts.map +1 -1
  120. package/dist/src/models/model.d.ts +15 -0
  121. package/dist/src/models/model.d.ts.map +1 -1
  122. package/dist/src/models/model.js +18 -0
  123. package/dist/src/models/model.js.map +1 -1
  124. package/dist/src/models/openai/__tests__/chat.test.js +45 -0
  125. package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
  126. package/dist/src/models/openai/model.d.ts.map +1 -1
  127. package/dist/src/models/openai/model.js +2 -2
  128. package/dist/src/models/openai/model.js.map +1 -1
  129. package/dist/src/multiagent/__tests__/graph.test.js +69 -0
  130. package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
  131. package/dist/src/multiagent/__tests__/nodes.test.js +13 -0
  132. package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
  133. package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
  134. package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
  135. package/dist/src/multiagent/graph.d.ts +22 -2
  136. package/dist/src/multiagent/graph.d.ts.map +1 -1
  137. package/dist/src/multiagent/graph.js +42 -3
  138. package/dist/src/multiagent/graph.js.map +1 -1
  139. package/dist/src/multiagent/multiagent.d.ts +5 -3
  140. package/dist/src/multiagent/multiagent.d.ts.map +1 -1
  141. package/dist/src/multiagent/nodes.d.ts +18 -0
  142. package/dist/src/multiagent/nodes.d.ts.map +1 -1
  143. package/dist/src/multiagent/nodes.js +14 -1
  144. package/dist/src/multiagent/nodes.js.map +1 -1
  145. package/dist/src/multiagent/swarm.d.ts +15 -1
  146. package/dist/src/multiagent/swarm.d.ts.map +1 -1
  147. package/dist/src/multiagent/swarm.js +46 -3
  148. package/dist/src/multiagent/swarm.js.map +1 -1
  149. package/dist/src/registry/__tests__/tool-registry.test.js +11 -0
  150. package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
  151. package/dist/src/registry/tool-registry.d.ts +4 -0
  152. package/dist/src/registry/tool-registry.d.ts.map +1 -1
  153. package/dist/src/registry/tool-registry.js +6 -0
  154. package/dist/src/registry/tool-registry.js.map +1 -1
  155. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
  156. package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
  157. package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
  158. package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
  159. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
  160. package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
  161. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
  162. package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
  163. package/dist/src/retry/backoff-strategy.d.ts +108 -0
  164. package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
  165. package/dist/src/retry/backoff-strategy.js +86 -0
  166. package/dist/src/retry/backoff-strategy.js.map +1 -0
  167. package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
  168. package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
  169. package/dist/src/retry/default-model-retry-strategy.js +104 -0
  170. package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
  171. package/dist/src/retry/index.d.ts +8 -0
  172. package/dist/src/retry/index.d.ts.map +1 -0
  173. package/dist/src/retry/index.js +7 -0
  174. package/dist/src/retry/index.js.map +1 -0
  175. package/dist/src/retry/model-retry-strategy.d.ts +80 -0
  176. package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
  177. package/dist/src/retry/model-retry-strategy.js +85 -0
  178. package/dist/src/retry/model-retry-strategy.js.map +1 -0
  179. package/dist/src/retry/retry-strategy.d.ts +34 -0
  180. package/dist/src/retry/retry-strategy.d.ts.map +1 -0
  181. package/dist/src/retry/retry-strategy.js +25 -0
  182. package/dist/src/retry/retry-strategy.js.map +1 -0
  183. package/dist/src/session/__tests__/session-manager.test.js +39 -0
  184. package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
  185. package/dist/src/session/session-manager.d.ts +6 -0
  186. package/dist/src/session/session-manager.d.ts.map +1 -1
  187. package/dist/src/session/session-manager.js +8 -0
  188. package/dist/src/session/session-manager.js.map +1 -1
  189. package/dist/src/tools/__tests__/tool.test.js +24 -1
  190. package/dist/src/tools/__tests__/tool.test.js.map +1 -1
  191. package/dist/src/tools/function-tool.d.ts.map +1 -1
  192. package/dist/src/tools/function-tool.js +6 -1
  193. package/dist/src/tools/function-tool.js.map +1 -1
  194. package/dist/src/tools/tool.d.ts +10 -1
  195. package/dist/src/tools/tool.d.ts.map +1 -1
  196. package/dist/src/tools/tool.js +12 -0
  197. package/dist/src/tools/tool.js.map +1 -1
  198. package/dist/src/tsconfig.tsbuildinfo +1 -1
  199. package/dist/src/types/agent.d.ts +22 -3
  200. package/dist/src/types/agent.d.ts.map +1 -1
  201. package/dist/src/types/agent.js +8 -0
  202. package/dist/src/types/agent.js.map +1 -1
  203. package/dist/src/types/interrupt.d.ts +103 -0
  204. package/dist/src/types/interrupt.d.ts.map +1 -0
  205. package/dist/src/types/interrupt.js +63 -0
  206. package/dist/src/types/interrupt.js.map +1 -0
  207. package/dist/src/types/messages.d.ts +2 -1
  208. package/dist/src/types/messages.d.ts.map +1 -1
  209. package/dist/src/types/messages.js.map +1 -1
  210. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
  211. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
  212. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
  213. package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
  214. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
  215. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
  216. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
  217. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
  218. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
  219. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
  220. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
  221. package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
  222. package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
  223. package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
  224. package/dist/src/vended-plugins/context-offloader/index.js +21 -0
  225. package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
  226. package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
  227. package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
  228. package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
  229. package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
  230. package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
  231. package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
  232. package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
  233. package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
  234. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +12 -0
  235. package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
  236. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +3 -0
  237. package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
  238. package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
  239. package/dist/src/vended-tools/bash/bash.js +0 -3
  240. package/dist/src/vended-tools/bash/bash.js.map +1 -1
  241. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +3 -0
  242. package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
  243. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +3 -0
  244. package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
  245. 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';
@@ -76,6 +76,10 @@ 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';
79
83
  /** Default name assigned to agents when none is provided. */
80
84
  const DEFAULT_AGENT_NAME = 'Strands Agent';
81
85
  /** Default identifier assigned to agents when none is provided. */
@@ -140,6 +144,8 @@ export class Agent {
140
144
  _tracer;
141
145
  /** Meter instance for accumulating loop metrics during invocation. */
142
146
  _meter;
147
+ /** Interrupt state for human-in-the-loop workflows. */
148
+ _interruptState;
143
149
  /** Strategy for executing tool calls from a single assistant turn. */
144
150
  _toolExecutor;
145
151
  /**
@@ -178,11 +184,26 @@ export class Agent {
178
184
  this._mcpClients = mcpClients;
179
185
  // Initialize hooks registry
180
186
  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.
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.
184
204
  this._pluginRegistry = new PluginRegistry([
185
205
  this._conversationManager,
206
+ ...retryStrategies,
186
207
  ...(config?.plugins ?? []),
187
208
  ...(config?.sessionManager ? [config.sessionManager] : []),
188
209
  new ModelPlugin(this.model),
@@ -201,6 +222,8 @@ export class Agent {
201
222
  this._tracer = new Tracer(config?.traceAttributes);
202
223
  // Initialize meter for local metrics accumulation
203
224
  this._meter = new Meter();
225
+ // Initialize interrupt state for human-in-the-loop workflows
226
+ this._interruptState = new InterruptState();
204
227
  this._toolExecutor = config?.toolExecutor ?? 'concurrent';
205
228
  this._initialized = false;
206
229
  }
@@ -209,6 +232,7 @@ export class Agent {
209
232
  *
210
233
  * @param eventType - The event class constructor to register the callback for
211
234
  * @param callback - The callback function to invoke when the event occurs
235
+ * @param options - Optional configuration including execution order
212
236
  * @returns Cleanup function that removes the callback when invoked
213
237
  *
214
238
  * @example
@@ -223,8 +247,8 @@ export class Agent {
223
247
  * cleanup()
224
248
  * ```
225
249
  */
226
- addHook(eventType, callback) {
227
- return this._hooksRegistry.addCallback(eventType, callback);
250
+ addHook(eventType, callback, options) {
251
+ return this._hooksRegistry.addCallback(eventType, callback, options);
228
252
  }
229
253
  async initialize() {
230
254
  if (this._initialized) {
@@ -383,53 +407,86 @@ export class Agent {
383
407
  async *stream(args, options) {
384
408
  const env_1 = { stack: [], error: void 0, hasError: false };
385
409
  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;
410
+ const _lock = __addDisposableResource(env_1, this.acquireLock(), false);
394
411
  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);
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();
420
435
  }
421
- else {
422
- 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
+ }
423
445
  }
424
446
  }
425
- catch (error) {
426
- 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
+ }));
427
455
  }
428
- result = await streamGenerator.next();
429
456
  }
430
- // Reset controller and signal for next invocation
431
- this._abortController = new AbortController();
432
- 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;
433
490
  }
434
491
  }
435
492
  catch (e_1) {
@@ -501,6 +558,15 @@ export class Agent {
501
558
  // AgentResult. Mutations by hooks/tools are visible across all recursive
502
559
  // agent loop cycles within this invocation.
503
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
+ }
504
570
  const beforeInvocationEvent = new BeforeInvocationEvent({ agent: this, invocationState });
505
571
  yield beforeInvocationEvent;
506
572
  if (beforeInvocationEvent.cancel) {
@@ -557,72 +623,115 @@ export class Agent {
557
623
  }
558
624
  currentArgs = undefined;
559
625
  }
560
- const modelResult = yield* this._invokeModel(invocationState, structuredOutputChoice);
561
- if (modelResult.stopReason !== 'toolUse') {
562
- // If structured output is required, force it
563
- 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);
564
649
  if (structuredOutputChoice) {
565
- throw new StructuredOutputError('The model failed to invoke the structured output tool even after it was forced.');
650
+ continue;
566
651
  }
567
- 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;
568
660
  }
569
- this._meter.endCycle(cycleStartTime);
570
- this._tracer.endAgentLoopSpan(cycleSpan);
571
- yield this._appendMessage(modelResult.message, invocationState);
572
- if (structuredOutputChoice) {
573
- 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;
574
682
  }
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;
683
+ assistantMessage = modelResult.message;
583
684
  }
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);
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) {
595
690
  this._meter.endCycle(cycleStartTime);
596
691
  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;
692
+ continue;
605
693
  }
606
- // Execute tools
607
- const toolResultMessage = yield* this.executeTools(modelResult.message, this._toolRegistry, invocationState);
694
+ const toolResultMessage = toolsResult.message;
608
695
  /**
609
696
  * Deferred append: both messages are added AFTER tool execution completes.
610
697
  * This keeps agent.messages in a valid, reinvokable state at all times.
611
698
  * If interrupted during tool execution, messages has no dangling toolUse
612
699
  * without a matching toolResult, so the agent can be reinvoked cleanly.
613
700
  */
614
- yield this._appendMessage(modelResult.message, invocationState);
701
+ yield this._appendMessage(assistantMessage, invocationState);
615
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
+ }
616
708
  this._meter.endCycle(cycleStartTime);
617
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
+ }
618
727
  // Structured output captured: exit
619
728
  const structuredOutput = structuredOutputTool
620
- ? this._extractStructuredOutput(modelResult.message, toolResultMessage)
729
+ ? this._extractStructuredOutput(assistantMessage, toolResultMessage)
621
730
  : undefined;
622
731
  if (structuredOutput !== undefined) {
623
732
  result = new AgentResult({
624
- stopReason: modelResult.stopReason,
625
- lastMessage: modelResult.message,
733
+ stopReason: 'toolUse',
734
+ lastMessage: assistantMessage,
626
735
  traces: this._tracer.localTraces,
627
736
  structuredOutput,
628
737
  metrics: this._meter.metrics,
@@ -656,6 +765,10 @@ export class Agent {
656
765
  });
657
766
  return result;
658
767
  }
768
+ if (error instanceof InterruptError) {
769
+ result = this._createInterruptResult(invocationState);
770
+ return result;
771
+ }
659
772
  caughtError = error;
660
773
  throw error;
661
774
  }
@@ -701,6 +814,51 @@ export class Agent {
701
814
  const firstContent = toolResult.content[0];
702
815
  return firstContent?.type === 'jsonBlock' ? firstContent.json : undefined;
703
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
+ }
704
862
  /**
705
863
  * Normalizes agent invocation input into an array of messages to append.
706
864
  *
@@ -720,6 +878,11 @@ export class Agent {
720
878
  }
721
879
  else if (Array.isArray(args) && args.length > 0) {
722
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
+ }
723
886
  // Check if it's Message[] or MessageData[]
724
887
  if ('role' in firstElement && typeof firstElement.role === 'string') {
725
888
  // Check if it's a Message instance or MessageData
@@ -773,108 +936,117 @@ export class Agent {
773
936
  if (toolChoice) {
774
937
  streamOptions.toolChoice = toolChoice;
775
938
  }
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);
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);
804
945
  }
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);
946
+ catch (e) {
947
+ logger.debug(`error=<${e}> | token estimation failed, proceeding without estimate`);
836
948
  }
837
- const stopData = {
838
- message: result.message,
839
- stopReason: result.stopReason,
840
- ...(result.redaction && { redaction: result.redaction }),
841
- };
842
- const afterModelCallEvent = new AfterModelCallEvent({
949
+ const beforeModelCallEvent = new BeforeModelCallEvent({
843
950
  agent: this,
844
951
  model: this.model,
845
- stopData,
846
952
  invocationState,
953
+ ...(projectedInputTokens !== undefined && { projectedInputTokens }),
847
954
  });
848
- yield afterModelCallEvent;
849
- if (afterModelCallEvent.retry) {
850
- return yield* this._invokeModel(invocationState, toolChoice);
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' };
851
973
  }
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,
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 }),
864
980
  });
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;
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;
871
1022
  }
872
- // After yielding, hooks have been invoked and may have set retry
873
- if (errorEvent.retry) {
874
- return yield* this._invokeModel(invocationState, toolChoice);
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
1048
+ throw error;
875
1049
  }
876
- // Re-throw error
877
- throw error;
878
1050
  }
879
1051
  }
880
1052
  /**
@@ -894,6 +1066,7 @@ export class Agent {
894
1066
  * @returns StreamAggregatedResult containing message, stop reason, and optional redaction message
895
1067
  */
896
1068
  async *_streamFromModel(messages, streamOptions, invocationState) {
1069
+ messages = normalizeToolUseNames(messages);
897
1070
  const streamGenerator = this.model.streamAggregated(messages, streamOptions);
898
1071
  let result = await streamGenerator.next();
899
1072
  while (!result.done) {
@@ -920,11 +1093,24 @@ export class Agent {
920
1093
  *
921
1094
  * @param assistantMessage - The assistant message containing tool use blocks
922
1095
  * @param toolRegistry - Registry containing available tools
923
- * @returns User message containing tool results
1096
+ * @returns Tool-result message and the dispatched AfterToolsEvent
924
1097
  */
925
- async *executeTools(assistantMessage, toolRegistry, invocationState) {
1098
+ async *executeTools(assistantMessage, toolRegistry, invocationState, completedToolResults) {
926
1099
  const beforeToolsEvent = new BeforeToolsEvent({ agent: this, message: assistantMessage, invocationState });
927
- yield beforeToolsEvent;
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
+ }
928
1114
  const toolUseBlocks = assistantMessage.content.filter((block) => block.type === 'toolUseBlock');
929
1115
  if (toolUseBlocks.length === 0) {
930
1116
  // Preserve BeforeToolsEvent/AfterToolsEvent bracket symmetry even on
@@ -946,9 +1132,9 @@ export class Agent {
946
1132
  }
947
1133
  switch (this._toolExecutor) {
948
1134
  case 'sequential':
949
- return yield* this._executeToolsSequential(toolUseBlocks, toolRegistry, invocationState);
1135
+ return yield* this._executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
950
1136
  case 'concurrent':
951
- return yield* this._executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState);
1137
+ return yield* this._executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage);
952
1138
  default: {
953
1139
  const _exhaustive = this._toolExecutor;
954
1140
  throw new Error(`Unknown toolExecutor: ${_exhaustive}`);
@@ -957,7 +1143,7 @@ export class Agent {
957
1143
  }
958
1144
  /**
959
1145
  * Emits a `ToolResultEvent` for every block plus an `AfterToolsEvent`, and
960
- * returns the resulting tool-result message. Used by the pre-launch cancel
1146
+ * returns the resulting tool-result message and dispatched event. Used by the pre-launch cancel
961
1147
  * paths shared across executors.
962
1148
  */
963
1149
  async *_yieldCancelledToolResults(toolUseBlocks, message, invocationState) {
@@ -966,18 +1152,28 @@ export class Agent {
966
1152
  yield new ToolResultEvent({ agent: this, result, invocationState });
967
1153
  }
968
1154
  const toolResultMessage = new Message({ role: 'user', content: cancelBlocks });
969
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
970
- return toolResultMessage;
1155
+ const afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1156
+ yield afterToolsEvent;
1157
+ return { message: toolResultMessage, afterToolsEvent };
971
1158
  }
972
1159
  /**
973
1160
  * Executes tools one at a time, honoring `agent.cancelSignal` between
974
1161
  * iterations to short-circuit not-yet-started tools.
975
1162
  */
976
- async *_executeToolsSequential(toolUseBlocks, toolRegistry, invocationState) {
1163
+ async *_executeToolsSequential(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
977
1164
  const toolResultBlocks = [];
978
1165
  let toolResultMessage;
1166
+ let afterToolsEvent;
979
1167
  try {
980
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;
1176
+ }
981
1177
  if (this.isCancelled) {
982
1178
  const cancelBlock = new ToolResultBlock({
983
1179
  toolUseId: toolUseBlock.toolUseId,
@@ -988,16 +1184,40 @@ export class Agent {
988
1184
  yield new ToolResultEvent({ agent: this, result: cancelBlock, invocationState });
989
1185
  continue;
990
1186
  }
991
- const toolResultBlock = yield* this.executeTool(toolUseBlock, toolRegistry, invocationState);
992
- toolResultBlocks.push(toolResultBlock);
993
- yield new ToolResultEvent({ agent: this, result: toolResultBlock, invocationState });
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,
1208
+ });
1209
+ throw error;
1210
+ }
1211
+ throw error;
1212
+ }
994
1213
  }
995
1214
  }
996
1215
  finally {
997
1216
  toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
998
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1217
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1218
+ yield afterToolsEvent;
999
1219
  }
1000
- return toolResultMessage;
1220
+ return { message: toolResultMessage, afterToolsEvent };
1001
1221
  }
1002
1222
  /**
1003
1223
  * Produces one error ToolResultBlock per tool use block, each carrying
@@ -1020,15 +1240,32 @@ export class Agent {
1020
1240
  * `executeTool`'s own `while(true)` loop, so one tool retrying does not
1021
1241
  * disturb its siblings.
1022
1242
  */
1023
- async *_executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState) {
1243
+ async *_executeToolsConcurrent(toolUseBlocks, toolRegistry, invocationState, completedToolResults, assistantMessage) {
1024
1244
  let toolResultMessage;
1245
+ let afterToolsEvent;
1025
1246
  const gens = toolUseBlocks.map((block) => ({
1026
1247
  block,
1027
- gen: this.executeTool(block, toolRegistry, invocationState),
1248
+ gen: completedToolResults?.has(block.toolUseId)
1249
+ ? undefined // Skip already-completed tools
1250
+ : this.executeTool(block, toolRegistry, invocationState),
1028
1251
  }));
1029
1252
  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)]));
1253
+ // Seed completed results from resume state
1031
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;
1032
1269
  try {
1033
1270
  while (pendingNext.size > 0) {
1034
1271
  const winner = await Promise.race(pendingNext.values());
@@ -1036,6 +1273,11 @@ export class Agent {
1036
1273
  const block = gens[idx].block;
1037
1274
  if (winner.kind === 'throw') {
1038
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;
1279
+ continue;
1280
+ }
1039
1281
  const err = normalizeError(winner.error);
1040
1282
  const result = new ToolResultBlock({
1041
1283
  toolUseId: block.toolUseId,
@@ -1053,10 +1295,33 @@ export class Agent {
1053
1295
  yield new ToolResultEvent({ agent: this, result: winner.res.value, invocationState });
1054
1296
  }
1055
1297
  else {
1056
- yield winner.res.value;
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
+ }
1057
1310
  pendingNext.set(idx, step(idx));
1058
1311
  }
1059
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
+ }
1060
1325
  }
1061
1326
  finally {
1062
1327
  // Close any generators still in-flight (e.g. consumer broke out of stream).
@@ -1079,9 +1344,10 @@ export class Agent {
1079
1344
  }
1080
1345
  }
1081
1346
  toolResultMessage = new Message({ role: 'user', content: toolResultBlocks });
1082
- yield new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1347
+ afterToolsEvent = new AfterToolsEvent({ agent: this, message: toolResultMessage, invocationState });
1348
+ yield afterToolsEvent;
1083
1349
  }
1084
- return toolResultMessage;
1350
+ return { message: toolResultMessage, afterToolsEvent };
1085
1351
  }
1086
1352
  /**
1087
1353
  * Executes a single tool and returns the result.
@@ -1094,8 +1360,9 @@ export class Agent {
1094
1360
  * @returns Tool result block
1095
1361
  */
1096
1362
  async *executeTool(toolUseBlock, toolRegistry, invocationState) {
1097
- const tool = toolRegistry.get(toolUseBlock.name);
1098
- // Create toolUse object for hook events and telemetry
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.
1099
1366
  const toolUse = {
1100
1367
  name: toolUseBlock.name,
1101
1368
  toolUseId: toolUseBlock.toolUseId,
@@ -1103,21 +1370,33 @@ export class Agent {
1103
1370
  };
1104
1371
  // Retry loop for tool execution
1105
1372
  while (true) {
1106
- const beforeToolCallEvent = new BeforeToolCallEvent({ agent: this, toolUse, tool, invocationState });
1373
+ const beforeToolCallEvent = new BeforeToolCallEvent({
1374
+ agent: this,
1375
+ toolUse,
1376
+ tool: registryTool,
1377
+ invocationState,
1378
+ });
1107
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);
1108
1387
  // Cancel individual tool if hook requested it
1109
1388
  if (beforeToolCallEvent.cancel) {
1110
1389
  const cancelMessage = typeof beforeToolCallEvent.cancel === 'string' ? beforeToolCallEvent.cancel : 'Tool cancelled by hook';
1111
- const toolResult = new ToolResultBlock({
1112
- toolUseId: toolUseBlock.toolUseId,
1390
+ const cancelResult = new ToolResultBlock({
1391
+ toolUseId: toolUse.toolUseId,
1113
1392
  status: 'error',
1114
1393
  content: [new TextBlock(cancelMessage)],
1115
1394
  });
1116
1395
  const afterToolCallEvent = new AfterToolCallEvent({
1117
1396
  agent: this,
1118
1397
  toolUse,
1119
- tool,
1120
- result: toolResult,
1398
+ tool: effectiveTool,
1399
+ result: cancelResult,
1121
1400
  invocationState,
1122
1401
  });
1123
1402
  yield afterToolCallEvent;
@@ -1134,24 +1413,27 @@ export class Agent {
1134
1413
  const toolStartTime = Date.now();
1135
1414
  let toolResult;
1136
1415
  let error;
1137
- if (!tool) {
1416
+ if (!effectiveTool) {
1138
1417
  // Tool not found
1139
1418
  toolResult = new ToolResultBlock({
1140
- toolUseId: toolUseBlock.toolUseId,
1419
+ toolUseId: toolUse.toolUseId,
1141
1420
  status: 'error',
1142
- content: [new TextBlock(`Tool '${toolUseBlock.name}' not found in registry`)],
1421
+ content: [new TextBlock(`Tool '${toolUse.name}' not found in registry`)],
1143
1422
  });
1144
1423
  }
1145
1424
  else {
1146
1425
  // Execute tool within the tool span context
1147
1426
  const toolContext = {
1148
1427
  toolUse: {
1149
- name: toolUseBlock.name,
1150
- toolUseId: toolUseBlock.toolUseId,
1151
- input: toolUseBlock.input,
1428
+ name: toolUse.name,
1429
+ toolUseId: toolUse.toolUseId,
1430
+ input: toolUse.input,
1152
1431
  },
1153
1432
  agent: this,
1154
1433
  invocationState,
1434
+ interrupt: (params) => {
1435
+ return interruptFromAgent(this, `tool:${toolUseBlock.toolUseId}:${params.name}`, params);
1436
+ },
1155
1437
  };
1156
1438
  try {
1157
1439
  // Manually iterate tool stream to wrap each ToolStreamEvent in ToolStreamUpdateEvent.
@@ -1159,7 +1441,7 @@ export class Agent {
1159
1441
  // without knowledge of agents or hooks, and we wrap at the boundary.
1160
1442
  // Tool execution is ran within the tool span's context so that
1161
1443
  // downstream calls (e.g., MCP clients) can propagate trace context
1162
- const toolGenerator = this._tracer.withSpanContext(toolSpan, () => tool.stream(toolContext));
1444
+ const toolGenerator = this._tracer.withSpanContext(toolSpan, () => effectiveTool.stream(toolContext));
1163
1445
  let toolNext = await this._tracer.withSpanContext(toolSpan, () => toolGenerator.next());
1164
1446
  while (!toolNext.done) {
1165
1447
  yield new ToolStreamUpdateEvent({ agent: this, event: toolNext.value, invocationState });
@@ -1169,9 +1451,9 @@ export class Agent {
1169
1451
  if (!result) {
1170
1452
  // Tool didn't return a result
1171
1453
  toolResult = new ToolResultBlock({
1172
- toolUseId: toolUseBlock.toolUseId,
1454
+ toolUseId: toolUse.toolUseId,
1173
1455
  status: 'error',
1174
- content: [new TextBlock(`Tool '${toolUseBlock.name}' did not return a result`)],
1456
+ content: [new TextBlock(`Tool '${toolUse.name}' did not return a result`)],
1175
1457
  });
1176
1458
  }
1177
1459
  else {
@@ -1180,17 +1462,22 @@ export class Agent {
1180
1462
  }
1181
1463
  }
1182
1464
  catch (e) {
1465
+ // Re-throw InterruptError to allow interrupt handling
1466
+ if (e instanceof InterruptError) {
1467
+ throw e;
1468
+ }
1183
1469
  // Tool execution failed with error
1184
1470
  error = normalizeError(e);
1185
1471
  toolResult = new ToolResultBlock({
1186
- toolUseId: toolUseBlock.toolUseId,
1472
+ toolUseId: toolUse.toolUseId,
1187
1473
  status: 'error',
1188
1474
  content: [new TextBlock(error.message)],
1189
1475
  error,
1190
1476
  });
1191
1477
  }
1192
1478
  }
1193
- // End tool span
1479
+ // End tool span with the raw tool result — telemetry reflects what the
1480
+ // tool actually returned, independent of AfterToolCallEvent mutations.
1194
1481
  this._tracer.endToolCallSpan(toolSpan, { toolResult, ...(error && { error }) });
1195
1482
  // End tool metrics tracking
1196
1483
  this._meter.endToolCall({
@@ -1202,7 +1489,7 @@ export class Agent {
1202
1489
  const afterToolCallEvent = new AfterToolCallEvent({
1203
1490
  agent: this,
1204
1491
  toolUse,
1205
- tool,
1492
+ tool: effectiveTool,
1206
1493
  result: toolResult,
1207
1494
  invocationState,
1208
1495
  ...(error !== undefined && { error }),
@@ -1211,6 +1498,8 @@ export class Agent {
1211
1498
  if (afterToolCallEvent.retry) {
1212
1499
  continue;
1213
1500
  }
1501
+ // Return the (possibly mutated) result so hook transformations propagate
1502
+ // to ToolResultEvent and the conversation message the model will see.
1214
1503
  return afterToolCallEvent.result;
1215
1504
  }
1216
1505
  }
@@ -1311,6 +1600,43 @@ export class Agent {
1311
1600
  return new MessageAddedEvent({ agent: this, message, invocationState });
1312
1601
  }
1313
1602
  }
1603
+ const INVALID_TOOL_NAME_PLACEHOLDER = 'INVALID_TOOL_NAME';
1604
+ /**
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.
1608
+ */
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;
1639
+ }
1314
1640
  /**
1315
1641
  * Recursively flattens nested arrays of tools into a single flat array.
1316
1642
  * @param tools - Tools or nested arrays of tools