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
When a long-running (async) tool's final FunctionResponse arrives as the latest session event — after other, unrelated tool exchanges have happened — the request contents sent to the model end on a stale exchange instead of the completion.
buildContentsDefault runs two rearrangement passes back-to-back:
rearrangeEventsForFunctionResponsesInHistory then rebuilds the list attaching every response event immediately after its matching call — including the merged response pass 1 just placed at the tail. The completion is dragged back next to its (much earlier) call, and whatever unrelated exchange happened last is left as the final content.
Models act on the final contents, so they answer the stale exchange and never see the completion.
Steps to Reproduce:
Build a session history where a long-running tool's final response is the latest event, with an unrelated tool exchange in between (this is exactly the event sequence of the existing test case "Rearrangement preserves unrelated function events" in contents_processor_test.go):
1. user: text "Run long process and search"
2. model: FunctionCall A (long_running_tool, id=long_call_123)
3. user: FunctionResponse A-intermediate ("processing")
4. model: FunctionCall B (search_tool, id=call_123)
5. user: FunctionResponse B ("results...")
6. user: FunctionResponse A-final ("completed") ← latest event
Run the contents pipeline (ContentsRequestProcessor / buildContentsDefault) over it.
Inspect the resulting LLMRequest.Contents ordering.
Expected Behavior:
The latest function response remains the final content. The unrelated B exchange stays preserved (the property #767 established) — before the completion rather than after it:
user text · B-call · B-resp · A-call · A-final(merged)
Observed Behavior:
The completion is buried mid-history and the stale B exchange is the tail:
user text · A-call · A-final(merged) · B-call · B-resp ← model answers B-resp
Unit-level demonstration on current main (cmp diff, -want +got, from a regression test we can contribute):
[]*genai.Content{
&{... "Plan how to add feature Q" ..., Role: "user"},
+ &{Parts: {&{FunctionCall: plan_tool ...}}, Role: "model"}, ← completion buried here
+ &{Parts: {&{FunctionResponse: plan_tool completed ...}}, Role: "user"},
&{Parts: {&{FunctionCall: status_tool ...}}, Role: "model"},
&{Parts: {&{FunctionResponse: status "running, 21s" ...}}, Role: "user"},
- &{Parts: {&{FunctionCall: plan_tool ...}}, Role: "model"}, ← where it must be
- &{Parts: {&{FunctionResponse: plan_tool completed ...}}, Role: "user"},
}
Real-world impact: in our production multi-agent supervisor this made three different models consistently blind to long-running tool completions — each re-summarized the stale status exchange at the tail; one literally repeated a "21 seconds elapsed, still running" status while the completed 5.5 KB result sat merged mid-history, immediately after a call issued minutes earlier.
Environment Details:
ADK Library Version: v1.5.0, v2.0.0, and current main (the three functions involved are byte-identical across all three)
OS: macOS (logic bug — OS-independent)
Go Version: go 1.25
Model Information:
Which model is being used: gemini-3.1-pro (native path); also reproduced with DeepSeek V3.2 and Kimi K2 through an OpenAI-compatible path — the contents are already wrong before any provider adapter runs, so the bug is model-independent.
🟡 Optional Information
Regression:
Partially — the burying behavior arrived with #767 (2026-05-28). Before #767, the same flow dropped the unrelated intervening events entirely (that data loss was #761). So no prior version handled this history correctly: pre-#767 lost events, post-#767 buries the completion. The fix proposed below is the first state with both properties (nothing dropped and the completion stays last).
Logs:
(see the cmp diff under Observed Behavior; happy to attach full pre/post contents dumps)
Minimal Reproduction Code:
Add this row to TestContentsRequestProcessor_Rearrange in internal/llminternal/contents_processor_test.go on unpatched main — it fails with the diff shown above (fixtures follow the file's existing style):
adk-python parity: adk-python cannot reach this state — its _rearrange_events_for_latest_function_response truncates history (result_events = events[:function_call_event_idx + 1] plus the merged response), so its tail is always the merged latest response, at the cost of dropping the unrelated exchange (the very behavior #767 fixed in Go). On the observable property ("what is the final content when a long-running tool completes late?") Go currently disagrees with adk-python; the proposed fix re-aligns the observable tail while keeping Go's superior no-drop behavior.
Proposed fix (PR to follow): keep pass 2's adjacency invariant (every call event immediately followed by its consolidated response) and add one rule — the call/response pair answered by the final event is emitted last. One-function change in rearrangeEventsForFunctionResponsesInHistory; provably a no-op whenever the last event is not a function response; one existing test expectation updates (the #767 case, with the reasoning above), four regression cases added. Live-verified end-to-end with gemini-3.1-pro against the patched module.
🔴 Required Information
Describe the Bug:
When a long-running (async) tool's final
FunctionResponsearrives as the latest session event — after other, unrelated tool exchanges have happened — the request contents sent to the model end on a stale exchange instead of the completion.buildContentsDefaultruns two rearrangement passes back-to-back:rearrangeEventsForLatestFunctionResponsehandles the late response correctly: it finds the matching call, merges intermediate + final responses, preserves the intervening unrelated tool events (the deliberate fix from fix: preserve unrelated function events #767 for Function response rearrangement can drop unrelated tool events from history #761), and appends the merged response last. At this point the contents are right.rearrangeEventsForFunctionResponsesInHistorythen rebuilds the list attaching every response event immediately after its matching call — including the merged response pass 1 just placed at the tail. The completion is dragged back next to its (much earlier) call, and whatever unrelated exchange happened last is left as the final content.Models act on the final contents, so they answer the stale exchange and never see the completion.
Steps to Reproduce:
"Rearrangement preserves unrelated function events"incontents_processor_test.go):ContentsRequestProcessor/buildContentsDefault) over it.LLMRequest.Contentsordering.Expected Behavior:
The latest function response remains the final content. The unrelated B exchange stays preserved (the property #767 established) — before the completion rather than after it:
Observed Behavior:
The completion is buried mid-history and the stale B exchange is the tail:
Unit-level demonstration on current
main(cmp diff,-want +got, from a regression test we can contribute):Real-world impact: in our production multi-agent supervisor this made three different models consistently blind to long-running tool completions — each re-summarized the stale status exchange at the tail; one literally repeated a "21 seconds elapsed, still running" status while the completed 5.5 KB result sat merged mid-history, immediately after a call issued minutes earlier.
Environment Details:
main(the three functions involved are byte-identical across all three)Model Information:
🟡 Optional Information
Regression:
Partially — the burying behavior arrived with #767 (2026-05-28). Before #767, the same flow dropped the unrelated intervening events entirely (that data loss was #761). So no prior version handled this history correctly: pre-#767 lost events, post-#767 buries the completion. The fix proposed below is the first state with both properties (nothing dropped and the completion stays last).
Logs:
Minimal Reproduction Code:
Add this row to
TestContentsRequestProcessor_Rearrangeininternal/llminternal/contents_processor_test.goon unpatchedmain— it fails with the diff shown above (fixtures follow the file's existing style):{ name: "Late async completion stays the final content", events: []*session.Event{ {Author: "user", LLMResponse: model.LLMResponse{Content: genai.NewContentFromText("Plan how to add feature Q", "user")}}, {Author: agentName, LLMResponse: model.LLMResponse{Content: NewContentFromFunctionCall(fcAsyncPlan, "model")}}, {Author: "user", LLMResponse: model.LLMResponse{Content: NewContentFromFunctionResponse(frAsyncPlanAck, "user")}}, {Author: agentName, LLMResponse: model.LLMResponse{Content: genai.NewContentFromText("I dispatched the planning task.", "model")}}, {Author: "user", LLMResponse: model.LLMResponse{Content: genai.NewContentFromText("What is the status?", "user")}}, {Author: agentName, LLMResponse: model.LLMResponse{Content: NewContentFromFunctionCall(fcAsyncStatus, "model")}}, {Author: "user", LLMResponse: model.LLMResponse{Content: NewContentFromFunctionResponse(frAsyncStatus, "user")}}, {Author: agentName, LLMResponse: model.LLMResponse{Content: genai.NewContentFromText("Still running.", "model")}}, {Author: "user", LLMResponse: model.LLMResponse{Content: NewContentFromFunctionResponse(frAsyncPlanFinal, "user")}}, }, want: []*genai.Content{ genai.NewContentFromText("Plan how to add feature Q", "user"), NewContentFromFunctionCall(fcAsyncStatus, "model"), NewContentFromFunctionResponse(frAsyncStatus, "user"), NewContentFromFunctionCall(fcAsyncPlan, "model"), NewContentFromFunctionResponse(frAsyncPlanFinal, "user"), }, },How often has this issue occurred?:
Additional Context:
Relationship to existing issues:
adk-python parity: adk-python cannot reach this state — its
_rearrange_events_for_latest_function_responsetruncates history (result_events = events[:function_call_event_idx + 1]plus the merged response), so its tail is always the merged latest response, at the cost of dropping the unrelated exchange (the very behavior #767 fixed in Go). On the observable property ("what is the final content when a long-running tool completes late?") Go currently disagrees with adk-python; the proposed fix re-aligns the observable tail while keeping Go's superior no-drop behavior.Proposed fix (PR to follow): keep pass 2's adjacency invariant (every call event immediately followed by its consolidated response) and add one rule — the call/response pair answered by the final event is emitted last. One-function change in
rearrangeEventsForFunctionResponsesInHistory; provably a no-op whenever the last event is not a function response; one existing test expectation updates (the #767 case, with the reasoning above), four regression cases added. Live-verified end-to-end with gemini-3.1-pro against the patched module.