Skip to content

Commit 27e480d

Browse files
awsarronSourabh SarupriaSourabh Sarupria
authored andcommitted
Fix agent default callback handler (strands-agents#170)
Co-authored-by: Sourabh Sarupria <[email protected]> Co-authored-by: Sourabh Sarupria <[email protected]>
1 parent 650ab3f commit 27e480d

File tree

2 files changed

+60
-6
lines changed

2 files changed

+60
-6
lines changed

src/strands/agent/agent.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,16 @@
4545
logger = logging.getLogger(__name__)
4646

4747

48+
# Sentinel class and object to distinguish between explicit None and default parameter value
49+
class _DefaultCallbackHandlerSentinel:
50+
"""Sentinel class to distinguish between explicit None and default parameter value."""
51+
52+
pass
53+
54+
55+
_DEFAULT_CALLBACK_HANDLER = _DefaultCallbackHandlerSentinel()
56+
57+
4858
class Agent:
4959
"""Core Agent interface.
5060
@@ -71,7 +81,7 @@ def __init__(self, agent: "Agent") -> None:
7181
# agent tools and thus break their execution.
7282
self._agent = agent
7383

74-
def __getattr__(self, name: str) -> Callable:
84+
def __getattr__(self, name: str) -> Callable[..., Any]:
7585
"""Call tool as a function.
7686
7787
This method enables the method-style interface (e.g., `agent.tool.tool_name(param="value")`).
@@ -178,7 +188,9 @@ def __init__(
178188
messages: Optional[Messages] = None,
179189
tools: Optional[List[Union[str, Dict[str, str], Any]]] = None,
180190
system_prompt: Optional[str] = None,
181-
callback_handler: Optional[Callable] = PrintingCallbackHandler(),
191+
callback_handler: Optional[
192+
Union[Callable[..., Any], _DefaultCallbackHandlerSentinel]
193+
] = _DEFAULT_CALLBACK_HANDLER,
182194
conversation_manager: Optional[ConversationManager] = None,
183195
tool_manager: Optional[ToolManager] = None,
184196
max_parallel_tools: int = os.cpu_count() or 1,
@@ -206,7 +218,8 @@ def __init__(
206218
system_prompt: System prompt to guide model behavior.
207219
If None, the model will behave according to its default settings.
208220
callback_handler: Callback for processing events as they happen during agent execution.
209-
Defaults to strands.handlers.PrintingCallbackHandler if None.
221+
If not provided (using the default), a new PrintingCallbackHandler instance is created.
222+
If explicitly set to None, null_callback_handler is used.
210223
conversation_manager: Manager for conversation history and context window.
211224
Defaults to strands.agent.conversation_manager.SlidingWindowConversationManager if None.
212225
max_parallel_tools: Maximum number of tools to run in parallel when the model returns multiple tool calls.
@@ -224,7 +237,17 @@ def __init__(
224237
self.messages = messages if messages is not None else []
225238

226239
self.system_prompt = system_prompt
227-
self.callback_handler = callback_handler or null_callback_handler
240+
241+
# If not provided, create a new PrintingCallbackHandler instance
242+
# If explicitly set to None, use null_callback_handler
243+
# Otherwise use the passed callback_handler
244+
self.callback_handler: Union[Callable[..., Any], PrintingCallbackHandler]
245+
if isinstance(callback_handler, _DefaultCallbackHandlerSentinel):
246+
self.callback_handler = PrintingCallbackHandler()
247+
elif callback_handler is None:
248+
self.callback_handler = null_callback_handler
249+
else:
250+
self.callback_handler = callback_handler
228251

229252
self.conversation_manager = conversation_manager if conversation_manager else SlidingWindowConversationManager()
230253

@@ -455,7 +478,7 @@ def target_callback() -> None:
455478
thread.join()
456479

457480
def _run_loop(
458-
self, prompt: str, kwargs: Any, supplementary_callback_handler: Optional[Callable] = None
481+
self, prompt: str, kwargs: Dict[str, Any], supplementary_callback_handler: Optional[Callable[..., Any]] = None
459482
) -> AgentResult:
460483
"""Execute the agent's event loop with the given prompt and parameters."""
461484
try:
@@ -481,7 +504,7 @@ def _run_loop(
481504
finally:
482505
self.conversation_manager.apply_management(self)
483506

484-
def _execute_event_loop_cycle(self, callback_handler: Callable, kwargs: dict[str, Any]) -> AgentResult:
507+
def _execute_event_loop_cycle(self, callback_handler: Callable[..., Any], kwargs: Dict[str, Any]) -> AgentResult:
485508
"""Execute the event loop cycle with retry logic for context window limits.
486509
487510
This internal method handles the execution of the event loop cycle and implements

tests/strands/agent/test_agent.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,37 @@ def test_agent_with_callback_handler_none_uses_null_handler():
686686
assert agent.callback_handler == null_callback_handler
687687

688688

689+
def test_agent_callback_handler_not_provided_creates_new_instances():
690+
"""Test that when callback_handler is not provided, new PrintingCallbackHandler instances are created."""
691+
# Create two agents without providing callback_handler
692+
agent1 = Agent()
693+
agent2 = Agent()
694+
695+
# Both should have PrintingCallbackHandler instances
696+
assert isinstance(agent1.callback_handler, PrintingCallbackHandler)
697+
assert isinstance(agent2.callback_handler, PrintingCallbackHandler)
698+
699+
# But they should be different object instances
700+
assert agent1.callback_handler is not agent2.callback_handler
701+
702+
703+
def test_agent_callback_handler_explicit_none_uses_null_handler():
704+
"""Test that when callback_handler is explicitly set to None, null_callback_handler is used."""
705+
agent = Agent(callback_handler=None)
706+
707+
# Should use null_callback_handler
708+
assert agent.callback_handler is null_callback_handler
709+
710+
711+
def test_agent_callback_handler_custom_handler_used():
712+
"""Test that when a custom callback_handler is provided, it is used."""
713+
custom_handler = unittest.mock.Mock()
714+
agent = Agent(callback_handler=custom_handler)
715+
716+
# Should use the provided custom handler
717+
assert agent.callback_handler is custom_handler
718+
719+
689720
@pytest.mark.asyncio
690721
async def test_stream_async_returns_all_events(mock_event_loop_cycle):
691722
agent = Agent()

0 commit comments

Comments
 (0)