Repository navigation
Conversation
Wire the a2ui.rpc helpers into the A2A and ADK integrations: - a2ui.a2a: get_a2ui_messages reads the A2UI messages in an A2A message's A2UI DataParts, and create_a2ui_parts wraps reply messages as A2UI parts (models dumped with by_alias=True, exclude_unset=True). - a2ui.adk: a2ui_rpc_callback returns a before_agent_callback that, when the user content holds only callAgentFunction/rendererFunctionResponse messages, dispatches them through a RendererMessageRouter and returns the replies as the agent's response, so the agent doesn't run. It reads and writes parts with ADK's own A2A part converters, so A2aAgentExecutor and A2uiEventConverter send the replies as A2UI DataParts. Any other content returns None. It composes with other callbacks through ADK's callback list. A2uiRendererCallsStore keeps a RendererFunctionCalls per session and disposes it on remove(). Tests run through ADK's real converters, Runner and A2aAgentExecutor with a stub agent, and pass against google-adk 2.3.0 and the 1.28.1 floor. Part of #3536. Part of #3535. Signed-off-by: Nan Yu <nanyu@google.com>
…into feat/python-a2a-adk-agent-function-rpc
…rpc' into feat/python-a2a-adk-agent-function-rpc
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces A2UI RPC helpers to facilitate handling A2UI v1.0 RPC messages within the A2A and ADK frameworks. Key additions include get_a2ui_messages and create_a2ui_parts for message processing, a2ui_rpc_callback to enable agents to answer RPC messages without a full model turn, and A2uiRendererCallsStore to manage session-specific renderer function calls. Comprehensive tests were added to verify these new functionalities. No review comments were provided for this pull request.
…rpc' into feat/python-a2a-adk-agent-function-rpc
…rpc' into feat/python-a2a-adk-agent-function-rpc
…rpc' into feat/python-a2a-adk-agent-function-rpc
…rpc' into feat/python-a2a-adk-agent-function-rpc Signed-off-by: Nan Yu <nanyu@google.com>
- a2ui_rpc_interceptor() returns an A2aAgentExecutor ExecuteInterceptor. Its before_agent hook notes tasks whose request message holds only A2UI callAgentFunction/rendererFunctionResponse parts, and its after_agent hook changes such a task's final status from working (ADK's state when the agent sent nothing) to completed. Other final states are untouched. - SendA2uiToClientToolset.process_llm_request removes A2UI RPC messages in both directions (callAgentFunction, agentFunctionResponse, callRendererFunction, rendererFunctionResponse) from the contents sent to the model, dropping contents left empty. Session events keep them, and other A2UI messages are kept. exclude_a2ui_rpc_from_prompt is the same filter as a before_model_callback for agents without the toolset. Both read parts through ADK's own A2A part converters and need the a2a extra; the toolset skips the filter without it. Tests run the real A2aAgentExecutor, Runner and LlmAgent with stub agents and a recording model, against google-adk 2.3.0 and 1.28.1. Part of #3535. Signed-off-by: Nan Yu <nanyu@google.com>
This branch has not been deployed
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.
Stacked on #3794. Review only commits 6809d4f (
feat(python-a2a,adk): route A2UI RPC messages) and de70693 (feat(python-adk): complete RPC-only tasks and keep RPC out of prompts); the other new commits merge the branches below them.Fixes #3535. Part of #3536.
Description
#3793 added
a2ui.rpc(AgentFunctionRegistry,RendererFunctionCalls,RendererMessageRouter) to the framework-agnostic agent SDK. This PR connects it to A2A and ADK, so an agent behind ADK's stockA2aAgentExecutoranswerscallAgentFunctionand settlesrendererFunctionResponsewithout a model turn.A2A part helpers (
parts.py):get_a2ui_messages(message_or_parts)returns the A2UI messages in the A2UIDataParts of an incoming A2A message, skipping other parts. It uses the existingget_a2ui_datapart.create_a2ui_parts(messages, version=None)wraps messages, such as the replies ofrouter.dispatch, as A2UI parts withcreate_a2ui_part. Models are serialized withmodel_dump(by_alias=True, exclude_unset=True), so anagentFunctionResponsekeeps"value": null(and, since the last fix on feat(python-agent): answer agent functions and call renderer functions #3793, acallRendererFunctionwithout a surface has nosurfaceId).ADK callback (
rpc_callback.py).a2ui_rpc_callback(agent_functions=..., renderer_calls=...)returns abefore_agent_callback:convert_genai_part_to_a2a_part.A2aAgentExecutorturns an A2UIDataPartintotext/plaininline data holding the whole part, and this is the inverse, so the callback doesn't depend on ADK's tag format.callAgentFunctionwith a registry,rendererFunctionResponsewith renderer calls), it dispatches them concurrently and returns the replies as aContent. Each reply goes throughcreate_a2ui_partsand ADK'sconvert_a2a_part_to_genai_part, soA2uiEventConverter/A2uiPartConverter(or ADK's default converter) send it back as an A2UIDataPart. ADK then ends the invocation, so the agent doesn't run.action, a mix of RPC messages and other parts, or no registry for a call) it returnsNoneand the agent runs.agent_functionsis a registry or a sync/async provider called with the context, so a provider can build the registry from the session's catalogs.renderer_callsis a provider of the session'sRendererFunctionCalls. Providers are only called when the content needs them.callAgentFunctionwithout afunctionCallId, or one that reaches a disposed registry, is logged and dropped instead of failing the request.before_agent_callback=[a2ui_rpc_callback(...), mine]); ADK runs them in order until one returns content.It lives in
a2ui.adk.a2a, because it needs thea2aextra, and is exported lazily froma2ui.adklike the converters.A2uiRendererCallsStore(renderer_calls_store.py) keeps oneRendererFunctionCallsper session in memory, next toA2uiProcessorStore.get(ctx)creates the session's instance on first use (and replaces one disposed elsewhere), so tools and the callback share it;remove(session_id)disposes it, failing its pending calls, anddispose()disposes all. It only needs ADK.a2ui_rpc_interceptor()(rpc_interceptor.py) returns anExecuteInterceptorforA2aAgentExecutorConfig.execute_interceptors. ADK's executor marks a taskcompletedonly when the final status message has parts, so a request holding onlyrendererFunctionResponsemessages, which has nothing to send back, would endworking. The interceptor'sbefore_agenthook checks the request message itself (every part an A2UIcallAgentFunctionorrendererFunctionResponse) and notes the task ID; itsafter_agenthook changes that task's final status fromworkingwith no message parts tocompleted. Other final states (failed,input_required,completedwith a result) and other tasks are untouched. It doesn't depend on the callback. A request that fails between the hooks never reachesafter_agent, so the noted IDs are capped at 1024.RPC kept out of the model's prompt (
rpc_prompt_filter.py). ADK stores RPC turns in the session's events, so later prompts would carry them.SendA2uiToClientToolset.process_llm_requestnow removes, from the contents sent to the model, every inline-data part that ADK'sconvert_genai_part_to_a2a_partturns into an A2UI RPC message in either direction (callAgentFunction,agentFunctionResponse,callRendererFunction,rendererFunctionResponse), and drops contents left empty. The session's events keep them, and other A2UI messages (action,createSurface, ...) and other parts are kept.callRendererFunctionmessages the agent sent are removed too, for consistency: the model asked for them through a tool, whose call and result already record the exchange.exclude_a2ui_rpc_from_promptis the same filter as abefore_model_callbackfor agents without the toolset. Both need thea2aextra, since they use ADK's A2A converter; the toolset skips the filter without it, because then no A2A parts reach the session.Docs. READMEs and CHANGELOGs of
a2ui_a2aanda2ui_adk, and a short "With A2A and ADK" section inagent_development.md.Two behaviours reviewers should know:
rendererFunctionResponsemessages gets a response with no parts. Witha2ui_rpc_interceptor()in the executor's interceptors, its task endscompleted; without it, ADK ends itworking, as for any turn whose agent sends nothing.SendA2uiToClientToolset(orexclude_a2ui_rpc_from_prompt) keeps them out of the model's prompt.google-adk >= 1.28.1stays: 1.28.1'sconvert_a2a_part_to_genai_partandconvert_genai_part_to_a2a_parthandle A2UIDataParts the same way as 2.3.0 (2.3.0 also copies the part metadata intopart_metadata, which the round trip doesn't need), and the new tests pass against both.No sample is updated: every Python ADK sample speaks v0.8 or v0.9, and agent functions need v1.0. A sample can use the callback once one moves to v1.0.
Tests
test_rpc_parts.py: reading A2UI messages from a message or parts, skipping other parts, and wrapping models (keeping a nullvalue) and dicts in order.test_rpc_callback.py, with ADK's real converters and no model (a stub agent answers with fixed text):Contentbuilt byconvert_a2a_part_to_genai_partfrom acallAgentFunctionpart gets aContentwhose part converts back, through ADK's converter and throughA2uiPartConverter, to an A2UIDataPartholding theagentFunctionResponse. Several calls answer in order; async providers get the context.action,actionplus call, text plus call, and a call with no registry returnNone. ArendererFunctionResponsewithout renderer calls goes to the agent.rendererFunctionResponsesettles the pending future in anA2uiRendererCallsStore.Runner: the agent doesn't run for RPC content and runs for text; in a callback list, the next callback runs only when this one returnsNone.A2aAgentExecutorwithA2uiEventConverter: the reply leaves as an A2UIDataPartin the working status update and the artifact, with no agent text, and the task completes; arendererFunctionResponserequest settles the call.test_renderer_calls_store.py: one instance per session, settings passed on, invalid settings,removeanddisposefailing pending calls, and replacing a disposed instance.test_lazy_a2a_exports.pycovers the new lazy exports.test_rpc_callback.py, through the realA2aAgentExecutorwith stub agents: an RPC-only request (rendererFunctionResponse) endscompleted; acallAgentFunctionrequest keeps its result andcompleted; a text request and a text-plus-RPC request whose agent sends nothing stayworking; a normal turn with a reply completes; a failed RPC request staysfailed.test_rpc_prompt_filter.py: the callback and the toolset remove all four RPC message kinds, drop emptied contents, and keep text,actionandcreateSurface; requests without RPC are unchanged. End to end withRunner, anLlmAgentand a model stub that records its requests, for both the toolset and the callback: after an RPC turn and a text turn, the model's request holds no RPC parts while the session's events still hold the call and its reply (without the filter, the same test sees both in the prompt).test_lazy_a2a_exports.pychecks the toolset's hook without thea2aextra.uv run pytest python/a2ui_a2a/tests python/a2ui_adk/tests python/a2ui_agent/tests samples/agent/adk/tests conformance/tests: 2120 passed, 2 xfailed.uv run mypy .,pyinkandscripts/fix_licenses.py --checkare clean. The new tests andtest_lazy_a2a_exports.pyalso pass in a venv withgoogle-adk[a2a]==1.28.1(62 passed after the second commit).