@strands-agents/sdk 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/__fixtures__/agent-helpers.d.ts +16 -1
- package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/agent-helpers.js +42 -0
- package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts +2 -1
- package/dist/src/__fixtures__/tool-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/tool-helpers.js +20 -3
- package/dist/src/__fixtures__/tool-helpers.js.map +1 -1
- package/dist/src/__tests__/interrupt.test.d.ts +2 -0
- package/dist/src/__tests__/interrupt.test.d.ts.map +1 -0
- package/dist/src/__tests__/interrupt.test.js +259 -0
- package/dist/src/__tests__/interrupt.test.js.map +1 -0
- package/dist/src/__tests__/mcp.test.js +226 -0
- package/dist/src/__tests__/mcp.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.hook.test.js +551 -1
- package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js +730 -0
- package/dist/src/agent/__tests__/agent.interrupt.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js +161 -0
- package/dist/src/agent/__tests__/agent.model-retry.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.test.js +118 -0
- package/dist/src/agent/__tests__/agent.test.js.map +1 -1
- package/dist/src/agent/__tests__/snapshot.test.js +50 -4
- package/dist/src/agent/__tests__/snapshot.test.js.map +1 -1
- package/dist/src/agent/agent.d.ts +35 -4
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +548 -222
- package/dist/src/agent/agent.js.map +1 -1
- package/dist/src/agent/snapshot.d.ts +2 -2
- package/dist/src/agent/snapshot.d.ts.map +1 -1
- package/dist/src/agent/snapshot.js +14 -2
- package/dist/src/agent/snapshot.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js +230 -9
- package/dist/src/conversation-manager/__tests__/conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js +19 -6
- package/dist/src/conversation-manager/__tests__/null-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +51 -2
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js +75 -1
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.d.ts +67 -22
- package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.js +65 -13
- package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/index.d.ts +1 -1
- package/dist/src/conversation-manager/index.d.ts.map +1 -1
- package/dist/src/conversation-manager/index.js +1 -1
- package/dist/src/conversation-manager/index.js.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +17 -3
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js +10 -4
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +23 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.js +39 -17
- package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
- package/dist/src/hooks/__tests__/events.test.js +99 -12
- package/dist/src/hooks/__tests__/events.test.js.map +1 -1
- package/dist/src/hooks/__tests__/registry.test.js +166 -2
- package/dist/src/hooks/__tests__/registry.test.js.map +1 -1
- package/dist/src/hooks/events.d.ts +102 -30
- package/dist/src/hooks/events.d.ts.map +1 -1
- package/dist/src/hooks/events.js +87 -6
- package/dist/src/hooks/events.js.map +1 -1
- package/dist/src/hooks/index.d.ts +3 -2
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/hooks/registry.d.ts +12 -12
- package/dist/src/hooks/registry.d.ts.map +1 -1
- package/dist/src/hooks/registry.js +55 -15
- package/dist/src/hooks/registry.js.map +1 -1
- package/dist/src/hooks/types.d.ts +23 -0
- package/dist/src/hooks/types.d.ts.map +1 -1
- package/dist/src/hooks/types.js +17 -1
- package/dist/src/hooks/types.js.map +1 -1
- package/dist/src/index.d.ts +9 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/interrupt.d.ts +220 -0
- package/dist/src/interrupt.d.ts.map +1 -0
- package/dist/src/interrupt.js +274 -0
- package/dist/src/interrupt.js.map +1 -0
- package/dist/src/mcp.d.ts +23 -2
- package/dist/src/mcp.d.ts.map +1 -1
- package/dist/src/mcp.js +77 -18
- package/dist/src/mcp.js.map +1 -1
- package/dist/src/models/__tests__/anthropic.test.js +55 -0
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
- package/dist/src/models/__tests__/bedrock.test.js +115 -0
- package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
- package/dist/src/models/__tests__/defaults.test.d.ts +2 -0
- package/dist/src/models/__tests__/defaults.test.d.ts.map +1 -0
- package/dist/src/models/__tests__/defaults.test.js +36 -0
- package/dist/src/models/__tests__/defaults.test.js.map +1 -0
- package/dist/src/models/__tests__/google.test.js +58 -0
- package/dist/src/models/__tests__/google.test.js.map +1 -1
- package/dist/src/models/anthropic.d.ts +8 -0
- package/dist/src/models/anthropic.d.ts.map +1 -1
- package/dist/src/models/anthropic.js +4 -2
- package/dist/src/models/anthropic.js.map +1 -1
- package/dist/src/models/bedrock.d.ts +15 -0
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +58 -4
- package/dist/src/models/bedrock.js.map +1 -1
- package/dist/src/models/defaults.d.ts +10 -0
- package/dist/src/models/defaults.d.ts.map +1 -1
- package/dist/src/models/defaults.js +129 -0
- package/dist/src/models/defaults.js.map +1 -1
- package/dist/src/models/google/model.d.ts.map +1 -1
- package/dist/src/models/google/model.js +4 -2
- package/dist/src/models/google/model.js.map +1 -1
- package/dist/src/models/google/types.d.ts +8 -0
- package/dist/src/models/google/types.d.ts.map +1 -1
- package/dist/src/models/model.d.ts +15 -0
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +18 -0
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/openai/__tests__/chat.test.js +45 -0
- package/dist/src/models/openai/__tests__/chat.test.js.map +1 -1
- package/dist/src/models/openai/model.d.ts.map +1 -1
- package/dist/src/models/openai/model.js +2 -2
- package/dist/src/models/openai/model.js.map +1 -1
- package/dist/src/multiagent/__tests__/graph.test.js +69 -0
- package/dist/src/multiagent/__tests__/graph.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/nodes.test.js +13 -0
- package/dist/src/multiagent/__tests__/nodes.test.js.map +1 -1
- package/dist/src/multiagent/__tests__/swarm.test.js +77 -0
- package/dist/src/multiagent/__tests__/swarm.test.js.map +1 -1
- package/dist/src/multiagent/graph.d.ts +22 -2
- package/dist/src/multiagent/graph.d.ts.map +1 -1
- package/dist/src/multiagent/graph.js +42 -3
- package/dist/src/multiagent/graph.js.map +1 -1
- package/dist/src/multiagent/multiagent.d.ts +5 -3
- package/dist/src/multiagent/multiagent.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.d.ts +18 -0
- package/dist/src/multiagent/nodes.d.ts.map +1 -1
- package/dist/src/multiagent/nodes.js +14 -1
- package/dist/src/multiagent/nodes.js.map +1 -1
- package/dist/src/multiagent/swarm.d.ts +15 -1
- package/dist/src/multiagent/swarm.d.ts.map +1 -1
- package/dist/src/multiagent/swarm.js +46 -3
- package/dist/src/multiagent/swarm.js.map +1 -1
- package/dist/src/registry/__tests__/tool-registry.test.js +11 -0
- package/dist/src/registry/__tests__/tool-registry.test.js.map +1 -1
- package/dist/src/registry/tool-registry.d.ts +4 -0
- package/dist/src/registry/tool-registry.d.ts.map +1 -1
- package/dist/src/registry/tool-registry.js +6 -0
- package/dist/src/registry/tool-registry.js.map +1 -1
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js +116 -0
- package/dist/src/retry/__tests__/backoff-strategy.test.js.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts +2 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.d.ts.map +1 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js +225 -0
- package/dist/src/retry/__tests__/default-model-retry-strategy.test.js.map +1 -0
- package/dist/src/retry/backoff-strategy.d.ts +108 -0
- package/dist/src/retry/backoff-strategy.d.ts.map +1 -0
- package/dist/src/retry/backoff-strategy.js +86 -0
- package/dist/src/retry/backoff-strategy.js.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts +76 -0
- package/dist/src/retry/default-model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/default-model-retry-strategy.js +104 -0
- package/dist/src/retry/default-model-retry-strategy.js.map +1 -0
- package/dist/src/retry/index.d.ts +8 -0
- package/dist/src/retry/index.d.ts.map +1 -0
- package/dist/src/retry/index.js +7 -0
- package/dist/src/retry/index.js.map +1 -0
- package/dist/src/retry/model-retry-strategy.d.ts +80 -0
- package/dist/src/retry/model-retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/model-retry-strategy.js +85 -0
- package/dist/src/retry/model-retry-strategy.js.map +1 -0
- package/dist/src/retry/retry-strategy.d.ts +34 -0
- package/dist/src/retry/retry-strategy.d.ts.map +1 -0
- package/dist/src/retry/retry-strategy.js +25 -0
- package/dist/src/retry/retry-strategy.js.map +1 -0
- package/dist/src/session/__tests__/session-manager.test.js +39 -0
- package/dist/src/session/__tests__/session-manager.test.js.map +1 -1
- package/dist/src/session/session-manager.d.ts +6 -0
- package/dist/src/session/session-manager.d.ts.map +1 -1
- package/dist/src/session/session-manager.js +8 -0
- package/dist/src/session/session-manager.js.map +1 -1
- package/dist/src/tools/__tests__/tool.test.js +24 -1
- package/dist/src/tools/__tests__/tool.test.js.map +1 -1
- package/dist/src/tools/function-tool.d.ts.map +1 -1
- package/dist/src/tools/function-tool.js +6 -1
- package/dist/src/tools/function-tool.js.map +1 -1
- package/dist/src/tools/tool.d.ts +10 -1
- package/dist/src/tools/tool.d.ts.map +1 -1
- package/dist/src/tools/tool.js +12 -0
- package/dist/src/tools/tool.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/types/agent.d.ts +22 -3
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/agent.js +8 -0
- package/dist/src/types/agent.js.map +1 -1
- package/dist/src/types/interrupt.d.ts +103 -0
- package/dist/src/types/interrupt.d.ts.map +1 -0
- package/dist/src/types/interrupt.js +63 -0
- package/dist/src/types/interrupt.js.map +1 -0
- package/dist/src/types/messages.d.ts +2 -1
- package/dist/src/types/messages.d.ts.map +1 -1
- package/dist/src/types/messages.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +292 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +148 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js +78 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.node.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts +23 -0
- package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/index.js +21 -0
- package/dist/src/vended-plugins/context-offloader/index.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts +48 -0
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js +244 -0
- package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts +114 -0
- package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/storage.js +204 -0
- package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +12 -0
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +3 -0
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/bash.js +0 -3
- package/dist/src/vended-tools/bash/bash.js.map +1 -1
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +3 -0
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js +3 -0
- package/dist/src/vended-tools/notebook/__tests__/notebook.test.js.map +1 -1
- package/package.json +9 -5
|
@@ -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, ModelMessageEvent, } 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;
|
|
@@ -35,6 +36,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
35
36
|
agent,
|
|
36
37
|
model: agent.model,
|
|
37
38
|
invocationState: {},
|
|
39
|
+
attemptCount: 1,
|
|
38
40
|
stopData: {
|
|
39
41
|
stopReason: 'endTurn',
|
|
40
42
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
@@ -70,6 +72,7 @@ describe('Agent Hooks Integration', () => {
|
|
|
70
72
|
agent,
|
|
71
73
|
model: agent.model,
|
|
72
74
|
invocationState: {},
|
|
75
|
+
attemptCount: 1,
|
|
73
76
|
stopData: {
|
|
74
77
|
stopReason: 'endTurn',
|
|
75
78
|
message: new Message({ role: 'assistant', content: [new TextBlock('Hello')] }),
|
|
@@ -668,6 +671,153 @@ describe('Agent Hooks Integration', () => {
|
|
|
668
671
|
}));
|
|
669
672
|
});
|
|
670
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
|
+
});
|
|
671
821
|
describe('cancel invocation via hooks', () => {
|
|
672
822
|
it('cancels invocation with default message when cancel is true', async () => {
|
|
673
823
|
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
@@ -778,5 +928,405 @@ describe('Agent Hooks Integration', () => {
|
|
|
778
928
|
expect(result.lastMessage.content[0]).toEqual(new TextBlock('Hello'));
|
|
779
929
|
});
|
|
780
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
|
+
});
|
|
1330
|
+
});
|
|
781
1331
|
});
|
|
782
1332
|
//# sourceMappingURL=agent.hook.test.js.map
|