feat(agent-tool): stream sub-agent events live (parity with adk-python #3991)#334
Open
cstavaru wants to merge 1 commit into
Open
feat(agent-tool): stream sub-agent events live (parity with adk-python #3991)#334cstavaru wants to merge 1 commit into
cstavaru wants to merge 1 commit into
Conversation
…n #3991) Previously, when an LlmAgent invoked a sub-agent via AgentTool, all of the sub-agent's intermediate events (model output, tool calls, nested AgentTool invocations, tool responses) were swallowed by AgentTool.runAsync and only a single final function-response event was emitted to the parent runner. This made it impossible to surface real-time sub-agent activity to the user, and broke parity with the Python ADK once it shipped the streaming variant. This change mirrors adk-python PR #3991 (google/adk-python#3991) for the JS SDK: functions.ts - Add handleFunctionCallsStreamingAsync(), an AsyncGenerator<Event> variant of handleFunctionCallsAsync(). It separates AgentTool calls from regular tool calls; for each AgentTool call it iterates AgentTool.runAsyncWithEvents() and yields every event live, while tracking the last content event to build the final tool-response. Regular tool calls continue to delegate to handleFunctionCallList. The terminal yield is always either the single response event or the merged parallel-function-response event - this contract lets the caller capture it with a one-step buffer. agent_tool.ts - Refactor AgentTool.runAsync(): split the runner/session setup into a private setupRunnerAndSession() and the result extraction into a public buildToolResultFromContent() helper. - Add AgentTool.runAsyncWithEvents(): an AsyncGenerator<Event> that yields the sub-agent's events as they are produced, without building the tool result. Callers (handleFunctionCallsStreamingAsync) are responsible for assembling the final response via buildToolResultFromContent(). - Pass invocationContext.runConfig down into runner.runAsync() so the sub-agent inherits the parent's run configuration, matching the Python implementation. llm_agent.ts - Switch the function-call handling site from handleFunctionCallsAsync to handleFunctionCallsStreamingAsync. Use a one-step buffer over the generator so every intermediate sub-agent event is yielded to the parent runner, and only the final function-response event is retained as functionResponseEvent for the subsequent processing. Refs: google/adk-python#3991
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Brings the JS SDK to parity with adk-python PR
#3991 ("Stream
sub-agent events from
AgentTool").Today, when an
LlmAgentinvokes a sub-agent viaAgentTool, everyintermediate event the sub-agent produces (model output, function
calls, nested
AgentToolinvocations, function responses) is consumedinside
AgentTool.runAsync()and never reaches the parent runner —the parent only sees a single final
function_responseevent withthe merged final text. This makes it impossible to render real-time
sub-agent activity in a CLI/UI, and is the JS counterpart to the
exact problem
adk-pythonfixed in #3991.Changes
core/src/agents/functions.tshandleFunctionCallsStreamingAsync(), anAsyncGenerator<Event>variant ofhandleFunctionCallsAsync().It splits the incoming function-call event into
AgentToolcallsand regular tool calls. For each
AgentToolcall it iteratesAgentTool.runAsyncWithEvents()and yields every event live, whiletracking the last content event in order to build the final tool
response. Regular tool calls continue to delegate to
handleFunctionCallList.function-response event or the merged
parallel-function-response event. Callers can therefore keep a
one-step buffer to capture it as
functionResponseEvent.core/src/tools/agent_tool.tsAgentTool.runAsync():setupRunnerAndSession().into a public
buildToolResultFromContent()helper thathandleFunctionCallsStreamingAsyncreuses.AgentTool.runAsyncWithEvents(): anAsyncGenerator<Event>that yields the sub-agent's events as they are produced and does
not build the tool result itself.
invocationContext.runConfigdown into the innerrunner.runAsync()so the sub-agent inherits the parent's runconfiguration (matches the Python implementation).
core/src/agents/llm_agent.tshandleFunctionCallsAsynctohandleFunctionCallsStreamingAsync.intermediate sub-agent event to the parent runner and retaining
only the terminal event as the
functionResponseEventfor therest of the flow.
Behavior
[function_call_event, function_response_event]for anAgentToolinvocation.[function_call_event, ...all sub-agent events..., function_response_event].The final
function_response_eventis byte-for-byte the same asbefore (still built by
buildToolResultFromContent), so any codethat only cared about the response event keeps working.
Verification
npm run lint— cleannpm run build— cleanLlmAgentwith anAgentTool-wrapped sub-agent: intermediate events from thesub-agent now stream into the parent runner exactly as a top-level
agent's events would, and the final tool response is unchanged.
Refs
feat(agent-tool): Add event streaming propagation from AgentTool sub-agents to Runner adk-python#3991