Repository navigation
Conversation
Add a2ui.rpc to the agent SDK for v1.0 function calls in both directions: - AgentFunctionRegistry answers callAgentFunction with the handlers of the catalogs' agent functions. It copies functionCallId, validates the args against the function's schema (applying defaults), and replies UNKNOWN_FUNCTION, INVALID_FUNCTION_CALL or EXECUTION_ERROR. Sync handlers run through asyncio.to_thread; max_concurrency, timeout and dispose() bound the work, and cancellation is never turned into a response. - RendererFunctionCalls builds callRendererFunction messages for one conversation and settles their futures from rendererFunctionResponse, with loop-bound state, thread-safe resolve, timeouts (30 s by default), max_pending and dispose(). - RendererMessageRouter passes each renderer RPC message to the helper that handles it. An end-to-end test validates a payload that uses an agent function on the agent (Express and Direct JSON) and feeds it to an a2ui_core MessageProcessor without the function: accepted and routed by default, rejected under strict validation, and accepted and routed under strict validation with a schema-only declaration. Part of #3536. Part of #3535. Signed-off-by: Nan Yu <nanyu@google.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces the a2ui.rpc package to support v1.0 function calls between the agent and the renderer. It implements AgentFunctionRegistry to handle renderer-initiated calls, RendererFunctionCalls to manage agent-initiated calls, and RendererMessageRouter to route incoming RPC messages. Additionally, comprehensive documentation and end-to-end tests have been added. Feedback on the changes suggests wrapping loop.call_soon_threadsafe in a try...except RuntimeError block within RendererFunctionCalls.dispose to safely handle scenarios where the event loop is closed or closing during teardown.
…surface A callRendererFunction without a surface set surfaceId to None, so model_dump(exclude_unset=True) wrote "surfaceId": null, which the v1.0 schema rejects. Leave it unset, so exclude_unset=True serializes every A2UI RPC message correctly. Signed-off-by: Nan Yu <nanyu@google.com>
This was referenced Oct 30, 2026
…' into feat/python-agent-function-rpc
…oop closes dispose() handed failing the pending calls to the bound loop with call_soon_threadsafe, which raises RuntimeError if the loop closes after the is_closed check. Fall back to failing them directly, and let _fail_all drop a call whose future can't be failed because its loop is closed. Signed-off-by: Nan Yu <nanyu@google.com>
…' into feat/python-agent-function-rpc 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 #3792. Review only the top commit (
feat(python-agent): answer agent functions and call renderer functions).Part of #3536. Part of #3535.
Description
#3792 lets an agent declare agent functions on its catalog, and the renderer stack (#3735, #3739–#3741) sends calls to them with
callAgentFunction. The agent SDK still had nothing that answers those calls, and nothing for the other direction,callRendererFunction(#3535).This PR adds
a2ui.rpctoa2ui_agent. It has no framework imports (tests/test_framework_agnostic.pychecks this); the A2A and ADK wiring comes in a later PR of this stack.AgentFunctionRegistry(agent_function_registry.py) indexes the catalogs'agent_functionsby(catalog_id, name).await registry.handle(message)takes acallAgentFunctionmessage (model or dict) and returns theAgentFunctionResponseMessageto send:functionCallIdverbatim.catalogIdgetsUNKNOWN_FUNCTION. A call withoutcatalogIdruns the function if exactly one catalog has it.PayloadValidator(catalog, config=STRICT_VALIDATION).validate_function, asa2ui_coredoes, so defaults are applied before the handler runs. Failures getINVALID_FUNCTION_CALLwith the validation message. A call to anagentOnlyfunction getsINVALID_FUNCTION_CALLtoo.EXECUTION_ERRORwith the exception message; the traceback is only logged. A handler that raisesA2uiRpcError(message, code=...)picks the code, so it can send the custom codes the spec shows (such asPROVIDER_NOT_FOUND).asyncio.to_thread.max_concurrency(a semaphore) andtimeout(per registry or perhandlecall; cancels the handler and repliesEXECUTION_ERROR) are optional.dispose()cancels running handlers; those calls raiseA2uiRpcError(DISPOSED) instead of returning a response.asyncio.CancelledErroris never turned into a response.RendererFunctionCalls(renderer_function_calls.py), one per conversation.call(catalog_id=..., name=..., args=..., surface_id=None, timeout=None)returns(CallRendererFunctionMessage, asyncio.Future)without waiting. It generates thefunctionCallId, always setscatalogId, and builds the message through the schema model.resolve(message)settles the matching future with the value, or withA2uiRpcErrorcarrying the renderer's code. It follows the renderer-sideRpcHandler:resolvemay be called from any thread and goes throughloop.call_soon_threadsafe.loop.call_later(30 s by default, as inRpcHandler). Settling or cancelling a call removes its entry and timer.dispose(reason)fails pending calls (CANCELLED) and makes latercall()s raise (DISPOSED).async withdisposes on exit.max_pendingmakescall()raise with codeTOO_MANY_PENDING.RendererMessageRouter(router.py).await router.dispatch(message)returns[agentFunctionResponse]forcallAgentFunction, and[]after resolving arendererFunctionResponse. Other messages (action,error, ...) belong to the agent's turn, sodispatchraisesA2uiValidationErrorfor them, androuter.handles(message)tells the caller beforehand. The ADK callback in a later PR can usehandlesto decide whether to skip the LLM turn.agent_development.mdcovers declaring agent functions, answering calls, calling renderer functions, the concurrency and lifecycle rules, and what renderers need: nothing with default validation;allowUnknownElements: trueor schema-only declarations with strict validation. The README and CHANGELOG are updated.Responses must be serialized with
model_dump(by_alias=True, exclude_unset=True):exclude_none=Truewould drop avalueofNone, which the response must carry. The docs and docstrings say so.Tests
python/a2ui_agent/tests/rpc/(74 tests, real asyncio with short timeouts and events, no sleeps that the result depends on):test_agent_function_registry.py: every response rule above, defaults and context, sync handlers on a worker thread and async ones on the loop, concurrency limits, timeouts, caller cancellation anddispose.test_renderer_function_calls.py: message shape, matching, error responses, unknown/late/duplicate/malformed responses,resolveanddisposefrom another thread, timeouts, cancellation,async with,max_pending, and loop binding.test_router.py: both message types as dicts and models, and the messages it refuses.test_agent_function_validation_end_to_end.py: the agent's copy of the basic v1.0 catalog declaresfetchFxRate; Express and Direct JSON output that binds aTextto it validates on the agent, and an undeclared name is still rejected. The same messages then go into ana2ui_coreMessageProcessorwhose catalog lacks the function: with the default config they are accepted and the call reaches the agent's router, whose response reaches the bound value; withSTRICT_VALIDATIONthey are rejected; with strict validation and a schema-only declaration they are accepted and routed, with validated args.uv run pytest python/a2ui_agent/tests python/a2ui_core/tests: 3411 passed, 7 skipped.uv run mypy .,pyinkandscripts/fix_licenses.py --checkare clean.