@strands-agents/sdk 1.0.0-rc.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +175 -0
- package/README.md +340 -0
- package/dist/src/__fixtures__/agent-helpers.d.ts +22 -1
- package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/agent-helpers.js +45 -1
- package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
- package/dist/src/__fixtures__/mock-plugin.d.ts.map +1 -1
- package/dist/src/__fixtures__/mock-plugin.js +3 -1
- package/dist/src/__fixtures__/mock-plugin.js.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts +5 -2
- package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.js +23 -4
- 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 +448 -2
- package/dist/src/__tests__/mcp.test.js.map +1 -1
- package/dist/src/a2a/__tests__/events.test.js +2 -0
- package/dist/src/a2a/__tests__/events.test.js.map +1 -1
- package/dist/src/a2a/__tests__/executor.test.js +16 -5
- package/dist/src/a2a/__tests__/executor.test.js.map +1 -1
- package/dist/src/a2a/a2a-agent.d.ts +8 -3
- package/dist/src/a2a/a2a-agent.d.ts.map +1 -1
- package/dist/src/a2a/a2a-agent.js +12 -6
- package/dist/src/a2a/a2a-agent.js.map +1 -1
- package/dist/src/a2a/executor.d.ts +13 -0
- package/dist/src/a2a/executor.d.ts.map +1 -1
- package/dist/src/a2a/executor.js +19 -1
- package/dist/src/a2a/executor.js.map +1 -1
- package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js +23 -0
- package/dist/src/agent/__tests__/agent-as-tool.invocation-state.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.cancel.test.js +1 -1
- package/dist/src/agent/__tests__/agent.cancel.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.concurrent.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.concurrent.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.concurrent.test.js +488 -0
- package/dist/src/agent/__tests__/agent.concurrent.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.hook.test.js +724 -12
- 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.invocation-state.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.invocation-state.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.invocation-state.test.js +219 -0
- package/dist/src/agent/__tests__/agent.invocation-state.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.stateful-model.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.stateful-model.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.stateful-model.test.js +169 -0
- package/dist/src/agent/__tests__/agent.stateful-model.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.test.js +217 -2
- package/dist/src/agent/__tests__/agent.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.tracer.test.node.js +39 -0
- package/dist/src/agent/__tests__/agent.tracer.test.node.js.map +1 -1
- package/dist/src/agent/__tests__/snapshot.test.js +51 -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 +4 -2
- package/dist/src/agent/agent-as-tool.js.map +1 -1
- package/dist/src/agent/agent.d.ts +109 -4
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +790 -224
- 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 +20 -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 +58 -4
- 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 +76 -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/errors.d.ts +11 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +12 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/hooks/__tests__/events.test.js +267 -73
- package/dist/src/hooks/__tests__/events.test.js.map +1 -1
- package/dist/src/hooks/__tests__/registry.test.js +182 -18
- package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
- package/dist/src/hooks/events.d.ts +193 -51
- package/dist/src/hooks/events.d.ts.map +1 -1
- package/dist/src/hooks/events.js +182 -26
- 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 +12 -7
- 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/logging/__tests__/warn-once.test.d.ts +2 -0
- package/dist/src/logging/__tests__/warn-once.test.d.ts.map +1 -0
- package/dist/src/logging/__tests__/warn-once.test.js +30 -0
- package/dist/src/logging/__tests__/warn-once.test.js.map +1 -0
- package/dist/src/logging/warn-once.d.ts +13 -0
- package/dist/src/logging/warn-once.d.ts.map +1 -0
- package/dist/src/logging/warn-once.js +18 -0
- package/dist/src/logging/warn-once.js.map +1 -0
- package/dist/src/mcp.d.ts +43 -3
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +85 -17
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/mime.d.ts +2 -1
- package/dist/src/mime.d.ts.map +1 -1
- package/dist/src/mime.js +1 -0
- package/dist/src/mime.js.map +1 -1
- package/dist/src/models/__tests__/anthropic.test.js +147 -3
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
- package/dist/src/models/__tests__/bedrock.test.js +228 -2
- 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 +135 -0
- package/dist/src/models/__tests__/google.test.js.map +1 -1
- package/dist/src/models/__tests__/model.test.js +149 -1
- package/dist/src/models/__tests__/model.test.js.map +1 -1
- package/dist/src/models/anthropic.d.ts +20 -1
- package/dist/src/models/anthropic.d.ts.map +1 -1
- package/dist/src/models/anthropic.js +42 -8
- package/dist/src/models/anthropic.js.map +1 -1
- package/dist/src/models/bedrock.d.ts +27 -1
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +100 -12
- package/dist/src/models/bedrock.js.map +1 -1
- package/dist/src/models/defaults.d.ts +47 -0
- package/dist/src/models/defaults.d.ts.map +1 -0
- package/dist/src/models/defaults.js +170 -0
- package/dist/src/models/defaults.js.map +1 -0
- package/dist/src/models/google/model.d.ts +14 -1
- package/dist/src/models/google/model.d.ts.map +1 -1
- package/dist/src/models/google/model.js +54 -8
- 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 +65 -0
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +138 -0
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/openai/__tests__/chat.test.d.ts +2 -0
- package/dist/src/models/openai/__tests__/chat.test.d.ts.map +1 -0
- package/dist/src/models/{__tests__/openai.test.js → openai/__tests__/chat.test.js} +117 -7
- package/dist/src/models/openai/__tests__/chat.test.js.map +1 -0
- package/dist/src/models/openai/__tests__/responses.test.d.ts +2 -0
- package/dist/src/models/openai/__tests__/responses.test.d.ts.map +1 -0
- package/dist/src/models/openai/__tests__/responses.test.js +668 -0
- package/dist/src/models/openai/__tests__/responses.test.js.map +1 -0
- package/dist/src/models/openai/chat-adapter.d.ts +33 -0
- package/dist/src/models/openai/chat-adapter.d.ts.map +1 -0
- package/dist/src/models/openai/chat-adapter.js +383 -0
- package/dist/src/models/openai/chat-adapter.js.map +1 -0
- package/dist/src/models/openai/errors.d.ts +16 -0
- package/dist/src/models/openai/errors.d.ts.map +1 -0
- package/dist/src/models/openai/errors.js +40 -0
- package/dist/src/models/openai/errors.js.map +1 -0
- package/dist/src/models/openai/formatting.d.ts +18 -0
- package/dist/src/models/openai/formatting.d.ts.map +1 -0
- package/dist/src/models/openai/formatting.js +38 -0
- package/dist/src/models/openai/formatting.js.map +1 -0
- package/dist/src/models/openai/index.d.ts +19 -0
- package/dist/src/models/openai/index.d.ts.map +1 -0
- package/dist/src/models/openai/index.js +18 -0
- package/dist/src/models/openai/index.js.map +1 -0
- package/dist/src/models/openai/model.d.ts +77 -0
- package/dist/src/models/openai/model.d.ts.map +1 -0
- package/dist/src/models/openai/model.js +211 -0
- package/dist/src/models/openai/model.js.map +1 -0
- package/dist/src/models/openai/responses-adapter.d.ts +78 -0
- package/dist/src/models/openai/responses-adapter.d.ts.map +1 -0
- package/dist/src/models/openai/responses-adapter.js +467 -0
- package/dist/src/models/openai/responses-adapter.js.map +1 -0
- package/dist/src/models/openai/types.d.ts +131 -0
- package/dist/src/models/openai/types.d.ts.map +1 -0
- package/dist/src/models/openai/types.js +5 -0
- package/dist/src/models/openai/types.js.map +1 -0
- package/dist/src/multiagent/__tests__/events.test.js +122 -28
- package/dist/src/multiagent/__tests__/events.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts +2 -0
- package/dist/src/multiagent/__tests__/graph.invocation-state.test.d.ts.map +1 -0
- package/dist/src/multiagent/__tests__/graph.invocation-state.test.js +95 -0
- package/dist/src/multiagent/__tests__/graph.invocation-state.test.js.map +1 -0
- 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 +18 -2
- package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts +2 -0
- package/dist/src/multiagent/__tests__/swarm.invocation-state.test.d.ts.map +1 -0
- package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js +56 -0
- package/dist/src/multiagent/__tests__/swarm.invocation-state.test.js.map +1 -0
- 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 +19 -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 +27 -5
- package/dist/src/multiagent/graph.d.ts.map +1 -1
- package/dist/src/multiagent/graph.js +61 -15
- package/dist/src/multiagent/graph.js.map +1 -1
- package/dist/src/multiagent/index.d.ts +1 -1
- package/dist/src/multiagent/index.d.ts.map +1 -1
- package/dist/src/multiagent/multiagent.d.ts +21 -6
- package/dist/src/multiagent/multiagent.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.d.ts +28 -3
- package/dist/src/multiagent/nodes.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.js +42 -7
- package/dist/src/multiagent/nodes.js.map +1 -1
- package/dist/src/multiagent/swarm.d.ts +20 -4
- package/dist/src/multiagent/swarm.d.ts.map +1 -1
- package/dist/src/multiagent/swarm.js +65 -16
- package/dist/src/multiagent/swarm.js.map +1 -1
- package/dist/src/plugins/__tests__/registry.test.js +1 -1
- package/dist/src/plugins/__tests__/registry.test.js.map +1 -1
- package/dist/src/plugins/model-plugin.d.ts +20 -0
- package/dist/src/plugins/model-plugin.d.ts.map +1 -0
- package/dist/src/plugins/model-plugin.js +29 -0
- package/dist/src/plugins/model-plugin.js.map +1 -0
- 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 +52 -11
- 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 +17 -0
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/telemetry/__tests__/meter.test.js +23 -0
- package/dist/src/telemetry/__tests__/meter.test.js.map +1 -1
- package/dist/src/telemetry/meter.d.ts +15 -0
- package/dist/src/telemetry/meter.d.ts.map +1 -1
- package/dist/src/telemetry/meter.js +14 -0
- 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 +24 -3
- package/dist/src/tools/mcp-tool.d.ts.map +1 -1
- package/dist/src/tools/mcp-tool.js +103 -31
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/tool.d.ts +21 -2
- 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 +48 -0
- package/dist/src/types/__tests__/agent.test.js.map +1 -1
- package/dist/src/types/agent.d.ts +77 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/agent.js +30 -6
- package/dist/src/types/agent.js.map +1 -1
- package/dist/src/types/elicitation.d.ts +15 -0
- package/dist/src/types/elicitation.d.ts.map +1 -0
- package/dist/src/types/elicitation.js +2 -0
- package/dist/src/types/elicitation.js.map +1 -0
- 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 +21 -5
- 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 +4 -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 +4 -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 +4 -0
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
- package/package.json +17 -9
- package/dist/src/models/__tests__/openai.test.d.ts +0 -2
- package/dist/src/models/__tests__/openai.test.d.ts.map +0 -1
- package/dist/src/models/__tests__/openai.test.js.map +0 -1
- package/dist/src/models/openai.d.ts +0 -312
- package/dist/src/models/openai.d.ts.map +0 -1
- package/dist/src/models/openai.js +0 -789
- package/dist/src/models/openai.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
2
|
import { Agent } from '../agent.js';
|
|
3
|
-
import { AfterInvocationEvent, AfterModelCallEvent, AfterToolCallEvent, AfterToolsEvent, BeforeInvocationEvent, BeforeModelCallEvent, BeforeToolCallEvent, BeforeToolsEvent, MessageAddedEvent, ModelStreamUpdateEvent, InitializedEvent, HookableEvent, } from '../../hooks/index.js';
|
|
3
|
+
import { AfterInvocationEvent, AfterModelCallEvent, AfterToolCallEvent, AfterToolsEvent, AgentResultEvent, BeforeInvocationEvent, BeforeModelCallEvent, BeforeToolCallEvent, BeforeToolsEvent, MessageAddedEvent, ModelStreamUpdateEvent, InitializedEvent, HookableEvent, ModelMessageEvent, } from '../../hooks/index.js';
|
|
4
4
|
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js';
|
|
5
5
|
import { MockPlugin } from '../../__fixtures__/mock-plugin.js';
|
|
6
6
|
import { collectIterator } from '../../__fixtures__/model-test-helpers.js';
|
|
7
7
|
import { createMockTool } from '../../__fixtures__/tool-helpers.js';
|
|
8
|
+
import { expectAgentResult } from '../../__fixtures__/agent-helpers.js';
|
|
8
9
|
import { Message, TextBlock, ToolResultBlock } from '../../types/messages.js';
|
|
9
10
|
describe('Agent Hooks Integration', () => {
|
|
10
11
|
let mockPlugin;
|
|
@@ -19,12 +20,23 @@ describe('Agent Hooks Integration', () => {
|
|
|
19
20
|
await agent.invoke('Hi');
|
|
20
21
|
expect(lifecyclePlugin.invocations).toHaveLength(7);
|
|
21
22
|
expect(lifecyclePlugin.invocations[0]).toEqual(new InitializedEvent({ agent }));
|
|
22
|
-
expect(lifecyclePlugin.invocations[1]).toEqual(new BeforeInvocationEvent({ agent }));
|
|
23
|
-
expect(lifecyclePlugin.invocations[2]).toEqual(new MessageAddedEvent({
|
|
24
|
-
|
|
23
|
+
expect(lifecyclePlugin.invocations[1]).toEqual(new BeforeInvocationEvent({ agent, invocationState: {} }));
|
|
24
|
+
expect(lifecyclePlugin.invocations[2]).toEqual(new MessageAddedEvent({
|
|
25
|
+
agent,
|
|
26
|
+
message: new Message({ role: 'user', content: [new TextBlock('Hi')] }),
|
|
27
|
+
invocationState: {},
|
|
28
|
+
}));
|
|
29
|
+
expect(lifecyclePlugin.invocations[3]).toEqual(new BeforeModelCallEvent({
|
|
30
|
+
agent,
|
|
31
|
+
model: agent.model,
|
|
32
|
+
invocationState: {},
|
|
33
|
+
projectedInputTokens: expect.any(Number),
|
|
34
|
+
}));
|
|
25
35
|
expect(lifecyclePlugin.invocations[4]).toEqual(new AfterModelCallEvent({
|
|
26
36
|
agent,
|
|
27
37
|
model: agent.model,
|
|
38
|
+
invocationState: {},
|
|
39
|
+
attemptCount: 1,
|
|
28
40
|
stopData: {
|
|
29
41
|
stopReason: 'endTurn',
|
|
30
42
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
@@ -33,8 +45,9 @@ describe('Agent Hooks Integration', () => {
|
|
|
33
45
|
expect(lifecyclePlugin.invocations[5]).toEqual(new MessageAddedEvent({
|
|
34
46
|
agent,
|
|
35
47
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
48
|
+
invocationState: {},
|
|
36
49
|
}));
|
|
37
|
-
expect(lifecyclePlugin.invocations[6]).toEqual(new AfterInvocationEvent({ agent }));
|
|
50
|
+
expect(lifecyclePlugin.invocations[6]).toEqual(new AfterInvocationEvent({ agent, invocationState: {} }));
|
|
38
51
|
});
|
|
39
52
|
it('fires hooks during stream', async () => {
|
|
40
53
|
const lifecyclePlugin = new MockPlugin();
|
|
@@ -43,15 +56,23 @@ describe('Agent Hooks Integration', () => {
|
|
|
43
56
|
await collectIterator(agent.stream('Hi'));
|
|
44
57
|
expect(lifecyclePlugin.invocations).toHaveLength(7);
|
|
45
58
|
expect(lifecyclePlugin.invocations[0]).toEqual(new InitializedEvent({ agent }));
|
|
46
|
-
expect(lifecyclePlugin.invocations[1]).toEqual(new BeforeInvocationEvent({ agent }));
|
|
59
|
+
expect(lifecyclePlugin.invocations[1]).toEqual(new BeforeInvocationEvent({ agent, invocationState: {} }));
|
|
47
60
|
expect(lifecyclePlugin.invocations[2]).toEqual(new MessageAddedEvent({
|
|
48
61
|
agent,
|
|
49
62
|
message: new Message({ role: 'user', content: [new TextBlock('Hi')] }),
|
|
63
|
+
invocationState: {},
|
|
64
|
+
}));
|
|
65
|
+
expect(lifecyclePlugin.invocations[3]).toEqual(new BeforeModelCallEvent({
|
|
66
|
+
agent,
|
|
67
|
+
model: agent.model,
|
|
68
|
+
invocationState: {},
|
|
69
|
+
projectedInputTokens: expect.any(Number),
|
|
50
70
|
}));
|
|
51
|
-
expect(lifecyclePlugin.invocations[3]).toEqual(new BeforeModelCallEvent({ agent, model: agent.model }));
|
|
52
71
|
expect(lifecyclePlugin.invocations[4]).toEqual(new AfterModelCallEvent({
|
|
53
72
|
agent,
|
|
54
73
|
model: agent.model,
|
|
74
|
+
invocationState: {},
|
|
75
|
+
attemptCount: 1,
|
|
55
76
|
stopData: {
|
|
56
77
|
stopReason: 'endTurn',
|
|
57
78
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
@@ -60,8 +81,9 @@ describe('Agent Hooks Integration', () => {
|
|
|
60
81
|
expect(lifecyclePlugin.invocations[5]).toEqual(new MessageAddedEvent({
|
|
61
82
|
agent,
|
|
62
83
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
84
|
+
invocationState: {},
|
|
63
85
|
}));
|
|
64
|
-
expect(lifecyclePlugin.invocations[6]).toEqual(new AfterInvocationEvent({ agent }));
|
|
86
|
+
expect(lifecyclePlugin.invocations[6]).toEqual(new AfterInvocationEvent({ agent, invocationState: {} }));
|
|
65
87
|
});
|
|
66
88
|
});
|
|
67
89
|
describe('runtime hook registration', () => {
|
|
@@ -78,8 +100,8 @@ describe('Agent Hooks Integration', () => {
|
|
|
78
100
|
});
|
|
79
101
|
await agent.invoke('Hi');
|
|
80
102
|
expect(invocations).toHaveLength(2);
|
|
81
|
-
expect(invocations[0]).toEqual(new BeforeInvocationEvent({ agent }));
|
|
82
|
-
expect(invocations[1]).toEqual(new AfterInvocationEvent({ agent }));
|
|
103
|
+
expect(invocations[0]).toEqual(new BeforeInvocationEvent({ agent, invocationState: {} }));
|
|
104
|
+
expect(invocations[1]).toEqual(new AfterInvocationEvent({ agent, invocationState: {} }));
|
|
83
105
|
});
|
|
84
106
|
});
|
|
85
107
|
describe('multi-turn conversations', () => {
|
|
@@ -129,6 +151,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
129
151
|
agent,
|
|
130
152
|
toolUse: { name: 'testTool', toolUseId: 'tool-1', input: {} },
|
|
131
153
|
tool,
|
|
154
|
+
invocationState: {},
|
|
132
155
|
}));
|
|
133
156
|
// Verify AfterToolCallEvent
|
|
134
157
|
const afterToolCall = afterToolCallEvents[0];
|
|
@@ -141,6 +164,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
141
164
|
status: 'success',
|
|
142
165
|
content: [new TextBlock('Tool result')],
|
|
143
166
|
}),
|
|
167
|
+
invocationState: {},
|
|
144
168
|
}));
|
|
145
169
|
});
|
|
146
170
|
it('fires AfterToolCallEvent with error when tool fails', async () => {
|
|
@@ -173,6 +197,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
173
197
|
content: [new TextBlock('Tool execution failed')],
|
|
174
198
|
}),
|
|
175
199
|
error: new Error('Tool execution failed'),
|
|
200
|
+
invocationState: {},
|
|
176
201
|
}));
|
|
177
202
|
});
|
|
178
203
|
});
|
|
@@ -214,10 +239,12 @@ describe('Agent Hooks Integration', () => {
|
|
|
214
239
|
expect(messageAddedEvents[0]).toEqual(new MessageAddedEvent({
|
|
215
240
|
agent,
|
|
216
241
|
message: new Message({ role: 'user', content: [new TextBlock('New message')] }),
|
|
242
|
+
invocationState: {},
|
|
217
243
|
}));
|
|
218
244
|
expect(messageAddedEvents[1]).toEqual(new MessageAddedEvent({
|
|
219
245
|
agent,
|
|
220
246
|
message: new Message({ role: 'assistant', content: [new TextBlock('Response')] }),
|
|
247
|
+
invocationState: {},
|
|
221
248
|
}));
|
|
222
249
|
});
|
|
223
250
|
});
|
|
@@ -411,7 +438,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
411
438
|
expect(afterEvent.result).toEqual(new ToolResultBlock({
|
|
412
439
|
toolUseId: 'tool-1',
|
|
413
440
|
status: 'error',
|
|
414
|
-
content: [new TextBlock('
|
|
441
|
+
content: [new TextBlock('Tool cancelled by hook')],
|
|
415
442
|
}));
|
|
416
443
|
});
|
|
417
444
|
it('cancels individual tool call with custom message when cancel is a string', async () => {
|
|
@@ -495,7 +522,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
495
522
|
expect(afterEvent.message.content[0]).toEqual(new ToolResultBlock({
|
|
496
523
|
toolUseId: 'tool-1',
|
|
497
524
|
status: 'error',
|
|
498
|
-
content: [new TextBlock('
|
|
525
|
+
content: [new TextBlock('Tool cancelled by hook')],
|
|
499
526
|
}));
|
|
500
527
|
});
|
|
501
528
|
it('cancels all tools with custom message when BeforeToolsEvent.cancel is a string', async () => {
|
|
@@ -615,6 +642,691 @@ describe('Agent Hooks Integration', () => {
|
|
|
615
642
|
expect(beforeCount).toBe(2);
|
|
616
643
|
expect(toolCallCount).toBe(1); // Only executed on second attempt
|
|
617
644
|
});
|
|
645
|
+
it('allows hooks to replace result on AfterToolCallEvent', async () => {
|
|
646
|
+
const tool = createMockTool('myTool', () => {
|
|
647
|
+
return new ToolResultBlock({
|
|
648
|
+
toolUseId: 'tool-1',
|
|
649
|
+
status: 'success',
|
|
650
|
+
content: [new TextBlock('original result')],
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
const model = new MockMessageModel()
|
|
654
|
+
.addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
|
|
655
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
656
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
657
|
+
agent.addHook(AfterToolCallEvent, (event) => {
|
|
658
|
+
event.result = new ToolResultBlock({
|
|
659
|
+
toolUseId: event.result.toolUseId,
|
|
660
|
+
status: 'success',
|
|
661
|
+
content: [new TextBlock('replaced result')],
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
await agent.invoke('Test');
|
|
665
|
+
const toolResultMessage = agent.messages.find((m) => m.role === 'user' && m.content.some((b) => b.type === 'toolResultBlock'));
|
|
666
|
+
const toolResultBlock = toolResultMessage.content.find((b) => b.type === 'toolResultBlock');
|
|
667
|
+
expect(toolResultBlock).toStrictEqual(new ToolResultBlock({
|
|
668
|
+
toolUseId: 'tool-1',
|
|
669
|
+
status: 'success',
|
|
670
|
+
content: [new TextBlock('replaced result')],
|
|
671
|
+
}));
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
describe('AfterToolsEvent.endTurn', () => {
|
|
675
|
+
const makeSingleToolSetup = () => ({
|
|
676
|
+
tool: createMockTool('myTool', () => {
|
|
677
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('result')] });
|
|
678
|
+
}),
|
|
679
|
+
model: new MockMessageModel()
|
|
680
|
+
.addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: {} })
|
|
681
|
+
.addTurn({ type: 'textBlock', text: 'Should not reach this' }),
|
|
682
|
+
});
|
|
683
|
+
it('halts the loop when endTurn is true with default message', async () => {
|
|
684
|
+
const { tool, model } = makeSingleToolSetup();
|
|
685
|
+
const agent = new Agent({ model, tools: [tool], plugins: [mockPlugin] });
|
|
686
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
687
|
+
event.endTurn = true;
|
|
688
|
+
});
|
|
689
|
+
const result = await agent.invoke('Test');
|
|
690
|
+
expect(result).toEqual(expect.objectContaining({
|
|
691
|
+
type: 'agentResult',
|
|
692
|
+
stopReason: 'endTurn',
|
|
693
|
+
lastMessage: expect.objectContaining({
|
|
694
|
+
role: 'assistant',
|
|
695
|
+
content: expect.arrayContaining([
|
|
696
|
+
expect.objectContaining({ type: 'textBlock', text: 'Turn ended early by hook after tool execution' }),
|
|
697
|
+
]),
|
|
698
|
+
}),
|
|
699
|
+
}));
|
|
700
|
+
expect(model.callCount).toBe(1);
|
|
701
|
+
});
|
|
702
|
+
it('halts the loop with custom assistant message when endTurn is a string', async () => {
|
|
703
|
+
const { tool, model } = makeSingleToolSetup();
|
|
704
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
705
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
706
|
+
event.endTurn = 'enough information gathered';
|
|
707
|
+
});
|
|
708
|
+
const result = await agent.invoke('Test');
|
|
709
|
+
expect(result).toEqual(expect.objectContaining({
|
|
710
|
+
type: 'agentResult',
|
|
711
|
+
stopReason: 'endTurn',
|
|
712
|
+
lastMessage: expect.objectContaining({
|
|
713
|
+
role: 'assistant',
|
|
714
|
+
content: expect.arrayContaining([
|
|
715
|
+
expect.objectContaining({ type: 'textBlock', text: 'enough information gathered' }),
|
|
716
|
+
]),
|
|
717
|
+
}),
|
|
718
|
+
}));
|
|
719
|
+
expect(model.callCount).toBe(1);
|
|
720
|
+
});
|
|
721
|
+
it('does not halt when endTurn is false (default)', async () => {
|
|
722
|
+
const { tool, model } = makeSingleToolSetup();
|
|
723
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
724
|
+
const result = await agent.invoke('Test');
|
|
725
|
+
expect(result).toEqual(expect.objectContaining({
|
|
726
|
+
type: 'agentResult',
|
|
727
|
+
stopReason: 'endTurn',
|
|
728
|
+
lastMessage: expect.objectContaining({ role: 'assistant' }),
|
|
729
|
+
}));
|
|
730
|
+
expect(model.callCount).toBe(2);
|
|
731
|
+
});
|
|
732
|
+
it('treats empty string endTurn as falsy (does not halt)', async () => {
|
|
733
|
+
const { tool, model } = makeSingleToolSetup();
|
|
734
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
735
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
736
|
+
event.endTurn = '';
|
|
737
|
+
});
|
|
738
|
+
const result = await agent.invoke('Test');
|
|
739
|
+
expect(result).toEqual(expect.objectContaining({
|
|
740
|
+
type: 'agentResult',
|
|
741
|
+
stopReason: 'endTurn',
|
|
742
|
+
lastMessage: expect.objectContaining({ role: 'assistant' }),
|
|
743
|
+
}));
|
|
744
|
+
expect(model.callCount).toBe(2);
|
|
745
|
+
});
|
|
746
|
+
it('appends tool results and default endTurn message to conversation history', async () => {
|
|
747
|
+
const { tool, model } = makeSingleToolSetup();
|
|
748
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
749
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
750
|
+
event.endTurn = true;
|
|
751
|
+
});
|
|
752
|
+
await agent.invoke('Test');
|
|
753
|
+
expect(agent.messages).toHaveLength(4);
|
|
754
|
+
expect(agent.messages[0].role).toBe('user');
|
|
755
|
+
expect(agent.messages[1].role).toBe('assistant');
|
|
756
|
+
expect(agent.messages[1].content).toEqual(expect.arrayContaining([expect.objectContaining({ type: 'toolUseBlock' })]));
|
|
757
|
+
expect(agent.messages[2].role).toBe('user');
|
|
758
|
+
expect(agent.messages[2].content).toEqual(expect.arrayContaining([expect.objectContaining({ type: 'toolResultBlock' })]));
|
|
759
|
+
expect(agent.messages[3].role).toBe('assistant');
|
|
760
|
+
expect(agent.messages[3].content).toEqual(expect.arrayContaining([
|
|
761
|
+
expect.objectContaining({ type: 'textBlock', text: 'Turn ended early by hook after tool execution' }),
|
|
762
|
+
]));
|
|
763
|
+
});
|
|
764
|
+
it('halts the loop with concurrent tool execution', async () => {
|
|
765
|
+
const tool1 = createMockTool('tool1', () => {
|
|
766
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('Result 1')] });
|
|
767
|
+
});
|
|
768
|
+
const tool2 = createMockTool('tool2', () => {
|
|
769
|
+
return new ToolResultBlock({ toolUseId: 'tool-2', status: 'success', content: [new TextBlock('Result 2')] });
|
|
770
|
+
});
|
|
771
|
+
const model = new MockMessageModel()
|
|
772
|
+
.addTurn([
|
|
773
|
+
{ type: 'toolUseBlock', name: 'tool1', toolUseId: 'tool-1', input: {} },
|
|
774
|
+
{ type: 'toolUseBlock', name: 'tool2', toolUseId: 'tool-2', input: {} },
|
|
775
|
+
])
|
|
776
|
+
.addTurn({ type: 'textBlock', text: 'Should not reach this' });
|
|
777
|
+
const agent = new Agent({ model, tools: [tool1, tool2], toolExecutor: 'concurrent' });
|
|
778
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
779
|
+
event.endTurn = true;
|
|
780
|
+
});
|
|
781
|
+
const result = await agent.invoke('Test');
|
|
782
|
+
expect(result).toEqual(expect.objectContaining({
|
|
783
|
+
type: 'agentResult',
|
|
784
|
+
stopReason: 'endTurn',
|
|
785
|
+
lastMessage: expect.objectContaining({ role: 'assistant' }),
|
|
786
|
+
}));
|
|
787
|
+
expect(model.callCount).toBe(1);
|
|
788
|
+
});
|
|
789
|
+
it('emits AfterToolsEvent with endTurn via stream()', async () => {
|
|
790
|
+
const { tool, model } = makeSingleToolSetup();
|
|
791
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
792
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
793
|
+
event.endTurn = true;
|
|
794
|
+
});
|
|
795
|
+
const items = await collectIterator(agent.stream('Test'));
|
|
796
|
+
const afterToolsEvents = items.filter((e) => e instanceof AfterToolsEvent);
|
|
797
|
+
expect(afterToolsEvents).toHaveLength(1);
|
|
798
|
+
expect(afterToolsEvents[0].endTurn).toBe(true);
|
|
799
|
+
const resultEvents = items.filter((e) => e instanceof AgentResultEvent);
|
|
800
|
+
expect(resultEvents).toHaveLength(1);
|
|
801
|
+
expect(resultEvents[0].result.stopReason).toBe('endTurn');
|
|
802
|
+
});
|
|
803
|
+
it('halts even when set on a cancelled-tools AfterToolsEvent', async () => {
|
|
804
|
+
const { tool, model } = makeSingleToolSetup();
|
|
805
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
806
|
+
agent.addHook(BeforeToolsEvent, (event) => {
|
|
807
|
+
event.cancel = true;
|
|
808
|
+
});
|
|
809
|
+
agent.addHook(AfterToolsEvent, (event) => {
|
|
810
|
+
event.endTurn = true;
|
|
811
|
+
});
|
|
812
|
+
const result = await agent.invoke('Test');
|
|
813
|
+
expect(result).toEqual(expect.objectContaining({
|
|
814
|
+
type: 'agentResult',
|
|
815
|
+
stopReason: 'endTurn',
|
|
816
|
+
lastMessage: expect.objectContaining({ role: 'assistant' }),
|
|
817
|
+
}));
|
|
818
|
+
expect(model.callCount).toBe(1);
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
describe('cancel invocation via hooks', () => {
|
|
822
|
+
it('cancels invocation with default message when cancel is true', async () => {
|
|
823
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
824
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
825
|
+
agent.addHook(BeforeInvocationEvent, (event) => {
|
|
826
|
+
event.cancel = true;
|
|
827
|
+
});
|
|
828
|
+
const result = await agent.invoke('Test');
|
|
829
|
+
expect(result.stopReason).toBe('endTurn');
|
|
830
|
+
expect(result.lastMessage.content[0]).toEqual(new TextBlock('invocation denied by hook'));
|
|
831
|
+
const beforeModelCallEvents = mockPlugin.invocations.filter((e) => e instanceof BeforeModelCallEvent);
|
|
832
|
+
expect(beforeModelCallEvents).toHaveLength(0);
|
|
833
|
+
});
|
|
834
|
+
it('cancels invocation with custom message when cancel is a string', async () => {
|
|
835
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
836
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
837
|
+
agent.addHook(BeforeInvocationEvent, (event) => {
|
|
838
|
+
event.cancel = 'Unauthorized user';
|
|
839
|
+
});
|
|
840
|
+
const result = await agent.invoke('Test');
|
|
841
|
+
expect(result.stopReason).toBe('endTurn');
|
|
842
|
+
expect(result.lastMessage.content[0]).toEqual(new TextBlock('Unauthorized user'));
|
|
843
|
+
});
|
|
844
|
+
it('does not append user message when invocation is cancelled', async () => {
|
|
845
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
846
|
+
const agent = new Agent({ model });
|
|
847
|
+
agent.addHook(BeforeInvocationEvent, (event) => {
|
|
848
|
+
event.cancel = true;
|
|
849
|
+
});
|
|
850
|
+
await agent.invoke('Test');
|
|
851
|
+
expect(agent.messages).toHaveLength(1);
|
|
852
|
+
expect(agent.messages[0].role).toBe('assistant');
|
|
853
|
+
});
|
|
854
|
+
it('emits AfterInvocationEvent when invocation is cancelled', async () => {
|
|
855
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
856
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
857
|
+
agent.addHook(BeforeInvocationEvent, (event) => {
|
|
858
|
+
event.cancel = true;
|
|
859
|
+
});
|
|
860
|
+
await agent.invoke('Test');
|
|
861
|
+
const beforeInvocationEvents = mockPlugin.invocations.filter((e) => e instanceof BeforeInvocationEvent);
|
|
862
|
+
const afterInvocationEvents = mockPlugin.invocations.filter((e) => e instanceof AfterInvocationEvent);
|
|
863
|
+
expect(beforeInvocationEvents).toHaveLength(1);
|
|
864
|
+
expect(afterInvocationEvents).toHaveLength(1);
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
describe('cancel model call via hooks', () => {
|
|
868
|
+
it('cancels model call with default message when cancel is true', async () => {
|
|
869
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
870
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
871
|
+
agent.addHook(BeforeModelCallEvent, (event) => {
|
|
872
|
+
event.cancel = true;
|
|
873
|
+
});
|
|
874
|
+
const result = await agent.invoke('Test');
|
|
875
|
+
expect(result.stopReason).toBe('endTurn');
|
|
876
|
+
expect(result.lastMessage.content[0]).toEqual(new TextBlock('model call denied by hook'));
|
|
877
|
+
});
|
|
878
|
+
it('cancels model call with custom message when cancel is a string', async () => {
|
|
879
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
880
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
881
|
+
agent.addHook(BeforeModelCallEvent, (event) => {
|
|
882
|
+
event.cancel = 'Rate limited';
|
|
883
|
+
});
|
|
884
|
+
const result = await agent.invoke('Test');
|
|
885
|
+
expect(result.stopReason).toBe('endTurn');
|
|
886
|
+
expect(result.lastMessage.content[0]).toEqual(new TextBlock('Rate limited'));
|
|
887
|
+
});
|
|
888
|
+
it('emits AfterModelCallEvent when model call is cancelled', async () => {
|
|
889
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
890
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
891
|
+
agent.addHook(BeforeModelCallEvent, (event) => {
|
|
892
|
+
event.cancel = true;
|
|
893
|
+
});
|
|
894
|
+
await agent.invoke('Test');
|
|
895
|
+
const beforeModelCallEvents = mockPlugin.invocations.filter((e) => e instanceof BeforeModelCallEvent);
|
|
896
|
+
const afterModelCallEvents = mockPlugin.invocations.filter((e) => e instanceof AfterModelCallEvent);
|
|
897
|
+
expect(beforeModelCallEvents).toHaveLength(1);
|
|
898
|
+
expect(afterModelCallEvents).toHaveLength(1);
|
|
899
|
+
});
|
|
900
|
+
it('does not emit ModelMessageEvent when model call is cancelled', async () => {
|
|
901
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
902
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
903
|
+
agent.addHook(BeforeModelCallEvent, (event) => {
|
|
904
|
+
event.cancel = true;
|
|
905
|
+
});
|
|
906
|
+
await agent.invoke('Test');
|
|
907
|
+
const modelMessageEvents = mockPlugin.invocations.filter((e) => e instanceof ModelMessageEvent);
|
|
908
|
+
expect(modelMessageEvents).toHaveLength(0);
|
|
909
|
+
});
|
|
910
|
+
it('allows retry after cancel on model call', async () => {
|
|
911
|
+
let beforeCount = 0;
|
|
912
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
913
|
+
const agent = new Agent({ model, plugins: [mockPlugin] });
|
|
914
|
+
agent.addHook(BeforeModelCallEvent, (event) => {
|
|
915
|
+
beforeCount++;
|
|
916
|
+
if (beforeCount === 1) {
|
|
917
|
+
event.cancel = 'Not yet';
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
agent.addHook(AfterModelCallEvent, (event) => {
|
|
921
|
+
if (beforeCount === 1) {
|
|
922
|
+
event.retry = true;
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
const result = await agent.invoke('Test');
|
|
926
|
+
expect(result.stopReason).toBe('endTurn');
|
|
927
|
+
expect(beforeCount).toBe(2);
|
|
928
|
+
expect(result.lastMessage.content[0]).toEqual(new TextBlock('Hello'));
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
describe('BeforeToolCallEvent selectedTool', () => {
|
|
932
|
+
it('invokes the replacement tool instead of the registry tool', async () => {
|
|
933
|
+
let originalExecuted = false;
|
|
934
|
+
let replacementExecuted = false;
|
|
935
|
+
const originalTool = createMockTool('originalTool', () => {
|
|
936
|
+
originalExecuted = true;
|
|
937
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('original')] });
|
|
938
|
+
});
|
|
939
|
+
const replacementTool = createMockTool('replacementTool', () => {
|
|
940
|
+
replacementExecuted = true;
|
|
941
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('replacement')] });
|
|
942
|
+
});
|
|
943
|
+
const model = new MockMessageModel()
|
|
944
|
+
.addTurn({ type: 'toolUseBlock', name: 'originalTool', toolUseId: 'tool-1', input: {} })
|
|
945
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
946
|
+
const agent = new Agent({ model, tools: [originalTool], plugins: [mockPlugin] });
|
|
947
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
948
|
+
event.selectedTool = replacementTool;
|
|
949
|
+
});
|
|
950
|
+
await agent.invoke('Test');
|
|
951
|
+
expect(originalExecuted).toBe(false);
|
|
952
|
+
expect(replacementExecuted).toBe(true);
|
|
953
|
+
const afterToolCallEvents = mockPlugin.invocations.filter((e) => e instanceof AfterToolCallEvent);
|
|
954
|
+
expect(afterToolCallEvents).toHaveLength(1);
|
|
955
|
+
expect(afterToolCallEvents[0].result.content).toEqual([new TextBlock('replacement')]);
|
|
956
|
+
});
|
|
957
|
+
it('cancel wins over selectedTool', async () => {
|
|
958
|
+
let replacementExecuted = false;
|
|
959
|
+
const replacementTool = createMockTool('replacementTool', () => {
|
|
960
|
+
replacementExecuted = true;
|
|
961
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('replacement')] });
|
|
962
|
+
});
|
|
963
|
+
const registryTool = createMockTool('registryTool', () => {
|
|
964
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('registry')] });
|
|
965
|
+
});
|
|
966
|
+
const model = new MockMessageModel()
|
|
967
|
+
.addTurn({ type: 'toolUseBlock', name: 'registryTool', toolUseId: 'tool-1', input: {} })
|
|
968
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
969
|
+
const agent = new Agent({ model, tools: [registryTool], plugins: [mockPlugin] });
|
|
970
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
971
|
+
event.selectedTool = replacementTool;
|
|
972
|
+
event.cancel = 'blocked';
|
|
973
|
+
});
|
|
974
|
+
await agent.invoke('Test');
|
|
975
|
+
expect(replacementExecuted).toBe(false);
|
|
976
|
+
// AfterToolCallEvent.tool should report the selectedTool even on the cancel path,
|
|
977
|
+
// so observability hooks see a consistent `tool` value regardless of branch.
|
|
978
|
+
const afterToolCallEvents = mockPlugin.invocations.filter((e) => e instanceof AfterToolCallEvent);
|
|
979
|
+
expect(afterToolCallEvents).toHaveLength(1);
|
|
980
|
+
expect(afterToolCallEvents[0].tool).toBe(replacementTool);
|
|
981
|
+
});
|
|
982
|
+
it('works with concurrent tool executor', async () => {
|
|
983
|
+
let originalExecuted = false;
|
|
984
|
+
let replacementExecuted = false;
|
|
985
|
+
const originalTool = createMockTool('originalTool', () => {
|
|
986
|
+
originalExecuted = true;
|
|
987
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('original')] });
|
|
988
|
+
});
|
|
989
|
+
const replacementTool = createMockTool('replacementTool', () => {
|
|
990
|
+
replacementExecuted = true;
|
|
991
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('replacement')] });
|
|
992
|
+
});
|
|
993
|
+
const otherTool = createMockTool('otherTool', () => {
|
|
994
|
+
return new ToolResultBlock({ toolUseId: 'tool-2', status: 'success', content: [new TextBlock('other')] });
|
|
995
|
+
});
|
|
996
|
+
const model = new MockMessageModel()
|
|
997
|
+
.addTurn([
|
|
998
|
+
{ type: 'toolUseBlock', name: 'originalTool', toolUseId: 'tool-1', input: {} },
|
|
999
|
+
{ type: 'toolUseBlock', name: 'otherTool', toolUseId: 'tool-2', input: {} },
|
|
1000
|
+
])
|
|
1001
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1002
|
+
const agent = new Agent({
|
|
1003
|
+
model,
|
|
1004
|
+
tools: [originalTool, otherTool],
|
|
1005
|
+
toolExecutor: 'concurrent',
|
|
1006
|
+
});
|
|
1007
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
1008
|
+
if (event.toolUse.name === 'originalTool') {
|
|
1009
|
+
event.selectedTool = replacementTool;
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
await agent.invoke('Test');
|
|
1013
|
+
expect(originalExecuted).toBe(false);
|
|
1014
|
+
expect(replacementExecuted).toBe(true);
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
describe('BeforeToolCallEvent toolUse mutation', () => {
|
|
1018
|
+
it('passes mutated input to the tool', async () => {
|
|
1019
|
+
const capturedInputs = [];
|
|
1020
|
+
const tool = createMockTool('tool', () => {
|
|
1021
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('ok')] });
|
|
1022
|
+
});
|
|
1023
|
+
// Wrap to capture input via the context the tool receives.
|
|
1024
|
+
const capturingTool = {
|
|
1025
|
+
...tool,
|
|
1026
|
+
async *stream(context) {
|
|
1027
|
+
capturedInputs.push(context.toolUse.input);
|
|
1028
|
+
return yield* tool.stream(context);
|
|
1029
|
+
},
|
|
1030
|
+
};
|
|
1031
|
+
const model = new MockMessageModel()
|
|
1032
|
+
.addTurn({ type: 'toolUseBlock', name: 'tool', toolUseId: 'tool-1', input: { a: 1 } })
|
|
1033
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1034
|
+
const agent = new Agent({ model, tools: [capturingTool] });
|
|
1035
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
1036
|
+
event.toolUse.input = { a: 2, injected: true };
|
|
1037
|
+
});
|
|
1038
|
+
await agent.invoke('Test');
|
|
1039
|
+
expect(capturedInputs).toEqual([{ a: 2, injected: true }]);
|
|
1040
|
+
});
|
|
1041
|
+
it('re-resolves the tool when hook renames toolUse.name', async () => {
|
|
1042
|
+
let origExecuted = false;
|
|
1043
|
+
let renamedExecuted = false;
|
|
1044
|
+
const origTool = createMockTool('orig', () => {
|
|
1045
|
+
origExecuted = true;
|
|
1046
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('orig')] });
|
|
1047
|
+
});
|
|
1048
|
+
const renamedTool = createMockTool('renamed', () => {
|
|
1049
|
+
renamedExecuted = true;
|
|
1050
|
+
return new ToolResultBlock({ toolUseId: 'tool-1', status: 'success', content: [new TextBlock('renamed')] });
|
|
1051
|
+
});
|
|
1052
|
+
const model = new MockMessageModel()
|
|
1053
|
+
.addTurn({ type: 'toolUseBlock', name: 'orig', toolUseId: 'tool-1', input: {} })
|
|
1054
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1055
|
+
const agent = new Agent({ model, tools: [origTool, renamedTool] });
|
|
1056
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
1057
|
+
event.toolUse.name = 'renamed';
|
|
1058
|
+
});
|
|
1059
|
+
await agent.invoke('Test');
|
|
1060
|
+
expect(origExecuted).toBe(false);
|
|
1061
|
+
expect(renamedExecuted).toBe(true);
|
|
1062
|
+
});
|
|
1063
|
+
it('works with concurrent tool executor', async () => {
|
|
1064
|
+
const capturedInputs = {};
|
|
1065
|
+
const baseA = createMockTool('toolA', () => {
|
|
1066
|
+
return new ToolResultBlock({ toolUseId: 'a', status: 'success', content: [new TextBlock('a done')] });
|
|
1067
|
+
});
|
|
1068
|
+
const baseB = createMockTool('toolB', () => {
|
|
1069
|
+
return new ToolResultBlock({ toolUseId: 'b', status: 'success', content: [new TextBlock('b done')] });
|
|
1070
|
+
});
|
|
1071
|
+
const toolA = {
|
|
1072
|
+
...baseA,
|
|
1073
|
+
async *stream(context) {
|
|
1074
|
+
capturedInputs[context.toolUse.name] = context.toolUse.input;
|
|
1075
|
+
return yield* baseA.stream(context);
|
|
1076
|
+
},
|
|
1077
|
+
};
|
|
1078
|
+
const toolB = {
|
|
1079
|
+
...baseB,
|
|
1080
|
+
async *stream(context) {
|
|
1081
|
+
capturedInputs[context.toolUse.name] = context.toolUse.input;
|
|
1082
|
+
return yield* baseB.stream(context);
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
const model = new MockMessageModel()
|
|
1086
|
+
.addTurn([
|
|
1087
|
+
{ type: 'toolUseBlock', name: 'toolA', toolUseId: 'a', input: { original: 'a' } },
|
|
1088
|
+
{ type: 'toolUseBlock', name: 'toolB', toolUseId: 'b', input: { original: 'b' } },
|
|
1089
|
+
])
|
|
1090
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1091
|
+
const agent = new Agent({ model, tools: [toolA, toolB], toolExecutor: 'concurrent' });
|
|
1092
|
+
agent.addHook(BeforeToolCallEvent, (event) => {
|
|
1093
|
+
event.toolUse.input = { mutated: event.toolUse.name };
|
|
1094
|
+
});
|
|
1095
|
+
await agent.invoke('Test');
|
|
1096
|
+
expect(capturedInputs).toEqual({
|
|
1097
|
+
toolA: { mutated: 'toolA' },
|
|
1098
|
+
toolB: { mutated: 'toolB' },
|
|
1099
|
+
});
|
|
1100
|
+
});
|
|
1101
|
+
});
|
|
1102
|
+
describe('AfterToolCallEvent result mutation', () => {
|
|
1103
|
+
it('propagates mutated result into the conversation message', async () => {
|
|
1104
|
+
const tool = createMockTool('tool', () => {
|
|
1105
|
+
return new ToolResultBlock({
|
|
1106
|
+
toolUseId: 'tool-1',
|
|
1107
|
+
status: 'success',
|
|
1108
|
+
content: [new TextBlock('SECRET_VALUE')],
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
const model = new MockMessageModel()
|
|
1112
|
+
.addTurn({ type: 'toolUseBlock', name: 'tool', toolUseId: 'tool-1', input: {} })
|
|
1113
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1114
|
+
const agent = new Agent({ model, tools: [tool] });
|
|
1115
|
+
agent.addHook(AfterToolCallEvent, (event) => {
|
|
1116
|
+
event.result = new ToolResultBlock({
|
|
1117
|
+
toolUseId: 'tool-1',
|
|
1118
|
+
status: 'success',
|
|
1119
|
+
content: [new TextBlock('[REDACTED]')],
|
|
1120
|
+
});
|
|
1121
|
+
});
|
|
1122
|
+
await agent.invoke('Test');
|
|
1123
|
+
const toolResultMessage = agent.messages.find((m) => m.content.some((b) => b.type === 'toolResultBlock' && b.toolUseId === 'tool-1'));
|
|
1124
|
+
expect(toolResultMessage).toBeDefined();
|
|
1125
|
+
const block = toolResultMessage.content.find((b) => b.type === 'toolResultBlock' && b.toolUseId === 'tool-1');
|
|
1126
|
+
expect(block.content).toEqual([new TextBlock('[REDACTED]')]);
|
|
1127
|
+
});
|
|
1128
|
+
it('propagates mutated result into AfterToolsEvent', async () => {
|
|
1129
|
+
const tool = createMockTool('tool', () => {
|
|
1130
|
+
return new ToolResultBlock({
|
|
1131
|
+
toolUseId: 'tool-1',
|
|
1132
|
+
status: 'success',
|
|
1133
|
+
content: [new TextBlock('SECRET_VALUE')],
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
const model = new MockMessageModel()
|
|
1137
|
+
.addTurn({ type: 'toolUseBlock', name: 'tool', toolUseId: 'tool-1', input: {} })
|
|
1138
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1139
|
+
const agent = new Agent({ model, tools: [tool], plugins: [mockPlugin] });
|
|
1140
|
+
agent.addHook(AfterToolCallEvent, (event) => {
|
|
1141
|
+
event.result = new ToolResultBlock({
|
|
1142
|
+
toolUseId: 'tool-1',
|
|
1143
|
+
status: 'success',
|
|
1144
|
+
content: [new TextBlock('[REDACTED]')],
|
|
1145
|
+
});
|
|
1146
|
+
});
|
|
1147
|
+
await agent.invoke('Test');
|
|
1148
|
+
const afterToolsEvents = mockPlugin.invocations.filter((e) => e instanceof AfterToolsEvent);
|
|
1149
|
+
expect(afterToolsEvents).toHaveLength(1);
|
|
1150
|
+
const block = afterToolsEvents[0].message.content.find((b) => b.type === 'toolResultBlock' && b.toolUseId === 'tool-1');
|
|
1151
|
+
expect(block.content).toEqual([new TextBlock('[REDACTED]')]);
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
describe('AfterInvocationEvent resume', () => {
|
|
1155
|
+
it('re-invokes the agent with the resume args', async () => {
|
|
1156
|
+
const model = new MockMessageModel()
|
|
1157
|
+
.addTurn({ type: 'textBlock', text: 'first' })
|
|
1158
|
+
.addTurn({ type: 'textBlock', text: 'second' });
|
|
1159
|
+
let invocationCount = 0;
|
|
1160
|
+
const agent = new Agent({ model });
|
|
1161
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1162
|
+
invocationCount++;
|
|
1163
|
+
if (invocationCount === 1) {
|
|
1164
|
+
event.resume = 'follow-up';
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
const result = await agent.invoke('initial');
|
|
1168
|
+
expect(invocationCount).toBe(2);
|
|
1169
|
+
expect(result).toEqual(expectAgentResult({
|
|
1170
|
+
stopReason: 'endTurn',
|
|
1171
|
+
messageText: 'second',
|
|
1172
|
+
// Meter cycleCount is cumulative across the resume chain (1 cycle per invocation x 2).
|
|
1173
|
+
cycleCount: 2,
|
|
1174
|
+
}));
|
|
1175
|
+
});
|
|
1176
|
+
it('chains multiple resumes', async () => {
|
|
1177
|
+
const model = new MockMessageModel()
|
|
1178
|
+
.addTurn({ type: 'textBlock', text: 'a' })
|
|
1179
|
+
.addTurn({ type: 'textBlock', text: 'b' })
|
|
1180
|
+
.addTurn({ type: 'textBlock', text: 'c' });
|
|
1181
|
+
let invocationCount = 0;
|
|
1182
|
+
const agent = new Agent({ model });
|
|
1183
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1184
|
+
invocationCount++;
|
|
1185
|
+
if (invocationCount === 1)
|
|
1186
|
+
event.resume = 'second';
|
|
1187
|
+
else if (invocationCount === 2)
|
|
1188
|
+
event.resume = 'third';
|
|
1189
|
+
});
|
|
1190
|
+
const result = await agent.invoke('first');
|
|
1191
|
+
expect(invocationCount).toBe(3);
|
|
1192
|
+
expect(result.lastMessage.content[0]).toEqual({ type: 'textBlock', text: 'c' });
|
|
1193
|
+
});
|
|
1194
|
+
it('does not resume when resume is left undefined', async () => {
|
|
1195
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'only' });
|
|
1196
|
+
let invocationCount = 0;
|
|
1197
|
+
const agent = new Agent({ model });
|
|
1198
|
+
agent.addHook(AfterInvocationEvent, () => {
|
|
1199
|
+
invocationCount++;
|
|
1200
|
+
});
|
|
1201
|
+
await agent.invoke('hi');
|
|
1202
|
+
expect(invocationCount).toBe(1);
|
|
1203
|
+
});
|
|
1204
|
+
it('does not resume when the invocation errors', async () => {
|
|
1205
|
+
const model = new MockMessageModel().addTurn(new Error('boom'));
|
|
1206
|
+
let invocationCount = 0;
|
|
1207
|
+
const agent = new Agent({ model });
|
|
1208
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1209
|
+
invocationCount++;
|
|
1210
|
+
event.resume = 'should-not-run';
|
|
1211
|
+
});
|
|
1212
|
+
await expect(agent.invoke('hi')).rejects.toThrow('boom');
|
|
1213
|
+
expect(invocationCount).toBe(1);
|
|
1214
|
+
});
|
|
1215
|
+
it('first-registered hook wins when multiple hooks set resume', async () => {
|
|
1216
|
+
// AfterInvocationEvent reverses callback order (_shouldReverseCallbacks=true),
|
|
1217
|
+
// so the first-registered hook fires last and its resume value wins.
|
|
1218
|
+
const model = new MockMessageModel()
|
|
1219
|
+
.addTurn({ type: 'textBlock', text: 'first' })
|
|
1220
|
+
.addTurn({ type: 'textBlock', text: 'second' });
|
|
1221
|
+
let invocationCount = 0;
|
|
1222
|
+
const agent = new Agent({ model });
|
|
1223
|
+
agent.addHook(BeforeInvocationEvent, () => {
|
|
1224
|
+
invocationCount++;
|
|
1225
|
+
});
|
|
1226
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1227
|
+
if (invocationCount === 1)
|
|
1228
|
+
event.resume = 'first-registered wins';
|
|
1229
|
+
});
|
|
1230
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1231
|
+
if (invocationCount === 1)
|
|
1232
|
+
event.resume = 'second-registered loses';
|
|
1233
|
+
});
|
|
1234
|
+
await agent.invoke('initial');
|
|
1235
|
+
const userTexts = agent.messages
|
|
1236
|
+
.filter((m) => m.role === 'user')
|
|
1237
|
+
.flatMap((m) => m.content.filter((b) => b.type === 'textBlock').map((b) => b.text));
|
|
1238
|
+
expect(userTexts).toEqual(['initial', 'first-registered wins']);
|
|
1239
|
+
});
|
|
1240
|
+
it('ignores resume set during an erroring invocation', async () => {
|
|
1241
|
+
// Resume should not fire when the invocation ends with an error, even if
|
|
1242
|
+
// AfterInvocationEvent (which fires in _stream's finally) still runs.
|
|
1243
|
+
const model = new MockMessageModel().addTurn(new Error('boom'));
|
|
1244
|
+
let resumeFired = false;
|
|
1245
|
+
const agent = new Agent({ model });
|
|
1246
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1247
|
+
event.resume = 'should not run';
|
|
1248
|
+
});
|
|
1249
|
+
agent.addHook(BeforeInvocationEvent, () => {
|
|
1250
|
+
// Track whether BeforeInvocationEvent fires a second time (would indicate resume ran).
|
|
1251
|
+
if (resumeFired)
|
|
1252
|
+
throw new Error('unexpected second invocation');
|
|
1253
|
+
resumeFired = true;
|
|
1254
|
+
});
|
|
1255
|
+
await expect(agent.invoke('hi')).rejects.toThrow('boom');
|
|
1256
|
+
});
|
|
1257
|
+
it('emits only one AgentResultEvent for a resumed chain', async () => {
|
|
1258
|
+
const model = new MockMessageModel()
|
|
1259
|
+
.addTurn({ type: 'textBlock', text: 'first' })
|
|
1260
|
+
.addTurn({ type: 'textBlock', text: 'second' });
|
|
1261
|
+
let invocationCount = 0;
|
|
1262
|
+
const agent = new Agent({ model });
|
|
1263
|
+
agent.addHook(AfterInvocationEvent, (event) => {
|
|
1264
|
+
invocationCount++;
|
|
1265
|
+
if (invocationCount === 1) {
|
|
1266
|
+
event.resume = 'follow-up';
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
const items = await collectIterator(agent.stream('initial'));
|
|
1270
|
+
const agentResults = items.filter((e) => e instanceof AgentResultEvent);
|
|
1271
|
+
expect(agentResults).toHaveLength(1);
|
|
1272
|
+
const afterInvocations = items.filter((e) => e instanceof AfterInvocationEvent);
|
|
1273
|
+
expect(afterInvocations).toHaveLength(2);
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
describe('queue-based lifecycle plugin (WASM bridge pattern)', () => {
|
|
1277
|
+
function createLifecycleBridgePlugin(queue) {
|
|
1278
|
+
return {
|
|
1279
|
+
name: 'strands:lifecycle-bridge',
|
|
1280
|
+
initAgent(agent) {
|
|
1281
|
+
agent.addHook(InitializedEvent, () => {
|
|
1282
|
+
queue.push('initialized');
|
|
1283
|
+
});
|
|
1284
|
+
agent.addHook(BeforeInvocationEvent, () => {
|
|
1285
|
+
queue.push('before-invocation');
|
|
1286
|
+
});
|
|
1287
|
+
agent.addHook(AfterInvocationEvent, () => {
|
|
1288
|
+
queue.push('after-invocation');
|
|
1289
|
+
});
|
|
1290
|
+
agent.addHook(BeforeModelCallEvent, () => {
|
|
1291
|
+
queue.push('before-model-call');
|
|
1292
|
+
});
|
|
1293
|
+
agent.addHook(AfterModelCallEvent, () => {
|
|
1294
|
+
queue.push('after-model-call');
|
|
1295
|
+
});
|
|
1296
|
+
agent.addHook(MessageAddedEvent, () => {
|
|
1297
|
+
queue.push('message-added');
|
|
1298
|
+
});
|
|
1299
|
+
agent.addHook(BeforeToolCallEvent, () => {
|
|
1300
|
+
queue.push('before-tool-call');
|
|
1301
|
+
});
|
|
1302
|
+
agent.addHook(AfterToolCallEvent, () => {
|
|
1303
|
+
queue.push('after-tool-call');
|
|
1304
|
+
});
|
|
1305
|
+
},
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
it('receives lifecycle events when registered via plugins config', async () => {
|
|
1309
|
+
const queue = [];
|
|
1310
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1311
|
+
const agent = new Agent({ model, plugins: [createLifecycleBridgePlugin(queue)] });
|
|
1312
|
+
await agent.invoke('Hi');
|
|
1313
|
+
expect(queue).toStrictEqual([
|
|
1314
|
+
'initialized',
|
|
1315
|
+
'before-invocation',
|
|
1316
|
+
'message-added',
|
|
1317
|
+
'before-model-call',
|
|
1318
|
+
'after-model-call',
|
|
1319
|
+
'message-added',
|
|
1320
|
+
'after-invocation',
|
|
1321
|
+
]);
|
|
1322
|
+
});
|
|
1323
|
+
it('receives no events when passed via non-existent hooks config field', async () => {
|
|
1324
|
+
const queue = [];
|
|
1325
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1326
|
+
const agent = new Agent({ model, hooks: [createLifecycleBridgePlugin(queue)] });
|
|
1327
|
+
await agent.invoke('Hi');
|
|
1328
|
+
expect(queue).toHaveLength(0);
|
|
1329
|
+
});
|
|
618
1330
|
});
|
|
619
1331
|
});
|
|
620
1332
|
//# sourceMappingURL=agent.hook.test.js.map
|