Skip to content
Open
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
11 changes: 10 additions & 1 deletion src/strands/agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,14 @@
from ..tools.registry import ToolRegistry
from ..tools.structured_output._structured_output_context import StructuredOutputContext
from ..tools.watcher import ToolWatcher
from ..types._events import AgentResultEvent, EventLoopStopEvent, InitEventLoopEvent, ModelStreamChunkEvent, TypedEvent
from ..types._events import (
AgentResultEvent,
ContextWindowFallbackStreamEvent,
EventLoopStopEvent,
InitEventLoopEvent,
ModelStreamChunkEvent,
TypedEvent,
)
from ..types.agent import AgentInput, ConcurrentInvocationMode
from ..types.content import ContentBlock, Message, Messages, SystemContentBlock
from ..types.exceptions import ConcurrencyException, ContextWindowOverflowException
Expand Down Expand Up @@ -963,6 +970,8 @@ async def _execute_event_loop_cycle(
yield event

except ContextWindowOverflowException as e:
# Notify callers that context compression is starting before the blocking operation
yield ContextWindowFallbackStreamEvent(message=str(e))
# Try reducing the context size and retrying
self.conversation_manager.reduce_context(self, e=e)

Expand Down
18 changes: 17 additions & 1 deletion src/strands/types/_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .citations import Citation
from .content import Message
from .event_loop import Metrics, StopReason, Usage
from .streaming import ContentBlockDelta, StreamEvent
from .streaming import ContentBlockDelta, ContextWindowFallbackEvent, StreamEvent
from .tools import ToolResult, ToolUse

if TYPE_CHECKING:
Expand Down Expand Up @@ -216,6 +216,22 @@ def is_callback_event(self) -> bool:
return False


class ContextWindowFallbackStreamEvent(TypedEvent):
"""Event emitted when the agent handles a context window overflow by reducing context.

Fired immediately before the conversation manager reduces the context, giving
callers a real-time signal that context compression is about to begin.
"""

def __init__(self, message: str) -> None:
"""Initialize with the overflow error message.

Args:
message: The overflow error message from the model provider.
"""
super().__init__({"contextWindowFallback": ContextWindowFallbackEvent(message=message)})


class EventLoopStopEvent(TypedEvent):
"""Event emitted when the agent execution completes normally."""

Expand Down
16 changes: 16 additions & 0 deletions src/strands/types/streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,13 +205,28 @@ class RedactContentEvent(TypedDict, total=False):
redactAssistantContentMessage: str | None


class ContextWindowFallbackEvent(TypedDict):
"""Event emitted when the agent handles a context window overflow.

Fired before the conversation manager reduces the context, giving callers a
real-time signal that context compression is about to begin. Useful for
showing progress indicators or logging during long-running agent sessions.

Attributes:
message: The overflow error message from the model provider.
"""

message: str


class StreamEvent(TypedDict, total=False):
"""The messages output stream.

Attributes:
contentBlockDelta: Delta content for a content block.
contentBlockStart: Start of a content block.
contentBlockStop: End of a content block.
contextWindowFallback: Context window overflow is being handled by reducing context.
internalServerException: Internal server error information.
messageStart: Start of a message.
messageStop: End of a message.
Expand All @@ -225,6 +240,7 @@ class StreamEvent(TypedDict, total=False):
contentBlockDelta: ContentBlockDeltaEvent
contentBlockStart: ContentBlockStartEvent
contentBlockStop: ContentBlockStopEvent
contextWindowFallback: ContextWindowFallbackEvent
internalServerException: ExceptionEvent
messageStart: MessageStartEvent
messageStop: MessageStopEvent
Expand Down
27 changes: 27 additions & 0 deletions tests/strands/agent/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2756,3 +2756,30 @@ def test_as_tool_defaults_description_when_agent_has_none():
tool = agent.as_tool()

assert tool.tool_spec["description"] == "Use the researcher agent as a tool by providing a natural language input"


@pytest.mark.asyncio
async def test_stream_async_emits_context_window_fallback_event(mock_model, agent, agenerator, alist):
"""stream_async yields contextWindowFallback before reducing context on overflow."""
overflow_message = "Input is too long for requested model"

mock_model.mock_stream.side_effect = [
ContextWindowOverflowException(RuntimeError(overflow_message)),
agenerator(
[
{"contentBlockStart": {"contentBlockIndex": 0, "start": {}}},
{"contentBlockDelta": {"contentBlockIndex": 0, "delta": {"text": "OK"}}},
{"contentBlockStop": {"contentBlockIndex": 0}},
{"messageStop": {"stopReason": "end_turn"}},
]
),
]

# Prevent reduce_context from raising when there are no messages to trim.
agent.conversation_manager.reduce_context = unittest.mock.Mock()

events = await alist(agent.stream_async("hello"))

fallback_events = [e for e in events if "contextWindowFallback" in e]
assert len(fallback_events) == 1
assert overflow_message in fallback_events[0]["contextWindowFallback"]["message"]