Skip to content

Commit 844cd36

Browse files
DeanChensjcopybara-github
authored andcommitted
refactor: Split Context.run_node to support internal orchestration
This PR extracts the core execution logic from the public `Context.run_node` method into a new internal method `_run_node_internal`. This is the first step in unifying static and dynamic node execution paths. By splitting these methods, we maintain a clean public API while providing the orchestration engines (such as the workflow engine and dynamic scheduler) with the necessary hooks to drive and inspect node execution. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 938226321
1 parent ec4446f commit 844cd36

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

src/google/adk/agents/context.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,10 +452,47 @@ async def run_node(
452452
use_sub_branch: If True, the dynamic node will be executed in a sub-branch
453453
to isolate its state and events from the main branch.
454454
override_branch: An optional branch to use instead of parent's branch.
455+
override_isolation_scope: An optional isolation scope to use instead of
456+
the parent's scope.
457+
raise_on_wait: If True, raises NodeInterruptedError when the child node
458+
is WAITING instead of returning None.
455459
456460
Returns:
457461
The output of the dynamically executed node, once it finishes executing.
458462
"""
463+
return await self._run_node_internal(
464+
node,
465+
node_input,
466+
use_as_output=use_as_output,
467+
run_id=run_id,
468+
use_sub_branch=use_sub_branch,
469+
override_branch=override_branch,
470+
override_isolation_scope=override_isolation_scope,
471+
raise_on_wait=raise_on_wait,
472+
resume_inputs=None,
473+
return_ctx=False,
474+
)
475+
476+
async def _run_node_internal(
477+
self,
478+
node: NodeLike,
479+
node_input: Any = None,
480+
*,
481+
use_as_output: bool = False,
482+
run_id: str | None = None,
483+
use_sub_branch: bool = False,
484+
override_branch: str | None = None,
485+
override_isolation_scope: str | None = None,
486+
raise_on_wait: bool = False,
487+
return_ctx: bool = False,
488+
resume_inputs: dict[str, Any] | None = None,
489+
) -> Any:
490+
"""Executes a node dynamically (Internal Orchestration API).
491+
492+
See public ``run_node`` for public argument details.
493+
Additional internal args:
494+
return_ctx: If True, returns the child's Context instead of its output.
495+
"""
459496

460497
if not self._node_rerun_on_resume:
461498
raise ValueError(
@@ -538,7 +575,7 @@ async def run_node(
538575
and not child_ctx.actions.transfer_to_agent
539576
):
540577
raise NodeInterruptedError()
541-
return child_ctx.output
578+
return child_ctx if return_ctx else child_ctx.output
542579

543580
# Mode 2: Standalone execution (outside of workflow).
544581
# Run the node directly via NodeRunner.
@@ -550,6 +587,7 @@ async def run_node(
550587
override_branch=override_branch,
551588
override_isolation_scope=override_isolation_scope,
552589
run_id=run_id,
590+
resume_inputs=resume_inputs,
553591
)
554592
if result.error:
555593
from ..workflow import _errors
@@ -568,7 +606,7 @@ async def run_node(
568606
from ..workflow._errors import NodeInterruptedError
569607

570608
raise NodeInterruptedError()
571-
return result.output
609+
return result if return_ctx else result.output
572610

573611
# ============================================================================
574612
# Artifact methods

tests/unittests/agents/test_context.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,3 +673,55 @@ def test_get_invocation_context_propagates_isolation_scope(
673673
}
674674
)
675675
assert result is mock_copy
676+
677+
678+
class TestContextRunNodeInternal:
679+
"""Tests for the internal Context._run_node_internal orchestration method."""
680+
681+
@pytest.mark.asyncio
682+
async def test_run_node_internal_returns_ctx_and_handles_resume_inputs(
683+
self, mock_invocation_context, mocker
684+
):
685+
"""Test that _run_node_internal correctly handles return_ctx and resume_inputs."""
686+
# Arrange
687+
from google.adk.agents.llm_agent import LlmAgent
688+
from google.adk.events.event_actions import EventActions
689+
690+
agent_a = LlmAgent(name="agent_a", rerun_on_resume=True)
691+
root = LlmAgent(name="root", sub_agents=[agent_a], rerun_on_resume=True)
692+
agent_a.parent_agent = root
693+
694+
root_ctx = Context(mock_invocation_context, node=root, run_id="1")
695+
696+
child_ctx_a = Context(
697+
mock_invocation_context,
698+
parent_ctx=root_ctx,
699+
node=agent_a,
700+
run_id="1",
701+
event_actions=EventActions(),
702+
)
703+
child_ctx_a.output = "a_output"
704+
705+
# Mock the standalone execution boundary
706+
mock_run_standalone = mocker.patch.object(
707+
Context,
708+
"_run_node_standalone",
709+
return_value=child_ctx_a,
710+
)
711+
712+
# Act 1: Call _run_node_internal with return_ctx=True
713+
result_ctx = await root_ctx._run_node_internal(
714+
agent_a,
715+
node_input="a_input",
716+
return_ctx=True,
717+
resume_inputs={"some_key": "some_val"},
718+
)
719+
720+
# Assert 1: It returns the child context object itself, not the output!
721+
assert result_ctx is child_ctx_a
722+
assert result_ctx.output == "a_output"
723+
724+
# Assert 2: resume_inputs was correctly passed to _run_node_standalone
725+
mock_run_standalone.assert_called_once()
726+
_, kwargs = mock_run_standalone.call_args
727+
assert kwargs.get("resume_inputs") == {"some_key": "some_val"}

0 commit comments

Comments
 (0)