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