Conversation
|
Hi @innoprej, We appreciate your contribution and the effort you put into this pull request. It is currently under review by our team. We will update you if any additional details are needed. Thank you. |
dosadczuk
left a comment
There was a problem hiding this comment.
Thanks for the fix and the detailed write-up. This matches what adk-python does, and the tests look good. Two small, optional suggestions inline.
| // The agent runs as part of the caller's invocation, so it follows the caller's RunConfig | ||
| // instead of the defaults; maxLlmCalls still counts the nested run on its own. It always runs | ||
| // unary, though: its events are not forwarded and the result is read from the last one, which | ||
| // in a streamed run may hold only the final chunk. |
There was a problem hiding this comment.
Could we shorten this to one line? The nested runner actually starts its own invocation, so "runs as part of the caller's invocation" is a bit misleading, and the longer explanation is already in the commit message. Maybe:
| // The agent runs as part of the caller's invocation, so it follows the caller's RunConfig | |
| // instead of the defaults; maxLlmCalls still counts the nested run on its own. It always runs | |
| // unary, though: its events are not forwarded and the result is read from the last one, which | |
| // in a streamed run may hold only the final chunk. | |
| // Follow the caller's RunConfig but run unary: only the last event becomes the result. |
There was a problem hiding this comment.
Done, replaced the four-line comment with your suggested one-liner. The longer explanation of the nested invocation and per-invocation limit remains in the commit message.
| } | ||
|
|
||
| @Test | ||
| public void call_withStreamingRunConfig_runsAgentWithoutStreaming() throws Exception { |
There was a problem hiding this comment.
Would you mind covering BIDI here too, if possible, for example by running this test with both SSE and BIDI? BIDI is the case that would actually break (the nested function calls would go to handleFunctionCallsLive), and right now the test would still pass if the check only handled SSE.
There was a problem hiding this comment.
Done, the existing test now runs with both SSE and BIDI and checks that only streamingMode changes to NONE while maxLlmCalls is preserved. I also temporarily changed the implementation to normalize SSE only: the test failed on BIDI, then passed after restoring the implementation. AgentToolTest passes all 29 tests in both the default-test and basic executions. The PR's testing description now states that both modes are covered.
AgentTool.runAsync started the wrapped agent with the three-argument Runner.runAsync, so the nested run always used the RunConfig defaults: a maxLlmCalls ceiling of 500, ToolExecutionMode.NONE and no customMetadata, whatever the caller had set. Pass the caller's RunConfig to the nested run instead, as adk-python does since google/adk-python@983c280. The nested run is its own invocation, so maxLlmCalls counts its LLM calls apart from the caller's. Run the nested agent unary even when the caller streams, as adk-python does since google/adk-python@0d5752b. Its events are not forwarded and the tool result is read from the last one, which a streaming model need not fill with the whole response: the contrib LangChain4j model emits each chunk as its own response. BIDI would also send the nested agent's function calls to the live-only handler. - Cover SSE and BIDI callers and keep the RunConfig comment concise.
b7195cc to
18560ba
Compare
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
1. Link to an existing issue (if applicable):
2. Or, if no issue exists, describe the change:
Problem:
AgentTool.runAsyncstarts the wrapped agent with the three-argumentRunner.runAsync, which usesRunConfig.builder().build(). An agent used as a tool therefore always runs with theRunConfigdefaults (maxLlmCalls500,ToolExecutionMode.NONE, emptycustomMetadata), whatever the caller passed toRunner.runAsync. For example, withmaxLlmCalls(5)on the caller, the wrapped agent in the issue's reproduction makes 500 LLM calls before the run fails.Solution:
Pass the caller's
RunConfig(toolContext.invocationContext().runConfig()) to the four-argumentrunAsync, as adk-python does since google/adk-python@983c280. The current adk-python code, including the unary change below, isagent_tool.pyL291-L315.Runnerstill creates its ownInvocationContext, somaxLlmCallsbounds the wrapped agent's LLM calls on their own instead of sharing the caller's count. adk-python counts them the same way.RunConfigwithStreamingMode.NONE, as in google/adk-python@0d5752b. The nested run's events are not forwarded to the caller, and the tool result is read from the last one, which a streaming model need not fill with the whole response:Geminiends a stream with an aggregated response, but the contribLangChain4jmodel emits each chunk as its own response.BIDIbelongs torunLive; in therunAsyncflow it would makeBaseLlmFlowsend the nested agent's function calls toFunctions.handleFunctionCallsLive. A caller that does not stream has itsRunConfigpassed through as is.support_cfcfor the nested run. Java'sRunConfighas no such field.groupFunctionResponsesInHistoryOverride(deprecated) now reaches the nested run as well: when the caller sets it, it also decides how the wrapped agent's function calls and responses are laid out in its requests, as it already does for sub-agents. The remaining fields do not change the nested run:autoCreateSession(the session is created before the run),saveInputBlobsAsArtifacts(the request is text only), and the modality, speech, avatar and transcription settings, which only end up in the nested request'sliveConnectConfigand are not read by the unarygenerateContentcall.Testing Plan
Unit Tests:
core; one Windows-only failure that is also onmain, see the table below)Two new tests in
AgentToolTestwrap aTestBaseAgentand check theRunConfigon theInvocationContextit receives:call_propagatesCallerRunConfig: the caller'stoolExecutionMode(SEQUENTIAL),maxLlmCalls(7)andcustomMetadatareach the wrapped agent.call_withStreamingRunConfig_runsAgentWithoutStreaming: bothSSEandBIDIcallers'RunConfigreach the wrapped agent with onlystreamingModechanged toNONE.Windows 11, Microsoft OpenJDK 17.0.19, Maven 4.0.0-rc-3 via
mvnw:mainmvn -pl core test -Dtest=AgentToolTesttoolExecutionMode=NONE,maxLlmCalls=500,customMetadata={})mvn -pl core test -Dmaven.test.failure.ignore=truedefault-testandbasiceach run 1885 tests, 24 skipped, 1 failure:LocalSkillSourceTest.testListResources, a Windows-only failure that is also onmain(#1541; fix in #1542). Without the flag the build stops at that failure indefault-test. Of the other four surefire executions, three pass andapigee-llm-proxy-urlruns no tests (#1557).Manual End-to-End (E2E) Tests:
Not tested against a live model. The reproduction in the issue drives the whole path (
Runner→ rootLlmAgent→AgentTool→ nestedRunner→ wrappedLlmAgent) withTestLlmdoubles andmaxLlmCalls(5)on the caller. The wrapped agent's model always asks for a tool again and answers on an RxJava IO thread, so the loop onmainruns into the 500-call limit rather than overflowing the stack first:mainLlmCallsLimitExceededException: Max number of llm calls limit of 500 exceededLlmCallsLimitExceededException: Max number of llm calls limit of 5 exceededWhen the model double answers on the calling thread instead,
mainfails earlier with aStackOverflowError, after about 270–290 nested calls in my runs; with this change it stops after 5.Checklist
core; see the table for the one failure that is also onmain)Additional context
This changes behavior for callers that set a lower
maxLlmCallsor atoolExecutionMode: both now apply inside agent tools too, which is what adk-python does.groupFunctionResponsesInHistoryOverride, when set, now applies there as well, and a caller that setsmaxLlmCallsto 0 or less (no limit) now also lifts the 500-call cap that agent tools had. #1434 adds acancellationTokentoRunConfig; with this change that token is handed to the wrapped agent as well.