Skip to content

Conversation

@github-actions
Copy link

Overview

Implements #5 - Allows hooks to retry model invocations by adding a retry_model field to AfterModelCallEvent.

Why This Change?

Users need flexibility to retry model calls on arbitrary exceptions (e.g., ServiceUnavailableException with 503 errors). The existing retry mechanism only handles ModelThrottledException. This change enables hooks to implement custom retry logic without hardcoding specific exception types into the framework.

What Changed?

Core Changes

  1. AfterModelCallEvent Enhancement (src/strands/hooks/events.py)

    • Added retry_model: bool = False field
    • Implemented _can_write() method to allow hook modification
    • Updated docstring to document retry behavior
  2. Event Loop Integration (src/strands/event_loop/event_loop.py)

    • Modified _handle_model_execution() to check retry_model after hook invocation
    • If retry_model=True and exception exists, continues retry loop
    • Maintains existing throttle retry logic unchanged
  3. Comprehensive Tests (tests/strands/agent/hooks/test_agent_events.py)

    • Test retry on exception with successful retry
    • Test retry ignored on successful calls (no exception)
    • Test multiple hooks modifying field (reverse callback ordering)
    • Test hook-controlled retry count limits
    • Test hook-controlled delay with exponential backoff

Public API Changes

New Field: AfterModelCallEvent.retry_model

from strands.hooks import AfterModelCallEvent, HookProvider

class RetryOnServiceUnavailable(HookProvider):
    def __init__(self, max_retries=3):
        self.max_retries = max_retries
        self.retry_counts = {}
    
    def register_hooks(self, registry):
        registry.add_callback(AfterModelCallEvent, self.handle_retry)
    
    async def handle_retry(self, event):
        if event.exception and "ServiceUnavailable" in str(event.exception):
            request_id = id(event)
            count = self.retry_counts.get(request_id, 0)
            
            if count < self.max_retries:
                self.retry_counts[request_id] = count + 1
                await asyncio.sleep(2 ** count)  # Exponential backoff
                event.retry_model = True
            else:
                self.retry_counts.pop(request_id, None)

Design Decisions

  1. Only Check on Exception: retry_model is ignored when no exception exists, preventing retry on successful calls
  2. Hook-Managed Retry Logic: Framework doesn't enforce retry limits or delays - hooks control their own logic
  3. Independent from Throttle Retry: Hook-initiated retries work alongside existing ModelThrottledException retry without interference
  4. Reverse Callback Ordering: Respects AfterModelCallEvent's existing reverse callback pattern - first registered hook's value wins

Testing

All unit tests pass (1627/1627):

  • ✅ Basic retry on exception
  • ✅ Retry ignored on success
  • ✅ Multiple hooks modifying field
  • ✅ Hook-controlled retry count
  • ✅ Hook-controlled delay
  • ✅ Existing throttle retry tests continue to pass

Resolves

Resolves #5

Add retry_model field to AfterModelCallEvent that hooks can set to
retry model calls when exceptions occur. This enables custom retry
logic for any exception type via hooks.

- Add retry_model: bool field to AfterModelCallEvent
- Implement _can_write() to allow hook modification
- Update event_loop to check retry_model after hook invocation
- Add comprehensive tests for retry functionality
- Only check retry_model when exception exists

Hooks can now implement their own retry logic including:
- Custom retry count limits
- Exponential backoff delays
- Exception type filtering

Resolves #5
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.

[FEATURE] Allow hooks to retry model invocations

2 participants