Skip to content

Re-implement conversation-manager as a hook provider #210

@zastrowm

Description

@zastrowm

Re-implement conversation-manager as a hook provider

Overview

Refactor the conversation-manager from a concrete class hierarchy to a hook-based system. This decouples conversation management from the Agent implementation and makes it consistent with the extensibility patterns in the SDK.

Note: This differs from the Python implementation approach.

Original Requirements

  • Update AfterModelCall to pass in an exception if there is an exception
  • Expose a AfterModelCall.retryModelCall that can be set to True/False (only valid if there's an exception)
  • Reimplement all conversation-managers to be hook providers. The NullProvider simply registers 0 hooks
  • Remove all hard-coded calls to the conversation manager and remove the base interface for the conversation manager. The existing apply/reduceContext should be simply hook callback messages

Implementation Requirements

Technical Approach

This is a breaking change that removes the ConversationManager abstract base class and converts conversation management to use the hooks system.

Current Implementation Context

  • ConversationManager: Abstract base class with applyManagement() and reduceContext() methods
  • Implementations: NullConversationManager and SlidingWindowConversationManager
  • Agent Integration: Direct method calls at:
    • Line 286: this.conversationManager.applyManagement(this) in finally block
    • Line 369: this.conversationManager.reduceContext(this, error) on ContextWindowOverflowError
  • Public API: ConversationManager is exported and used in AgentConfig.conversationManager

Changes Required

1. Update AfterModelCallEvent (src/hooks/events.ts)

  • Add retryModelCall?: boolean field to AfterModelCallEvent class
  • This field is only valid when error field is present
  • Hook callbacks can set this field to request a retry: event.retryModelCall = true
  • Document that this enables hooks to trigger model call retry after reducing context

2. Convert NullConversationManager to HookProvider (src/conversation-manager/null-conversation-manager.ts)

  • Change from extends ConversationManager to implements HookProvider
  • Implement registerCallbacks(registry: HookRegistry): void method
  • Register 0 hooks (empty implementation - no callbacks registered)
  • Remove applyManagement() and reduceContext() methods
  • Update TSDoc to reflect it's a no-op HookProvider

3. Convert SlidingWindowConversationManager to HookProvider (src/conversation-manager/sliding-window-conversation-manager.ts)

  • Change from extends ConversationManager to implements HookProvider
  • Implement registerCallbacks(registry: HookRegistry): void method that registers:
    • AfterInvocationEvent callback: Calls logic from current applyManagement() method
    • AfterModelCallEvent callback: When error is present, calls logic from current reduceContext() method and sets event.retryModelCall = true if successful
  • Keep existing private helper methods (truncateToolResults, findLastMessageWithToolResults, etc.)
  • Keep SlidingWindowConversationManagerConfig interface unchanged
  • Maintain all existing behavior, just triggered via hooks instead of direct calls

4. Update Agent Class (src/agent/agent.ts)

  • Remove conversationManager field
  • Remove conversationManager from AgentConfig type
  • Remove line 286: this.conversationManager.applyManagement(this) call (now handled by AfterInvocationEvent hook)
  • Update lines 361-374 (error handling in invokeModel):
    // Current code:
    catch (error) {
      const modelError = normalizeError(error)
      await this.hooks.invokeCallbacks(new AfterModelCallEvent({ agent: this, error: modelError }))
      
      if (error instanceof ContextWindowOverflowError) {
        this.conversationManager.reduceContext(this, error)
        return yield* this.invokeModel(args)
      }
      throw error
    }
    
    // New code:
    catch (error) {
      const modelError = normalizeError(error)
      const event = await this.hooks.invokeCallbacks(
        new AfterModelCallEvent({ agent: this, error: modelError })
      )
      
      if (event.retryModelCall) {
        return yield* this.invokeModel(args)
      }
      throw error
    }
  • Update constructor to remove default SlidingWindowConversationManager initialization
  • Update TSDoc to reflect removal of conversationManager config

5. Remove ConversationManager Base Class

  • Delete src/conversation-manager/conversation-manager.ts
  • Update src/conversation-manager/index.ts:
    • Remove exports for ConversationManager and ConversationContext
    • Keep exports for NullConversationManager and SlidingWindowConversationManager
  • Delete or minimize src/conversation-manager/__tests__/conversation-manager.test.ts

6. Update Public API Exports (src/index.ts)

  • Remove line 122: export { ConversationManager }
  • Keep export { NullConversationManager } (line 123)
  • Keep export { SlidingWindowConversationManager, type SlidingWindowConversationManagerConfig } (lines 124-126)

7. Update Tests

  • Update src/conversation-manager/__tests__/null-conversation-manager.test.ts:
    • Test HookProvider interface implementation
    • Test that registerCallbacks registers 0 hooks
  • Update src/conversation-manager/__tests__/sliding-window-conversation-manager.test.ts:
    • Test HookProvider interface implementation
    • Test hook callbacks for AfterInvocationEvent and AfterModelCallEvent
    • Test that retryModelCall is set correctly
    • Test message trimming and truncation logic through hooks
  • Update src/agent/__tests__/agent.test.ts:
    • Remove tests for conversationManager field
    • Add tests for retry behavior via retryModelCall flag
    • Test that hooks properly trigger context management
  • Maintain 80%+ test coverage across all modified files

Migration Guide for Users

Before (Old Pattern):

const agent = new Agent({
  model,
  conversationManager: new SlidingWindowConversationManager({ windowSize: 20 })
})

After (New Pattern):

const agent = new Agent({
  model,
  hooks: [new SlidingWindowConversationManager({ windowSize: 20 })]
})

Files to Modify

Source Files (8-10 files):

  • src/hooks/events.ts - Add retryModelCall field
  • src/conversation-manager/null-conversation-manager.ts - Convert to HookProvider
  • src/conversation-manager/sliding-window-conversation-manager.ts - Convert to HookProvider
  • src/conversation-manager/conversation-manager.ts - DELETE
  • src/conversation-manager/index.ts - Update exports
  • src/agent/agent.ts - Remove direct calls, add retry logic
  • src/index.ts - Update public exports

Test Files (~4 files):

  • src/conversation-manager/__tests__/null-conversation-manager.test.ts
  • src/conversation-manager/__tests__/sliding-window-conversation-manager.test.ts
  • src/conversation-manager/__tests__/conversation-manager.test.ts - DELETE or minimize
  • src/agent/__tests__/agent.test.ts

Acceptance Criteria

  • AfterModelCallEvent has retryModelCall field
  • NullConversationManager implements HookProvider with 0 hooks
  • SlidingWindowConversationManager implements HookProvider with proper callbacks
  • Agent class has no conversationManager field or direct calls
  • Agent properly handles retryModelCall flag for context overflow retry
  • ConversationManager base class is removed
  • Public API exports updated (breaking change)
  • All tests pass with 80%+ coverage
  • TSDoc comments updated for all modified public APIs
  • Existing conversation management behavior preserved (just triggered differently)

Estimated Scope

  • Complexity: Medium-High (breaking change, core agent loop modification)
  • Files Modified: ~10-12 files
  • Breaking Changes: Yes - removal of ConversationManager base class and AgentConfig.conversationManager field
  • Estimated Effort: 2-3 days for implementation and testing

Notes

  • This is intentionally different from Python implementation
  • Breaking change requires version bump and migration documentation
  • All existing conversation management behavior is preserved, just triggered via hooks instead of direct method calls
  • The retry mechanism via retryModelCall provides more flexibility for custom hook implementations

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions