🔴 Required Information
Please ensure all items in this section are completed to allow for efficient
triaging. If an item is not applicable to you - please mark it as N/A
Describe the Bug:
When an LLMAgent with IncludeContents: "none" runs as a sub-agent inside a ParallelAgent, Gemini rejects the model call with:
Error 400: Please ensure that function response turn comes immediately after a function call turn.
The root cause is a missing branch-filtering guard in `buildContentsCurrentTurnContextOnly. The Python SDK has this guard; Golang does not.
Steps to Reproduce:
- A
ParallelAgent with ≥2 LLMAgent sub-agents
- At least one sub-agent has
IncludeContents: "none"
- One sub-agent yields an event (e.g. a
FunctionCall) before the other sub-agent's model call reads the session
This is timing-dependent on the first model call but deterministic on subsequent model calls (after tool responses), because the session will always contain cross-branch events by that point.
Expected Behavior:
Defined sub agents with IncludeContents set to none should not inherit chat history of their parent agent.
Observed Behavior:
Error 1 — Gemini 400
The affected sub-agent's model call receives a malformed conversation history — either completely empty or missing the initial user message. Gemini rejects it:
failed to run sub-agent "run-beta": failed to call model: Error 400,
Message: Please ensure that function response turn comes immediately
after a function call turn., Status: INVALID_ARGUMENT
Error 2 — Cascading "execution not found"
When one sub-agent errors, the errgroup.WithContext in parallelagent/agent.go:71 cancels all sibling goroutines. Any pending work in sibling agents (e.g. tool confirmations waiting for user input) is orphaned. Attempting to interact with the now-dead execution produces secondary errors.
Environment Details:
- ADK Library Version: 1.0.0
- OS: MacOS
- Go Version: 1.25.7
Model Information:
- Which model is being used: Gemini 3 flash preview
🟡 Optional Information
buildContentsCurrentTurnContextOnly (contents_processor.go:444) scans backward over all session events to find the "current turn start". It anchors on either a "user" event or an "other agent reply" (isOtherAgentReply). It then slices the events from that anchor forward and delegates to buildContentsDefault, which applies branch filtering.
The backward scan does not filter by branch. In a ParallelAgent, sub-agents share a single session. Events from sibling branches (e.g. "parallel.run-alpha") are visible in the raw event list. When isOtherAgentReply encounters a sibling's event, it anchors there — skipping the user message at an earlier index. buildContentsDefault then filters out the sibling's event by branch, but the user message has already been excluded from the slice.
Regression:
Did this work in a previous version of ADK? (Yes/No) If so, which one?
N/A
Logs:
Please attach relevant logs. Wrap them in code blocks (```) or attach a
text file.
N/A
Screenshots / Video:
If applicable, add screenshots or screen recordings to help explain
your problem.
N/A
Additional Context:
Add any other context about the problem here.
N/A
Minimal Reproduction Code:
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
// TestContentsRequestProcessor_IncludeContentsNone_ParallelBranch reproduces
// the composition bug between IncludeContents="none" and ParallelAgent
// branching. When two sub-agents run in parallel, each on its own branch,
// buildContentsCurrentTurnContextOnly scans backward over ALL events
// (unfiltered) and may anchor on the other branch's event via
// isOtherAgentReply, skipping the user message. The subsequent
// buildContentsDefault then filters out the other branch's event by branch,
// leaving a contents list without the initial user message — which Gemini
// rejects with "function response must follow function call" (Error 400).
//
// Scenario: ParallelAgent with sub-agents "run-alpha" and "run-beta",
// branches "parallel.run-alpha" and "parallel.run-beta". The user message
// has empty branch (visible to all). After alpha yields its FunctionCall
// event, beta's model call reads the session.
func TestContentsRequestProcessor_IncludeContentsNone_ParallelBranch(t *testing.T) {
t.Parallel()
const (
betaAgent = "run-beta"
alphaBranch = "parallel.run-alpha"
betaBranch = "parallel.run-beta"
)
testModel := &testModel{}
betaFC := &genai.FunctionCall{
ID: "beta_call_1",
Name: "run_command",
Args: map[string]any{"command": "ls"},
}
betaFR := &genai.FunctionResponse{
ID: "beta_call_1",
Name: "run_command",
Response: map[string]any{"output": "file1 file2"},
}
testCases := []struct {
name string
events []*session.Event
want []*genai.Content
}{
{
// Beta's first model call. Session has:
// [user(branch=""), alpha:FC(branch=alpha)]
// Expected: beta should see [user message] only.
// Bug: buildContentsCurrentTurnContextOnly finds alpha:FC
// as "other agent reply", skips user message, then
// buildContentsDefault filters alpha:FC by branch → empty.
name: "FirstModelCall_AlphaFCPresent",
events: []*session.Event{
{
Author: "user",
Branch: "", // User message — empty branch, visible to all.
LLMResponse: model.LLMResponse{
Content: genai.NewContentFromText("Run the parallel smoke test", "user"),
},
},
{
Author: "run-alpha",
Branch: alphaBranch, // Alpha's FunctionCall on alpha's branch.
LLMResponse: model.LLMResponse{
Content: NewContentFromFunctionCall(
&genai.FunctionCall{
ID: "alpha_call_1",
Name: "run_command",
Args: map[string]any{"command": "pwd"},
}, "model"),
},
},
},
want: []*genai.Content{
// Must include the user message.
genai.NewContentFromText("Run the parallel smoke test", "user"),
},
},
{
// Beta's second model call (after beta's tool confirmation).
// Session has:
// [user(branch=""), alpha:FC(branch=alpha), beta:FC(branch=beta), beta:FR(branch=beta)]
// Expected: beta should see [user, beta:FC, beta:FR].
// Bug: backward scan finds alpha:FC as "other agent reply",
// starts from there, skips user. After branch filter:
// [beta:FC, beta:FR] — no user message → Gemini 400.
name: "SecondModelCall_BetaFCFRPresent",
events: []*session.Event{
{
Author: "user",
Branch: "",
LLMResponse: model.LLMResponse{
Content: genai.NewContentFromText("Run the parallel smoke test", "user"),
},
},
{
Author: "run-alpha",
Branch: alphaBranch,
LLMResponse: model.LLMResponse{
Content: NewContentFromFunctionCall(
&genai.FunctionCall{
ID: "alpha_call_1",
Name: "run_command",
Args: map[string]any{"command": "pwd"},
}, "model"),
},
},
{
Author: betaAgent,
Branch: betaBranch,
LLMResponse: model.LLMResponse{
Content: NewContentFromFunctionCall(betaFC, "model"),
},
},
{
Author: betaAgent,
Branch: betaBranch,
LLMResponse: model.LLMResponse{
Content: NewContentFromFunctionResponse(betaFR, "user"),
},
},
},
want: []*genai.Content{
genai.NewContentFromText("Run the parallel smoke test", "user"),
NewContentFromFunctionCall(betaFC, "model"),
NewContentFromFunctionResponse(betaFR, "user"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
testAgent := utils.Must(llmagent.New(llmagent.Config{
Name: betaAgent,
Model: testModel,
IncludeContents: llmagent.IncludeContentsNone,
}))
ctx := icontext.NewInvocationContext(t.Context(), icontext.InvocationContextParams{
Agent: testAgent,
Branch: betaBranch,
Session: &fakeSession{
events: tc.events,
},
})
req := &model.LLMRequest{}
for ev, err := range llminternal.ContentsRequestProcessor(ctx, req, &llminternal.Flow{}) {
if ev != nil {
t.Fatal("ContentsRequestProcessor generated an unexpected event")
}
if err != nil {
t.Fatalf("ContentsRequestProcessor failed: %v", err)
}
}
got := req.Contents
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("LLMRequest.Contents mismatch (-want +got):\n%s", diff)
}
})
}
}
How often has this issue occurred?:
Always
🔴 Required Information
Please ensure all items in this section are completed to allow for efficient
triaging. If an item is not applicable to you - please mark it as N/A
Describe the Bug:
When an
LLMAgentwithIncludeContents: "none"runs as a sub-agent inside aParallelAgent, Gemini rejects the model call with:The root cause is a missing branch-filtering guard in `buildContentsCurrentTurnContextOnly. The Python SDK has this guard; Golang does not.
Steps to Reproduce:
ParallelAgentwith ≥2LLMAgentsub-agentsIncludeContents: "none"FunctionCall) before the other sub-agent's model call reads the sessionThis is timing-dependent on the first model call but deterministic on subsequent model calls (after tool responses), because the session will always contain cross-branch events by that point.
Expected Behavior:
Defined sub agents with IncludeContents set to none should not inherit chat history of their parent agent.
Observed Behavior:
Error 1 — Gemini 400
The affected sub-agent's model call receives a malformed conversation history — either completely empty or missing the initial user message. Gemini rejects it:
Error 2 — Cascading "execution not found"
When one sub-agent errors, the
errgroup.WithContextinparallelagent/agent.go:71cancels all sibling goroutines. Any pending work in sibling agents (e.g. tool confirmations waiting for user input) is orphaned. Attempting to interact with the now-dead execution produces secondary errors.Environment Details:
Model Information:
🟡 Optional Information
buildContentsCurrentTurnContextOnly(contents_processor.go:444) scans backward over all session events to find the "current turn start". It anchors on either a"user"event or an "other agent reply" (isOtherAgentReply). It then slices the events from that anchor forward and delegates tobuildContentsDefault, which applies branch filtering.The backward scan does not filter by branch. In a
ParallelAgent, sub-agents share a single session. Events from sibling branches (e.g."parallel.run-alpha") are visible in the raw event list. WhenisOtherAgentReplyencounters a sibling's event, it anchors there — skipping the user message at an earlier index.buildContentsDefaultthen filters out the sibling's event by branch, but the user message has already been excluded from the slice.Regression:
Did this work in a previous version of ADK? (Yes/No) If so, which one?
N/A
Logs:
Please attach relevant logs. Wrap them in code blocks (```) or attach a
text file.
N/A
Screenshots / Video:
If applicable, add screenshots or screen recordings to help explain
your problem.
N/A
Additional Context:
Add any other context about the problem here.
N/A
Minimal Reproduction Code:
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
How often has this issue occurred?:
Always