Skip to content

Commit a680cea

Browse files
haranrkcopybara-github
authored andcommitted
feat(agents): bridge node_input to user_content for ManagedAgent nodes
Override `ManagedAgent._run_impl` so that, when the agent runs as a node (e.g. as a single-turn tool), the parent's tool-call argument arriving as `node_input` is surfaced as the agent's `user_content` and forwarded to the interactions API. When `node_input` is `None` (classic agent-tree run), behavior matches `BaseAgent._run_impl`. Uses the shared `node_input_to_content` helper. Co-authored-by: Haran Rajkumar <haranrk@google.com> PiperOrigin-RevId: 945949834
1 parent ce2e4ca commit a680cea

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

src/google/adk/agents/_managed_agent.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from pydantic import ConfigDict
3232
from pydantic import Field
3333
from pydantic import PrivateAttr
34+
from typing_extensions import override
3435

3536
from ..events.event import Event
3637
from ..flows.llm_flows.interactions_processor import _find_previous_interaction_state
@@ -47,9 +48,11 @@
4748
from ..tools.tool_context import ToolContext
4849
from ..utils._google_client_headers import get_tracking_http_options
4950
from ..utils._google_client_headers import merge_tracking_headers
51+
from ..utils.content_utils import to_user_content
5052
from ..utils.context_utils import Aclosing
5153
from ..utils.env_utils import is_enterprise_mode_enabled
5254
from .base_agent import BaseAgent
55+
from .context import Context
5356
from .invocation_context import InvocationContext
5457
from .readonly_context import ReadonlyContext
5558
from .run_config import StreamingMode
@@ -311,6 +314,30 @@ def _error_event(
311314
turn_complete=True,
312315
)
313316

317+
@override
318+
async def _run_impl(
319+
self, *, ctx: Context, node_input: Any
320+
) -> AsyncGenerator[Event, None]:
321+
"""Runs the ManagedAgent as a node, threading node_input into user_content.
322+
323+
When invoked as a single-turn tool (``mode='single_turn'``), the parent's
324+
tool-call argument arrives as ``node_input``; surface it as the agent's
325+
``user_content`` so ``_run_async_impl`` sends it to the interactions API.
326+
When ``node_input`` is ``None`` (classic agent-tree run), behavior is
327+
identical to ``BaseAgent._run_impl``.
328+
"""
329+
parent_context = ctx.get_invocation_context()
330+
if node_input is not None:
331+
parent_context = parent_context.model_copy(
332+
update={'user_content': to_user_content(node_input)}
333+
)
334+
async for event in self.run_async(parent_context=parent_context):
335+
if event.author:
336+
ctx.event_author = event.author
337+
if not event.node_info.path and event.author == self.name:
338+
event.node_info.path = ctx.node_path
339+
yield event
340+
314341
async def _run_async_impl(
315342
self, ctx: InvocationContext
316343
) -> AsyncGenerator[Event, None]:

tests/unittests/agents/test_managed_agent.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,12 +775,51 @@ def test_mode_single_turn_is_accepted():
775775

776776
def test_mode_chat_is_rejected():
777777
from pydantic import ValidationError
778-
import pytest
779778

780779
with pytest.raises(ValidationError):
781780
ManagedAgent(name='m', agent_id='a', mode='chat')
782781

783782

783+
async def test_run_impl_bridges_node_input_to_user_content():
784+
from google.adk.agents.context import Context
785+
from google.adk.agents.invocation_context import InvocationContext
786+
from google.adk.sessions.in_memory_session_service import InMemorySessionService
787+
from google.adk.sessions.session import Session
788+
from google.adk.utils.content_utils import to_user_content
789+
790+
captured = {}
791+
792+
# ManagedAgent is a Pydantic model, so `run_async` cannot be reassigned on an
793+
# instance; subclassing to override it is the pydantic-safe equivalent of
794+
# stubbing it. The override captures the `parent_context.user_content` that
795+
# `_run_impl` threads in from `node_input`.
796+
class _CapturingManagedAgent(ManagedAgent):
797+
798+
async def run_async(self, parent_context):
799+
captured['user_content'] = parent_context.user_content
800+
return
801+
yield # make this an async generator
802+
803+
agent = _CapturingManagedAgent(name='m', agent_id='a', mode='single_turn')
804+
805+
# Build a minimal node Context (mirrors
806+
# tests/unittests/workflow/test_agent_node.py).
807+
session = Session(app_name='test', user_id='user', id='session')
808+
ic = InvocationContext(
809+
invocation_id='inv',
810+
session=session,
811+
session_service=InMemorySessionService(),
812+
)
813+
ctx = Context(ic, node_path='wf')
814+
815+
events = [
816+
e async for e in agent._run_impl(ctx=ctx, node_input='compute primes')
817+
]
818+
819+
assert events == []
820+
assert captured['user_content'] == to_user_content('compute primes')
821+
822+
784823
async def _drain(agen):
785824
async for _ in agen:
786825
pass

0 commit comments

Comments
 (0)