Conversation
|
Hi @innoprej, thank you for your contribution We appreciate you taking the time to submit this pull request. Currently this PR is under review by our team we will keep you posted if any additional information is required. thank you. |
78c63ee to
5d92339
Compare
|
Helllo, could you please merge the newest changes that were introduced in the code? Afterwards, please ensure that the code still works correctly. |
BaseLlmFlow.run subscribed to the next step from inside the previous step's completion, through concatWith, toList, flatMapPublisher and PersistBarrier.awaitPersisted(...).andThen(run(...)). When a step completed on the subscribing thread, as it does with the core models and the session services, each step ran 23 stack frames deeper than the one before. With a 1 MB thread stack, the default on x86-64, an agent that kept calling tools failed with StackOverflowError after roughly 250-300 LLM calls, before the default maxLlmCalls of 500 could end the run, and the error never reached onError. Drive the steps with repeatUntil instead, as LoopAgent does on its resumable path. repeatUntil resubscribes from a loop when the source completes synchronously, so every step starts at the same stack depth, like the while loop in adk-python's BaseLlmFlow.run_async. The end-of-flow checks, the pause on a pending long-running call, the PersistBarrier wait between steps and maxSteps are unchanged.
5d92339 to
7470b43
Compare
|
Done. I rebased the single commit onto the current On Windows 11 with JDK 17, |
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):
StackOverflowErrorinsidePersistBarrier.awaitPersisted(a single step with many events)2. Or, if no issue exists, describe the change:
Problem:
BaseLlmFlow.runsubscribes to each step from inside the previous step's completion (concatWith→toList→flatMapPublisher→PersistBarrier.awaitPersisted(...).andThen(run(...))). When a step completes on the subscribing thread, the next step starts on the same call stack, 23 frames deeper. The built-in models and session services complete on the subscribing thread (GeminithroughFlowable.fromFuture,Claudeand the non-streamingLangChain4jandSpringAIpaths throughFlowable.justafter a blocking call, the session services throughSingle.justorSingle.fromCallable), so with a 1 MB thread stack, the default on x86-64, an agent that keeps calling tools fails withStackOverflowErrorafter roughly 250–300 LLM calls, before the defaultmaxLlmCallsof 500 can end the run. On aarch64 the default thread stack is 2 MB; on a 2 MB stack the same agent made 635–663 calls in my runs, so there the default limit usually comes first. The error never reachesonError: it is thrown out ofsubscribe(), or, when the run is subscribed on a scheduler thread, it goes toRxJavaPluginsas anUndeliverableExceptionand the subscriber never receives a terminal signal (in my runs,blockingGet()returned only when a 15-secondtimeoutfired). The issue has the measurements.Solution:
Drive the steps with
repeatUntil, asLoopAgentloops over its sub-agents on its resumable path:run(InvocationContext)now runs one step per subscription of an innerFlowable.defer(...)and letsrepeatUntilsubscribe again until the flow ends. When a step completes synchronously,repeatUntilresubscribes from a loop instead of from the completion callback (FlowableRepeatUntil.RepeatSubscriber.subscribeNextin RxJava 3.1.12), so every step starts at the same stack depth. This is the Java counterpart of thewhile True:loop in adk-python'sBaseLlmFlow.run_async.runtorunStep, since it no longer runs the rest of the flow, and gets a short Javadoc. Its end-of-flow checks, the pause on a pending long-running call and themaxStepscut-off are unchanged. Where it used to subscribe to the next step, it now sets acontinueFlowflag and ends the step with thePersistBarrierwait.Flowable.defer.Events are still emitted as each step produces them, the next step still starts only after the
Runnerhas persisted the previous step's events, and an error still ends the run. One difference: subscribing twice to the sameFlowablereturned byrun(ctx)now runs the flow twice from the start, where the old code replayed the first step from itscache()and ran the later steps again. Agents do not do this:BaseAgentcallsrunAsyncImplagain for every subscription (throughFlowable.defer), andLlmAgent.runAsyncImplcallsruneach time.Testing Plan
Unit Tests:
core; one Windows-only failure that is also onmain, see the table below)BaseLlmFlowTest.run_modelRespondingOnSubscribingThread_reachesMaxLlmCallsWithoutStackOverflowruns the flow with a model that returns a function call throughFlowable.justevery time andmaxLlmCallsset to 2,000, and expects the run to end withLlmCallsLimitExceededExceptionafter 2,000 requests. It runs the flow without aRunner, so the history does not grow, and it takes about a second. LikePersistBarrierTest.largeStep_awaitsAllWithoutStackOverflowfrom #1336, it depends on the thread stack size: onmainit fails with any stack from 256 KB up to 4 MB, which includes the 2 MB aarch64 default, while an 8 MB stack would let the old code reach 2,000 steps.Windows 11 on x86-64, Microsoft OpenJDK 17.0.19, Maven 4.0.0-rc-3 via
mvnw:main(BaseLlmFlow.javafrommain, with the new test)./mvnw -pl core test -Dtest=BaseLlmFlowTestjava.lang.StackOverflowError./mvnw -pl core test -Dmaven.test.failure.ignore=truedefault-testandbasiceach run 1884 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 runs an
LlmAgentwith anEchoToolin anInMemoryRunner, with aTestLlmthat asks for the tool on every call and answers on the subscribing thread:mainStackOverflowErrorafter 253–265 LLM calls (14 runs)LlmCallsLimitExceededExceptionafter 500-Xss256kStackOverflowErrorafter 42LlmCallsLimitExceededExceptionafter 500-Xss2m(the aarch64 default size)maxLlmCallsraised to 2,000,StackOverflowErrorafter 635–663 callsLlmCallsLimitExceededExceptionat the limit (500, or 2,000 when raised).subscribeOn(Schedulers.io())and.timeout(15, SECONDS)UndeliverableExceptiontoRxJavaPlugins, no signal until the timeout firesLlmCallsLimitExceededExceptionafter 500, in under 2 sRecording the
StackWalkerdepth at each model response shows 23 more frames per step onmain, and the same depth at every step with this change, also over 20,000 steps on a 256 KB stack.Checklist
core; see the table for the one failure that is also onmain)Additional context
Rebased onto
mainat 2d4a59e, which already importsAtomicBooleanand adds thelegacyResumptioncheck thatrunStepkeeps. The only conflict was theAtomicIntegerimport.