Skip to content

feat(agents): annotate events with the LoopAgent iteration#6268

Open
ferponse wants to merge 2 commits into
google:mainfrom
ferponse:feat/loop-agent-iteration-metadata
Open

feat(agents): annotate events with the LoopAgent iteration#6268
ferponse wants to merge 2 commits into
google:mainfrom
ferponse:feat/loop-agent-iteration-metadata

Conversation

@ferponse

@ferponse ferponse commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What

LoopAgent re-runs its sub-agents each iteration, but events produced in different iterations are indistinguishable in the stream — same author, branch, and invocation_id. So a consumer cannot tell iteration 1 from iteration 2. This is most visible with a ParallelAgent nested in a LoopAgent, whose branch ids (par.p, par.q) repeat every iteration.

This records the current 0-based iteration under Event.custom_metadata['loop_iteration'] as each sub-agent event bubbles up through the loop.

Fixes #6266.

Why

We're building the AG-UI ↔ ADK middleware (ag-ui-adk), which maps ADK workflow nodes to AG-UI STEP_STARTED / STEP_FINISHED events. It infers node boundaries from the runner.run_async stream (author = node, branch = structural path). This works for every serial and parallel topology except a parallel block inside a loop, where iterations are indistinguishable, so per-iteration steps can't be reconstructed. Verified on google-adk 2.3.0.

How

  • New LoopAgent.LOOP_ITERATION_KEY = 'loop_iteration' constant.
  • _annotate_loop_iteration(event, times_looped) stamps event.custom_metadata via setdefault, so the nearest enclosing loop wins when loops are nested.
  • Called on each sub-agent event before it is yielded.

Additive and behavior-preserving: no new events, no schema change (custom_metadata is the existing custom bucket).

LoopAgent(max_iterations=2, sub_agents=[ParallelAgent(sub_agents=[p, q])]) — after this change:

author='p' branch='par.p'  custom_metadata['loop_iteration']=0   ← iteration 1
author='q' branch='par.q'  custom_metadata['loop_iteration']=0
author='p' branch='par.p'  custom_metadata['loop_iteration']=1   ← iteration 2 (now distinguishable)
author='q' branch='par.q'  custom_metadata['loop_iteration']=1

Tests

tests/unittests/agents/test_loop_agent.py: added test_run_async_annotates_loop_iteration (iterations 0/1/2) and test_run_async_loop_iteration_survives_nested_loops (nested loops — inner wins). The full test_loop_agent.py passes.

Notes

  • LoopAgent is @deprecated in favor of Workflow; this fixes the still-widely-used classic loop. The same iteration signal would be valuable from the Workflow graph engine (out of scope here).
  • Companion issue Emit agent/node lifecycle events (start/finish) in runner.run_async #6267 proposes the general form (explicit agent/node lifecycle events in the stream), which would subsume this.

ferponse and others added 2 commits July 2, 2026 03:32
LoopAgent re-runs its sub-agents each iteration, but events from different
iterations are indistinguishable in the stream (same author, branch and
invocation_id), so consumers cannot tell iterations apart — e.g. a ParallelAgent
nested in a LoopAgent, whose branch ids repeat every iteration.

Record the current 0-based iteration under Event.custom_metadata['loop_iteration']
as each sub-agent event bubbles up. Additive and behavior-preserving; setdefault
means the nearest enclosing loop wins when loops are nested.

Fixes google#6266.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

LoopAgent: surface the current iteration index in the event stream

1 participant