Skip to content

Commit 7329f7d

Browse files
DeanChensjcopybara-github
authored andcommitted
chore: Extract transfer loop out of dynamic scheduler
This moves the transfer loop from _dynamic_node_scheduler to Context.run_node, making run_node the unified entrypoint for transfers in both static workflows and dynamic agent calls. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 940687129
1 parent 044cd7b commit 7329f7d

3 files changed

Lines changed: 80 additions & 587 deletions

File tree

src/google/adk/workflow/_dynamic_node_scheduler.py

Lines changed: 63 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -153,138 +153,77 @@ async def __call__(
153153
Returns:
154154
Child Context with output, route, and interrupt_ids set.
155155
"""
156-
curr_node = node
157-
curr_name = node_name or node.name
158-
curr_run_id = run_id
159-
curr_input = node_input
160-
curr_parent_ctx: Context | None = ctx
161-
162-
while True:
163-
curr_parent_path = curr_parent_ctx.node_path if curr_parent_ctx else None
164-
base_path_builder = (
165-
_NodePathBuilder.from_string(curr_parent_path)
166-
if curr_parent_path
167-
else _NodePathBuilder([])
168-
)
169-
node_path = str(base_path_builder.append(curr_name, curr_run_id))
170-
171-
# Rehydration chronological sequence barrier setup for the parent path
172-
parent_path = curr_parent_ctx.node_path if curr_parent_ctx else ''
173-
if parent_path and parent_path not in self._parent_sequence_barriers:
174-
seq = self._scan_parent_child_sequence(curr_parent_ctx, parent_path)
175-
self._parent_sequence_barriers[parent_path] = ReplaySequenceBarrier(seq)
176-
177-
# Runtime schema validation.
178-
if curr_input is not None:
179-
try:
180-
curr_input = curr_node._validate_input_data(curr_input)
181-
except ValidationError as e:
182-
raise ValueError(
183-
'Runtime schema validation failed for dynamic node'
184-
f" '{curr_name}'. Input does not match input_schema: {e}"
185-
) from e
186-
187-
logger.debug('node %s schedule start.', node_path)
188-
189-
# Phase 1: Lazy rehydration from session events.
190-
if node_path not in self._state.runs:
191-
self._rehydrate_from_events(curr_parent_ctx, node_path)
156+
curr_parent_path = ctx.node_path if ctx else None
157+
base_path_builder = (
158+
_NodePathBuilder.from_string(curr_parent_path)
159+
if curr_parent_path
160+
else _NodePathBuilder([])
161+
)
162+
node_path = str(base_path_builder.append(node_name or node.name, run_id))
163+
164+
# Rehydration chronological sequence barrier setup for the parent path
165+
parent_path = ctx.node_path if ctx else ''
166+
if parent_path and parent_path not in self._parent_sequence_barriers:
167+
seq = self._scan_parent_child_sequence(ctx, parent_path)
168+
self._parent_sequence_barriers[parent_path] = ReplaySequenceBarrier(seq)
169+
170+
# Runtime schema validation.
171+
if node_input is not None:
172+
try:
173+
node_input = node._validate_input_data(node_input)
174+
except ValidationError as e:
175+
raise ValueError(
176+
'Runtime schema validation failed for dynamic node'
177+
f" '{node_name or node.name}'. Input does not match"
178+
f' input_schema: {e}'
179+
) from e
180+
181+
logger.debug('node %s schedule start.', node_path)
182+
183+
# Phase 1: Lazy rehydration from session events.
184+
if node_path not in self._state.runs:
185+
self._rehydrate_from_events(ctx, node_path)
186+
187+
# Check existing run and determine if fresh execution is needed.
188+
child_ctx, run_completed = await self._check_existing_run(
189+
ctx,
190+
node,
191+
node_name or node.name,
192+
node_path,
193+
run_id,
194+
node_input,
195+
use_as_output,
196+
use_sub_branch,
197+
override_branch,
198+
override_isolation_scope=override_isolation_scope,
199+
)
192200

193-
# Check existing run and determine if fresh execution is needed.
194-
child_ctx, run_completed = await self._check_existing_run(
195-
curr_parent_ctx,
196-
curr_node,
197-
curr_name,
201+
if not run_completed:
202+
# Phase 3: Fresh execution.
203+
logger.debug('node %s schedule: Fresh execution.', node_path)
204+
child_ctx = await self._run_node_internal(
205+
ctx,
206+
node,
207+
node_name or node.name,
198208
node_path,
199-
curr_run_id,
200-
curr_input,
209+
run_id,
210+
node_input,
201211
use_as_output,
202-
use_sub_branch,
203-
override_branch,
212+
is_fresh=True,
213+
use_sub_branch=use_sub_branch,
214+
override_branch=override_branch,
204215
override_isolation_scope=override_isolation_scope,
205216
)
206217

207-
if not run_completed:
208-
# Phase 3: Fresh execution.
209-
logger.debug('node %s schedule: Fresh execution.', node_path)
210-
child_ctx = await self._run_node_internal(
211-
curr_parent_ctx,
212-
curr_node,
213-
curr_name,
214-
node_path,
215-
curr_run_id,
216-
curr_input,
217-
use_as_output,
218-
is_fresh=True,
219-
use_sub_branch=use_sub_branch,
220-
override_branch=override_branch,
221-
override_isolation_scope=override_isolation_scope,
222-
)
223-
224-
logger.debug('node %s schedule end.', node_path)
225-
226-
# Advance chronological sequence for this parent path and key
227-
parent_path = curr_parent_ctx.node_path if curr_parent_ctx else ''
228-
key = f'{curr_name}@{curr_run_id}'
229-
if parent_path in self._parent_sequence_barriers:
230-
self._parent_sequence_barriers[parent_path].check_and_advance(key)
231-
232-
# Check for transfer_to_agent signal.
233-
transfer_to_agent = (
234-
child_ctx.actions.transfer_to_agent if child_ctx else None
235-
)
236-
if isinstance(transfer_to_agent, str):
237-
target_name = transfer_to_agent
238-
root_agent = getattr(curr_node, 'root_agent', None)
239-
if not root_agent:
240-
raise ValueError(f'Cannot find root_agent on node {curr_node.name}')
241-
242-
# Local import to avoid runtime circular dependencies with Context
243-
from .utils._transfer_utils import resolve_and_derive_transfer_context
244-
245-
target_agent, next_parent_ctx = resolve_and_derive_transfer_context(
246-
target_name=target_name,
247-
current_agent=curr_node,
248-
root_agent=root_agent,
249-
curr_ctx=child_ctx,
250-
curr_parent_ctx=curr_parent_ctx,
251-
)
252-
if not target_agent:
253-
raise ValueError(f"Transfer target agent '{target_name}' not found.")
254-
if not next_parent_ctx:
255-
available = []
256-
if hasattr(curr_node, '_get_available_agent_names'):
257-
available = curr_node._get_available_agent_names()
258-
available_str = (
259-
f"\nAvailable agents: {', '.join(available)}" if available else ''
260-
)
261-
raise ValueError(
262-
f"Cannot transfer from '{curr_name}' to unrelated agent"
263-
f" '{target_name}'.{available_str}"
264-
)
265-
curr_parent_ctx = next_parent_ctx
266-
267-
# Set up parameters for next iteration.
268-
curr_node = target_agent
269-
curr_name = target_agent.name
270-
271-
if not curr_parent_ctx:
272-
raise AssertionError(
273-
'curr_parent_ctx cannot be None during active workflow execution'
274-
)
275-
276-
curr_parent_ctx._child_run_counters[target_agent.name] = (
277-
curr_parent_ctx._child_run_counters.get(target_agent.name, 0) + 1
278-
)
279-
curr_run_id = str(
280-
curr_parent_ctx._child_run_counters[target_agent.name]
281-
)
282-
curr_input = None # Input for transfer target is usually empty.
218+
logger.debug('node %s schedule end.', node_path)
283219

284-
# Loop continues to execute the next agent
285-
continue
220+
# Advance chronological sequence for this parent path and key
221+
parent_path = ctx.node_path if ctx else ''
222+
key = f'{node_name or node.name}@{run_id}'
223+
if parent_path in self._parent_sequence_barriers:
224+
self._parent_sequence_barriers[parent_path].check_and_advance(key)
286225

287-
return child_ctx
226+
return child_ctx
288227

289228
async def _check_existing_run(
290229
self,

src/google/adk/workflow/_llm_agent_wrapper.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -394,19 +394,23 @@ async def run_llm_agent_as_node(
394394
break # close this run_iter; outer loop re-enters
395395
if event.actions.transfer_to_agent:
396396
target_name = event.actions.transfer_to_agent
397-
if target_name != agent.name:
398-
from ..agents.llm_agent import LlmAgent
399-
400-
if (
401-
isinstance(agent, LlmAgent)
402-
and ctx._invocation_context.is_resumable
403-
):
404-
ctx._invocation_context.set_agent_state(
405-
agent.name, end_of_agent=True
406-
)
407-
yield agent._create_agent_state_event(ctx._invocation_context)
408-
transferred = True
409-
break
397+
if target_name == agent.name:
398+
raise ValueError(
399+
f"Agent '{target_name}' cannot transfer to itself."
400+
)
401+
402+
from ..agents.llm_agent import LlmAgent
403+
404+
if (
405+
isinstance(agent, LlmAgent)
406+
and ctx._invocation_context.is_resumable
407+
):
408+
ctx._invocation_context.set_agent_state(
409+
agent.name, end_of_agent=True
410+
)
411+
yield agent._create_agent_state_event(ctx._invocation_context)
412+
transferred = True
413+
break
410414
if not had_task_fc or transferred:
411415
# LLM finished without delegating (or transferred away);
412416
# nothing more for this wrapper to do.

0 commit comments

Comments
 (0)