Repository navigation
RFC: supervised task handles, queued steering, and explicit result merges #7642
Description
Implementation update
PR #7643 contains one native
contribution for the collaboration contract and the Agent worker concurrency
limit from #7641. It is ready for review. The standalone experiment below remains
design evidence; its separate Runner runtime is not part of the upstream PR.
ADK already supports dynamic execution through Workflow and ctx.run_node().
The PR adds Context.parallel_tasks() around the existing node scheduler,
session, Runner event stream, and resume machinery. A coordinator can continue
working while children execute, inspect individual handles, steer a worker,
accept the first satisfactory result, and cancel unnecessary unfinished work.
@node(rerun_on_resume=True)
async def coordinate(ctx: Context, node_input: str):
async with ctx.parallel_tasks(name="research", max_parallel_workers=3) as tasks:
handles = [tasks.spawn(agent, node_input) for agent in researchers]
handles[2].send_message("Check the counterexamples as well.")
winner = await tasks.first(accept=verify_answer)
await tasks.merge_result(winner, keys=["answer", "sources"])
return winner.outputThe usage guide
and verified-winner sample
are included in that PR.
Native contract in the PR
- Scoped handles.
spawnreturns immediately. Handles expose identity,
status, output, and error; the group supports wait-any/all and ordered gather.
IDs identify children within an invocation. Groups own workers during one
coordinator execution, rather than providing a persistent background task
service across arbitrary model turns. - Bounded execution.
max_parallel_workersbounds active children; extra
handles remain queued. The same limit is exposed on Agent/LlmAgent parallel
workers. It is not a global budget for all nested descendants or a bound on
retained task handles. - Native progress. Child events carry task correlation in the existing
Runner stream. The PR does not add a separate cursor journal or observer
retention service. Handle status is authoritative after completion. - Selective cancellation and accepted results. An owned task can be
cancelled independently.first(accept=...)supports synchronous or
asynchronous validation, rejects unsuitable answers, persists the winner,
and cancels and drains unfinished siblings before returning. Completed
sibling results remain available, including after native resume. - Queued steering. Sending returns immediately with a receipt. Bounded
messages are delivered FIFO at the addressed agent's next model request
boundary and persisted in native history. Receipts distinguish queued,
delivered, and undelivered. Delivery does not acknowledge provider receipt
or compliance; if no later model request occurs, the message is undelivered. - Explicit merge and resume. Workers can return mapping proposals. A merge
checks all selected keys against the group's starting snapshot or explicit
expectations, validates the schema, then persists the state delta and merge
decision in one event. Reapplying a task/key merge is idempotent. Winner,
cancellation, and merge decisions participate in native human-input replay.
Workers retain shared session state. Explicit merging does not isolate or
roll back direct worker writes or external tool effects. Cancellation is
cooperative: it cannot interrupt blocking Python code or guarantee a provider
stops an already sent request. A drain timeout reports incomplete termination;
the enclosing Workflow may still wait for uncooperative work. Queued mailboxes
and receipts are in memory, and this is not a durable distributed task service.
The guide documents the stable naming/order requirements for resume.
Alignment with existing proposals
- feat(tools): Add AgentDispatcherToolset for runtime agents #6592 proposes persistent child-session dispatch, poll/await, callbacks,
and follow-ups. Its currentmessage_agentwaits for active work. The native
PR instead scopes owned tasks to a coordinator execution and admits steering
without waiting for the current model call to finish. The proposals have
different lifetimes; this RFC does not claim their APIs are unified or that
either has maintainer approval. - Feature request: let a background AgentTool/dispatched agent's own intermediate events be observed live (not just wait=false + poll for the final result) #7094 requests an observer interface for isolated background Runners. The
native PR correlates events already flowing through the parent Runner; a
separate isolated-Runner observer interface remains outside its scope. - Feature Proposal: Native Support for Multi-Agent Group Chat (GroupChat / Dialogue Orchestration) API #6214 requests shared conversation and speaker selection. This PR supplies
task lifecycle operations without choosing those conversation policies.
Maintainer feedback on the scoped API and its fit with these proposals is
welcome. The concurrency limit and collaboration contract are being reviewed
in the same PR, rather than as separate contributions.
Validation of the native implementation
The final revision is d36495faae14b7514bf05e9d41fe32fdf5e1298c.
- The full Linux unit matrix
passes using uv on Python 3.11–3.14: respectively 20,754 / 20,745 / 20,745 /
20,746 passed, with no failures or errors. Runs retain 93–94 skips, 18
expected failures, and two non-strict unexpected passes. - 407 focused tests pass across task lifecycle, workers, tool execution,
messaging, state conflicts, cancellation races, and both native resume modes.
Applicable pre-commit hooks and wheel/source builds pass; strict mypy adds no
errors relative to the unchanged upstream baseline. - Installed-wheel smoke tests exercise four concurrent real Agent instances
with scripted models, parent progress, queued steering, rejection of an early
answer, acceptance of a later answer, closed losing generators, and persisted
selected state. Agent worker limits 1, 2, and unlimited produce peaks 1, 2,
and 4 with ordered outputs. - The offline benchmark and raw measurements
record 12 repetitions per policy: median wall time 138.4 ms for gathering
every answer versus 80.6 ms for the first acceptable result, with two
workers cancelled and all generators closed. Fixed simulated delays measure
selection behavior, not production latency, token savings, or Python versus
Rust performance.
Earlier standalone experiment
The public experiment has 48
conformance tests against published google-adk==2.12.0, plus a four-worker
coordinator demo. It explored a broader application-level contract using public
Runner/session/plugin/tool APIs. Its behavior differs from the native PR:
| Area | Native PR #7643 | Standalone prototype |
|---|---|---|
| State | Shared session state; explicit proposal merge | Private child sessions with declared state exports |
| Progress | Correlation in the native Runner stream | Separate bounded cursor journal |
| Message during the final call | Undelivered if no subsequent model request | Bounded follow-up on the child session |
| Human-input resume | Native replay of task decisions | In-process experiment without resumable human-input tasks |
A Rust rewrite would not establish these lifecycle semantics. This contribution
first makes the concurrent behavior explicit and testable; language/runtime
changes would need separate evidence of a relevant bottleneck.
Activity