Skip to content
Draft
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
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/__fixtures__/mock-hook-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
BeforeModelCallEvent,
AfterModelCallEvent,
ModelStreamEventHook,
ContentBlockHook,
} from '../hooks/index.js'
import type { HookEventConstructor } from '../hooks/types.js'

Expand All @@ -33,7 +34,7 @@ export class MockHookProvider implements HookProvider {
AfterModelCallEvent,
]

const modelEvents: HookEventConstructor[] = [ModelStreamEventHook]
const modelEvents: HookEventConstructor[] = [ModelStreamEventHook, ContentBlockHook]

const eventTypes = this.includeModelEvents ? [...lifecycleEvents, ...modelEvents] : lifecycleEvents

Expand Down
91 changes: 91 additions & 0 deletions src/agent/__tests__/agent.hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
BeforeToolCallEvent,
MessageAddedEvent,
ModelStreamEventHook,
ContentBlockHook,
type HookRegistry,
} from '../../hooks/index.js'
import { MockMessageModel } from '../../__fixtures__/mock-message-model.js'
Expand Down Expand Up @@ -269,6 +270,96 @@ describe('Agent Hooks Integration', () => {
})
})

describe('ContentBlockHook', () => {
it('fires for content blocks from model streaming', async () => {
const model = new MockMessageModel().addTurn([
{ type: 'textBlock', text: 'Hello' },
{ type: 'reasoningBlock', text: 'Thinking' },
])

const agent = new Agent({
model,
hooks: [mockProvider],
})

await collectIterator(agent.stream('Test'))

const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook)

// Should have 2 content blocks (TextBlock and ReasoningBlock)
expect(contentBlockHooks.length).toBe(2)

const textBlockHook = contentBlockHooks[0] as ContentBlockHook
expect(textBlockHook.block.type).toBe('textBlock')

const reasoningBlockHook = contentBlockHooks[1] as ContentBlockHook
expect(reasoningBlockHook.block.type).toBe('reasoningBlock')
})

it('fires for tool result blocks', async () => {
const tool = new FunctionTool({
name: 'testTool',
description: 'Test tool',
inputSchema: {},
callback: () => 'Tool result',
})

const model = new MockMessageModel()
.addTurn({ type: 'toolUseBlock', name: 'testTool', toolUseId: 'tool-1', input: {} })
.addTurn({ type: 'textBlock', text: 'Done' })

const agent = new Agent({
model,
tools: [tool],
hooks: [mockProvider],
})

await collectIterator(agent.stream('Test'))

const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook)

// Should have content blocks from model streaming and tool result
const toolResultHooks = contentBlockHooks.filter((e) => (e as ContentBlockHook).block.type === 'toolResultBlock')
expect(toolResultHooks.length).toBe(1)

const toolResultHook = toolResultHooks[0] as ContentBlockHook
expect(toolResultHook.block.type).toBe('toolResultBlock')
})

it('fires for all content block types in comprehensive scenario', async () => {
const tool = new FunctionTool({
name: 'calc',
description: 'Calculator',
inputSchema: {},
callback: () => '42',
})

const model = new MockMessageModel()
.addTurn([
{ type: 'textBlock', text: 'Let me calculate' },
{ type: 'reasoningBlock', text: 'Need to use tool' },
{ type: 'toolUseBlock', name: 'calc', toolUseId: 'tool-1', input: {} },
])
.addTurn([{ type: 'textBlock', text: 'Result is 42' }])

const agent = new Agent({
model,
tools: [tool],
hooks: [mockProvider],
})

await collectIterator(agent.stream('Test'))

const contentBlockHooks = mockProvider.invocations.filter((e) => e instanceof ContentBlockHook)

// Should have: TextBlock, ReasoningBlock, ToolUseBlock, ToolResultBlock, TextBlock
expect(contentBlockHooks.length).toBe(5)

const blockTypes = contentBlockHooks.map((e) => (e as ContentBlockHook).block.type)
expect(blockTypes).toEqual(['textBlock', 'reasoningBlock', 'toolUseBlock', 'toolResultBlock', 'textBlock'])
})
})

describe('MessageAddedEvent', () => {
it('fires for initial user input', async () => {
const initialMessage = { role: 'user' as const, content: [{ type: 'textBlock' as const, text: 'Initial' }] }
Expand Down
8 changes: 5 additions & 3 deletions src/agent/__tests__/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ describe('Agent', () => {

// Create agent with custom printer for testing
const agent = new Agent({ model, printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand All @@ -329,15 +329,17 @@ describe('Agent', () => {
const agent = new Agent({ model, printer: false })

expect(agent).toBeDefined()
expect((agent as any)._printer).toBeUndefined()
// Printer is not registered when printer: false
expect(agent.hooks).toBeDefined()
})

it('defaults to printer=true when not specified', () => {
const model = new MockMessageModel().addTurn({ type: 'textBlock', text: 'Hello' })
const agent = new Agent({ model })

expect(agent).toBeDefined()
expect((agent as any)._printer).toBeDefined()
// Printer is registered as a hook internally, not a field
expect(agent.hooks).toBeDefined()
})

it('agent works correctly with printer disabled', async () => {
Expand Down
14 changes: 7 additions & 7 deletions src/agent/__tests__/printer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand All @@ -30,7 +30,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand All @@ -48,7 +48,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand All @@ -66,7 +66,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand Down Expand Up @@ -96,7 +96,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, tools: [tool], printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand All @@ -120,7 +120,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, tools: [tool], printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand Down Expand Up @@ -163,7 +163,7 @@ describe('AgentPrinter', () => {
const mockAppender = (text: string) => outputs.push(text)

const agent = new Agent({ model, tools: [calcTool, validatorTool], printer: false })
;(agent as any)._printer = new AgentPrinter(mockAppender)
agent.hooks.addHook(new AgentPrinter(mockAppender))

await collectGenerator(agent.stream('Test'))

Expand Down
Loading