Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 28 additions & 69 deletions strands-ts/src/agent/__tests__/agent.context-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ import { describe, expect, it } from 'vitest'
import { Agent } from '../agent.js'
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js'
import { SlidingWindowConversationManager } from '../../conversation-manager/sliding-window-conversation-manager.js'
import { SummarizingConversationManager } from '../../conversation-manager/summarizing-conversation-manager.js'
import { NullConversationManager } from '../../conversation-manager/null-conversation-manager.js'
import { ContextOffloader } from '../../vended-plugins/context-offloader/plugin.js'
import { InMemoryStorage as LegacyInMemoryStorage } from '../../vended-plugins/context-offloader/storage.js'
import { ContextManager } from '../../context-manager/context-manager.js'
import { NAMESPACED } from '../../storage/storage.js'
import type { ConversationManager } from '../../conversation-manager/conversation-manager.js'

function internals(agent: Agent): any {
Expand All @@ -18,10 +14,6 @@ function getConversationManager(agent: Agent): ConversationManager {
return internals(agent)._conversationManager
}

function getPending(agent: Agent): any[] {
return internals(agent)._pluginRegistry._pending
}

describe('Agent contextManager', () => {
describe('when undefined (default)', () => {
it('uses SlidingWindowConversationManager', () => {
Expand All @@ -30,76 +22,53 @@ describe('Agent contextManager', () => {
expect(getConversationManager(agent)).toBeInstanceOf(SlidingWindowConversationManager)
})

it('does not add ContextOffloader plugin', () => {
it('does not set contextManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model })
const pending = getPending(agent)
expect(pending.find((p: any) => p.name === 'strands:context-offloader')).toBeUndefined()
expect(agent.contextManager).toBeUndefined()
})
})

describe('when "auto"', () => {
it('uses SummarizingConversationManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto' })
expect(getConversationManager(agent)).toBeInstanceOf(SummarizingConversationManager)
})

it('sets summaryRatio to 0.3', () => {
it('uses NullConversationManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto' })
const conversationManager = getConversationManager(agent) as any
expect(conversationManager._summaryRatio).toBe(0.3)
expect(getConversationManager(agent)).toBeInstanceOf(NullConversationManager)
})

it('enables proactive compression at 0.85', () => {
it('creates a ContextManager instance', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto' })
const conversationManager = getConversationManager(agent) as any
expect(conversationManager._compressionThreshold).toBe(0.85)
expect(agent.contextManager).toBeInstanceOf(ContextManager)
})

it('adds ContextOffloader plugin with benchmark defaults', async () => {
it('registers ContextManager as a plugin', async () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto' })
await agent.invoke('hi')
const plugins = internals(agent)._pluginRegistry._plugins
const offloader = plugins.get('strands:context-offloader') as any
expect(offloader).toBeDefined()
expect(offloader._maxResultTokens).toBe(1500)
expect(offloader._previewTokens).toBe(750)
expect(NAMESPACED in offloader._storage).toBe(true)
expect(plugins.get('strands:context-manager')).toBe(agent.contextManager)
})
})

describe('coexistence with conversationManager', () => {
it('respects user-provided conversationManager', () => {
it('ignores user-provided conversationManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const userCm = new SlidingWindowConversationManager({ windowSize: 20 })
const agent = new Agent({ model, contextManager: 'auto', conversationManager: userCm })
expect(getConversationManager(agent)).toBe(userCm)
expect(getConversationManager(agent)).toBeInstanceOf(NullConversationManager)
})
})

it('still adds ContextOffloader when user provides conversationManager', () => {
describe('when "agentic"', () => {
it('uses NullConversationManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const userCm = new SlidingWindowConversationManager({ windowSize: 20 })
const agent = new Agent({ model, contextManager: 'auto', conversationManager: userCm })
const pending = getPending(agent)
expect(pending.find((p: any) => p.name === 'strands:context-offloader')).toBeDefined()
const agent = new Agent({ model, contextManager: 'agentic' })
expect(getConversationManager(agent)).toBeInstanceOf(NullConversationManager)
})

it('does not add duplicate ContextOffloader if user provides one', () => {
it('creates a ContextManager instance', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const userOffloader = new ContextOffloader({
storage: new LegacyInMemoryStorage(),
maxResultTokens: 3000,
previewTokens: 1000,
})
const agent = new Agent({ model, contextManager: 'auto', plugins: [userOffloader] })
const pending = getPending(agent)
const offloaders = pending.filter((p: any) => p.name === 'strands:context-offloader')
expect(offloaders).toHaveLength(1)
expect((offloaders[0] as any)._maxResultTokens).toBe(3000)
const agent = new Agent({ model, contextManager: 'agentic' })
expect(agent.contextManager).toBeInstanceOf(ContextManager)
})
})

Expand Down Expand Up @@ -128,37 +97,27 @@ describe('Agent contextManager', () => {
const agent = new Agent({ model, contextManager: false, conversationManager: userCm })
expect(getConversationManager(agent)).toBe(userCm)
})

it('does not set contextManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: false })
expect(agent.contextManager).toBeUndefined()
})
})

describe('when ContextManager instance', () => {
describe('when ContextManagerConfig object', () => {
it('uses NullConversationManager', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const cm = new ContextManager({ strategies: [{ name: 'noop', apply: async () => false }] })
const agent = new Agent({ model, contextManager: cm })
const agent = new Agent({ model, contextManager: { strategies: [{ name: 'noop', apply: async () => false }] } })
expect(getConversationManager(agent)).toBeInstanceOf(NullConversationManager)
})

it('registers ContextManager as a plugin', async () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const cm = new ContextManager({ strategies: [{ name: 'noop', apply: async () => false }] })
const agent = new Agent({ model, contextManager: cm })
const agent = new Agent({ model, contextManager: { strategies: [{ name: 'noop', apply: async () => false }] } })
await agent.invoke('hi')
const plugins = internals(agent)._pluginRegistry._plugins
expect(plugins.get('strands:context-manager')).toBe(cm)
})

it('throws if ContextManager is passed in both param and plugins', async () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const cm = new ContextManager({ strategies: [{ name: 'noop', apply: async () => false }] })
const agent = new Agent({ model, contextManager: cm, plugins: [cm] })
await expect(agent.invoke('hi')).rejects.toThrow('plugin already registered')
})
})

describe('unsupported value', () => {
it('throws for invalid contextManager value', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
expect(() => new Agent({ model, contextManager: 'manual' as any })).toThrow('Unsupported contextManager value')
expect(plugins.get('strands:context-manager')).toBeInstanceOf(ContextManager)
})
})
})
50 changes: 7 additions & 43 deletions strands-ts/src/agent/__tests__/agent.storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,25 @@ import { describe, expect, it } from 'vitest'
import { Agent } from '../agent.js'
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js'
import { InMemoryStorage } from '../../storage/in-memory-storage.js'
import { NAMESPACED } from '../../storage/storage.js'
import { ContextOffloader } from '../../vended-plugins/context-offloader/plugin.js'
import { SessionManager } from '../../session/session-manager.js'

function internals(agent: Agent): any {
return agent as any
}

describe('Agent storage', () => {
describe('agent-level storage flows to ContextOffloader', () => {
it('auto-created offloader uses agent storage when provided', async () => {
describe('agent-level storage flows to ContextManager stash', () => {
it('auto-created ContextManager uses agent storage when provided', async () => {
const storage = new InMemoryStorage()
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto', storage })
await agent.invoke('hi')
const plugins = internals(agent)._pluginRegistry._plugins
const offloader = plugins.get('strands:context-offloader') as any
expect(offloader._storage).toBeDefined()
expect(NAMESPACED in offloader._storage).toBe(true)

await offloader._storage.write('test-key', new TextEncoder().encode('test-value'))
const stored = await storage.read('offloader/test-key')
expect(stored).not.toBeNull()
expect(new TextDecoder().decode(stored!)).toBe('test-value')
expect(agent.contextManager).toBeDefined()
expect(agent.contextManager!.stash).toBeDefined()
Comment thread
lizradway marked this conversation as resolved.
})

it('auto-created offloader falls back to InMemoryStorage when no agent storage', async () => {
it('auto-created ContextManager uses InMemoryStorage when no agent storage', async () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({ model, contextManager: 'auto' })
await agent.invoke('hi')
const plugins = internals(agent)._pluginRegistry._plugins
const offloader = plugins.get('strands:context-offloader') as any
expect(offloader._storage).toBeDefined()
expect(NAMESPACED in offloader._storage).toBe(true)
})

it('explicit offloader storage overrides agent storage', async () => {
const agentStorage = new InMemoryStorage()
const offloaderStorage = new InMemoryStorage()
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'hi' })
const agent = new Agent({
model,
storage: agentStorage,
plugins: [new ContextOffloader({ storage: offloaderStorage, maxResultTokens: 1500, previewTokens: 750 })],
contextManager: 'auto',
})
await agent.invoke('hi')
const plugins = internals(agent)._pluginRegistry._plugins
const offloader = plugins.get('strands:context-offloader') as any

await offloader._storage.write('test-key', new TextEncoder().encode('test-value'))
const inOffloader = await offloaderStorage.read('offloader/test-key')
expect(inOffloader).not.toBeNull()
const inAgent = await agentStorage.read('offloader/test-key')
expect(inAgent).toBeNull()
expect(agent.contextManager).toBeDefined()
expect(agent.contextManager!.stash).toBeDefined()
})
})

Expand Down
Loading
Loading