@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.
- package/dist/src/__fixtures__/agent-helpers.d.ts +16 -1
- package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/agent-helpers.js +42 -0
- package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts +2 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.js +20 -3
- package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
- package/dist/src/__tests__/interrupt.test.d.ts +2 -0
- package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
- package/dist/src/__tests__/interrupt.test.js +259 -0
- package/dist/src/__tests__/interrupt.test.js.map +1 -0
- package/dist/src/__tests__/mcp.test.js +226 -0
- package/dist/src/__tests__/mcp.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.hook.test.js +551 -1
- package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js +730 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.test.js +118 -0
- package/dist/src/agent/__tests__/agent.test.js.map +1 -1
- package/dist/src/agent/__tests__/snapshot.test.js +50 -4
- package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/agent/agent.d.ts +35 -4
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +548 -222
- package/dist/src/agent/agent.js.map +1 -1
- package/dist/src/agent/snapshot.d.ts +2 -2
- package/dist/src/agent/snapshot.d.ts.map +1 -1
- package/dist/src/agent/snapshot.js +14 -2
- package/dist/src/agent/snapshot.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +51 -2
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +75 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
- package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.js +65 -13
- package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/index.d.ts +1 -1
- package/dist/src/conversation-manager/index.d.ts.map +1 -1
- package/dist/src/conversation-manager/index.js +1 -1
- package/dist/src/conversation-manager/index.js.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +17 -3
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js +10 -4
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
- package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
- package/dist/src/hooks/__tests__/events.test.js +99 -12
- package/dist/src/hooks/__tests__/events.test.js.map +1 -1
- package/dist/src/hooks/__tests__/registry.test.js +166 -2
- package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
- package/dist/src/hooks/events.d.ts +102 -30
- package/dist/src/hooks/events.d.ts.map +1 -1
- package/dist/src/hooks/events.js +87 -6
- package/dist/src/hooks/events.js.map +1 -1
- package/dist/src/hooks/index.d.ts +3 -2
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/registry.d.ts +12 -12
- package/dist/src/hooks/registry.d.ts.map +1 -1
- package/dist/src/hooks/registry.js +55 -15
- package/dist/src/hooks/registry.js.map +1 -1
- package/dist/src/hooks/types.d.ts +23 -0
- package/dist/src/hooks/types.d.ts.map +1 -1
- package/dist/src/hooks/types.js +17 -1
- package/dist/src/hooks/types.js.map +1 -1
- package/dist/src/index.d.ts +9 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/interrupt.d.ts +220 -0
- package/dist/src/interrupt.d.ts.map +1 -0
- package/dist/src/interrupt.js +274 -0
- package/dist/src/interrupt.js.map +1 -0
- package/dist/src/mcp.d.ts +23 -2
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +77 -18
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/models/__tests__/anthropic.test.js +55 -0
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
- package/dist/src/models/__tests__/bedrock.test.js +115 -0
- package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
- package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
- package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/src/models/__tests__/defaults.test.js +36 -0
- package/dist/src/models/__tests__/defaults.test.js.map +1 -0
- package/dist/src/models/__tests__/google.test.js +58 -0
- package/dist/src/models/__tests__/google.test.js.map +1 -1
- package/dist/src/models/anthropic.d.ts +8 -0
- package/dist/src/models/anthropic.d.ts.map +1 -1
- package/dist/src/models/anthropic.js +4 -2
- package/dist/src/models/anthropic.js.map +1 -1
- package/dist/src/models/bedrock.d.ts +15 -0
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +58 -4
- package/dist/src/models/bedrock.js.map +1 -1
- package/dist/src/models/defaults.d.ts +10 -0
- package/dist/src/models/defaults.d.ts.map +1 -1
- package/dist/src/models/defaults.js +129 -0
- package/dist/src/models/defaults.js.map +1 -1
- package/dist/src/models/google/model.d.ts.map +1 -1
- package/dist/src/models/google/model.js +4 -2
- package/dist/src/models/google/model.js.map +1 -1
- package/dist/src/models/google/types.d.ts +8 -0
- package/dist/src/models/google/types.d.ts.map +1 -1
- package/dist/src/models/model.d.ts +15 -0
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +18 -0
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/openai/__tests__/chat.test.js +45 -0
- package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
- package/dist/src/models/openai/model.d.ts.map +1 -1
- package/dist/src/models/openai/model.js +2 -2
- package/dist/src/models/openai/model.js.map +1 -1
- package/dist/src/multiagent/__tests__/graph.test.js +69 -0
- package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/nodes.test.js +13 -0
- package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
- package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
- package/dist/src/multiagent/graph.d.ts +22 -2
- package/dist/src/multiagent/graph.d.ts.map +1 -1
- package/dist/src/multiagent/graph.js +42 -3
- package/dist/src/multiagent/graph.js.map +1 -1
- package/dist/src/multiagent/multiagent.d.ts +5 -3
- package/dist/src/multiagent/multiagent.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.d.ts +18 -0
- package/dist/src/multiagent/nodes.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.js +14 -1
- package/dist/src/multiagent/nodes.js.map +1 -1
- package/dist/src/multiagent/swarm.d.ts +15 -1
- package/dist/src/multiagent/swarm.d.ts.map +1 -1
- package/dist/src/multiagent/swarm.js +46 -3
- package/dist/src/multiagent/swarm.js.map +1 -1
- package/dist/src/registry/__tests__/tool-registry.test.js +11 -0
- package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
- package/dist/src/registry/tool-registry.d.ts +4 -0
- package/dist/src/registry/tool-registry.d.ts.map +1 -1
- package/dist/src/registry/tool-registry.js +6 -0
- package/dist/src/registry/tool-registry.js.map +1 -1
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
- package/dist/src/retry/backoff-strategy.d.ts +108 -0
- package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
- package/dist/src/retry/backoff-strategy.js +86 -0
- package/dist/src/retry/backoff-strategy.js.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.js +104 -0
- package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
- package/dist/src/retry/index.d.ts +8 -0
- package/dist/src/retry/index.d.ts.map +1 -0
- package/dist/src/retry/index.js +7 -0
- package/dist/src/retry/index.js.map +1 -0
- package/dist/src/retry/model-retry-strategy.d.ts +80 -0
- package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/model-retry-strategy.js +85 -0
- package/dist/src/retry/model-retry-strategy.js.map +1 -0
- package/dist/src/retry/retry-strategy.d.ts +34 -0
- package/dist/src/retry/retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/retry-strategy.js +25 -0
- package/dist/src/retry/retry-strategy.js.map +1 -0
- package/dist/src/session/__tests__/session-manager.test.js +39 -0
- package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
- package/dist/src/session/session-manager.d.ts +6 -0
- package/dist/src/session/session-manager.d.ts.map +1 -1
- package/dist/src/session/session-manager.js +8 -0
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/tools/__tests__/tool.test.js +24 -1
- package/dist/src/tools/__tests__/tool.test.js.map +1 -1
- package/dist/src/tools/function-tool.d.ts.map +1 -1
- package/dist/src/tools/function-tool.js +6 -1
- package/dist/src/tools/function-tool.js.map +1 -1
- package/dist/src/tools/tool.d.ts +10 -1
- package/dist/src/tools/tool.d.ts.map +1 -1
- package/dist/src/tools/tool.js +12 -0
- package/dist/src/tools/tool.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/types/agent.d.ts +22 -3
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/agent.js +8 -0
- package/dist/src/types/agent.js.map +1 -1
- package/dist/src/types/interrupt.d.ts +103 -0
- package/dist/src/types/interrupt.d.ts.map +1 -0
- package/dist/src/types/interrupt.js +63 -0
- package/dist/src/types/interrupt.js.map +1 -0
- package/dist/src/types/messages.d.ts +2 -1
- package/dist/src/types/messages.d.ts.map +1 -1
- package/dist/src/types/messages.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.js +21 -0
- package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
- package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +12 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +3 -0
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/bash.js +0 -3
- package/dist/src/vended-tools/bash/bash.js.map +1 -1
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +3 -0
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +3 -0
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
- package/package.json +9 -5
package/dist/src/agent/agent.js
CHANGED
|
@@ -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
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
650
|
+
continue;
|
|
566
651
|
}
|
|
567
|
-
|
|
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
|
-
|
|
570
|
-
this.
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
729
|
+
? this._extractStructuredOutput(assistantMessage, toolResultMessage)
|
|
621
730
|
: undefined;
|
|
622
731
|
if (structuredOutput !== undefined) {
|
|
623
732
|
result = new AgentResult({
|
|
624
|
-
stopReason:
|
|
625
|
-
lastMessage:
|
|
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
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
projectedInputTokens
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
849
|
-
if (
|
|
850
|
-
|
|
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
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
970
|
-
|
|
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
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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({
|
|
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
|
|
1112
|
-
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:
|
|
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 (!
|
|
1416
|
+
if (!effectiveTool) {
|
|
1138
1417
|
// Tool not found
|
|
1139
1418
|
toolResult = new ToolResultBlock({
|
|
1140
|
-
toolUseId:
|
|
1419
|
+
toolUseId: toolUse.toolUseId,
|
|
1141
1420
|
status: 'error',
|
|
1142
|
-
content: [new TextBlock(`Tool '${
|
|
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:
|
|
1150
|
-
toolUseId:
|
|
1151
|
-
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, () =>
|
|
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:
|
|
1454
|
+
toolUseId: toolUse.toolUseId,
|
|
1173
1455
|
status: 'error',
|
|
1174
|
-
content: [new TextBlock(`Tool '${
|
|
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:
|
|
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
|