Skip to content

Commit a5a3f2e

Browse files
DeanChensjcopybara-github
authored andcommitted
fix: Preserve event details when output is delegated
When a node delegates its output, the event is now enqueued with the `output` field set to None, while other event details such as actions and content are preserved. Previously, the entire event was skipped, leading to loss of information. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 930758396
1 parent 6af4562 commit a5a3f2e

2 files changed

Lines changed: 41 additions & 4 deletions

File tree

src/google/adk/workflow/_node_runner.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
logger = logging.getLogger("google_adk." + __name__)
4141

4242

43+
def _has_non_output_content(event: Event) -> bool:
44+
if event.actions:
45+
if event.actions.state_delta or event.actions.artifact_delta:
46+
return True
47+
return False
48+
49+
4350
class NodeRunner:
4451
"""Per-node executor. Drives BaseNode.run(), enriches events.
4552
@@ -314,12 +321,14 @@ def _track_event_in_context(self, event: Event, ctx: Context) -> None:
314321
async def _enqueue_event(self, event: Event, ctx: Context) -> None:
315322
"""Enrich and enqueue event to the session.
316323
317-
Skips enqueueing if output is delegated via use_as_output
318-
the child already emitted it. Pending deltas stay in ctx for
319-
_flush_output_and_deltas.
324+
Suppresses output if output is delegated via use_as_output (since the child
325+
already emitted it), but preserves other event details. Pending deltas stay
326+
in ctx for _flush_output_and_deltas.
320327
"""
321328
if event.output is not None and ctx._output_delegated:
322-
return
329+
if not _has_non_output_content(event):
330+
return
331+
event = event.model_copy(update={"output": None})
323332

324333
self._enrich_event(event, ctx)
325334
if not event.partial:

tests/unittests/workflow/test_node_runner_ctx.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,34 @@ async def _run_impl(self, *, ctx, node_input):
396396
assert len(output_events) == 0
397397

398398

399+
@pytest.mark.asyncio
400+
async def test_delegated_output_preserves_event_details():
401+
"""When output is delegated, the event is enqueued but output is suppressed."""
402+
from google.adk.events.event_actions import EventActions
403+
404+
class _Node(BaseNode):
405+
406+
async def _run_impl(self, *, ctx, node_input):
407+
ctx._output_delegated = True
408+
yield Event(
409+
output='delegated_value',
410+
actions=EventActions(state_delta={'foo': 'bar'}),
411+
content=types.Content(role='model', parts=[types.Part(text='hello')]),
412+
)
413+
414+
parent_ctx, events = _make_ctx()
415+
child_ctx = await NodeRunner(
416+
node=_Node(name='n'), parent_ctx=parent_ctx
417+
).run()
418+
419+
assert child_ctx.output == 'delegated_value'
420+
assert len(events) == 1
421+
event = events[0]
422+
assert event.output is None
423+
assert event.actions.state_delta == {'foo': 'bar'}
424+
assert event.content.parts[0].text == 'hello'
425+
426+
399427
# =========================================================================
400428
# Context as INPUT — resume state provided to NodeRunner at construction
401429
# =========================================================================

0 commit comments

Comments
 (0)