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
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ dev = [
"ruff>=0.12.0",
"websockets>=14.1",
"wheel>=0.45.1",
"strands-agents>=1.46.0",
"strands-agents>=1.56.0",
"strands-agents-evals>=1.0.3,<2.0.0",
"deepeval>=3.5.0,<5.0.0",
"autoevals>=0.3.0,<1.0.0",
Expand All @@ -169,7 +169,7 @@ a2a = ["a2a-sdk[http-server]>=0.3,<0.4"]
a2a-v1 = ["a2a-sdk[http-server]>=1.0.1,<2.0"]
ag-ui = ["ag-ui-protocol>=0.1.10"]
strands-agents = [
"strands-agents>=1.46.0",
"strands-agents>=1.56.0",
"mcp>=1.23.0,<2.0.0",
]
langgraph = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@

import boto3
from botocore.config import Config as BotocoreConfig
from strands.experimental.hooks.events import (
BidiAfterInvocationEvent,
BidiAgentInitializedEvent,
BidiMessageAddedEvent,
)
from strands.experimental.bidi.hooks import BidiAgentStopEvent
from strands.experimental.hooks.multiagent.events import (
AfterMultiAgentInvocationEvent,
AfterNodeCallEvent,
Expand Down Expand Up @@ -45,7 +41,7 @@
from .converters import MemoryConverter

if TYPE_CHECKING:
from strands.agent.agent import Agent
from strands.types.agent import LocalAgent

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -829,7 +825,7 @@ def _filter_restored_tool_context(self, messages: list[SessionMessage]) -> list[

# region RepositorySessionManager overrides
@override
def append_message(self, message: Message, agent: "Agent", **kwargs: Any) -> None:
def append_message(self, message: Message, agent: "LocalAgent", **kwargs: Any) -> None:
"""Append a message to the agent's session using AgentCore's eventId as message_id.

Args:
Expand Down Expand Up @@ -951,9 +947,8 @@ def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
return

# Async mode: register async callbacks that offload the existing sync
# methods to a worker thread via asyncio.to_thread. AgentInitializedEvent
# and BidiAgentInitializedEvent must stay sync (Strands disallows async
# callbacks for AgentInitializedEvent — see strands/hooks/registry.py:227).
# methods to a worker thread via asyncio.to_thread. Initialization
# callbacks must stay synchronous.
logger.warning(
"AgentCoreMemorySessionManager async_mode=True: the agent must be invoked "
"via the async path (e.g. agent.stream_async(...) or agent.invoke_async(...)). "
Expand Down Expand Up @@ -990,21 +985,10 @@ async def _on_message_added_persist(event: MessageAddedEvent) -> None:
registry.add_callback(AfterNodeCallEvent, _offload(self.sync_multi_agent, lambda e: e.source))
registry.add_callback(AfterMultiAgentInvocationEvent, _offload(self.sync_multi_agent, lambda e: e.source))

# Register BidiAgent callbacks so async-mode parity matches sync-mode.
# BidiAgentInitializedEvent dispatches through invoke_callbacks (sync),
# so its callback must stay sync; the other two dispatch through
# invoke_callbacks_async, so async wrappers are safe.
registry.add_callback(BidiAgentInitializedEvent, lambda event: self.initialize_bidi_agent(event.agent))

async def _on_bidi_message_added(event: BidiMessageAddedEvent) -> None:
await asyncio.to_thread(self.append_bidi_message, event.message, event.agent)
await asyncio.to_thread(self.sync_bidi_agent, event.agent)

registry.add_callback(BidiMessageAddedEvent, _on_bidi_message_added)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Now that bidi uses MessageAddedEvent, will retrieve_customer_context run for bidi too? It looks like it updates agent.messages, while the model still receives the original input event. could we skip retrieval for bidi or inject it into the outgoing event?

registry.add_callback(BidiAfterInvocationEvent, _offload(self.sync_bidi_agent, lambda e: e.agent))
registry.add_callback(BidiAgentStopEvent, _offload(self.sync_agent, lambda e: e.agent))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I might be missing something here, but with batching enabled, doesn’t sync_agent() just add the state to the buffer?

since Bidi doesn’t fire AfterInvocationEvent, what flushes the pending messages and state after agent.stop()?


@override
def initialize(self, agent: "Agent", **kwargs: Any) -> None:
def initialize(self, agent: "LocalAgent", **kwargs: Any) -> None:
if self.has_existing_agent:
logger.warning(
"An Agent already exists in session %s. We currently support one agent per session.", self.session_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@
from botocore.config import Config as BotocoreConfig
from botocore.exceptions import ClientError
from strands.agent.agent import Agent
from strands.experimental.hooks.events import (
BidiAfterInvocationEvent,
BidiAgentInitializedEvent,
BidiMessageAddedEvent,
)
from strands.experimental.bidi.hooks import BidiAgentStopEvent
from strands.experimental.hooks.multiagent.events import (
AfterMultiAgentInvocationEvent,
AfterNodeCallEvent,
MultiAgentInitializedEvent,
)
from strands.hooks import AfterInvocationEvent, MessageAddedEvent
from strands.hooks import AfterInvocationEvent, AgentInitializedEvent, MessageAddedEvent
from strands.hooks.registry import HookRegistry
from strands.types.exceptions import SessionException
from strands.types.session import Session, SessionAgent, SessionMessage, SessionType
Expand Down Expand Up @@ -3749,16 +3745,13 @@ def test_async_mode_registers_bidi_agent_callbacks(self, mock_memory_client):
registry = HookRegistry()
manager.register_hooks(registry)

# BidiAgentInitializedEvent dispatches via the sync hook path, so its callback must NOT be a coroutine.
init_callbacks = list(registry.get_callbacks_for(BidiAgentInitializedEvent(agent=Mock())))
assert init_callbacks, "No callbacks registered for BidiAgentInitializedEvent"
init_callbacks = list(registry.get_callbacks_for(AgentInitializedEvent(agent=Mock())))
assert init_callbacks
assert not any(asyncio.iscoroutinefunction(cb) for cb in init_callbacks)

# BidiMessageAddedEvent and BidiAfterInvocationEvent dispatch via invoke_callbacks_async,
# so their callbacks should be async to keep the event loop unblocked.
for event in (
BidiMessageAddedEvent(agent=Mock(), message={"role": "user", "content": [{"text": "x"}]}),
BidiAfterInvocationEvent(agent=Mock()),
MessageAddedEvent(agent=Mock(), message={"role": "user", "content": [{"text": "x"}]}),
BidiAgentStopEvent(agent=Mock()),
):
callbacks = list(registry.get_callbacks_for(event))
assert callbacks, f"No callbacks registered for {type(event).__name__}"
Expand Down
10 changes: 5 additions & 5 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading