ParallelAgent: two things at once
When two steps do not need each other's answers, running them one after the other only adds time.
account = LlmAgent(
name="account",
model=PretendModel(replies=[say("active")]),
instruction="Report the account status.",
output_key="account",
)
payments = LlmAgent(
name="payments",
model=PretendModel(replies=[say("two charges on 3 March")]),
instruction="Report recent payments.",
output_key="payments",
)Two independent lookups. Neither reads the other's key, which is the condition for running them together.
checks = ParallelAgent(name="checks", sub_agents=[account, payments])session = await run(checks, "Look up this customer")
print("state:", dict(session.state))account active
payments two charges on 3 March
state: {'account': 'active', 'payments': 'two charges on 3 March'}Both ran and both left their result in state under their own key. Nothing was shared between them while they worked.
Separate branches, one state
Concurrent does not mean isolated. Each sub-agent runs on a branch of its own, so their events do not mix:
runner = InMemoryRunner(agent=checks, app_name="demo")
session = await runner.session_service.create_session(app_name="demo", user_id="u1")
message = types.Content(role="user", parts=[types.Part(text="Look up this customer")])
async for event in runner.run_async(user_id="u1", session_id=session.id, new_message=message):
print(f"{event.author:<9} branch {event.branch}")account branch checks.account payments branch checks.payments
The branch is the path of agent names, so nothing either agent says appears in the other's history. The session they write into is not branched, though: there is one session.state dictionary, and both of them write into it. That is what output_key puts a value in.
So two agents in the same parallel group must not share an output_key. Give them the same one and only one value survives:
same_key = ParallelAgent(name="checks", sub_agents=[
LlmAgent(name="account", model=PretendModel(replies=[say("active")]),
instruction="Report the account status.", output_key="finding"),
LlmAgent(name="payments", model=PretendModel(replies=[say("two charges on 3 March")]),
instruction="Report recent payments.", output_key="finding"),
])
session = await run(same_key, "Look up this customer")
print("keys in state:", sorted(session.state))account active payments two charges on 3 March keys in state: ['finding']
One key, not two. Which of the two answers is in it depends on which agent finished last, so a program written this way gives a different result on a different day. The fix is the one above: a key per agent, and a step afterwards that reads both.
Running this prints a deprecation warning: as of ADK 2.x the three template workflow agents are deprecated in favour of a newer Workflow API. They still work, they are still what most existing code uses, and there is one thing only they can do. Lesson 20 covers the newer way and when each is right.
The rule for using it
Every agent in a parallel group must be able to do its job without the others' output. If one needs what another produces, they belong in a sequence.
Mixing the two is normal: a parallel group inside a sequence, so the slow independent work happens at once and a final step reads both results.
gather = ParallelAgent(name="gather", sub_agents=[account, payments])
summarise = LlmAgent(
name="summarise",
model="gemini-flash-latest",
instruction="Summarise for an agent: account {account}, payments {payments}.",
)
desk = SequentialAgent(name="desk", sub_agents=[gather, summarise])That shape is the common one in real projects: fan out for the lookups, then one agent to write the answer. It reads exactly as it runs.
Pick one to watch it run, step by step.
- Add a third agent to the group and print its branch.
- Wrap the parallel group in a sequence with a summarising step.
Slow is fine. Stopping is the only problem.