Skip to content

Commit 95feafa

Browse files
March-77copybara-github
authored andcommitted
fix: isolate delegated task branches
Merge #6495 Fixes #6457 PiperOrigin-RevId: 954877821
1 parent 75c773e commit 95feafa

6 files changed

Lines changed: 110 additions & 6 deletions

File tree

contributing/samples/multi_agent/task_sub_agent/tests/10_burgers.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
{
4444
"author": "order_collector",
45+
"branch": "order_collector@fc-1",
4546
"content": {
4647
"parts": [
4748
{
@@ -99,6 +100,7 @@
99100
},
100101
{
101102
"author": "order_collector",
103+
"branch": "order_collector@fc-1",
102104
"content": {
103105
"parts": [
104106
{
@@ -133,6 +135,7 @@
133135
},
134136
{
135137
"author": "order_collector",
138+
"branch": "order_collector@fc-1",
136139
"content": {
137140
"parts": [
138141
{
@@ -156,6 +159,7 @@
156159
},
157160
{
158161
"author": "order_collector",
162+
"branch": "order_collector@fc-1",
159163
"content": {
160164
"parts": [
161165
{
@@ -199,6 +203,7 @@
199203
"skipSummarization": true
200204
},
201205
"author": "order_collector",
206+
"branch": "order_collector@fc-1",
202207
"content": {
203208
"parts": [
204209
{

contributing/samples/multi_agent/task_sub_agent/tests/3_burgers.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
{
4444
"author": "order_collector",
45+
"branch": "order_collector@fc-1",
4546
"content": {
4647
"parts": [
4748
{
@@ -76,6 +77,7 @@
7677
},
7778
{
7879
"author": "order_collector",
80+
"branch": "order_collector@fc-1",
7981
"content": {
8082
"parts": [
8183
{
@@ -106,6 +108,7 @@
106108
},
107109
{
108110
"author": "order_collector",
111+
"branch": "order_collector@fc-1",
109112
"content": {
110113
"parts": [
111114
{
@@ -189,6 +192,7 @@
189192
},
190193
{
191194
"author": "payment_collector",
195+
"branch": "payment_collector@fc-3",
192196
"content": {
193197
"parts": [
194198
{
@@ -223,6 +227,7 @@
223227
},
224228
{
225229
"author": "payment_collector",
230+
"branch": "payment_collector@fc-3",
226231
"content": {
227232
"parts": [
228233
{
@@ -257,6 +262,7 @@
257262
},
258263
{
259264
"author": "payment_collector",
265+
"branch": "payment_collector@fc-3",
260266
"content": {
261267
"parts": [
262268
{
@@ -283,6 +289,7 @@
283289
},
284290
{
285291
"author": "payment_collector",
292+
"branch": "payment_collector@fc-3",
286293
"content": {
287294
"parts": [
288295
{

contributing/samples/multi_agent/task_sub_agent/tests/credit_card.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
{
4444
"author": "order_collector",
45+
"branch": "order_collector@fc-1",
4546
"content": {
4647
"parts": [
4748
{
@@ -76,6 +77,7 @@
7677
},
7778
{
7879
"author": "order_collector",
80+
"branch": "order_collector@fc-1",
7981
"content": {
8082
"parts": [
8183
{

contributing/samples/multi_agent/task_sub_agent/tests/order_food.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
{
4444
"author": "order_collector",
45+
"branch": "order_collector@fc-1",
4546
"content": {
4647
"parts": [
4748
{
@@ -76,6 +77,7 @@
7677
},
7778
{
7879
"author": "order_collector",
80+
"branch": "order_collector@fc-1",
7981
"content": {
8082
"parts": [
8183
{
@@ -110,6 +112,7 @@
110112
},
111113
{
112114
"author": "order_collector",
115+
"branch": "order_collector@fc-1",
113116
"content": {
114117
"parts": [
115118
{
@@ -144,6 +147,7 @@
144147
},
145148
{
146149
"author": "order_collector",
150+
"branch": "order_collector@fc-1",
147151
"content": {
148152
"parts": [
149153
{
@@ -178,6 +182,7 @@
178182
},
179183
{
180184
"author": "order_collector",
185+
"branch": "order_collector@fc-1",
181186
"content": {
182187
"parts": [
183188
{

src/google/adk/workflow/_llm_agent_wrapper.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,10 @@ async def _dispatch_task_fc(
137137
"""Dispatch a task-delegation FC via ``ctx.run_node`` and return the output.
138138
139139
``run_id=fc.id`` makes the child run idempotent across resumes (same
140-
FC always maps to the same scheduler-tracked child run). Scope is
141-
carried by ``isolation_scope`` (``override_isolation_scope=fc.id``); we
142-
intentionally do NOT set a branch — task-mode and single_turn-mode
143-
agents share the parent's branch and rely on isolation_scope for
144-
scoping instead.
140+
FC always maps to the same scheduler-tracked child run). Each task
141+
runs in a stable sub-branch so resumable LLM flow logic sees only the
142+
task's own function calls. ``isolation_scope`` remains keyed by the
143+
FC id to keep task history scoped independently of branch ancestry.
145144
"""
146145
target_agent = parent_agent.root_agent.find_agent(fc.name)
147146
if target_agent is None:
@@ -154,6 +153,7 @@ async def _dispatch_task_fc(
154153
wrapped_target,
155154
node_input=fc.args,
156155
run_id=fc.id,
156+
use_sub_branch=True,
157157
override_isolation_scope=fc.id,
158158
raise_on_wait=True,
159159
)

tests/unittests/workflow/test_task_api_e2e.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@
3434
from google.adk.agents.context import Context
3535
from google.adk.agents.llm_agent import LlmAgent
3636
from google.adk.apps.app import App
37+
from google.adk.apps.app import ResumabilityConfig
3738
from google.adk.events.event import Event
39+
from google.adk.flows.llm_flows.functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
40+
from google.adk.tools.function_tool import FunctionTool
41+
from google.adk.tools.tool_context import ToolContext
3842
from google.adk.workflow import node
3943
from google.adk.workflow import START
4044
from google.adk.workflow._base_node import BaseNode
@@ -66,6 +70,11 @@ def _text_part(text: str) -> types.Part:
6670
return types.Part.from_text(text=text)
6771

6872

73+
def _confirmed_task_step(tool_context: ToolContext) -> dict[str, bool]:
74+
"""Return whether the resumable task step was confirmed."""
75+
return {'confirmed': tool_context.tool_confirmation.confirmed}
76+
77+
6978
def _make_task_agent(
7079
name: str,
7180
responses: list,
@@ -450,7 +459,83 @@ async def test_chat_coordinator_resumes_unresolved_task_fc(
450459

451460

452461
# ---------------------------------------------------------------------------
453-
# 9. Strict isolation filtering: a stranger event with a foreign
462+
# 9. Resumable task delegation: a task sub-agent that pauses for tool
463+
# confirmation resumes without executing its parent's delegation FC.
464+
# ---------------------------------------------------------------------------
465+
466+
467+
@pytest.mark.asyncio
468+
async def test_task_sub_agent_resumes_without_parent_delegation_fc(
469+
request: pytest.FixtureRequest,
470+
):
471+
"""A resumed task child does not execute its parent's delegation call."""
472+
confirmation_tool = FunctionTool(
473+
func=_confirmed_task_step,
474+
require_confirmation=True,
475+
)
476+
child = _make_task_agent(
477+
name='child',
478+
responses=[
479+
types.Part.from_function_call(
480+
name=confirmation_tool.name,
481+
args={},
482+
),
483+
_finish_part({'result': 'confirmed'}),
484+
],
485+
)
486+
child.tools.append(confirmation_tool)
487+
488+
root = LlmAgent(
489+
name='root',
490+
model=testing_utils.MockModel.create(
491+
responses=[
492+
_delegate_part('child', 'perform a confirmed step'),
493+
'Task confirmed.',
494+
]
495+
),
496+
sub_agents=[child],
497+
)
498+
app = App(
499+
name=request.function.__name__,
500+
root_agent=root,
501+
resumability_config=ResumabilityConfig(is_resumable=True),
502+
)
503+
runner = testing_utils.InMemoryRunner(app=app)
504+
505+
first_events = await runner.run_async(testing_utils.get_user_content('start'))
506+
confirmation_fc = next(
507+
fc
508+
for event in first_events
509+
for fc in event.get_function_calls()
510+
if fc.name == REQUEST_CONFIRMATION_FUNCTION_CALL_NAME
511+
)
512+
invocation_id = next(
513+
event.invocation_id
514+
for event in first_events
515+
if confirmation_fc in event.get_function_calls()
516+
)
517+
518+
resumed_events = await runner.run_async(
519+
testing_utils.UserContent(
520+
types.Part(
521+
function_response=types.FunctionResponse(
522+
id=confirmation_fc.id,
523+
name=REQUEST_CONFIRMATION_FUNCTION_CALL_NAME,
524+
response={'confirmed': True},
525+
)
526+
)
527+
),
528+
invocation_id=invocation_id,
529+
)
530+
531+
assert {'result': 'confirmed'} in _collect_finish_outputs(resumed_events)
532+
assert any(
533+
'Task confirmed.' in text for text in _get_text_responses(resumed_events)
534+
)
535+
536+
537+
# ---------------------------------------------------------------------------
538+
# 10. Strict isolation filtering: a stranger event with a foreign
454539
# isolation_scope must NOT appear in the task agent's LLM context.
455540
# ---------------------------------------------------------------------------
456541

0 commit comments

Comments
 (0)