Describe the bug
Passing skipSummarization: true to an AgentTool that is used mid-turn (i.e. not as the very last thing the parent agent does) terminates the parent LlmAgent's run loop the moment the sub-agent's functionResponse is yielded. The parent never gets another generation pass, so — when it has an outputSchema — it never emits its schema-constrained JSON. The runner sees a "final response" event that contains only the sub-agent's functionResponse part and zero text.
The behavior is counter-intuitive given the property's public description ("Whether to skip summarization of the agent output"): a reasonable reading is "hand the sub-agent's text back to the parent verbatim instead of re-summarising it", not "mark this tool-response event as the parent's final response and abort the parent's turn".
This is particularly painful for anyone pairing AgentTool with outputSchema — a very common pattern for structured-output pipelines — because the symptom is simply "agent produced no final-response text", with no log line pointing at skipSummarization as the culprit.
To Reproduce
Minimal reproducer (TypeScript, @google/adk@1.0.0, any Gemini model that supports function calling):
import { AgentTool, InMemoryRunner, LlmAgent } from "@google/adk";
import { z } from "zod";
const child = new LlmAgent({
name: "quick_fact",
model: "gemini-2.5-flash",
description: "Answer a short factual question.",
instruction: "Answer in one short sentence.",
});
const parent = new LlmAgent({
name: "parent",
model: "gemini-2.5-pro",
description: "Produce a structured JSON summary.",
instruction:
'Call quick_fact once to gather context, then respond with JSON: {\"summary\": \"...\"}',
tools: [
new AgentTool({
agent: child,
skipSummarization: true, // flip to false (or omit) to see it work
}),
],
outputSchema: z.object({ summary: z.string() }),
});
const runner = new InMemoryRunner({ agent: parent, appName: "repro" });
const session = await runner.sessionService.createSession({
appName: "repro",
userId: "u1",
});
for await (const ev of runner.runAsync({
userId: session.userId,
sessionId: session.id,
newMessage: {
role: "user",
parts: [{ text: "Summarise: what is TypeScript?" }],
},
})) {
console.log(ev.author, {
parts: ev.content?.parts?.map((p) =>
p.functionCall
? \`fcall:\${p.functionCall.name}\`
: p.functionResponse
? \`fresp:\${p.functionResponse.name}\`
: \`text(\${(p.text ?? \"\").length})\`,
),
skipSummarization: ev.actions?.skipSummarization,
});
}
Observed (with skipSummarization: true):
- Last event:
author=parent, parts [\"fresp:quick_fact\"], skipSummarization: true.
- No subsequent parent event, no text parts, no schema-validated JSON.
- Downstream `AgentTool`/runner consumers see an empty final response.
Expected (and what actually happens when skipSummarization is removed/false):
- Parent yields
fcall:quick_fact, then fresp:quick_fact, then runs one more generation pass.
- Final parent event contains a
text(...) part whose JSON parses against the schema.
Expected behavior
skipSummarization on AgentTool should only affect whether the sub-agent's textual output is re-summarised / rewrapped before being handed to the parent. It should NOT terminate the parent LlmAgent's run loop. The parent must still be allowed to continue generating after receiving the tool result — especially when it has an outputSchema.
Desktop (please complete the following information):
- OS: macOS 15 (also reproduced on Linux Cloud Run)
- TS version/environment: TypeScript 5.x, Node 20.x
- ADK version:
@google/adk@1.0.0 (also reproduced on 0.4.0)
- Model backend: Vertex AI (
GOOGLE_GENAI_USE_VERTEXAI=1); also Gemini API
Additional context — root cause
Chain of causation in current main:
core/src/tools/agent_tool.ts — AgentTool.runAsync propagates the config flag into the shared EventActions, so it leaks onto the tool-response event yielded back to the parent:
override async runAsync({ args, toolContext }: RunAsyncToolRequest): Promise {
if (this.skipSummarization) {
toolContext.actions.skipSummarization = true;
}
// ...
}
core/src/events/event.ts — isFinalResponse treats skipSummarization as an unconditional terminal marker, regardless of whether the event is merely a tool response or an actual model reply:
export function isFinalResponse(event: Event) {
if (
event.actions.skipSummarization ||
(event.longRunningToolIds && event.longRunningToolIds.length > 0)
) {
return true;
}
return (
getFunctionCalls(event).length === 0 &&
getFunctionResponses(event).length === 0 &&
!event.partial &&
!hasTrailingCodeExecutionResult(event)
);
}
core/src/agents/llm_agent.ts — the parent's run loop breaks as soon as isFinalResponse(lastEvent) is true:
if (!lastEvent || isFinalResponse(lastEvent)) {
break;
}
Net effect: the parent's very first view of the sub-agent's functionResponse is instantly classified as "the parent is done", and the parent never gets to generate the text that outputSchema is waiting for.
Suggested fix (happy to PR whichever direction you prefer)
- Minimal: drop
event.actions.skipSummarization from the isFinalResponse check. skipSummarization is semantically about whether ADK should re-summarise sub-agent output before returning it, not about whether the parent agent has finished. The remaining functionCalls.length === 0 && functionResponses.length === 0 && !partial && !hasTrailingCodeExecutionResult(event) branch already correctly identifies truly-terminal events.
- Scope-the-flag: have
AgentTool.runAsync consume skipSummarization locally and clear it before the tool-response event is yielded into the parent's stream (or author/stamp the resulting event so the parent's own isFinalResponse ignores it). That preserves the "skip re-summarisation" intent without leaking into the parent's loop.
- Docs-only (short-term): explicitly document that
AgentTool({ skipSummarization: true }) hard-terminates the parent turn on return, and is therefore incompatible with mid-workflow AgentTool calls and with parents that declare an outputSchema. (Not a substitute for 1 or 2 — this is a sharp, surprising footgun.)
Until this is fixed, a safe workaround is to simply omit skipSummarization from AgentTool configs unless the wrapped sub-agent is known to be the last step of the parent's turn.
Describe the bug
Passing
skipSummarization: trueto anAgentToolthat is used mid-turn (i.e. not as the very last thing the parent agent does) terminates the parentLlmAgent's run loop the moment the sub-agent'sfunctionResponseis yielded. The parent never gets another generation pass, so — when it has anoutputSchema— it never emits its schema-constrained JSON. The runner sees a "final response" event that contains only the sub-agent'sfunctionResponsepart and zero text.The behavior is counter-intuitive given the property's public description ("Whether to skip summarization of the agent output"): a reasonable reading is "hand the sub-agent's text back to the parent verbatim instead of re-summarising it", not "mark this tool-response event as the parent's final response and abort the parent's turn".
This is particularly painful for anyone pairing
AgentToolwithoutputSchema— a very common pattern for structured-output pipelines — because the symptom is simply "agent produced no final-response text", with no log line pointing atskipSummarizationas the culprit.To Reproduce
Minimal reproducer (TypeScript,
@google/adk@1.0.0, any Gemini model that supports function calling):Observed (with
skipSummarization: true):author=parent, parts[\"fresp:quick_fact\"],skipSummarization: true.Expected (and what actually happens when
skipSummarizationis removed/false):fcall:quick_fact, thenfresp:quick_fact, then runs one more generation pass.text(...)part whose JSON parses against the schema.Expected behavior
skipSummarizationonAgentToolshould only affect whether the sub-agent's textual output is re-summarised / rewrapped before being handed to the parent. It should NOT terminate the parentLlmAgent's run loop. The parent must still be allowed to continue generating after receiving the tool result — especially when it has anoutputSchema.Desktop (please complete the following information):
@google/adk@1.0.0(also reproduced on0.4.0)GOOGLE_GENAI_USE_VERTEXAI=1); also Gemini APIAdditional context — root cause
Chain of causation in current
main:core/src/tools/agent_tool.ts—AgentTool.runAsyncpropagates the config flag into the sharedEventActions, so it leaks onto the tool-response event yielded back to the parent:core/src/events/event.ts—isFinalResponsetreatsskipSummarizationas an unconditional terminal marker, regardless of whether the event is merely a tool response or an actual model reply:core/src/agents/llm_agent.ts— the parent's run loop breaks as soon asisFinalResponse(lastEvent)is true:Net effect: the parent's very first view of the sub-agent's
functionResponseis instantly classified as "the parent is done", and the parent never gets to generate the text thatoutputSchemais waiting for.Suggested fix (happy to PR whichever direction you prefer)
event.actions.skipSummarizationfrom theisFinalResponsecheck.skipSummarizationis semantically about whether ADK should re-summarise sub-agent output before returning it, not about whether the parent agent has finished. The remainingfunctionCalls.length === 0 && functionResponses.length === 0 && !partial && !hasTrailingCodeExecutionResult(event)branch already correctly identifies truly-terminal events.AgentTool.runAsyncconsumeskipSummarizationlocally and clear it before the tool-response event is yielded into the parent's stream (or author/stamp the resulting event so the parent's ownisFinalResponseignores it). That preserves the "skip re-summarisation" intent without leaking into the parent's loop.AgentTool({ skipSummarization: true })hard-terminates the parent turn on return, and is therefore incompatible with mid-workflowAgentToolcalls and with parents that declare anoutputSchema. (Not a substitute for 1 or 2 — this is a sharp, surprising footgun.)Until this is fixed, a safe workaround is to simply omit
skipSummarizationfromAgentToolconfigs unless the wrapped sub-agent is known to be the last step of the parent's turn.