@strands-agents/sdk 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/src/__fixtures__/agent-helpers.d.ts.map +1 -1
- package/dist/src/__fixtures__/agent-helpers.js +9 -0
- package/dist/src/__fixtures__/agent-helpers.js.map +1 -1
- package/dist/src/__fixtures__/register-node-defaults.d.ts +2 -0
- package/dist/src/__fixtures__/register-node-defaults.d.ts.map +1 -0
- package/dist/src/__fixtures__/register-node-defaults.js +6 -0
- package/dist/src/__fixtures__/register-node-defaults.js.map +1 -0
- package/dist/src/__fixtures__/test-sandbox.node.d.ts.map +1 -1
- package/dist/src/__fixtures__/test-sandbox.node.js +4 -3
- package/dist/src/__fixtures__/test-sandbox.node.js.map +1 -1
- package/dist/src/__tests__/default-slot.test.d.ts +2 -0
- package/dist/src/__tests__/default-slot.test.d.ts.map +1 -0
- package/dist/src/__tests__/default-slot.test.js +33 -0
- package/dist/src/__tests__/default-slot.test.js.map +1 -0
- package/dist/src/a2a/__tests__/async-lock.test.d.ts +2 -0
- package/dist/src/a2a/__tests__/async-lock.test.d.ts.map +1 -0
- package/dist/src/a2a/__tests__/async-lock.test.js +137 -0
- package/dist/src/a2a/__tests__/async-lock.test.js.map +1 -0
- package/dist/src/a2a/__tests__/executor.test.js +146 -8
- package/dist/src/a2a/__tests__/executor.test.js.map +1 -1
- package/dist/src/a2a/__tests__/server.test.js +20 -0
- package/dist/src/a2a/__tests__/server.test.js.map +1 -1
- package/dist/src/a2a/async-lock.d.ts +22 -0
- package/dist/src/a2a/async-lock.d.ts.map +1 -0
- package/dist/src/a2a/async-lock.js +38 -0
- package/dist/src/a2a/async-lock.js.map +1 -0
- package/dist/src/a2a/executor.d.ts +59 -24
- package/dist/src/a2a/executor.d.ts.map +1 -1
- package/dist/src/a2a/executor.js +209 -32
- package/dist/src/a2a/executor.js.map +1 -1
- package/dist/src/a2a/index.d.ts +1 -1
- package/dist/src/a2a/index.d.ts.map +1 -1
- package/dist/src/a2a/index.js +1 -1
- package/dist/src/a2a/index.js.map +1 -1
- package/dist/src/a2a/server.d.ts +18 -2
- package/dist/src/a2a/server.d.ts.map +1 -1
- package/dist/src/a2a/server.js +13 -2
- package/dist/src/a2a/server.js.map +1 -1
- package/dist/src/agent/__tests__/agent.context-manager.test.d.ts +2 -0
- package/dist/src/agent/__tests__/agent.context-manager.test.d.ts.map +1 -0
- package/dist/src/agent/__tests__/agent.context-manager.test.js +107 -0
- package/dist/src/agent/__tests__/agent.context-manager.test.js.map +1 -0
- package/dist/src/agent/__tests__/agent.stateful-model.test.js +2 -2
- package/dist/src/agent/__tests__/agent.stateful-model.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.tracer.test.node.js +14 -0
- package/dist/src/agent/__tests__/agent.tracer.test.node.js.map +1 -1
- package/dist/src/agent/agent.d.ts +135 -2
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +506 -189
- package/dist/src/agent/agent.js.map +1 -1
- package/dist/src/context-manager/modes/agentic/agentic-context.d.ts +19 -0
- package/dist/src/context-manager/modes/agentic/agentic-context.d.ts.map +1 -0
- package/dist/src/context-manager/modes/agentic/agentic-context.js +245 -0
- package/dist/src/context-manager/modes/agentic/agentic-context.js.map +1 -0
- package/dist/src/conversation-manager/__tests__/agentic-context.test.d.ts +2 -0
- package/dist/src/conversation-manager/__tests__/agentic-context.test.d.ts.map +1 -0
- package/dist/src/conversation-manager/__tests__/agentic-context.test.js +332 -0
- package/dist/src/conversation-manager/__tests__/agentic-context.test.js.map +1 -0
- package/dist/src/conversation-manager/__tests__/context-compression.test.d.ts +2 -0
- package/dist/src/conversation-manager/__tests__/context-compression.test.d.ts.map +1 -0
- package/dist/src/conversation-manager/__tests__/context-compression.test.js +176 -0
- package/dist/src/conversation-manager/__tests__/context-compression.test.js.map +1 -0
- package/dist/src/conversation-manager/__tests__/pin.test.d.ts +2 -0
- package/dist/src/conversation-manager/__tests__/pin.test.d.ts.map +1 -0
- package/dist/src/conversation-manager/__tests__/pin.test.js +119 -0
- package/dist/src/conversation-manager/__tests__/pin.test.js.map +1 -0
- package/dist/src/conversation-manager/__tests__/sliding-window-conversation-manager.test.js +49 -0
- 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 +58 -0
- package/dist/src/conversation-manager/__tests__/summarizing-conversation-manager.test.js.map +1 -1
- package/dist/src/conversation-manager/__tests__/token-usage-middleware.test.d.ts +2 -0
- package/dist/src/conversation-manager/__tests__/token-usage-middleware.test.d.ts.map +1 -0
- package/dist/src/conversation-manager/__tests__/token-usage-middleware.test.js +138 -0
- package/dist/src/conversation-manager/__tests__/token-usage-middleware.test.js.map +1 -0
- package/dist/src/conversation-manager/compression/context-compression.d.ts +39 -0
- package/dist/src/conversation-manager/compression/context-compression.d.ts.map +1 -0
- package/dist/src/conversation-manager/compression/context-compression.js +150 -0
- package/dist/src/conversation-manager/compression/context-compression.js.map +1 -0
- package/dist/src/conversation-manager/compression/pin-message.d.ts +45 -0
- package/dist/src/conversation-manager/compression/pin-message.d.ts.map +1 -0
- package/dist/src/conversation-manager/compression/pin-message.js +106 -0
- package/dist/src/conversation-manager/compression/pin-message.js.map +1 -0
- package/dist/src/conversation-manager/conversation-manager.d.ts +2 -0
- package/dist/src/conversation-manager/conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/conversation-manager.js +2 -2
- package/dist/src/conversation-manager/conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts +7 -0
- package/dist/src/conversation-manager/sliding-window-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js +30 -38
- package/dist/src/conversation-manager/sliding-window-conversation-manager.js.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts +7 -19
- package/dist/src/conversation-manager/summarizing-conversation-manager.d.ts.map +1 -1
- package/dist/src/conversation-manager/summarizing-conversation-manager.js +20 -109
- package/dist/src/conversation-manager/summarizing-conversation-manager.js.map +1 -1
- package/dist/src/default-slot.d.ts +6 -0
- package/dist/src/default-slot.d.ts.map +1 -0
- package/dist/src/default-slot.js +18 -0
- package/dist/src/default-slot.js.map +1 -0
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +11 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +8 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/index.node.d.ts +2 -0
- package/dist/src/index.node.d.ts.map +1 -0
- package/dist/src/index.node.js +9 -0
- package/dist/src/index.node.js.map +1 -0
- package/dist/src/injection/__tests__/message-injection.test.d.ts +2 -0
- package/dist/src/injection/__tests__/message-injection.test.d.ts.map +1 -0
- package/dist/src/injection/__tests__/message-injection.test.js +200 -0
- package/dist/src/injection/__tests__/message-injection.test.js.map +1 -0
- package/dist/src/injection/index.d.ts +6 -0
- package/dist/src/injection/index.d.ts.map +1 -0
- package/dist/src/injection/index.js +2 -0
- package/dist/src/injection/index.js.map +1 -0
- package/dist/src/injection/message-injection.d.ts +65 -0
- package/dist/src/injection/message-injection.d.ts.map +1 -0
- package/dist/src/injection/message-injection.js +134 -0
- package/dist/src/injection/message-injection.js.map +1 -0
- package/dist/src/injection/types.d.ts +63 -0
- package/dist/src/injection/types.d.ts.map +1 -0
- package/dist/src/injection/types.js +2 -0
- package/dist/src/injection/types.js.map +1 -0
- package/dist/src/injection/xml.d.ts +27 -0
- package/dist/src/injection/xml.d.ts.map +1 -0
- package/dist/src/injection/xml.js +31 -0
- package/dist/src/injection/xml.js.map +1 -0
- package/dist/src/interrupt.d.ts +5 -1
- package/dist/src/interrupt.d.ts.map +1 -1
- package/dist/src/interrupt.js +6 -0
- package/dist/src/interrupt.js.map +1 -1
- package/dist/src/memory/__tests__/memory-manager.test.d.ts +2 -0
- package/dist/src/memory/__tests__/memory-manager.test.d.ts.map +1 -0
- package/dist/src/memory/__tests__/memory-manager.test.js +679 -0
- package/dist/src/memory/__tests__/memory-manager.test.js.map +1 -0
- package/dist/src/memory/extraction/__tests__/extraction.test.d.ts +2 -0
- package/dist/src/memory/extraction/__tests__/extraction.test.d.ts.map +1 -0
- package/dist/src/memory/extraction/__tests__/extraction.test.js +637 -0
- package/dist/src/memory/extraction/__tests__/extraction.test.js.map +1 -0
- package/dist/src/memory/extraction/__tests__/model-extractor.test.d.ts +2 -0
- package/dist/src/memory/extraction/__tests__/model-extractor.test.d.ts.map +1 -0
- package/dist/src/memory/extraction/__tests__/model-extractor.test.js +68 -0
- package/dist/src/memory/extraction/__tests__/model-extractor.test.js.map +1 -0
- package/dist/src/memory/extraction/__tests__/resolve-extraction-config.test.d.ts +2 -0
- package/dist/src/memory/extraction/__tests__/resolve-extraction-config.test.d.ts.map +1 -0
- package/dist/src/memory/extraction/__tests__/resolve-extraction-config.test.js +81 -0
- package/dist/src/memory/extraction/__tests__/resolve-extraction-config.test.js.map +1 -0
- package/dist/src/memory/extraction/coordinator.d.ts +128 -0
- package/dist/src/memory/extraction/coordinator.d.ts.map +1 -0
- package/dist/src/memory/extraction/coordinator.js +245 -0
- package/dist/src/memory/extraction/coordinator.js.map +1 -0
- package/dist/src/memory/extraction/model-extractor.d.ts +32 -0
- package/dist/src/memory/extraction/model-extractor.d.ts.map +1 -0
- package/dist/src/memory/extraction/model-extractor.js +118 -0
- package/dist/src/memory/extraction/model-extractor.js.map +1 -0
- package/dist/src/memory/extraction/resolve-extraction-config.d.ts +46 -0
- package/dist/src/memory/extraction/resolve-extraction-config.d.ts.map +1 -0
- package/dist/src/memory/extraction/resolve-extraction-config.js +59 -0
- package/dist/src/memory/extraction/resolve-extraction-config.js.map +1 -0
- package/dist/src/memory/extraction/triggers.d.ts +41 -0
- package/dist/src/memory/extraction/triggers.d.ts.map +1 -0
- package/dist/src/memory/extraction/triggers.js +59 -0
- package/dist/src/memory/extraction/triggers.js.map +1 -0
- package/dist/src/memory/extraction/types.d.ts +133 -0
- package/dist/src/memory/extraction/types.d.ts.map +1 -0
- package/dist/src/memory/extraction/types.js +19 -0
- package/dist/src/memory/extraction/types.js.map +1 -0
- package/dist/src/memory/index.d.ts +10 -0
- package/dist/src/memory/index.d.ts.map +1 -0
- package/dist/src/memory/index.js +5 -0
- package/dist/src/memory/index.js.map +1 -0
- package/dist/src/memory/memory-manager.d.ts +178 -0
- package/dist/src/memory/memory-manager.d.ts.map +1 -0
- package/dist/src/memory/memory-manager.js +526 -0
- package/dist/src/memory/memory-manager.js.map +1 -0
- package/dist/src/memory/types.d.ts +278 -0
- package/dist/src/memory/types.d.ts.map +1 -0
- package/dist/src/memory/types.js +2 -0
- package/dist/src/memory/types.js.map +1 -0
- package/dist/src/middleware/__tests__/agent-middleware.test.d.ts +2 -0
- package/dist/src/middleware/__tests__/agent-middleware.test.d.ts.map +1 -0
- package/dist/src/middleware/__tests__/agent-middleware.test.js +1206 -0
- package/dist/src/middleware/__tests__/agent-middleware.test.js.map +1 -0
- package/dist/src/middleware/__tests__/copy-on-input.test.d.ts +2 -0
- package/dist/src/middleware/__tests__/copy-on-input.test.d.ts.map +1 -0
- package/dist/src/middleware/__tests__/copy-on-input.test.js +379 -0
- package/dist/src/middleware/__tests__/copy-on-input.test.js.map +1 -0
- package/dist/src/middleware/__tests__/custom-stages.test.d.ts +2 -0
- package/dist/src/middleware/__tests__/custom-stages.test.d.ts.map +1 -0
- package/dist/src/middleware/__tests__/custom-stages.test.js +97 -0
- package/dist/src/middleware/__tests__/custom-stages.test.js.map +1 -0
- package/dist/src/middleware/__tests__/middleware-interrupts.test.d.ts +2 -0
- package/dist/src/middleware/__tests__/middleware-interrupts.test.d.ts.map +1 -0
- package/dist/src/middleware/__tests__/middleware-interrupts.test.js +267 -0
- package/dist/src/middleware/__tests__/middleware-interrupts.test.js.map +1 -0
- package/dist/src/middleware/__tests__/registry.test.d.ts +2 -0
- package/dist/src/middleware/__tests__/registry.test.d.ts.map +1 -0
- package/dist/src/middleware/__tests__/registry.test.js +525 -0
- package/dist/src/middleware/__tests__/registry.test.js.map +1 -0
- package/dist/src/middleware/index.d.ts +5 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/index.js +3 -0
- package/dist/src/middleware/index.js.map +1 -0
- package/dist/src/middleware/registry.d.ts +58 -0
- package/dist/src/middleware/registry.d.ts.map +1 -0
- package/dist/src/middleware/registry.js +107 -0
- package/dist/src/middleware/registry.js.map +1 -0
- package/dist/src/middleware/stages.d.ts +145 -0
- package/dist/src/middleware/stages.d.ts.map +1 -0
- package/dist/src/middleware/stages.js +34 -0
- package/dist/src/middleware/stages.js.map +1 -0
- package/dist/src/middleware/types.d.ts +88 -0
- package/dist/src/middleware/types.d.ts.map +1 -0
- package/dist/src/middleware/types.js +2 -0
- package/dist/src/middleware/types.js.map +1 -0
- package/dist/src/models/__tests__/anthropic.test.js +16 -1
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -1
- package/dist/src/models/__tests__/bedrock.test.js +39 -0
- package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
- package/dist/src/models/__tests__/model.test.js +46 -3
- package/dist/src/models/__tests__/model.test.js.map +1 -1
- package/dist/src/models/anthropic.js +2 -2
- package/dist/src/models/anthropic.js.map +1 -1
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +12 -5
- package/dist/src/models/bedrock.js.map +1 -1
- package/dist/src/models/defaults.d.ts.map +1 -1
- package/dist/src/models/defaults.js +2 -0
- package/dist/src/models/defaults.js.map +1 -1
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +7 -3
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/openai/__tests__/responses.test.js +8 -14
- package/dist/src/models/openai/__tests__/responses.test.js.map +1 -1
- package/dist/src/sandbox/__tests__/default.test.browser.d.ts +2 -0
- package/dist/src/sandbox/__tests__/default.test.browser.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/default.test.browser.js +11 -0
- package/dist/src/sandbox/__tests__/default.test.browser.js.map +1 -0
- package/dist/src/sandbox/__tests__/default.test.node.d.ts +2 -0
- package/dist/src/sandbox/__tests__/default.test.node.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/default.test.node.js +23 -0
- package/dist/src/sandbox/__tests__/default.test.node.js.map +1 -0
- package/dist/src/sandbox/__tests__/docker.test.node.d.ts +2 -0
- package/dist/src/sandbox/__tests__/docker.test.node.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/docker.test.node.js +89 -0
- package/dist/src/sandbox/__tests__/docker.test.node.js.map +1 -0
- package/dist/src/sandbox/__tests__/errors.test.node.d.ts +2 -0
- package/dist/src/sandbox/__tests__/errors.test.node.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/errors.test.node.js +33 -0
- package/dist/src/sandbox/__tests__/errors.test.node.js.map +1 -0
- package/dist/src/sandbox/__tests__/not-a-sandbox-local-environment.test.node.d.ts +2 -0
- package/dist/src/sandbox/__tests__/not-a-sandbox-local-environment.test.node.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/not-a-sandbox-local-environment.test.node.js +124 -0
- package/dist/src/sandbox/__tests__/not-a-sandbox-local-environment.test.node.js.map +1 -0
- package/dist/src/sandbox/__tests__/posix-shell.test.node.js +50 -4
- package/dist/src/sandbox/__tests__/posix-shell.test.node.js.map +1 -1
- package/dist/src/sandbox/__tests__/ssh.test.node.d.ts +2 -0
- package/dist/src/sandbox/__tests__/ssh.test.node.d.ts.map +1 -0
- package/dist/src/sandbox/__tests__/ssh.test.node.js +262 -0
- package/dist/src/sandbox/__tests__/ssh.test.node.js.map +1 -0
- package/dist/src/sandbox/base.d.ts +17 -4
- package/dist/src/sandbox/base.d.ts.map +1 -1
- package/dist/src/sandbox/base.js +10 -2
- package/dist/src/sandbox/base.js.map +1 -1
- package/dist/src/sandbox/constants.d.ts +18 -0
- package/dist/src/sandbox/constants.d.ts.map +1 -1
- package/dist/src/sandbox/constants.js +20 -0
- package/dist/src/sandbox/constants.js.map +1 -1
- package/dist/src/sandbox/default.d.ts +3 -0
- package/dist/src/sandbox/default.d.ts.map +1 -0
- package/dist/src/sandbox/default.js +3 -0
- package/dist/src/sandbox/default.js.map +1 -0
- package/dist/src/sandbox/docker.d.ts +38 -0
- package/dist/src/sandbox/docker.d.ts.map +1 -0
- package/dist/src/sandbox/docker.js +61 -0
- package/dist/src/sandbox/docker.js.map +1 -0
- package/dist/src/sandbox/errors.d.ts +26 -0
- package/dist/src/sandbox/errors.d.ts.map +1 -0
- package/dist/src/sandbox/errors.js +35 -0
- package/dist/src/sandbox/errors.js.map +1 -0
- package/dist/src/sandbox/index.d.ts +5 -0
- package/dist/src/sandbox/index.d.ts.map +1 -0
- package/dist/src/sandbox/index.js +4 -0
- package/dist/src/sandbox/index.js.map +1 -0
- package/dist/src/sandbox/not-a-sandbox-local-environment.d.ts +16 -0
- package/dist/src/sandbox/not-a-sandbox-local-environment.d.ts.map +1 -0
- package/dist/src/sandbox/not-a-sandbox-local-environment.js +83 -0
- package/dist/src/sandbox/not-a-sandbox-local-environment.js.map +1 -0
- package/dist/src/sandbox/posix-shell.d.ts +21 -0
- package/dist/src/sandbox/posix-shell.d.ts.map +1 -1
- package/dist/src/sandbox/posix-shell.js +41 -3
- package/dist/src/sandbox/posix-shell.js.map +1 -1
- package/dist/src/sandbox/ssh.d.ts +56 -0
- package/dist/src/sandbox/ssh.d.ts.map +1 -0
- package/dist/src/sandbox/ssh.js +121 -0
- package/dist/src/sandbox/ssh.js.map +1 -0
- package/dist/src/sandbox/stream-process.d.ts.map +1 -1
- package/dist/src/sandbox/stream-process.js +3 -2
- package/dist/src/sandbox/stream-process.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/types/agent.d.ts +21 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/agent.js.map +1 -1
- package/dist/src/types/messages.d.ts +9 -1
- package/dist/src/types/messages.d.ts.map +1 -1
- package/dist/src/types/messages.js +13 -1
- package/dist/src/types/messages.js.map +1 -1
- package/dist/src/vended-interventions/cedar/__tests__/cedar.test.node.d.ts +2 -0
- package/dist/src/vended-interventions/cedar/__tests__/cedar.test.node.d.ts.map +1 -0
- package/dist/src/vended-interventions/cedar/__tests__/cedar.test.node.js +675 -0
- package/dist/src/vended-interventions/cedar/__tests__/cedar.test.node.js.map +1 -0
- package/dist/src/vended-interventions/cedar/cedar.d.ts +102 -0
- package/dist/src/vended-interventions/cedar/cedar.d.ts.map +1 -0
- package/dist/src/vended-interventions/cedar/cedar.js +228 -0
- package/dist/src/vended-interventions/cedar/cedar.js.map +1 -0
- package/dist/src/vended-interventions/cedar/index.d.ts +3 -0
- package/dist/src/vended-interventions/cedar/index.d.ts.map +1 -0
- package/dist/src/vended-interventions/cedar/index.js +2 -0
- package/dist/src/vended-interventions/cedar/index.js.map +1 -0
- package/dist/src/vended-interventions/cedar/schema-generator.d.ts +10 -0
- package/dist/src/vended-interventions/cedar/schema-generator.d.ts.map +1 -0
- package/dist/src/vended-interventions/cedar/schema-generator.js +33 -0
- package/dist/src/vended-interventions/cedar/schema-generator.js.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/__tests__/bedrock-knowledge-base-store.test.d.ts +2 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/__tests__/bedrock-knowledge-base-store.test.d.ts.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/__tests__/bedrock-knowledge-base-store.test.js +611 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/__tests__/bedrock-knowledge-base-store.test.js.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/index.d.ts +2 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/index.d.ts.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/index.js +2 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/index.js.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/store.d.ts +230 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/store.d.ts.map +1 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/store.js +370 -0
- package/dist/src/vended-memory-stores/bedrock-knowledge-base/store.js.map +1 -0
- package/dist/src/vended-plugins/context-injector/__tests__/plugin.test.d.ts +2 -0
- package/dist/src/vended-plugins/context-injector/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-injector/__tests__/plugin.test.js +96 -0
- package/dist/src/vended-plugins/context-injector/__tests__/plugin.test.js.map +1 -0
- package/dist/src/vended-plugins/context-injector/index.d.ts +25 -0
- package/dist/src/vended-plugins/context-injector/index.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-injector/index.js +23 -0
- package/dist/src/vended-plugins/context-injector/index.js.map +1 -0
- package/dist/src/vended-plugins/context-injector/plugin.d.ts +55 -0
- package/dist/src/vended-plugins/context-injector/plugin.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-injector/plugin.js +41 -0
- package/dist/src/vended-plugins/context-injector/plugin.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/file-storage-sandbox.test.node.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/file-storage-sandbox.test.node.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/file-storage-sandbox.test.node.js +68 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/file-storage-sandbox.test.node.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js +43 -4
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.node.d.ts +2 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.node.d.ts.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.node.js +93 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/plugin.test.node.js.map +1 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js +68 -0
- package/dist/src/vended-plugins/context-offloader/__tests__/storage.test.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/index.d.ts +1 -1
- package/dist/src/vended-plugins/context-offloader/index.d.ts.map +1 -1
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts +4 -1
- package/dist/src/vended-plugins/context-offloader/plugin.d.ts.map +1 -1
- package/dist/src/vended-plugins/context-offloader/plugin.js +40 -8
- package/dist/src/vended-plugins/context-offloader/plugin.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/search.d.ts.map +1 -1
- package/dist/src/vended-plugins/context-offloader/search.js +3 -5
- package/dist/src/vended-plugins/context-offloader/search.js.map +1 -1
- package/dist/src/vended-plugins/context-offloader/storage.d.ts +58 -6
- package/dist/src/vended-plugins/context-offloader/storage.d.ts.map +1 -1
- package/dist/src/vended-plugins/context-offloader/storage.js +136 -14
- package/dist/src/vended-plugins/context-offloader/storage.js.map +1 -1
- package/dist/src/vended-plugins/goal/__tests__/plugin.test.d.ts +2 -0
- package/dist/src/vended-plugins/goal/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/src/vended-plugins/goal/__tests__/plugin.test.js +736 -0
- package/dist/src/vended-plugins/goal/__tests__/plugin.test.js.map +1 -0
- package/dist/src/vended-plugins/goal/index.d.ts +21 -0
- package/dist/src/vended-plugins/goal/index.d.ts.map +1 -0
- package/dist/src/vended-plugins/goal/index.js +20 -0
- package/dist/src/vended-plugins/goal/index.js.map +1 -0
- package/dist/src/vended-plugins/goal/judge.d.ts +41 -0
- package/dist/src/vended-plugins/goal/judge.d.ts.map +1 -0
- package/dist/src/vended-plugins/goal/judge.js +92 -0
- package/dist/src/vended-plugins/goal/judge.js.map +1 -0
- package/dist/src/vended-plugins/goal/plugin.d.ts +214 -0
- package/dist/src/vended-plugins/goal/plugin.d.ts.map +1 -0
- package/dist/src/vended-plugins/goal/plugin.js +287 -0
- package/dist/src/vended-plugins/goal/plugin.js.map +1 -0
- package/dist/src/vended-plugins/index.d.ts +3 -1
- package/dist/src/vended-plugins/index.d.ts.map +1 -1
- package/dist/src/vended-plugins/index.js +3 -1
- package/dist/src/vended-plugins/index.js.map +1 -1
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js +17 -7
- package/dist/src/vended-plugins/skills/__tests__/agent-skills.test.node.js.map +1 -1
- package/dist/src/vended-plugins/skills/agent-skills.d.ts +21 -7
- package/dist/src/vended-plugins/skills/agent-skills.d.ts.map +1 -1
- package/dist/src/vended-plugins/skills/agent-skills.js +144 -77
- package/dist/src/vended-plugins/skills/agent-skills.js.map +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js +44 -4
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -1
- package/dist/src/vended-tools/bash/bash.d.ts +3 -24
- package/dist/src/vended-tools/bash/bash.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/bash.js +9 -9
- package/dist/src/vended-tools/bash/bash.js.map +1 -1
- package/dist/src/vended-tools/bash/index.d.ts +3 -1
- package/dist/src/vended-tools/bash/index.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/index.js +2 -1
- package/dist/src/vended-tools/bash/index.js.map +1 -1
- package/dist/src/vended-tools/bash/make-bash.d.ts +22 -0
- package/dist/src/vended-tools/bash/make-bash.d.ts.map +1 -0
- package/dist/src/vended-tools/bash/make-bash.js +40 -0
- package/dist/src/vended-tools/bash/make-bash.js.map +1 -0
- package/dist/src/vended-tools/bash/types.d.ts +1 -0
- package/dist/src/vended-tools/bash/types.d.ts.map +1 -1
- package/dist/src/vended-tools/bash/types.js +2 -0
- package/dist/src/vended-tools/bash/types.js.map +1 -1
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js +83 -1
- package/dist/src/vended-tools/file-editor/__tests__/file-editor.test.node.js.map +1 -1
- package/dist/src/vended-tools/file-editor/file-editor.d.ts +19 -10
- package/dist/src/vended-tools/file-editor/file-editor.d.ts.map +1 -1
- package/dist/src/vended-tools/file-editor/file-editor.js +188 -218
- package/dist/src/vended-tools/file-editor/file-editor.js.map +1 -1
- package/dist/src/vended-tools/file-editor/index.d.ts +2 -1
- package/dist/src/vended-tools/file-editor/index.d.ts.map +1 -1
- package/dist/src/vended-tools/file-editor/index.js +1 -1
- package/dist/src/vended-tools/file-editor/index.js.map +1 -1
- package/package.json +59 -6
- package/dist/src/utils/shell-quote.d.ts +0 -12
- package/dist/src/utils/shell-quote.d.ts.map +0 -1
- package/dist/src/utils/shell-quote.js +0 -14
- package/dist/src/utils/shell-quote.js.map +0 -1
|
@@ -0,0 +1,1206 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Agent } from '../../agent/agent.js';
|
|
3
|
+
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js';
|
|
4
|
+
import { collectGenerator } from '../../__fixtures__/model-test-helpers.js';
|
|
5
|
+
import { createMockTool } from '../../__fixtures__/tool-helpers.js';
|
|
6
|
+
import { AgentStreamStage, ExecuteToolStage, InvokeModelStage } from '../stages.js';
|
|
7
|
+
import { TextBlock, ToolResultBlock, Message } from '../../types/messages.js';
|
|
8
|
+
import { AfterToolCallEvent, BeforeModelCallEvent, AfterModelCallEvent, BeforeToolCallEvent, BeforeInvocationEvent, AfterInvocationEvent, AgentResultEvent, ContentBlockEvent, } from '../../hooks/events.js';
|
|
9
|
+
describe('Agent middleware integration — InvokeModelStage', () => {
|
|
10
|
+
describe('addMiddleware registers handler and it executes on model call', () => {
|
|
11
|
+
it('middleware handler is invoked during model call', async () => {
|
|
12
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
13
|
+
const agent = new Agent({ model, printer: false });
|
|
14
|
+
const middlewareCalled = vi.fn();
|
|
15
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
16
|
+
middlewareCalled();
|
|
17
|
+
return yield* next(context);
|
|
18
|
+
});
|
|
19
|
+
await agent.invoke('Test prompt');
|
|
20
|
+
expect(middlewareCalled).toHaveBeenCalledOnce();
|
|
21
|
+
});
|
|
22
|
+
it('middleware receives InvokeModelContext with correct fields', async () => {
|
|
23
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
24
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
25
|
+
toolUseId: 'tool-1',
|
|
26
|
+
status: 'success',
|
|
27
|
+
content: [new TextBlock('ok')],
|
|
28
|
+
}));
|
|
29
|
+
const agent = new Agent({ model, tools: [tool], printer: false, systemPrompt: 'Be helpful' });
|
|
30
|
+
let receivedContext;
|
|
31
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
32
|
+
receivedContext = context;
|
|
33
|
+
return yield* next(context);
|
|
34
|
+
});
|
|
35
|
+
await agent.invoke('Test prompt');
|
|
36
|
+
expect(receivedContext?.agent).toBe(agent);
|
|
37
|
+
expect(receivedContext).toMatchObject({
|
|
38
|
+
systemPrompt: 'Be helpful',
|
|
39
|
+
messages: expect.arrayContaining([expect.any(Message)]),
|
|
40
|
+
toolSpecs: expect.arrayContaining([expect.objectContaining({ name: 'testTool' })]),
|
|
41
|
+
invocationState: expect.anything(),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('middleware result is used as the model call result', async () => {
|
|
45
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
46
|
+
const agent = new Agent({ model, printer: false });
|
|
47
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
48
|
+
const result = yield* next(context);
|
|
49
|
+
return result;
|
|
50
|
+
});
|
|
51
|
+
const result = await agent.invoke('Test prompt');
|
|
52
|
+
expect(result.stopReason).toBe('endTurn');
|
|
53
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Hello')]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('middleware can short-circuit model call with synthetic result', () => {
|
|
57
|
+
it('returns synthetic result without calling the model', async () => {
|
|
58
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Real response' });
|
|
59
|
+
const agent = new Agent({ model, printer: false });
|
|
60
|
+
agent.addMiddleware(InvokeModelStage,
|
|
61
|
+
// eslint-disable-next-line require-yield
|
|
62
|
+
async function* () {
|
|
63
|
+
return {
|
|
64
|
+
result: {
|
|
65
|
+
message: new Message({ role: 'assistant', content: [new TextBlock('Cached response')] }),
|
|
66
|
+
stopReason: 'endTurn',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
const result = await agent.invoke('Test prompt');
|
|
71
|
+
expect(result.stopReason).toBe('endTurn');
|
|
72
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Cached response')]);
|
|
73
|
+
});
|
|
74
|
+
it('model is not called when middleware short-circuits', async () => {
|
|
75
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Real response' });
|
|
76
|
+
const agent = new Agent({ model, printer: false });
|
|
77
|
+
const streamSpy = vi.spyOn(model, 'stream');
|
|
78
|
+
agent.addMiddleware(InvokeModelStage,
|
|
79
|
+
// eslint-disable-next-line require-yield
|
|
80
|
+
async function* () {
|
|
81
|
+
return {
|
|
82
|
+
result: {
|
|
83
|
+
message: new Message({ role: 'assistant', content: [new TextBlock('Cached')] }),
|
|
84
|
+
stopReason: 'endTurn',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
await agent.invoke('Test prompt');
|
|
89
|
+
expect(streamSpy).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('middleware can transform context (messages, toolSpecs) before model call', () => {
|
|
93
|
+
it('modified messages are passed to the model', async () => {
|
|
94
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
95
|
+
const agent = new Agent({ model, printer: false });
|
|
96
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
97
|
+
const modifiedContext = {
|
|
98
|
+
...context,
|
|
99
|
+
messages: [...context.messages, new Message({ role: 'user', content: [new TextBlock('Injected message')] })],
|
|
100
|
+
};
|
|
101
|
+
return yield* next(modifiedContext);
|
|
102
|
+
});
|
|
103
|
+
const streamSpy = vi.spyOn(model, 'stream');
|
|
104
|
+
await agent.invoke('Test prompt');
|
|
105
|
+
expect(streamSpy).toHaveBeenCalled();
|
|
106
|
+
const calledMessages = streamSpy.mock.calls[0][0];
|
|
107
|
+
expect(calledMessages.length).toBeGreaterThan(1);
|
|
108
|
+
});
|
|
109
|
+
it('modified toolSpecs are passed to the model', async () => {
|
|
110
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
111
|
+
const agent = new Agent({ model, printer: false });
|
|
112
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
113
|
+
const modifiedContext = {
|
|
114
|
+
...context,
|
|
115
|
+
toolSpecs: [],
|
|
116
|
+
};
|
|
117
|
+
return yield* next(modifiedContext);
|
|
118
|
+
});
|
|
119
|
+
const streamSpy = vi.spyOn(model, 'stream');
|
|
120
|
+
await agent.invoke('Test prompt');
|
|
121
|
+
expect(streamSpy).toHaveBeenCalled();
|
|
122
|
+
const calledOptions = streamSpy.mock.calls[0][1];
|
|
123
|
+
expect(calledOptions?.toolSpecs).toStrictEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe('hooks fire around middleware', () => {
|
|
127
|
+
it('BeforeModelCallEvent fires before middleware executes', async () => {
|
|
128
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
129
|
+
const agent = new Agent({ model, printer: false });
|
|
130
|
+
const order = [];
|
|
131
|
+
agent.addHook(BeforeModelCallEvent, () => {
|
|
132
|
+
order.push('beforeModelCall');
|
|
133
|
+
});
|
|
134
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
135
|
+
order.push('middleware');
|
|
136
|
+
return yield* next(context);
|
|
137
|
+
});
|
|
138
|
+
await agent.invoke('Test prompt');
|
|
139
|
+
expect(order.indexOf('beforeModelCall')).toBeLessThan(order.indexOf('middleware'));
|
|
140
|
+
});
|
|
141
|
+
it('AfterModelCallEvent fires after middleware completes', async () => {
|
|
142
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
143
|
+
const agent = new Agent({ model, printer: false });
|
|
144
|
+
const order = [];
|
|
145
|
+
agent.addHook(AfterModelCallEvent, () => {
|
|
146
|
+
order.push('afterModelCall');
|
|
147
|
+
});
|
|
148
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
149
|
+
order.push('middleware-start');
|
|
150
|
+
const result = yield* next(context);
|
|
151
|
+
order.push('middleware-end');
|
|
152
|
+
return result;
|
|
153
|
+
});
|
|
154
|
+
await agent.invoke('Test prompt');
|
|
155
|
+
expect(order.indexOf('middleware-end')).toBeLessThan(order.indexOf('afterModelCall'));
|
|
156
|
+
});
|
|
157
|
+
it('Before/After hooks fire even when middleware short-circuits', async () => {
|
|
158
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
159
|
+
const agent = new Agent({ model, printer: false });
|
|
160
|
+
const beforeCalled = vi.fn();
|
|
161
|
+
const afterCalled = vi.fn();
|
|
162
|
+
agent.addHook(BeforeModelCallEvent, beforeCalled);
|
|
163
|
+
agent.addHook(AfterModelCallEvent, afterCalled);
|
|
164
|
+
agent.addMiddleware(InvokeModelStage,
|
|
165
|
+
// eslint-disable-next-line require-yield
|
|
166
|
+
async function* () {
|
|
167
|
+
return {
|
|
168
|
+
result: {
|
|
169
|
+
message: new Message({ role: 'assistant', content: [new TextBlock('Cached')] }),
|
|
170
|
+
stopReason: 'endTurn',
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
await agent.invoke('Test prompt');
|
|
175
|
+
expect(beforeCalled).toHaveBeenCalled();
|
|
176
|
+
expect(afterCalled).toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('addMiddleware returns cleanup function', () => {
|
|
180
|
+
it('returns a function that removes the middleware', async () => {
|
|
181
|
+
const model = new MockMessageModel()
|
|
182
|
+
.addTurn({ type: 'textBlock', text: 'First' })
|
|
183
|
+
.addTurn({ type: 'textBlock', text: 'Second' });
|
|
184
|
+
const agent = new Agent({ model, printer: false });
|
|
185
|
+
let middlewareCalled = false;
|
|
186
|
+
const cleanup = agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
187
|
+
middlewareCalled = true;
|
|
188
|
+
return yield* next(context);
|
|
189
|
+
});
|
|
190
|
+
await agent.invoke('First call');
|
|
191
|
+
expect(middlewareCalled).toBe(true);
|
|
192
|
+
middlewareCalled = false;
|
|
193
|
+
cleanup();
|
|
194
|
+
await agent.invoke('Second call');
|
|
195
|
+
expect(middlewareCalled).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
it('only removes the specific handler, not others on the same stage', async () => {
|
|
198
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Result' });
|
|
199
|
+
const agent = new Agent({ model, printer: false });
|
|
200
|
+
const calls = [];
|
|
201
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
202
|
+
calls.push('keeper');
|
|
203
|
+
return yield* next(context);
|
|
204
|
+
});
|
|
205
|
+
const cleanup = agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
206
|
+
calls.push('removed');
|
|
207
|
+
return yield* next(context);
|
|
208
|
+
});
|
|
209
|
+
cleanup();
|
|
210
|
+
await agent.invoke('Test');
|
|
211
|
+
expect(calls).toStrictEqual(['keeper']);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe('no middleware registered', () => {
|
|
215
|
+
it('agent works correctly without any middleware', async () => {
|
|
216
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
217
|
+
const agent = new Agent({ model, printer: false });
|
|
218
|
+
const result = await agent.invoke('Test prompt');
|
|
219
|
+
expect(result.stopReason).toBe('endTurn');
|
|
220
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Hello')]);
|
|
221
|
+
});
|
|
222
|
+
it('agent with tools works correctly without middleware', async () => {
|
|
223
|
+
const model = new MockMessageModel()
|
|
224
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
225
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
226
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
227
|
+
toolUseId: 'tool-1',
|
|
228
|
+
status: 'success',
|
|
229
|
+
content: [new TextBlock('Tool executed')],
|
|
230
|
+
}));
|
|
231
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
232
|
+
const result = await agent.invoke('Use the tool');
|
|
233
|
+
expect(result.stopReason).toBe('endTurn');
|
|
234
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Done')]);
|
|
235
|
+
});
|
|
236
|
+
it('stream works correctly without middleware', async () => {
|
|
237
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
238
|
+
const agent = new Agent({ model, printer: false });
|
|
239
|
+
const { result } = await collectGenerator(agent.stream('Test prompt'));
|
|
240
|
+
expect(result.stopReason).toBe('endTurn');
|
|
241
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Hello')]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
describe('AgentStreamStage integration', () => {
|
|
246
|
+
describe('middleware wraps the entire agent stream', () => {
|
|
247
|
+
it('middleware executes around the full agent stream', async () => {
|
|
248
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
249
|
+
const agent = new Agent({ model, printer: false });
|
|
250
|
+
const callOrder = [];
|
|
251
|
+
const middleware = async function* (context, next) {
|
|
252
|
+
callOrder.push('middleware-before');
|
|
253
|
+
const result = yield* next(context);
|
|
254
|
+
callOrder.push('middleware-after');
|
|
255
|
+
return result;
|
|
256
|
+
};
|
|
257
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
258
|
+
const { result } = await collectGenerator(agent.stream('Test prompt'));
|
|
259
|
+
expect(callOrder).toStrictEqual(['middleware-before', 'middleware-after']);
|
|
260
|
+
expect(result.stopReason).toBe('endTurn');
|
|
261
|
+
});
|
|
262
|
+
it('middleware receives AgentStreamContext with agent, args, and options', async () => {
|
|
263
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
264
|
+
const agent = new Agent({ model, printer: false });
|
|
265
|
+
let receivedContext;
|
|
266
|
+
const middleware = async function* (context, next) {
|
|
267
|
+
receivedContext = context;
|
|
268
|
+
return yield* next(context);
|
|
269
|
+
};
|
|
270
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
271
|
+
await collectGenerator(agent.stream('Test prompt'));
|
|
272
|
+
expect(receivedContext).toBeDefined();
|
|
273
|
+
expect(receivedContext.agent).toBe(agent);
|
|
274
|
+
expect(receivedContext.args).toBe('Test prompt');
|
|
275
|
+
});
|
|
276
|
+
it('middleware receives options when provided', async () => {
|
|
277
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
278
|
+
const agent = new Agent({ model, printer: false });
|
|
279
|
+
let receivedContext;
|
|
280
|
+
const options = { invocationState: { key: 'value' } };
|
|
281
|
+
const middleware = async function* (context, next) {
|
|
282
|
+
receivedContext = context;
|
|
283
|
+
return yield* next(context);
|
|
284
|
+
};
|
|
285
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
286
|
+
await collectGenerator(agent.stream('Test prompt', options));
|
|
287
|
+
expect(receivedContext.options).toBe(options);
|
|
288
|
+
});
|
|
289
|
+
it('middleware can short-circuit the entire agent stream', async () => {
|
|
290
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Should not reach' });
|
|
291
|
+
const agent = new Agent({ model, printer: false });
|
|
292
|
+
// eslint-disable-next-line require-yield
|
|
293
|
+
const middleware = async function* () {
|
|
294
|
+
return {
|
|
295
|
+
result: {
|
|
296
|
+
stopReason: 'endTurn',
|
|
297
|
+
lastMessage: { type: 'message', role: 'assistant', content: [] },
|
|
298
|
+
metrics: { cycleCount: 0, accumulatedUsage: {}, accumulatedMetrics: {}, toolMetrics: {} },
|
|
299
|
+
invocationState: {},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
304
|
+
const { items, result } = await collectGenerator(agent.stream('Test prompt'));
|
|
305
|
+
// BeforeInvocationEvent fires outside middleware (always fires per design decision)
|
|
306
|
+
const nonHookItems = items.filter((e) => !(e instanceof BeforeInvocationEvent) &&
|
|
307
|
+
!(e instanceof AfterInvocationEvent) &&
|
|
308
|
+
!(e instanceof AgentResultEvent));
|
|
309
|
+
expect(nonHookItems).toStrictEqual([]);
|
|
310
|
+
expect(result.stopReason).toBe('endTurn');
|
|
311
|
+
});
|
|
312
|
+
it('multiple middleware execute in registration order', async () => {
|
|
313
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
314
|
+
const agent = new Agent({ model, printer: false });
|
|
315
|
+
const callOrder = [];
|
|
316
|
+
const outer = async function* (context, next) {
|
|
317
|
+
callOrder.push('outer-before');
|
|
318
|
+
const result = yield* next(context);
|
|
319
|
+
callOrder.push('outer-after');
|
|
320
|
+
return result;
|
|
321
|
+
};
|
|
322
|
+
const inner = async function* (context, next) {
|
|
323
|
+
callOrder.push('inner-before');
|
|
324
|
+
const result = yield* next(context);
|
|
325
|
+
callOrder.push('inner-after');
|
|
326
|
+
return result;
|
|
327
|
+
};
|
|
328
|
+
agent.addMiddleware(AgentStreamStage, outer);
|
|
329
|
+
agent.addMiddleware(AgentStreamStage, inner);
|
|
330
|
+
await collectGenerator(agent.stream('Test prompt'));
|
|
331
|
+
expect(callOrder).toStrictEqual(['outer-before', 'inner-before', 'inner-after', 'outer-after']);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
describe('middleware can filter events from the stream', () => {
|
|
335
|
+
it('filters out specific event types', async () => {
|
|
336
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
337
|
+
const agent = new Agent({ model, printer: false });
|
|
338
|
+
const middleware = async function* (context, next) {
|
|
339
|
+
const gen = next(context);
|
|
340
|
+
let iterResult = await gen.next();
|
|
341
|
+
while (!iterResult.done) {
|
|
342
|
+
const event = iterResult.value;
|
|
343
|
+
// Filter out modelStreamUpdate events
|
|
344
|
+
if (event.type !== 'modelStreamUpdateEvent') {
|
|
345
|
+
yield event;
|
|
346
|
+
}
|
|
347
|
+
iterResult = await gen.next();
|
|
348
|
+
}
|
|
349
|
+
return iterResult.value;
|
|
350
|
+
};
|
|
351
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
352
|
+
const { items } = await collectGenerator(agent.stream('Test prompt'));
|
|
353
|
+
const modelStreamEvents = items.filter((e) => e.type === 'modelStreamUpdateEvent');
|
|
354
|
+
expect(modelStreamEvents).toStrictEqual([]);
|
|
355
|
+
// Other events should still be present
|
|
356
|
+
expect(items.length).toBeGreaterThan(0);
|
|
357
|
+
});
|
|
358
|
+
it('preserves the result when filtering events', async () => {
|
|
359
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
360
|
+
const agent = new Agent({ model, printer: false });
|
|
361
|
+
const middleware = async function* (context, next) {
|
|
362
|
+
const gen = next(context);
|
|
363
|
+
let iterResult = await gen.next();
|
|
364
|
+
while (!iterResult.done) {
|
|
365
|
+
const event = iterResult.value;
|
|
366
|
+
if (event.type !== 'contentBlockEvent') {
|
|
367
|
+
yield event;
|
|
368
|
+
}
|
|
369
|
+
iterResult = await gen.next();
|
|
370
|
+
}
|
|
371
|
+
return iterResult.value;
|
|
372
|
+
};
|
|
373
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
374
|
+
const { result } = await collectGenerator(agent.stream('Test prompt'));
|
|
375
|
+
expect(result.stopReason).toBe('endTurn');
|
|
376
|
+
});
|
|
377
|
+
it('can suppress all events while still returning the result', async () => {
|
|
378
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
379
|
+
const agent = new Agent({ model, printer: false });
|
|
380
|
+
// eslint-disable-next-line require-yield
|
|
381
|
+
const middleware = async function* (context, next) {
|
|
382
|
+
const gen = next(context);
|
|
383
|
+
let iterResult = await gen.next();
|
|
384
|
+
while (!iterResult.done) {
|
|
385
|
+
// Suppress all events — do not yield
|
|
386
|
+
iterResult = await gen.next();
|
|
387
|
+
}
|
|
388
|
+
return iterResult.value;
|
|
389
|
+
};
|
|
390
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
391
|
+
const { items, result } = await collectGenerator(agent.stream('Test prompt'));
|
|
392
|
+
// Middleware suppresses stream events but BeforeInvocationEvent fires outside middleware
|
|
393
|
+
const nonHookItems = items.filter((e) => !(e instanceof BeforeInvocationEvent) &&
|
|
394
|
+
!(e instanceof AfterInvocationEvent) &&
|
|
395
|
+
!(e instanceof AgentResultEvent));
|
|
396
|
+
expect(nonHookItems).toStrictEqual([]);
|
|
397
|
+
expect(result.stopReason).toBe('endTurn');
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
describe('middleware can inject synthetic events', () => {
|
|
401
|
+
it('injects events before the stream', async () => {
|
|
402
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
403
|
+
const agent = new Agent({ model, printer: false });
|
|
404
|
+
const syntheticEvent = { type: 'contentBlockEvent' };
|
|
405
|
+
const middleware = async function* (context, next) {
|
|
406
|
+
yield syntheticEvent;
|
|
407
|
+
return yield* next(context);
|
|
408
|
+
};
|
|
409
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
410
|
+
const { items } = await collectGenerator(agent.stream('Test prompt'));
|
|
411
|
+
// items[0] is BeforeInvocationEvent (fires outside middleware); synthetic is first middleware event
|
|
412
|
+
expect(items[1]).toBe(syntheticEvent);
|
|
413
|
+
expect(items.length).toBeGreaterThan(2);
|
|
414
|
+
});
|
|
415
|
+
it('injects events after the stream', async () => {
|
|
416
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
417
|
+
const agent = new Agent({ model, printer: false });
|
|
418
|
+
const syntheticEvent = { type: 'contentBlockEvent' };
|
|
419
|
+
const middleware = async function* (context, next) {
|
|
420
|
+
const result = yield* next(context);
|
|
421
|
+
yield syntheticEvent;
|
|
422
|
+
return result;
|
|
423
|
+
};
|
|
424
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
425
|
+
const { items } = await collectGenerator(agent.stream('Test prompt'));
|
|
426
|
+
// Lifecycle events fire outside middleware; synthetic event is last middleware-yielded event
|
|
427
|
+
const middlewareItems = items.filter((e) => !(e instanceof BeforeInvocationEvent) &&
|
|
428
|
+
!(e instanceof AfterInvocationEvent) &&
|
|
429
|
+
!(e instanceof AgentResultEvent));
|
|
430
|
+
expect(middlewareItems[middlewareItems.length - 1]).toBe(syntheticEvent);
|
|
431
|
+
});
|
|
432
|
+
it('injects events alongside real events via manual iteration', async () => {
|
|
433
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
434
|
+
const agent = new Agent({ model, printer: false });
|
|
435
|
+
const syntheticEvent = { type: 'contentBlockEvent', synthetic: true };
|
|
436
|
+
const middleware = async function* (context, next) {
|
|
437
|
+
const gen = next(context);
|
|
438
|
+
let iterResult = await gen.next();
|
|
439
|
+
let injected = false;
|
|
440
|
+
while (!iterResult.done) {
|
|
441
|
+
yield iterResult.value;
|
|
442
|
+
if (!injected) {
|
|
443
|
+
yield syntheticEvent;
|
|
444
|
+
injected = true;
|
|
445
|
+
}
|
|
446
|
+
iterResult = await gen.next();
|
|
447
|
+
}
|
|
448
|
+
return iterResult.value;
|
|
449
|
+
};
|
|
450
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
451
|
+
const { items } = await collectGenerator(agent.stream('Test prompt'));
|
|
452
|
+
// items[0] is BeforeInvocationEvent; items[1] is first real event from stream; items[2] is synthetic
|
|
453
|
+
expect(items[2]).toBe(syntheticEvent);
|
|
454
|
+
// Total events should include exactly one synthetic event
|
|
455
|
+
expect(items.filter((e) => e === syntheticEvent)).toHaveLength(1);
|
|
456
|
+
});
|
|
457
|
+
it('can yield events without calling next (pure synthetic stream)', async () => {
|
|
458
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Should not reach' });
|
|
459
|
+
const agent = new Agent({ model, printer: false });
|
|
460
|
+
const syntheticEvent1 = { type: 'contentBlockEvent', id: 1 };
|
|
461
|
+
const syntheticEvent2 = { type: 'contentBlockEvent', id: 2 };
|
|
462
|
+
const middleware = async function* () {
|
|
463
|
+
yield syntheticEvent1;
|
|
464
|
+
yield syntheticEvent2;
|
|
465
|
+
return {
|
|
466
|
+
result: {
|
|
467
|
+
stopReason: 'endTurn',
|
|
468
|
+
lastMessage: { type: 'message', role: 'assistant', content: [] },
|
|
469
|
+
metrics: { cycleCount: 0, accumulatedUsage: {}, accumulatedMetrics: {}, toolMetrics: {} },
|
|
470
|
+
invocationState: {},
|
|
471
|
+
},
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
agent.addMiddleware(AgentStreamStage, middleware);
|
|
475
|
+
const { items } = await collectGenerator(agent.stream('Test prompt'));
|
|
476
|
+
// BeforeInvocationEvent fires outside middleware, then the two synthetic events
|
|
477
|
+
const nonHookItems = items.filter((e) => !(e instanceof BeforeInvocationEvent) &&
|
|
478
|
+
!(e instanceof AfterInvocationEvent) &&
|
|
479
|
+
!(e instanceof AgentResultEvent));
|
|
480
|
+
expect(nonHookItems).toStrictEqual([syntheticEvent1, syntheticEvent2]);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
describe('no AgentStreamStage middleware registered', () => {
|
|
484
|
+
it('agent streams directly without middleware overhead', async () => {
|
|
485
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
486
|
+
const agent = new Agent({ model, printer: false });
|
|
487
|
+
const { items, result } = await collectGenerator(agent.stream('Test prompt'));
|
|
488
|
+
expect(result.stopReason).toBe('endTurn');
|
|
489
|
+
expect(items.length).toBeGreaterThan(0);
|
|
490
|
+
});
|
|
491
|
+
it('existing behavior is unchanged when no middleware is registered', async () => {
|
|
492
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello world' });
|
|
493
|
+
const agent = new Agent({ model, printer: false });
|
|
494
|
+
const { items, result } = await collectGenerator(agent.stream('Test prompt'));
|
|
495
|
+
const beforeInvocation = items.find((e) => e.type === 'beforeInvocationEvent');
|
|
496
|
+
const afterInvocation = items.find((e) => e.type === 'afterInvocationEvent');
|
|
497
|
+
expect(beforeInvocation).toBeDefined();
|
|
498
|
+
expect(afterInvocation).toBeDefined();
|
|
499
|
+
expect(result.stopReason).toBe('endTurn');
|
|
500
|
+
});
|
|
501
|
+
it('middleware on other stages does not affect AgentStreamStage', async () => {
|
|
502
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
503
|
+
const agent = new Agent({ model, printer: false });
|
|
504
|
+
// Register middleware on InvokeModelStage only
|
|
505
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
506
|
+
return yield* next(context);
|
|
507
|
+
});
|
|
508
|
+
const { result } = await collectGenerator(agent.stream('Test prompt'));
|
|
509
|
+
expect(result.stopReason).toBe('endTurn');
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
describe('hooks fire outside middleware (per design decision)', () => {
|
|
513
|
+
it('BeforeInvocationEvent fires even when middleware short-circuits', async () => {
|
|
514
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
515
|
+
const agent = new Agent({ model, printer: false });
|
|
516
|
+
const beforeCalled = vi.fn();
|
|
517
|
+
agent.addHook(BeforeInvocationEvent, beforeCalled);
|
|
518
|
+
agent.addMiddleware(AgentStreamStage,
|
|
519
|
+
// eslint-disable-next-line require-yield
|
|
520
|
+
async function* () {
|
|
521
|
+
return { result: { stopReason: 'endTurn' } };
|
|
522
|
+
});
|
|
523
|
+
await agent.invoke('Test');
|
|
524
|
+
expect(beforeCalled).toHaveBeenCalled();
|
|
525
|
+
});
|
|
526
|
+
it('AfterInvocationEvent fires even when middleware short-circuits', async () => {
|
|
527
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
528
|
+
const agent = new Agent({ model, printer: false });
|
|
529
|
+
const afterCalled = vi.fn();
|
|
530
|
+
agent.addHook(AfterInvocationEvent, afterCalled);
|
|
531
|
+
agent.addMiddleware(AgentStreamStage,
|
|
532
|
+
// eslint-disable-next-line require-yield
|
|
533
|
+
async function* () {
|
|
534
|
+
return { result: { stopReason: 'endTurn' } };
|
|
535
|
+
});
|
|
536
|
+
await agent.invoke('Test');
|
|
537
|
+
expect(afterCalled).toHaveBeenCalled();
|
|
538
|
+
});
|
|
539
|
+
it('BeforeInvocationEvent fires before middleware executes', async () => {
|
|
540
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
541
|
+
const agent = new Agent({ model, printer: false });
|
|
542
|
+
const order = [];
|
|
543
|
+
agent.addHook(BeforeInvocationEvent, () => {
|
|
544
|
+
order.push('before-hook');
|
|
545
|
+
});
|
|
546
|
+
agent.addMiddleware(AgentStreamStage, async function* (context, next) {
|
|
547
|
+
order.push('middleware');
|
|
548
|
+
return yield* next(context);
|
|
549
|
+
});
|
|
550
|
+
await agent.invoke('Test');
|
|
551
|
+
expect(order.indexOf('before-hook')).toBeLessThan(order.indexOf('middleware'));
|
|
552
|
+
});
|
|
553
|
+
it('AfterInvocationEvent fires after middleware completes', async () => {
|
|
554
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
555
|
+
const agent = new Agent({ model, printer: false });
|
|
556
|
+
const order = [];
|
|
557
|
+
agent.addHook(AfterInvocationEvent, () => {
|
|
558
|
+
order.push('after-hook');
|
|
559
|
+
});
|
|
560
|
+
agent.addMiddleware(AgentStreamStage, async function* (context, next) {
|
|
561
|
+
order.push('middleware-start');
|
|
562
|
+
const result = yield* next(context);
|
|
563
|
+
order.push('middleware-end');
|
|
564
|
+
return result;
|
|
565
|
+
});
|
|
566
|
+
await agent.invoke('Test');
|
|
567
|
+
expect(order.indexOf('middleware-end')).toBeLessThan(order.indexOf('after-hook'));
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
describe('ExecuteToolStage integration', () => {
|
|
572
|
+
describe('middleware executes around tool calls', () => {
|
|
573
|
+
it('middleware handler is invoked during tool execution', async () => {
|
|
574
|
+
const model = new MockMessageModel()
|
|
575
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: { x: 1 } })
|
|
576
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
577
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
578
|
+
toolUseId: 'tool-1',
|
|
579
|
+
status: 'success',
|
|
580
|
+
content: [new TextBlock('executed')],
|
|
581
|
+
}));
|
|
582
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
583
|
+
const executionOrder = [];
|
|
584
|
+
const middleware = async function* (context, next) {
|
|
585
|
+
executionOrder.push('middleware:before');
|
|
586
|
+
const result = yield* next(context);
|
|
587
|
+
executionOrder.push('middleware:after');
|
|
588
|
+
return result;
|
|
589
|
+
};
|
|
590
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
591
|
+
await agent.invoke('Use the tool');
|
|
592
|
+
expect(executionOrder).toStrictEqual(['middleware:before', 'middleware:after']);
|
|
593
|
+
});
|
|
594
|
+
it('middleware receives ExecuteToolContext with correct fields', async () => {
|
|
595
|
+
const model = new MockMessageModel()
|
|
596
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: { key: 'val' } })
|
|
597
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
598
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
599
|
+
toolUseId: 'tool-1',
|
|
600
|
+
status: 'success',
|
|
601
|
+
content: [new TextBlock('ok')],
|
|
602
|
+
}));
|
|
603
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
604
|
+
let receivedContext;
|
|
605
|
+
const middleware = async function* (context, next) {
|
|
606
|
+
receivedContext = context;
|
|
607
|
+
return yield* next(context);
|
|
608
|
+
};
|
|
609
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
610
|
+
await agent.invoke('Use the tool');
|
|
611
|
+
expect(receivedContext).toBeDefined();
|
|
612
|
+
expect(receivedContext.agent).toBe(agent);
|
|
613
|
+
expect(receivedContext.tool).toBeDefined();
|
|
614
|
+
expect(receivedContext.tool.name).toBe('testTool');
|
|
615
|
+
expect(receivedContext.toolUse).toStrictEqual({
|
|
616
|
+
name: 'testTool',
|
|
617
|
+
toolUseId: 'tool-1',
|
|
618
|
+
input: { key: 'val' },
|
|
619
|
+
});
|
|
620
|
+
expect(receivedContext.invocationState).toBeDefined();
|
|
621
|
+
});
|
|
622
|
+
it('multiple middleware execute in registration order', async () => {
|
|
623
|
+
const model = new MockMessageModel()
|
|
624
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
625
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
626
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
627
|
+
toolUseId: 'tool-1',
|
|
628
|
+
status: 'success',
|
|
629
|
+
content: [new TextBlock('ok')],
|
|
630
|
+
}));
|
|
631
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
632
|
+
const callOrder = [];
|
|
633
|
+
const outer = async function* (context, next) {
|
|
634
|
+
callOrder.push('outer-before');
|
|
635
|
+
const result = yield* next(context);
|
|
636
|
+
callOrder.push('outer-after');
|
|
637
|
+
return result;
|
|
638
|
+
};
|
|
639
|
+
const inner = async function* (context, next) {
|
|
640
|
+
callOrder.push('inner-before');
|
|
641
|
+
const result = yield* next(context);
|
|
642
|
+
callOrder.push('inner-after');
|
|
643
|
+
return result;
|
|
644
|
+
};
|
|
645
|
+
agent.addMiddleware(ExecuteToolStage, outer);
|
|
646
|
+
agent.addMiddleware(ExecuteToolStage, inner);
|
|
647
|
+
await agent.invoke('Use the tool');
|
|
648
|
+
expect(callOrder).toStrictEqual(['outer-before', 'inner-before', 'inner-after', 'outer-after']);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
describe('middleware can mock tool responses (short-circuit)', () => {
|
|
652
|
+
it('returns mock result without executing the real tool', async () => {
|
|
653
|
+
const model = new MockMessageModel()
|
|
654
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
655
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
656
|
+
const toolFn = vi.fn(() => new ToolResultBlock({
|
|
657
|
+
toolUseId: 'tool-1',
|
|
658
|
+
status: 'success',
|
|
659
|
+
content: [new TextBlock('real result')],
|
|
660
|
+
}));
|
|
661
|
+
const tool = createMockTool('testTool', toolFn);
|
|
662
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
663
|
+
// eslint-disable-next-line require-yield
|
|
664
|
+
const middleware = async function* (context) {
|
|
665
|
+
return {
|
|
666
|
+
result: new ToolResultBlock({
|
|
667
|
+
toolUseId: context.toolUse.toolUseId,
|
|
668
|
+
status: 'success',
|
|
669
|
+
content: [new TextBlock('mocked result')],
|
|
670
|
+
}),
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
674
|
+
const result = await agent.invoke('Use the tool');
|
|
675
|
+
// The real tool function should NOT have been called
|
|
676
|
+
expect(toolFn).not.toHaveBeenCalled();
|
|
677
|
+
// The agent should still complete successfully
|
|
678
|
+
expect(result.stopReason).toBe('endTurn');
|
|
679
|
+
});
|
|
680
|
+
it('short-circuit result is used in the conversation', async () => {
|
|
681
|
+
const model = new MockMessageModel()
|
|
682
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
683
|
+
.addTurn({ type: 'textBlock', text: 'Got the mocked data' });
|
|
684
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
685
|
+
toolUseId: 'tool-1',
|
|
686
|
+
status: 'success',
|
|
687
|
+
content: [new TextBlock('real')],
|
|
688
|
+
}));
|
|
689
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
690
|
+
// eslint-disable-next-line require-yield
|
|
691
|
+
const middleware = async function* (context) {
|
|
692
|
+
return {
|
|
693
|
+
result: new ToolResultBlock({
|
|
694
|
+
toolUseId: context.toolUse.toolUseId,
|
|
695
|
+
status: 'success',
|
|
696
|
+
content: [new TextBlock('mocked data')],
|
|
697
|
+
}),
|
|
698
|
+
};
|
|
699
|
+
};
|
|
700
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
701
|
+
await agent.invoke('Use the tool');
|
|
702
|
+
// The tool result message in conversation should contain the mocked result
|
|
703
|
+
const toolResultMessage = agent.messages.find((m) => m.role === 'user' && m.content.some((c) => c.type === 'toolResultBlock'));
|
|
704
|
+
expect(toolResultMessage).toBeDefined();
|
|
705
|
+
const toolResultBlock = toolResultMessage.content.find((c) => c.type === 'toolResultBlock');
|
|
706
|
+
expect(toolResultBlock).toStrictEqual(new ToolResultBlock({
|
|
707
|
+
toolUseId: 'tool-1',
|
|
708
|
+
status: 'success',
|
|
709
|
+
content: [new TextBlock('mocked data')],
|
|
710
|
+
}));
|
|
711
|
+
});
|
|
712
|
+
});
|
|
713
|
+
describe('middleware can transform tool input via context modification', () => {
|
|
714
|
+
it('modified input reaches the tool', async () => {
|
|
715
|
+
const model = new MockMessageModel()
|
|
716
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: { value: 'original' } })
|
|
717
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
718
|
+
let receivedInput;
|
|
719
|
+
const tool = createMockTool('testTool', (context) => {
|
|
720
|
+
receivedInput = context.toolUse.input;
|
|
721
|
+
return new ToolResultBlock({
|
|
722
|
+
toolUseId: context.toolUse.toolUseId,
|
|
723
|
+
status: 'success',
|
|
724
|
+
content: [new TextBlock('ok')],
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
728
|
+
const middleware = async function* (context, next) {
|
|
729
|
+
const modifiedContext = {
|
|
730
|
+
...context,
|
|
731
|
+
toolUse: {
|
|
732
|
+
...context.toolUse,
|
|
733
|
+
input: { value: 'transformed' },
|
|
734
|
+
},
|
|
735
|
+
};
|
|
736
|
+
return yield* next(modifiedContext);
|
|
737
|
+
};
|
|
738
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
739
|
+
await agent.invoke('Use the tool');
|
|
740
|
+
expect(receivedInput).toStrictEqual({ value: 'transformed' });
|
|
741
|
+
});
|
|
742
|
+
it('original context is not mutated', async () => {
|
|
743
|
+
const model = new MockMessageModel()
|
|
744
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: { value: 'original' } })
|
|
745
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
746
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
747
|
+
toolUseId: 'tool-1',
|
|
748
|
+
status: 'success',
|
|
749
|
+
content: [new TextBlock('ok')],
|
|
750
|
+
}));
|
|
751
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
752
|
+
let originalInput;
|
|
753
|
+
const middleware = async function* (context, next) {
|
|
754
|
+
originalInput = context.toolUse.input;
|
|
755
|
+
const modifiedContext = {
|
|
756
|
+
...context,
|
|
757
|
+
toolUse: {
|
|
758
|
+
...context.toolUse,
|
|
759
|
+
input: { value: 'modified' },
|
|
760
|
+
},
|
|
761
|
+
};
|
|
762
|
+
return yield* next(modifiedContext);
|
|
763
|
+
};
|
|
764
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
765
|
+
await agent.invoke('Use the tool');
|
|
766
|
+
// The original context input should remain unchanged
|
|
767
|
+
expect(originalInput).toStrictEqual({ value: 'original' });
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
describe('hooks fire around middleware for tool execution', () => {
|
|
771
|
+
it('BeforeToolCallEvent fires before middleware, AfterToolCallEvent fires after', async () => {
|
|
772
|
+
const model = new MockMessageModel()
|
|
773
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
774
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
775
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
776
|
+
toolUseId: 'tool-1',
|
|
777
|
+
status: 'success',
|
|
778
|
+
content: [new TextBlock('executed')],
|
|
779
|
+
}));
|
|
780
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
781
|
+
const executionOrder = [];
|
|
782
|
+
agent.addHook(BeforeToolCallEvent, () => {
|
|
783
|
+
executionOrder.push('hook:beforeToolCall');
|
|
784
|
+
});
|
|
785
|
+
agent.addHook(AfterToolCallEvent, () => {
|
|
786
|
+
executionOrder.push('hook:afterToolCall');
|
|
787
|
+
});
|
|
788
|
+
const middleware = async function* (context, next) {
|
|
789
|
+
executionOrder.push('middleware:before');
|
|
790
|
+
const result = yield* next(context);
|
|
791
|
+
executionOrder.push('middleware:after');
|
|
792
|
+
return result;
|
|
793
|
+
};
|
|
794
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
795
|
+
await agent.invoke('Use the tool');
|
|
796
|
+
// Hooks fire OUTSIDE middleware: Before hook → middleware → After hook
|
|
797
|
+
expect(executionOrder).toStrictEqual([
|
|
798
|
+
'hook:beforeToolCall',
|
|
799
|
+
'middleware:before',
|
|
800
|
+
'middleware:after',
|
|
801
|
+
'hook:afterToolCall',
|
|
802
|
+
]);
|
|
803
|
+
});
|
|
804
|
+
it('hooks fire even when middleware short-circuits', async () => {
|
|
805
|
+
const model = new MockMessageModel()
|
|
806
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
807
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
808
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
809
|
+
toolUseId: 'tool-1',
|
|
810
|
+
status: 'success',
|
|
811
|
+
content: [new TextBlock('real')],
|
|
812
|
+
}));
|
|
813
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
814
|
+
const beforeCalled = vi.fn();
|
|
815
|
+
const afterCalled = vi.fn();
|
|
816
|
+
agent.addHook(BeforeToolCallEvent, beforeCalled);
|
|
817
|
+
agent.addHook(AfterToolCallEvent, afterCalled);
|
|
818
|
+
// eslint-disable-next-line require-yield
|
|
819
|
+
const middleware = async function* (context) {
|
|
820
|
+
return {
|
|
821
|
+
result: new ToolResultBlock({
|
|
822
|
+
toolUseId: context.toolUse.toolUseId,
|
|
823
|
+
status: 'success',
|
|
824
|
+
content: [new TextBlock('mocked')],
|
|
825
|
+
}),
|
|
826
|
+
};
|
|
827
|
+
};
|
|
828
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
829
|
+
await agent.invoke('Use the tool');
|
|
830
|
+
expect(beforeCalled).toHaveBeenCalled();
|
|
831
|
+
expect(afterCalled).toHaveBeenCalled();
|
|
832
|
+
});
|
|
833
|
+
it('AfterToolCallEvent receives the middleware result when short-circuited', async () => {
|
|
834
|
+
const model = new MockMessageModel()
|
|
835
|
+
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
|
|
836
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
837
|
+
const tool = createMockTool('testTool', () => new ToolResultBlock({
|
|
838
|
+
toolUseId: 'tool-1',
|
|
839
|
+
status: 'success',
|
|
840
|
+
content: [new TextBlock('real')],
|
|
841
|
+
}));
|
|
842
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
843
|
+
let afterToolResult;
|
|
844
|
+
agent.addHook(AfterToolCallEvent, (event) => {
|
|
845
|
+
afterToolResult = event.result;
|
|
846
|
+
});
|
|
847
|
+
// eslint-disable-next-line require-yield
|
|
848
|
+
const middleware = async function* (context) {
|
|
849
|
+
return {
|
|
850
|
+
result: new ToolResultBlock({
|
|
851
|
+
toolUseId: context.toolUse.toolUseId,
|
|
852
|
+
status: 'success',
|
|
853
|
+
content: [new TextBlock('from middleware')],
|
|
854
|
+
}),
|
|
855
|
+
};
|
|
856
|
+
};
|
|
857
|
+
agent.addMiddleware(ExecuteToolStage, middleware);
|
|
858
|
+
await agent.invoke('Use the tool');
|
|
859
|
+
expect(afterToolResult).toBeDefined();
|
|
860
|
+
expect(afterToolResult.content).toStrictEqual([new TextBlock('from middleware')]);
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
describe('Middleware use cases', () => {
|
|
865
|
+
describe('caching tool results', () => {
|
|
866
|
+
class ToolResultCache {
|
|
867
|
+
name = 'tool-result-cache';
|
|
868
|
+
_cache = new Map();
|
|
869
|
+
initAgent(agent) {
|
|
870
|
+
const cache = this._cache;
|
|
871
|
+
agent.addMiddleware(ExecuteToolStage, async function* (context, next) {
|
|
872
|
+
const key = `${context.toolUse.name}:${JSON.stringify(context.toolUse.input)}`;
|
|
873
|
+
const cached = cache.get(key);
|
|
874
|
+
if (cached) {
|
|
875
|
+
return {
|
|
876
|
+
result: new ToolResultBlock({
|
|
877
|
+
toolUseId: context.toolUse.toolUseId,
|
|
878
|
+
status: cached.status,
|
|
879
|
+
content: cached.content,
|
|
880
|
+
}),
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
const result = yield* next(context);
|
|
884
|
+
cache.set(key, result.result);
|
|
885
|
+
return result;
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
it('returns cached result on second call, skipping real execution', async () => {
|
|
890
|
+
const model = new MockMessageModel()
|
|
891
|
+
.addTurn({ type: 'toolUseBlock', name: 'expensiveApi', toolUseId: 'call-1', input: { query: 'weather' } })
|
|
892
|
+
.addTurn({ type: 'textBlock', text: 'First done' })
|
|
893
|
+
.addTurn({ type: 'toolUseBlock', name: 'expensiveApi', toolUseId: 'call-2', input: { query: 'weather' } })
|
|
894
|
+
.addTurn({ type: 'textBlock', text: 'Second done' });
|
|
895
|
+
const realCallCount = vi.fn();
|
|
896
|
+
const tool = createMockTool('expensiveApi', (ctx) => {
|
|
897
|
+
realCallCount();
|
|
898
|
+
return new ToolResultBlock({
|
|
899
|
+
toolUseId: ctx.toolUse.toolUseId,
|
|
900
|
+
status: 'success',
|
|
901
|
+
content: [new TextBlock('sunny, 72°F')],
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
const agent = new Agent({ model, tools: [tool], plugins: [new ToolResultCache()], printer: false });
|
|
905
|
+
await agent.invoke('What is the weather?');
|
|
906
|
+
expect(realCallCount).toHaveBeenCalledTimes(1);
|
|
907
|
+
await agent.invoke('What is the weather?');
|
|
908
|
+
expect(realCallCount).toHaveBeenCalledTimes(1); // cache hit
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
describe('auto-retrying model invocations', () => {
|
|
912
|
+
class RetryOnThrottle {
|
|
913
|
+
name = 'retry-on-throttle';
|
|
914
|
+
_maxRetries;
|
|
915
|
+
constructor(maxRetries = 3) {
|
|
916
|
+
this._maxRetries = maxRetries;
|
|
917
|
+
}
|
|
918
|
+
initAgent(agent) {
|
|
919
|
+
const maxRetries = this._maxRetries;
|
|
920
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
921
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
922
|
+
try {
|
|
923
|
+
return yield* next(context);
|
|
924
|
+
}
|
|
925
|
+
catch (e) {
|
|
926
|
+
const isRetryable = e.message.includes('ThrottlingException');
|
|
927
|
+
if (!isRetryable || attempt === maxRetries - 1)
|
|
928
|
+
throw e;
|
|
929
|
+
// In production: await sleep(backoff(attempt))
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
throw new Error('exhausted retries');
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
it('retries on transient error and succeeds on second attempt', async () => {
|
|
937
|
+
let callCount = 0;
|
|
938
|
+
const model = new MockMessageModel();
|
|
939
|
+
model.addTurn({ type: 'textBlock', text: 'Success after retry' });
|
|
940
|
+
const agent = new Agent({ model, plugins: [new RetryOnThrottle(3)], printer: false });
|
|
941
|
+
const originalStream = model.stream.bind(model);
|
|
942
|
+
vi.spyOn(model, 'stream').mockImplementation((...args) => {
|
|
943
|
+
callCount++;
|
|
944
|
+
if (callCount === 1)
|
|
945
|
+
throw new Error('ThrottlingException: rate limit exceeded');
|
|
946
|
+
return originalStream(...args);
|
|
947
|
+
});
|
|
948
|
+
const result = await agent.invoke('Hello');
|
|
949
|
+
expect(callCount).toBe(2);
|
|
950
|
+
expect(result.stopReason).toBe('endTurn');
|
|
951
|
+
expect(result.lastMessage.content).toEqual([new TextBlock('Success after retry')]);
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
describe('stream final turn only (buffer intermediate turns)', () => {
|
|
955
|
+
class StreamFinalTurnOnly {
|
|
956
|
+
name = 'stream-final-turn-only';
|
|
957
|
+
initAgent(agent) {
|
|
958
|
+
agent.addMiddleware(AgentStreamStage, (...args) => this._handler(...args));
|
|
959
|
+
}
|
|
960
|
+
async *_handler(...[context, next]) {
|
|
961
|
+
let buffer = [];
|
|
962
|
+
const gen = next(context);
|
|
963
|
+
let iterResult = await gen.next();
|
|
964
|
+
while (!iterResult.done) {
|
|
965
|
+
const event = iterResult.value;
|
|
966
|
+
if (event.type === 'contentBlockEvent' || event.type === 'modelStreamUpdateEvent') {
|
|
967
|
+
buffer.push(event);
|
|
968
|
+
}
|
|
969
|
+
else if (event.type === 'afterModelCallEvent') {
|
|
970
|
+
const stopReason = event.stopData?.stopReason;
|
|
971
|
+
if (stopReason === 'endTurn') {
|
|
972
|
+
for (const buffered of buffer)
|
|
973
|
+
yield buffered;
|
|
974
|
+
}
|
|
975
|
+
buffer = [];
|
|
976
|
+
yield event;
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
yield event;
|
|
980
|
+
}
|
|
981
|
+
iterResult = await gen.next();
|
|
982
|
+
}
|
|
983
|
+
for (const buffered of buffer)
|
|
984
|
+
yield buffered;
|
|
985
|
+
return iterResult.value;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
it('suppresses content events from intermediate tool-use turns, emits only final turn', async () => {
|
|
989
|
+
const model = new MockMessageModel()
|
|
990
|
+
.addTurn([
|
|
991
|
+
{ type: 'textBlock', text: 'Let me check that for you' },
|
|
992
|
+
{ type: 'toolUseBlock', name: 'lookup', toolUseId: 'tool-1', input: {} },
|
|
993
|
+
])
|
|
994
|
+
.addTurn({ type: 'textBlock', text: 'The answer is 42' });
|
|
995
|
+
const tool = createMockTool('lookup', (ctx) => new ToolResultBlock({
|
|
996
|
+
toolUseId: ctx.toolUse.toolUseId,
|
|
997
|
+
status: 'success',
|
|
998
|
+
content: [new TextBlock('42')],
|
|
999
|
+
}));
|
|
1000
|
+
const agent = new Agent({ model, tools: [tool], plugins: [new StreamFinalTurnOnly()], printer: false });
|
|
1001
|
+
const { items, result } = await collectGenerator(agent.stream('What is the meaning of life?'));
|
|
1002
|
+
const contentEvents = items.filter((e) => e.type === 'contentBlockEvent');
|
|
1003
|
+
expect(contentEvents).toHaveLength(1);
|
|
1004
|
+
expect(contentEvents[0].contentBlock).toStrictEqual(new TextBlock('The answer is 42'));
|
|
1005
|
+
expect(result.stopReason).toBe('endTurn');
|
|
1006
|
+
});
|
|
1007
|
+
it('passes through all events when there is only one turn (no tool use)', async () => {
|
|
1008
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Simple answer' });
|
|
1009
|
+
const agent = new Agent({ model, plugins: [new StreamFinalTurnOnly()], printer: false });
|
|
1010
|
+
const { items, result } = await collectGenerator(agent.stream('Hello'));
|
|
1011
|
+
const contentEvents = items.filter((e) => e.type === 'contentBlockEvent');
|
|
1012
|
+
expect(contentEvents).toHaveLength(1);
|
|
1013
|
+
expect(contentEvents[0].contentBlock).toStrictEqual(new TextBlock('Simple answer'));
|
|
1014
|
+
expect(result.stopReason).toBe('endTurn');
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
describe('Middleware phases (Input / Wrap / Output)', () => {
|
|
1019
|
+
describe('InvokeModelStage.Input', () => {
|
|
1020
|
+
it('transforms context before the model call', async () => {
|
|
1021
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1022
|
+
const agent = new Agent({ model, printer: false });
|
|
1023
|
+
let receivedMessages;
|
|
1024
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => ({
|
|
1025
|
+
...context,
|
|
1026
|
+
messages: [...context.messages, new Message({ role: 'user', content: [new TextBlock('injected')] })],
|
|
1027
|
+
}));
|
|
1028
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
1029
|
+
receivedMessages = context.messages;
|
|
1030
|
+
return yield* next(context);
|
|
1031
|
+
});
|
|
1032
|
+
await agent.invoke('Original');
|
|
1033
|
+
expect(receivedMessages.length).toBe(2);
|
|
1034
|
+
expect(receivedMessages[1].content[0]).toEqual(new TextBlock('injected'));
|
|
1035
|
+
});
|
|
1036
|
+
it('runs before Wrap handlers regardless of registration order', async () => {
|
|
1037
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1038
|
+
const agent = new Agent({ model, printer: false });
|
|
1039
|
+
const order = [];
|
|
1040
|
+
// Register Wrap first, then Input
|
|
1041
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
1042
|
+
order.push('wrap');
|
|
1043
|
+
return yield* next(context);
|
|
1044
|
+
});
|
|
1045
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1046
|
+
order.push('input');
|
|
1047
|
+
return context;
|
|
1048
|
+
});
|
|
1049
|
+
await agent.invoke('Test');
|
|
1050
|
+
expect(order).toStrictEqual(['input', 'wrap']);
|
|
1051
|
+
});
|
|
1052
|
+
it('multiple Input handlers compose in registration order', async () => {
|
|
1053
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1054
|
+
const agent = new Agent({ model, printer: false });
|
|
1055
|
+
const order = [];
|
|
1056
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1057
|
+
order.push('input-1');
|
|
1058
|
+
return context;
|
|
1059
|
+
});
|
|
1060
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1061
|
+
order.push('input-2');
|
|
1062
|
+
return context;
|
|
1063
|
+
});
|
|
1064
|
+
await agent.invoke('Test');
|
|
1065
|
+
expect(order).toStrictEqual(['input-1', 'input-2']);
|
|
1066
|
+
});
|
|
1067
|
+
});
|
|
1068
|
+
describe('InvokeModelStage.Output', () => {
|
|
1069
|
+
it('transforms result after the model call', async () => {
|
|
1070
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1071
|
+
const agent = new Agent({ model, printer: false });
|
|
1072
|
+
let outputSeen = false;
|
|
1073
|
+
agent.addMiddleware(InvokeModelStage.Output, async (result) => {
|
|
1074
|
+
outputSeen = true;
|
|
1075
|
+
expect(result.result.stopReason).toBe('endTurn');
|
|
1076
|
+
return result;
|
|
1077
|
+
});
|
|
1078
|
+
await agent.invoke('Test');
|
|
1079
|
+
expect(outputSeen).toBe(true);
|
|
1080
|
+
});
|
|
1081
|
+
it('runs after Wrap handlers (wraps them)', async () => {
|
|
1082
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1083
|
+
const agent = new Agent({ model, printer: false });
|
|
1084
|
+
const order = [];
|
|
1085
|
+
agent.addMiddleware(InvokeModelStage.Output, async (result) => {
|
|
1086
|
+
order.push('output');
|
|
1087
|
+
return result;
|
|
1088
|
+
});
|
|
1089
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
1090
|
+
order.push('wrap-before');
|
|
1091
|
+
const result = yield* next(context);
|
|
1092
|
+
order.push('wrap-after');
|
|
1093
|
+
return result;
|
|
1094
|
+
});
|
|
1095
|
+
await agent.invoke('Test');
|
|
1096
|
+
// Output wraps Wrap: output sees the result after wrap-after
|
|
1097
|
+
expect(order).toStrictEqual(['wrap-before', 'wrap-after', 'output']);
|
|
1098
|
+
});
|
|
1099
|
+
});
|
|
1100
|
+
describe('InvokeModelStage (Wrap phase)', () => {
|
|
1101
|
+
it('bare InvokeModelStage registers as Wrap phase and executes between Input and Output', async () => {
|
|
1102
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1103
|
+
const agent = new Agent({ model, printer: false });
|
|
1104
|
+
const order = [];
|
|
1105
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1106
|
+
order.push('input');
|
|
1107
|
+
return context;
|
|
1108
|
+
});
|
|
1109
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
1110
|
+
order.push('wrap');
|
|
1111
|
+
return yield* next(context);
|
|
1112
|
+
});
|
|
1113
|
+
agent.addMiddleware(InvokeModelStage.Output, async (result) => {
|
|
1114
|
+
order.push('output');
|
|
1115
|
+
return result;
|
|
1116
|
+
});
|
|
1117
|
+
await agent.invoke('Test prompt');
|
|
1118
|
+
expect(order).toStrictEqual(['input', 'wrap', 'output']);
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
describe('phase ordering', () => {
|
|
1122
|
+
it('Input → Wrap → Output execution order', async () => {
|
|
1123
|
+
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' });
|
|
1124
|
+
const agent = new Agent({ model, printer: false });
|
|
1125
|
+
const order = [];
|
|
1126
|
+
// Register in reverse order to prove phase ordering overrides registration order
|
|
1127
|
+
agent.addMiddleware(InvokeModelStage.Output, async (result) => {
|
|
1128
|
+
order.push('output');
|
|
1129
|
+
return result;
|
|
1130
|
+
});
|
|
1131
|
+
agent.addMiddleware(InvokeModelStage, async function* (context, next) {
|
|
1132
|
+
order.push('wrap');
|
|
1133
|
+
return yield* next(context);
|
|
1134
|
+
});
|
|
1135
|
+
agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1136
|
+
order.push('input');
|
|
1137
|
+
return context;
|
|
1138
|
+
});
|
|
1139
|
+
await agent.invoke('Test');
|
|
1140
|
+
expect(order).toStrictEqual(['input', 'wrap', 'output']);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
describe('ExecuteToolStage phases', () => {
|
|
1144
|
+
it('Input transforms tool context before execution', async () => {
|
|
1145
|
+
const model = new MockMessageModel()
|
|
1146
|
+
.addTurn({ type: 'toolUseBlock', name: 'myTool', toolUseId: 'tool-1', input: { x: 1 } })
|
|
1147
|
+
.addTurn({ type: 'textBlock', text: 'Done' });
|
|
1148
|
+
let receivedInput;
|
|
1149
|
+
const tool = createMockTool('myTool', (ctx) => {
|
|
1150
|
+
receivedInput = ctx.toolUse.input;
|
|
1151
|
+
return 'ok';
|
|
1152
|
+
});
|
|
1153
|
+
const agent = new Agent({ model, tools: [tool], printer: false });
|
|
1154
|
+
agent.addMiddleware(ExecuteToolStage.Input, async (context) => ({
|
|
1155
|
+
...context,
|
|
1156
|
+
toolUse: { ...context.toolUse, input: { x: 2 } },
|
|
1157
|
+
}));
|
|
1158
|
+
await agent.invoke('Test');
|
|
1159
|
+
expect(receivedInput).toEqual({ x: 2 });
|
|
1160
|
+
});
|
|
1161
|
+
});
|
|
1162
|
+
describe('cleanup', () => {
|
|
1163
|
+
it('cleanup removes Input handler', async () => {
|
|
1164
|
+
const model = new MockMessageModel()
|
|
1165
|
+
.addTurn({ type: 'textBlock', text: 'First' })
|
|
1166
|
+
.addTurn({ type: 'textBlock', text: 'Second' });
|
|
1167
|
+
const agent = new Agent({ model, printer: false });
|
|
1168
|
+
let inputCalled = false;
|
|
1169
|
+
const cleanup = agent.addMiddleware(InvokeModelStage.Input, async (context) => {
|
|
1170
|
+
inputCalled = true;
|
|
1171
|
+
return context;
|
|
1172
|
+
});
|
|
1173
|
+
await agent.invoke('First');
|
|
1174
|
+
expect(inputCalled).toBe(true);
|
|
1175
|
+
inputCalled = false;
|
|
1176
|
+
cleanup();
|
|
1177
|
+
await agent.invoke('Second');
|
|
1178
|
+
expect(inputCalled).toBe(false);
|
|
1179
|
+
});
|
|
1180
|
+
it('cleanup removes Output handler', async () => {
|
|
1181
|
+
const model = new MockMessageModel()
|
|
1182
|
+
.addTurn({ type: 'textBlock', text: 'First' })
|
|
1183
|
+
.addTurn({ type: 'textBlock', text: 'Second' });
|
|
1184
|
+
const agent = new Agent({ model, printer: false });
|
|
1185
|
+
let outputCalled = false;
|
|
1186
|
+
const cleanup = agent.addMiddleware(InvokeModelStage.Output, async (result) => {
|
|
1187
|
+
outputCalled = true;
|
|
1188
|
+
return result;
|
|
1189
|
+
});
|
|
1190
|
+
await agent.invoke('First');
|
|
1191
|
+
expect(outputCalled).toBe(true);
|
|
1192
|
+
outputCalled = false;
|
|
1193
|
+
cleanup();
|
|
1194
|
+
await agent.invoke('Second');
|
|
1195
|
+
expect(outputCalled).toBe(false);
|
|
1196
|
+
});
|
|
1197
|
+
});
|
|
1198
|
+
describe('error handling', () => {
|
|
1199
|
+
it('throws on unknown phase', () => {
|
|
1200
|
+
const agent = new Agent({ model: 'test', printer: false });
|
|
1201
|
+
const fakePhase = { _phase: 'unknown', _stage: InvokeModelStage };
|
|
1202
|
+
expect(() => agent.addMiddleware(fakePhase, (() => { }))).toThrow('Unknown middleware phase: unknown');
|
|
1203
|
+
});
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
//# sourceMappingURL=agent-middleware.test.js.map
|