Skip to content

Conversation

@zastrowm
Copy link
Member

Resolves: #270

Overview

Refactors the printer implementation from direct integration into Agent to a hook-based approach. This is an internal refactoring with NO PUBLIC API CHANGES - the printer?: boolean config remains, and default behavior stays the same.

Key Changes

  • Removed Printer interface (no longer needed)
  • Made AgentPrinter implement HookProvider interface
  • Agent registers AgentPrinter as a hook internally when printer config is true (default)
  • Removed direct _printer?.processEvent(event) calls from Agent
  • Subscribe to ModelStreamEventHook to receive all printing events
  • Wrap ToolResultBlock in ModelStreamEventHook when yielding

Implementation Details

The printer now follows the same pattern as SlidingWindowConversationManager:

  1. Implements HookProvider interface
  2. Registers callback for ModelStreamEventHook events
  3. Processes both ModelStreamEvent and ContentBlock types through the hook system

This makes the printer extensible via hooks while maintaining complete backward compatibility with the existing public API.

Testing

  • All 636 unit tests passing
  • Test coverage: 89.97% (printer.ts: 96.49%)
  • No linting errors
  • No type errors
  • Public API unchanged - all existing code continues to work

Files Modified

  • src/agent/printer.ts - Removed interface, added HookProvider implementation
  • src/agent/agent.ts - Removed printer field, register as hook instead
  • src/agent/__tests__/printer.test.ts - Updated test setup to use hooks
  • src/agent/__tests__/agent.test.ts - Updated printer-related tests

- Remove Printer interface (no longer needed)
- Make AgentPrinter implement HookProvider interface
- Agent registers AgentPrinter as hook when printer config is true
- Remove direct _printer?.processEvent() calls from Agent
- Wrap ToolResultBlock in ModelStreamEventHook for printer processing
- Update tests to use hooks.addHook() pattern
- Maintain backward compatibility (printer?: boolean still works)

Resolves: #270
toolResultBlocks.push(toolResultBlock)

// Wrap ToolResultBlock in hook event for printer
yield new ModelStreamEventHook({ agent: this, event: toolResultBlock })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shows a gap in our hook implementation. This doesn't make sense to yield a tool result block for ModelStreamEventHook, but it also doesn't make sense that we introduce ToolResultBlockHook just for toolResults.

Instead, update ModelStreamEventHook to only be created for non-ContentBlocks, and let's add a new hook for ContentBlocks that is used for both Model ContentBlocks and ToolContent blocks. Then we would yield that here instead. Make sure to add associated test for the new hook, mimicking our existing hook documentation

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed! Created ContentBlockHook for unified content block handling. ModelStreamEventHook now only accepts ModelStreamEvent, and printer subscribes to both hooks.

@zastrowm
Copy link
Member Author

/strands implement

@github-actions github-actions bot added the strands-running <strands-managed> Whether or not an agent is currently running label Nov 28, 2025
- Create ContentBlockHook for handling content blocks from model and tools
- Update ModelStreamEventHook to only accept ModelStreamEvent (no ContentBlock)
- Agent yields ContentBlockHook for content blocks from model streaming
- Agent yields ContentBlockHook for tool result blocks
- AgentPrinter subscribes to both ModelStreamEventHook and ContentBlockHook
- Add 3 tests for ContentBlockHook following existing hook test patterns

This addresses architectural concern where ToolResultBlock shouldn't be
wrapped in ModelStreamEventHook. ContentBlockHook provides a unified way
to handle content blocks regardless of their source.
@github-actions github-actions bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Nov 28, 2025
// Yield hook event for observability
yield new ModelStreamEventHook({ agent: this, event })
// Yield appropriate hook event for observability
if ('type' in event && typeof event.type === 'string' && event.type.endsWith('Block')) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than doing this much work, let's add a base class to all content blocks, BaseContentBlock for now. Make it non-public and add a TODO to the code to api-bar-raise it.

Then do the type check here for that using instanceof rather than this event.type checking

* Handle model stream events and process them for printing.
* @param event - The model stream event to handle
*/
private handleModelStreamEvent = (event: ModelStreamEventHook): void => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just use inline lambdas in the registerCallbacks method and just forward the events processEvent directly

it('returns false for _shouldReverseCallbacks', () => {
const agent = new Agent()
const contentBlock = new TextBlock('Hello world')
const hookEvent = new ContentBlockHook({ agent, block: contentBlock })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need tests in agent.hook.test.ts as well to verify the lifecyles. Follow the existing conventions and add tests there too

@zastrowm
Copy link
Member Author

/strands implement

@github-actions github-actions bot added the strands-running <strands-managed> Whether or not an agent is currently running label Nov 28, 2025
- Create BaseContentBlock abstract class in separate file
- Update all content block classes to extend BaseContentBlock
- Replace string-based type checking with instanceof in agent
- Simplify printer callbacks to use inline lambdas
- Add lifecycle tests for ContentBlockHook
- Fix circular dependency by moving BaseContentBlock to own file

This addresses PR feedback to use class-based type discrimination
instead of string checking and simplify callback registration.
@github-actions
Copy link

Updates Based on Second Review Feedback

I've implemented all three requested changes:

1. Use BaseContentBlock for Type Discrimination ✅

  • Created src/types/base-content-block.ts with abstract BaseContentBlock class
  • Added TODO comment to make it public in a future release
  • Updated all content block classes to extend BaseContentBlock:
    • In messages.ts: TextBlock, ReasoningBlock, ToolUseBlock, ToolResultBlock, CachePointBlock, GuardContentBlock
    • In media.ts: ImageBlock, VideoBlock, DocumentBlock
  • Updated agent.ts to use instanceof BaseContentBlock instead of string-based type checking
  • Fixed circular dependency by moving BaseContentBlock to its own file

2. Simplify Printer Callbacks ✅

  • Replaced separate handleModelStreamEvent and handleContentBlock methods with inline lambdas in registerCallbacks
  • Code is now more concise and follows simpler patterns

3. Add Lifecycle Tests ✅

  • Added 3 new tests in agent.hook.test.ts:
    • fires for content blocks from model streaming - verifies TextBlock and ReasoningBlock hooks
    • fires for tool result blocks - verifies ToolResultBlock hooks
    • fires for all content block types in comprehensive scenario - verifies all content block types including ToolUseBlock

Validation Results

  • ✅ All 642 unit tests passing
  • ✅ Linting passed
  • ✅ Type checking passed
  • ✅ Code formatting passed
  • ✅ Coverage: 90.08%

Commits

The changes are in commit 3de350b on branch agent-tasks/270 (will be pushed after sandbox operations complete).

Ready for follow-up review!

@github-actions github-actions bot removed the strands-running <strands-managed> Whether or not an agent is currently running label Nov 28, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convert printer to a hook

2 participants