You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Is your feature request related to a specific problem?
Yes. Consider a common multi-agent chat pattern: several specialist sub-agents
share one session, and a root agent dispatches each user turn to one of them,
forking a sub-branch per delegation. Event.branch isolation is the
documented tool for exactly this ("Branch is used when multiple sub-agent
shouldn't see their peer agents' conversation history", events/event.py) —
but it is all-or-nothing at the event level. A sibling agent later in the same
conversation sees the user's turns (branch=None) but not the previous
agent's final answers: they are filtered out together with that agent's tool
internals, because both live on the same foreign branch.
Concrete example — a travel assistant with two specialists sharing one
session: bookings (answers questions about the user's reservations via an
internal reservations-lookup tool) and local_guide (recommends places to
eat and visit, no access to reservation systems):
T1 user -> bookings: "When do I land in Tokyo?"
bookings -> [reservation_lookup call]
[raw result: fare-class codes, internal record IDs, PNR data]
bookings -> "You land on Tuesday at 14:05 local time."
T2 user -> local_guide: "Great - dinner recommendations near my hotel
for that evening?"
To answer T2 sensibly, local_guide must know what the user was told at
T1 ("that evening" = Tuesday evening, after a 14:05 arrival). With branch
isolation, it only sees the user's questions: it knows the user asked about
landing in Tokyo, but not the answer. The end user, looking at their chat
window, sees both — so the agent is strictly less informed than the human it
is talking to.
At the same time, local_guide must not see the reservation_lookup
call and its raw result (internal record IDs, fare-class codes, PNR data):
guardrails covering that content exist only on bookings, and a
restaurant-recommendation agent has no business carrying raw reservation
records in its prompt.
The existing knobs don't offer this middle ground:
No branch fork: then everything is visible, and _present_other_agent_message (flows/llm_flows/contents.py) verbalizes
foreign tool calls and tool results into the model context
("[bookings] called tool reservation_lookup with parameters ...", "[bookings] tool returned result: ..."). The raw reservation record lands
verbatim in the restaurant agent's prompt.
output_key + instruction templating shares only the last answer per
agent under a static key — not the conversation flow.
What is missing is a middle setting between "everything" and "nothing":
share only what the end user has already seen (final response text), never the
tool internals.
Describe the Solution You'd Like
An opt-in, prose-only visibility of sibling final responses during content
assembly. Two possible shapes (either would work):
Option A — consumer-side flag on LlmAgent (preferred):
Semantics in _get_contents: an event from a foreign branch is included iff
event.is_final_response() is true, and
its content carries only text parts (no function_call,
no function_response, thought parts excluded — the same part filtering
that output_key applies in LlmAgent.__maybe_save_output_to_state), and
it is presented via the existing _present_other_agent_message path
(role='user', "[author] said: ..."), so authorship stays unambiguous.
With the flag set, local_guide's context at T2 would contain:
user: "When do I land in Tokyo?"
user: "For context: [bookings] said: You land on Tuesday at 14:05 local time."
user: "Great - dinner recommendations near my hotel for that evening?"
— and still no trace of the reservation_lookup call or its raw result.
User events and same-branch behavior are unchanged. Default False keeps
today's behavior exactly.
Option B — producer-side marker: a public EventActions field (e.g. share_final_response_across_branches: bool) that the framework sets on final
response events of agents which opted in (here: bookings); the branch filter
then admits only events carrying the marker. This variant makes the contract
durable in the session log itself (and trivially serializable, unlike isolation_scope).
Impact on your work
We run a multi-agent chat following the pattern above (root dispatcher, one
shared session, sub-branch per delegated turn). Branch isolation is
load-bearing for us: one specialist's tool events contain content that other
specialists must never see, because the guardrails built for that content
exist only on the producing agent.
Without this feature we maintain a custom Q&A log in session.state (the root
agent appends {question, agent, answer} after each turn; other agents read
it via an instruction provider). It works and stays on public APIs, but it
reimplements, outside the framework, a filtered view the content builder could
provide natively — and any ADK adopter with a routing-style multi-agent chat
will need the same thing. Not timeline-critical (the workaround holds), but it
would let us delete workaround code and would make branch-isolated multi-agent
chats viable out of the box.
Willingness to contribute
Yes — we are open to submitting a PR for Option A (flag + content-builder
filter + tests) if maintainers agree on the shape.
🟡 Recommended Information
Describe Alternatives You've Considered
No branch fork + everything visible: leaks verbalized tool internals of
sibling agents into every model context (see above); also inflates prompts
with foreign tool traffic.
isolation_scope: explicitly internal and not persisted by VertexAiSessionService; also all-or-nothing, not prose-only.
output_key + {key?} instruction templating: last answer only, one
static key per agent; no conversation flow, no ordering.
Custom Q&A log in session.state (our current workaround): the root
agent is the single writer, sub-agents read via an instruction provider.
Works, but is framework-external duplication of context assembly.
EventsCompactionConfig: summarizes within a visibility scope; does
not cross branches and is LLM-paraphrased rather than verbatim.
MemoryService / PreloadMemoryTool: designed for cross-session memory;
asynchronous ingestion and LLM-extracted paraphrase make it unsuitable for
faithful same-conversation continuity.
Why the native hub pattern is not a substitute
A natural response would be: "use the built-in hub pattern instead — a chat
root LlmAgent with sub-agents as tools (mode='single_turn'/'task') or transfer_to_agent; the root's own history then carries the conversation
flow." That pattern is valuable, but it cannot replace a branch-forking
dispatcher for a whole class of applications, for independent reasons:
Some deployments need the dispatch decision to be deterministic, not
model-made. When reaching a given specialist is an entitlement
(per-user feature gating), when some entry points must force a specific
specialist, or when per-agent attribution feeds billing/quotas, the
"who handles this turn" decision must be auditable code with measurable
quality (scores, thresholds, regression sets) — not a tool choice made by
an LLM whose behavior shifts with model versions and prompt phrasing.
The framework already embraces custom BaseAgent orchestration as a
first-class pattern; this request fills its continuity gap.
The hub taxes every turn with an extra LLM pass. The root model runs
before any specialist token is produced (time-to-first-token on streaming
answers), and trivial turns (greetings, out-of-scope deflections) can no
longer be served template-only, because the root is a model.
Specialists-as-tools don't see the conversation at all — they receive a
request string packed by the root model. In the travel example, bookings
would receive a synthetic request like "check arrival time for the user's
Tokyo flight" instead of the dialogue itself: continuity becomes a
generative paraphrase of the conversation, which is exactly what this
request tries to avoid (faithful, verbatim Q&A flow).
In short: LLM-driven hubs and deterministic dispatchers are complementary
routing models, and only the framework can make prose-level continuity work
for the second one — which is why we propose it as a content-builder feature
rather than another application-side workaround.
Minimal reproducer (current behavior)
"""Shows that a sibling agent does not see another agent's final answer.ADK 2.4.0/2.5.0. The same script with include_sibling_final_responses=True(proposed) would include "[bookings] said: You land on Tuesday at 14:05..."in local_guide's contents, and still exclude the reservation_lookupcall/result events."""importasynciofromgoogle.adk.agentsimportBaseAgent, LlmAgentfromgoogle.adk.eventsimportEventfromgoogle.adk.runnersimportInMemoryRunnerfromgoogle.genaiimporttypesclassDispatcher(BaseAgent):
"""Delegates turn 1 to bookings, turn 2 to local_guide, forking branches."""asyncdef_run_async_impl(self, ctx):
target=bookingsifnotctx.session.state.get("t1_done") elselocal_guidectx.branch=f"{self.name}.{target.name}"asyncforeventintarget.run_async(ctx):
yieldeventyieldEvent(author=self.name,
actions=dict(state_delta={"t1_done": True}))
defreservation_lookup(booking_ref: str) ->dict:
"""Stands in for a tool whose output must stay agent-private."""return {"pnr": "ABC123", "fare_class": "W/OPAQUE",
"arrival": "TUE 14:05 NRT", "internal_record_id": 998877}
bookings=LlmAgent(name="bookings", model="gemini-2.5-flash",
tools=[reservation_lookup],
instruction="Answer questions about the user's ""reservations using reservation_lookup.")
local_guide=LlmAgent(name="local_guide", model="gemini-2.5-flash",
instruction="Recommend restaurants and activities.")
root=Dispatcher(name="dispatcher", sub_agents=[bookings, local_guide])
asyncdefmain():
runner=InMemoryRunner(agent=root)
session=awaitrunner.session_service.create_session(
app_name=runner.app_name, user_id="u1")
asyncfor_inrunner.run_async(
user_id="u1", session_id=session.id,
new_message=types.Content(role="user", parts=[
types.Part(text="When do I land in Tokyo?")])):
pass# Turn 2: local_guide's contents (inspect via before_model_callback or# _get_contents) include the user turns from T1/T2 but NOT bookings'# final answer - it sits on branch "dispatcher.bookings" and is filtered# out together with the reservation_lookup call/result events.asyncfor_inrunner.run_async(
user_id="u1", session_id=session.id,
new_message=types.Content(role="user", parts=[
types.Part(text="Dinner recommendations near my hotel ""for that evening?")])):
passasyncio.run(main())
Observed: local_guide's model request contains the T1 user question but no
trace of bookings' answer — "that evening" cannot be resolved. Desired
(opt-in): the final answer text is present, attributed via the "[bookings] said: ..." convention, while the reservation_lookup call and
its raw result (PNR, fare class, internal record id) remain excluded.
The part-level filtering needed here already exists in two places and could
be reused: LlmAgent.__maybe_save_output_to_state (text-only, no thoughts)
and _present_other_agent_message (attribution format).
🔴 Required Information
Is your feature request related to a specific problem?
Yes. Consider a common multi-agent chat pattern: several specialist sub-agents
share one session, and a root agent dispatches each user turn to one of them,
forking a sub-branch per delegation.
Event.branchisolation is thedocumented tool for exactly this ("Branch is used when multiple sub-agent
shouldn't see their peer agents' conversation history",
events/event.py) —but it is all-or-nothing at the event level. A sibling agent later in the same
conversation sees the user's turns (
branch=None) but not the previousagent's final answers: they are filtered out together with that agent's tool
internals, because both live on the same foreign branch.
Concrete example — a travel assistant with two specialists sharing one
session:
bookings(answers questions about the user's reservations via aninternal reservations-lookup tool) and
local_guide(recommends places toeat and visit, no access to reservation systems):
To answer T2 sensibly,
local_guidemust know what the user was told atT1 ("that evening" = Tuesday evening, after a 14:05 arrival). With branch
isolation, it only sees the user's questions: it knows the user asked about
landing in Tokyo, but not the answer. The end user, looking at their chat
window, sees both — so the agent is strictly less informed than the human it
is talking to.
At the same time,
local_guidemust not see thereservation_lookupcall and its raw result (internal record IDs, fare-class codes, PNR data):
guardrails covering that content exist only on
bookings, and arestaurant-recommendation agent has no business carrying raw reservation
records in its prompt.
The existing knobs don't offer this middle ground:
_present_other_agent_message(flows/llm_flows/contents.py) verbalizesforeign tool calls and tool results into the model context
(
"[bookings] called tool reservation_lookup with parameters ...","[bookings] tool returned result: ..."). The raw reservation record landsverbatim in the restaurant agent's prompt.
isolation_scopeis documented as internal("DO NOT USE THIS FIELD DIRECTLY",
events/event.py) and is notserialized by
VertexAiSessionService, so it cannot carry a durablevisibility contract on persisted sessions. Related bug class: Concurrent tool-using LlmAgents run via
ctx.run_nodecrash with "No function call event found for function responses ids" (single_turn appends branchless/scopeless user event to shared session.events) #5989(branchless/scopeless events become visible to every branch and scope).
output_key+ instruction templating shares only the last answer peragent under a static key — not the conversation flow.
What is missing is a middle setting between "everything" and "nothing":
share only what the end user has already seen (final response text), never the
tool internals.
Describe the Solution You'd Like
An opt-in, prose-only visibility of sibling final responses during content
assembly. Two possible shapes (either would work):
Option A — consumer-side flag on
LlmAgent(preferred):Semantics in
_get_contents: an event from a foreign branch is included iffevent.is_final_response()is true, andfunction_call,no
function_response,thoughtparts excluded — the same part filteringthat
output_keyapplies inLlmAgent.__maybe_save_output_to_state), and_present_other_agent_messagepath(
role='user',"[author] said: ..."), so authorship stays unambiguous.With the flag set,
local_guide's context at T2 would contain:— and still no trace of the
reservation_lookupcall or its raw result.User events and same-branch behavior are unchanged. Default
Falsekeepstoday's behavior exactly.
Option B — producer-side marker: a public
EventActionsfield (e.g.share_final_response_across_branches: bool) that the framework sets on finalresponse events of agents which opted in (here:
bookings); the branch filterthen admits only events carrying the marker. This variant makes the contract
durable in the session log itself (and trivially serializable, unlike
isolation_scope).Impact on your work
We run a multi-agent chat following the pattern above (root dispatcher, one
shared session, sub-branch per delegated turn). Branch isolation is
load-bearing for us: one specialist's tool events contain content that other
specialists must never see, because the guardrails built for that content
exist only on the producing agent.
Without this feature we maintain a custom Q&A log in
session.state(the rootagent appends
{question, agent, answer}after each turn; other agents readit via an instruction provider). It works and stays on public APIs, but it
reimplements, outside the framework, a filtered view the content builder could
provide natively — and any ADK adopter with a routing-style multi-agent chat
will need the same thing. Not timeline-critical (the workaround holds), but it
would let us delete workaround code and would make branch-isolated multi-agent
chats viable out of the box.
Willingness to contribute
Yes — we are open to submitting a PR for Option A (flag + content-builder
filter + tests) if maintainers agree on the shape.
🟡 Recommended Information
Describe Alternatives You've Considered
sibling agents into every model context (see above); also inflates prompts
with foreign tool traffic.
isolation_scope: explicitly internal and not persisted byVertexAiSessionService; also all-or-nothing, not prose-only.output_key+{key?}instruction templating: last answer only, onestatic key per agent; no conversation flow, no ordering.
session.state(our current workaround): the rootagent is the single writer, sub-agents read via an instruction provider.
Works, but is framework-external duplication of context assembly.
EventsCompactionConfig: summarizes within a visibility scope; doesnot cross branches and is LLM-paraphrased rather than verbatim.
PreloadMemoryTool: designed for cross-session memory;asynchronous ingestion and LLM-extracted paraphrase make it unsuitable for
faithful same-conversation continuity.
Why the native hub pattern is not a substitute
A natural response would be: "use the built-in hub pattern instead — a chat
root
LlmAgentwith sub-agents as tools (mode='single_turn'/'task') ortransfer_to_agent; the root's own history then carries the conversationflow." That pattern is valuable, but it cannot replace a branch-forking
dispatcher for a whole class of applications, for independent reasons:
model-made. When reaching a given specialist is an entitlement
(per-user feature gating), when some entry points must force a specific
specialist, or when per-agent attribution feeds billing/quotas, the
"who handles this turn" decision must be auditable code with measurable
quality (scores, thresholds, regression sets) — not a tool choice made by
an LLM whose behavior shifts with model versions and prompt phrasing.
The framework already embraces custom
BaseAgentorchestration as afirst-class pattern; this request fills its continuity gap.
before any specialist token is produced (time-to-first-token on streaming
answers), and trivial turns (greetings, out-of-scope deflections) can no
longer be served template-only, because the root is a model.
request string packed by the root model. In the travel example,
bookingswould receive a synthetic request like "check arrival time for the user's
Tokyo flight" instead of the dialogue itself: continuity becomes a
generative paraphrase of the conversation, which is exactly what this
request tries to avoid (faithful, verbatim Q&A flow).
runs with
branch=None, and the branch filter's empty-branch early-returnadmits every event — so the root's context includes sub-agents' verbalized
tool calls and raw tool results (the raw reservation record again).
mode='single_turn'sets noisolation_scopeon the sub-agent, and thetaskmode's scope is not serialized byVertexAiSessionService(seealso Concurrent tool-using LlmAgents run via
ctx.run_nodecrash with "No function call event found for function responses ids" (single_turn appends branchless/scopeless user event to shared session.events) #5989 for this bug class). So even teams willing to acceptmodel-driven dispatch don't get "internals stay private" out of the box
today.
In short: LLM-driven hubs and deterministic dispatchers are complementary
routing models, and only the framework can make prose-level continuity work
for the second one — which is why we propose it as a content-builder feature
rather than another application-side workaround.
Minimal reproducer (current behavior)
Observed:
local_guide's model request contains the T1 user question but notrace of
bookings' answer — "that evening" cannot be resolved. Desired(opt-in): the final answer text is present, attributed via the
"[bookings] said: ..."convention, while thereservation_lookupcall andits raw result (PNR, fare class, internal record id) remain excluded.
Additional context
ctx.run_nodecrash with "No function call event found for function responses ids" (single_turn appends branchless/scopeless user event to shared session.events) #5989 — branchless/scopeless events appended bysingle_turnbecame visible to every branch/scope; it shows visibility semantics in
multi-agent trees are a real-world pain point and that event-level
visibility contracts need to be explicit and serializable.
be reused:
LlmAgent.__maybe_save_output_to_state(text-only, no thoughts)and
_present_other_agent_message(attribution format).