Skip to content

Commit e51b2a2

Browse files
AlexeyChernenkoPlatohangfei
authored andcommitted
fix: double function response processing issue
Merge #2588 ## Description Fixes an issue in `base_llm_flow.py` where, in Bidi-streaming (live) mode, the multi-agent structure causes duplicated responses after tool calling. ## Problem In Bidi-streaming (live) mode, when utilizing a multi-agent structure, the leaf-level sub-agent and its parent agent both process the same function call response, leading to duplicate replies. This duplication occurs because the parent agent's live connection remains open while initiating a new connection with the child agent. ## Root Cause The issue originated from the placement of agent transfer logic in the `_postprocess_live` method at lines 547-557. When a `transfer_to_agent` function call was made: 1. The function response was processed in `_postprocess_live` 2. A recursive call to `agent_to_run.run_live` was initiated 3. This prevented the closure of the parent agent's connection at line 175 of the `run_live` method, as that code path was never reached 4. Both the parent and child agents remained active, causing both to process subsequent function responses ## Solution This PR addresses the issue by ensuring the parent agent's live connection is closed before initiating a new one with the child agent. Changes made: **Connection Management**: Moved the agent transfer logic from `_postprocess_live` method to the `run_live` method, specifically: - Removed agent transfer handling from lines 547-557 in `_postprocess_live` - Added agent transfer handling after connection closure at lines 176-184 in `run_live` **Code Refactoring**: The agent transfer now occurs in the proper sequence: 1. Parent agent processes the `transfer_to_agent` function response 2. Parent agent's live connection is properly closed (line 175) 3. New connection with child agent is initiated (line 182) 4. Child agent handles subsequent function calls without duplication **Improved Flow Control**: This ensures that each agent processes function call responses without duplication, maintaining proper connection lifecycle management in multi-agent structures. ## Testing To verify this fix works correctly: 1. **Multi-Agent Structure Test**: Set up a multi-agent structure with a parent agent that transfers to a child agent via `transfer_to_agent` function call 2. **Bidi-Streaming Mode**: Enable Bidi-streaming (live) mode in the configuration 3. **Function Call Verification**: Trigger a function call that results in agent transfer 4. **Response Monitoring**: Verify that only one response is generated (not duplicated) 5. **Connection Management**: Confirm that parent agent's connection is properly closed before child agent starts **Expected Behavior**: - Single function response per call - Clean agent handoffs without connection leaks - Proper connection lifecycle management ## Backward Compatibility This change is **fully backward compatible**: - No changes to public APIs or method signatures - Existing single-agent flows remain unaffected - Non-live (regular async) flows continue to work as before - Only affects the internal flow control in live multi-agent scenarios Co-authored-by: Hangfei Lin <[email protected]> COPYBARA_INTEGRATE_REVIEW=#2588 from AlexeyChernenkoPlato:fix/double-function-response-processing-issue 3339260 PiperOrigin-RevId: 835619170
1 parent 4e42a19 commit e51b2a2

File tree

1 file changed

+25
-10
lines changed

1 file changed

+25
-10
lines changed

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,24 @@ async def run_live(
156156
break
157157
logger.debug('Receive new event: %s', event)
158158
yield event
159-
# send back the function response
159+
# send back the function response to models
160160
if event.get_function_responses():
161161
logger.debug(
162162
'Sending back last function response event: %s', event
163163
)
164164
invocation_context.live_request_queue.send_content(
165165
event.content
166166
)
167+
# We handle agent transfer here in `run_live` rather than
168+
# in `_postprocess_live` to prevent duplication of function
169+
# response processing. If agent transfer were handled in
170+
# `_postprocess_live`, events yielded from child agent's
171+
# `run_live` would bubble up to parent agent's `run_live`,
172+
# causing `event.get_function_responses()` to be true in both
173+
# child and parent, and `send_content()` to be called twice for
174+
# the same function response. By handling agent transfer here,
175+
# we ensure that only child agent processes its own function
176+
# responses after the transfer.
167177
if (
168178
event.content
169179
and event.content.parts
@@ -174,7 +184,21 @@ async def run_live(
174184
await asyncio.sleep(DEFAULT_TRANSFER_AGENT_DELAY)
175185
# cancel the tasks that belongs to the closed connection.
176186
send_task.cancel()
187+
logger.debug('Closing live connection')
177188
await llm_connection.close()
189+
logger.debug('Live connection closed.')
190+
# transfer to the sub agent.
191+
transfer_to_agent = event.actions.transfer_to_agent
192+
if transfer_to_agent:
193+
logger.debug('Transferring to agent: %s', transfer_to_agent)
194+
agent_to_run = self._get_agent_to_run(
195+
invocation_context, transfer_to_agent
196+
)
197+
async with Aclosing(
198+
agent_to_run.run_live(invocation_context)
199+
) as agen:
200+
async for item in agen:
201+
yield item
178202
if (
179203
event.content
180204
and event.content.parts
@@ -638,15 +662,6 @@ async def _postprocess_live(
638662
)
639663
yield final_event
640664

641-
transfer_to_agent = function_response_event.actions.transfer_to_agent
642-
if transfer_to_agent:
643-
agent_to_run = self._get_agent_to_run(
644-
invocation_context, transfer_to_agent
645-
)
646-
async with Aclosing(agent_to_run.run_live(invocation_context)) as agen:
647-
async for item in agen:
648-
yield item
649-
650665
async def _postprocess_run_processors_async(
651666
self, invocation_context: InvocationContext, llm_response: LlmResponse
652667
) -> AsyncGenerator[Event, None]:

0 commit comments

Comments
 (0)