Skip to content

feat(python-agent): call renderer functions from the agent - #3939

Open
nan-yu wants to merge 11 commits into
feat/python-a2a-adk-agent-function-rpcfrom
feat/python-renderer-function-calls
Open

nan-yu wants to merge 11 commits into
feat/python-a2a-adk-agent-function-rpcfrom
feat/python-renderer-function-calls

Conversation

@nan-yu

@nan-yu nan-yu commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #3795. Review the top ten commits: feat(python-agent): call renderer functions from the agent (#3535), followed by the review-fix and conformance commits covering pending call settlement, RendererFunctionCalls.resolve declining unknown responses, catalog target checks, the shared validation order, and non-empty error.message assertions. Draft until #3792-#3795 land and the open questions below are settled.
Part of #3535.

Description

#3793 first shipped the agent's calls to renderer functions (callRendererFunction) together with agent functions. The review on #3793 questioned whether in-memory pending calls fit stateless HTTP servers, where the rendererFunctionResponse can arrive in a later request on another replica, and how responses to calls a model wrote itself reach the model. To keep agent functions moving, renderer function calls were split out of #3793-#3795 into this PR, on top of RendererToAgentMessageProcessor. The code is the code reviewed on #3793, renamed to fit the processor, with the review fixes below. TypeScript (#3940) and Dart (#3941) follow the same split.

  1. RendererFunctionCalls in a2ui.rpc, one per conversation. call(catalog_id=..., name=..., args=..., surface_id=None, timeout=None) returns (CallRendererFunctionMessage, asyncio.Future) without waiting. It generates the functionCallId, always sets catalogId, and builds the message through the schema model. resolve(message) settles the matching future with the value, or with A2uiRpcError carrying the renderer's code. It follows the renderer-side RpcHandler:

    1. The instance is bound to the loop it is first used on; resolve may be called from any thread and goes through loop.call_soon_threadsafe.
    2. Timeouts use loop.call_later (30 s by default, DEFAULT_RENDERER_CALL_TIMEOUT, as in RpcHandler). Settling or cancelling a call removes its entry and timer.
    3. resolve returns True when the response named a pending call, which it settles (or fails, if the response is malformed), and False for an unknown, late or duplicate response, which it logs at debug level and ignores. Nothing from the network raises.
    4. dispose(reason) fails pending calls (CANCELLED) and makes later call()s raise (DISPOSED). async with disposes on exit. max_pending makes call() raise with RpcErrorCode.TOO_MANY_PENDING, which this PR adds to a2ui_core, as TS and Dart have it.
    5. is_pending(function_call_id) tells whether a response answers one of the instance's calls.
  2. How it plugs into the processor: resolve is the on_renderer_function_response callback of RendererToAgentMessageProcessor. The processor validates the response and hands it over, and because resolve returns False for a response it doesn't own, the processor reports that response as not handled. The inference formats also compile a call the model writes on its own (such as <a2ui>getLocation()</a2ui>) into a callRendererFunction that RendererFunctionCalls never saw, so responses to those calls reach the model's turn. The processor's API from feat(python-agent): answer agent functions through RendererToAgentMessageProcessor #3793 doesn't change:

    calls = RendererFunctionCalls()
    processor = RendererToAgentMessageProcessor(
        request_processor.active_catalogs,
        on_renderer_function_response=calls.resolve,
    )
  3. ADK (rpc_callback.py). a2ui_rpc_callback takes renderer_calls, a sync or async provider of the session's RendererFunctionCalls, and catalogs becomes optional (pass one or both). It processes each A2UI message of the user content with process_message, using calls.resolve as on_renderer_function_response:

    1. Responses (and the other messages that aren't agent function calls) are processed first, so a pending call is always settled, even in a request that goes to the agent because it also holds text, a response to an unknown call, or a callAgentFunction with no catalogs. A malformed response that names a pending call fails that call.
    2. The request is answered, and the agent doesn't run, only when every part is an A2UI message and every message is handled. Only then are the callAgentFunction messages run, and their replies returned. Otherwise the callback returns None and the agent runs with the whole request.
  4. A2uiRendererCallsStore keeps one RendererFunctionCalls per session in memory, next to A2uiProcessorStore. get(ctx) creates the session's instance on first use (and replaces one disposed elsewhere), so tools and the callback share it; it is the renderer_calls provider. remove(session_id) disposes it, failing its pending calls, and dispose() disposes all.

  5. a2ui_rpc_interceptor() returns an ExecuteInterceptor for A2aAgentExecutorConfig.execute_interceptors. ADK's executor marks a task completed only when the final status message has parts, so a request holding only rendererFunctionResponse messages, which has nothing to send back, would end working. The interceptor notes the task when every part of the request is a callAgentFunction or rendererFunctionResponse, and changes that task's final status from working with no message parts to completed. Other final states and other tasks are untouched. The noted IDs are capped at 1024, since a request that fails between the hooks never reaches after_agent.

  6. Prompt. callRendererFunction and rendererFunctionResponse are no longer removed from the model's prompt (the first version of feat(python-a2a,adk): answer agent function calls in A2A and ADK #3795 removed them; feat(python-a2a,adk): answer agent function calls in A2A and ADK #3795 now removes only callAgentFunction and agentFunctionResponse), since a response that answers no pending call now goes to the model.

  7. Conformance. New suite conformance/agent/renderer_function_calls.yaml (30 cases). Steps (call, respond, advance_ms, dispose) run on a fake clock that only advance_ms moves, so no case waits on real time. The cases cover the message a call builds; a response settling the matching call with a value (including null) or the renderer's error code; unknown, duplicate and late responses being ignored; the 30 s default timeout, per-call and per-instance timeouts, and no timeout after a call settles; dispose; max_pending; catalog checks; and validation order. conformance_schema.json gets CallRendererFunctionTest, RendererCallState and RendererCallStep, and the README and harness guide describe the suite. The Python runner delivers responses through RendererToAgentMessageProcessor and checks that a response is handled when and only when its call is pending. web_core's conformance runner skips call_renderer_function as agent-side only.

  8. Docs. A "Calling Renderer Functions" section in agent_development.md, and the READMEs and CHANGELOGs of a2ui_agent, a2ui_adk and a2ui_core.

Review fixes from the first review of #3793, applied here:

  1. An empty function name is rejected with A2uiRpcError (INVALID_FUNCTION_CALL), like an empty catalog_id.
  2. An empty surface_id counts as omitted: the message leaves surfaceId out.
  3. A malformed rendererFunctionResponse whose functionCallId names a pending call fails that call at once (UNKNOWN_ERROR), instead of leaving it to time out. A malformed response for no pending call is still logged and ignored.
  4. The test for settling a call before its timeout asserts timer.cancelled().

After the second review of #3793, callbacks may decline a message by returning False, so resolve now returns whether it owned the response, and the ADK callback relies on that instead of checking is_pending for every response itself.

Open design questions, kept for discussion:

  1. max_pending and the TOO_MANY_PENDING code, which the protocol doesn't define. The agent's own code starts these calls, so a limit may not be needed; it is there to stop a looping model from piling up calls.
  2. Stateless servers. Pending calls live in memory, which suits a server that keeps a conversation on one process, such as one holding a streaming connection. A stateless multi-replica server can't settle them this way; it handles rendererFunctionResponse in its own on_renderer_function_response callback instead, for example by looking up saved state by functionCallId. The docs say so.

Catalog checks (after review on #3793, #3793 (comment)): RendererFunctionCalls takes an optional catalogs argument. With it, call() checks the target before it builds a message, registers a pending call or starts a timer. An unknown catalogId, or a name that isn't one of that catalog's renderer functions (agent functions included), gets UNKNOWN_FUNCTION. A rendererOnly function (or one that leaves out allowedCallers) gets INVALID_FUNCTION_CALL, and so does one with requiresUserActivation: true, since a call from the agent has no user gesture. Two catalogs with the same ID are a catalog error. Without catalogs, nothing changes. A2uiRendererCallsStore doesn't pass catalogs yet, since the active catalogs differ per session. call() runs its checks in the same order in all three SDKs:

  1. an invalid timeout
  2. disposed
  3. an empty given functionCallId
  4. a duplicate id
  5. max_pending
  6. an empty name, then an empty catalogId
  7. a name that isn't an identifier
  8. the args checks: a JSON object, identifier keys, and JSON values (no NaN or infinity)
  9. these catalog checks

Only the instance default timeout can be "no limit".

Changes after the latest review: Third review: after an off-thread dispose(), is_pending, _fail_malformed, _settle and _time_out return early, so the call ends CANCELLED; a2ui_rpc_interceptor.before_agent clears a stale task id and refreshes eviction order; the conformance docs say only the Python harness runs renderer_function_calls.yaml on this branch; the schema gets closed RendererCallError ({code, message?}) and RendererCallsBuildError ({category: CatalogError, message?}) shapes that the runner enforces; _process_other doesn't warn about a malformed response that resolve took.

Tests

  1. test_renderer_function_calls.py: message shape, empty name and surface_id, matching, error responses, unknown/late/duplicate/malformed responses (including a malformed response to a pending call) and what resolve returns for each, is_pending, resolve as the processor callback (handled or declined), resolve and dispose from another thread, timeouts with the timer cancelled on settle, cancellation, async with, max_pending, and loop binding.
  2. test_renderer_function_calls_conformance.py runs all 30 cases of the new suite.
  3. In a2ui_adk: test_rpc_callback.py covers settling a pending call, calls and responses together, a response to an unknown call going to the agent, a pending call settled in a request that goes to the agent (next to a response to an unknown call, next to text, or next to a call with no catalogs), a repeated response being ignored, a malformed response failing the pending call, the settle through A2aAgentExecutor, and the interceptor (an RPC-only task ends completed; a task with a result, other requests and a failed RPC task are unchanged). test_renderer_calls_store.py covers the store.
  4. uv run pytest python/a2ui_agent/tests/rpc python/a2ui_agent/tests/conformance: 251 passed.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements the agent-side capability to call renderer functions via callRendererFunction (v1.0). It introduces RendererFunctionCalls to manage pending calls and futures, A2uiRendererCallsStore to maintain these calls per session, and updates a2ui_rpc_callback and a2ui_rpc_interceptor to handle and complete RPC-only tasks. Conformance tests and schemas have also been updated. The review feedback highlights a potential issue in rpc_callback.py where a mixed batch of messages can cause pending responses to time out without being resolved, and suggests using the RpcErrorCode enum instead of a hardcoded string in renderer_function_calls.py for consistency.

@nan-yu
nan-yu added this pull request to stack #3849 November 4, 2026 05:26
@nan-yu
nan-yu marked this pull request as ready for review November 4, 2026 05:33
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch 2 times, most recently from 4b79034 to fb71c3a Compare November 4, 2026 18:35
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from fb71c3a to 1ac6f09 Compare November 4, 2026 19:22
nan-yu added a commit that referenced this pull request Nov 4, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from 1ac6f09 to 0f8ad2c Compare November 4, 2026 20:23
nan-yu added a commit that referenced this pull request Nov 4, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Nov 4, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from 0f8ad2c to ad3c4ab Compare November 4, 2026 22:04
nan-yu added a commit that referenced this pull request Nov 4, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from ad3c4ab to 52eeed5 Compare November 4, 2026 22:56
nan-yu added a commit that referenced this pull request Nov 4, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from 52eeed5 to 3615281 Compare November 4, 2026 23:18
nan-yu added a commit that referenced this pull request Nov 5, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from 3615281 to bfa38e4 Compare November 5, 2026 00:20
nan-yu added a commit that referenced this pull request Nov 5, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from bfa38e4 to 8aa700d Compare November 5, 2026 00:39

@gspencergoog gspencergoog left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request (#3939) implements v1.0 agent-to-renderer function calls (RendererFunctionCalls, RpcErrorCode.TOO_MANY_PENDING, A2uiRendererCallsStore, a2ui_rpc_interceptor, a2ui_rpc_callback renderer_calls support, and the 30-case conformance/agent/renderer_function_calls.yaml suite). Both threads from the prior review round are resolved cleanly in commit 3933a79cf. No merge blockers were found, though two medium-severity state edge cases (M1 in off-thread dispose() handling and M2 in _RpcTaskTracker stale task_id retention), one medium-severity conformance documentation mismatch (M3), and two low-severity assertion and logging items (L1, L2) remain.

🤖 Agent-generated

nan-yu added 11 commits November 5, 2026 17:58
Restore the agent's calls to renderer functions, split out of #3793-#3795
during review, on top of RendererToAgentMessageProcessor:

- a2ui.rpc.RendererFunctionCalls (with DEFAULT_RENDERER_CALL_TIMEOUT and
  RendererFunctionResponseInput) builds callRendererFunction messages and
  settles their futures. Its resolve method is the processor's
  on_renderer_function_response callback.
- Review fixes from #3793: an empty function name is rejected with
  INVALID_FUNCTION_CALL; an empty surface_id counts as omitted; a malformed
  rendererFunctionResponse whose functionCallId names a pending call fails
  that call at once (UNKNOWN_ERROR) instead of leaving it to time out; the
  timer test asserts timer.cancelled().
- New is_pending(function_call_id), so a framework settles only responses to
  its own calls and passes others (such as responses to calls a model wrote
  through an inference format) to the model's turn, as the review asked.
- a2ui-adk: a2ui_rpc_callback takes renderer_calls (catalogs becomes
  optional), settles responses to pending calls through the processor, and
  fails a pending call on a malformed response. A2uiRendererCallsStore and
  a2ui_rpc_interceptor come back. callRendererFunction and
  rendererFunctionResponse stay in the model's prompt, since unmatched
  responses now reach the model.
- Conformance: conformance/agent/renderer_function_calls.yaml (the 14
  call_renderer_function cases plus empty-name and empty-surfaceId cases),
  CallRendererFunctionTest / RendererCallState / RendererCallStep in the
  schema, README and harness-guide text, and a Python runner on a fake clock
  that delivers responses through the processor.
- README, CHANGELOG and agent_development.md sections.

Open design question from the review, left as is: whether max_pending and
the non-spec TOO_MANY_PENDING code are needed, since the agent's own code
starts these calls. Pending calls are in-memory, so this suits a server that
keeps a conversation on one process; stateless multi-replica servers handle
rendererFunctionResponse in their own callback.

Signed-off-by: Nan Yu <nanyu@google.com>
web_core's harness reads every conformance suite, and call_renderer_function
is agent-side only, like the other agent actions it already skips.

Signed-off-by: Nan Yu <nanyu@google.com>
… the agent

A request whose rendererFunctionResponse messages answered both pending
calls and calls the session didn't make (or that also held text, or a
callAgentFunction with no catalogs) went to the agent without settling
the pending calls, so they waited for their timeout. The callback now
settles those calls first and then lets the agent run.

Also add RpcErrorCode.TOO_MANY_PENDING to a2ui_core, matching TS and
Dart, and raise it by enum in RendererFunctionCalls.

Signed-off-by: Nan Yu <nanyu@google.com>
… responses

- RendererFunctionCalls.resolve returns True when the response named a
  pending call (settled, or failed if malformed) and False for an unknown,
  late or duplicate one, so as on_renderer_function_response it declines
  responses it doesn't own and the processor reports them as not handled.
- a2ui_rpc_callback processes each message with process_message. Responses
  (and other non-call messages) go first through a processor whose
  on_renderer_function_response is calls.resolve, so pending calls are
  always settled, even in requests that go to the agent; the request is
  answered only when every part is A2UI and every message is handled, and
  only then are agent function calls run. Otherwise it returns None.
- The renderer-call conformance runner checks that a response is handled
  exactly when its call is pending; README, dev guide and CHANGELOGs follow.

Signed-off-by: Nan Yu <nanyu@google.com>
call_renderer_function cases take an optional args.catalogs, a list of
RendererCallCatalog entries ({catalogId, functions: {name: {allowedCallers?,
requiresUserActivation?}}}) that the harness builds in code as v1.0
catalogs, and a case may expect building the helper to fail
(expect_error) instead of having steps. Seven new cases in
renderer_function_calls.yaml cover rendererOnly and omitted allowedCallers,
requiresUserActivation, a function or catalog the agent's catalogs lack, an
allowed rendererOrAgent call, and two catalogs with one id. README and the
harness guide describe the shape; the Python runner builds the catalogs
and passes them to RendererFunctionCalls (implemented in the next commit).

Signed-off-by: Nan Yu <nanyu@google.com>
… catalogs

RendererFunctionCalls takes optional catalogs. Two catalogs with one id
raise A2uiCatalogError at construction. With catalogs, call() checks the
target after the empty-name, empty-catalogId and duplicate-id checks and
before anything is built, registered or timed: a catalogId that names no
catalog, or a function the catalog lacks, raises UNKNOWN_FUNCTION; a
rendererOnly function (allowedCallers set to it or left unset) raises
INVALID_FUNCTION_CALL saying the agent may not call it; a function that
requires user activation raises INVALID_FUNCTION_CALL, since an agent call
has no user gesture. Arguments aren't validated. Without catalogs nothing
changes. Unit tests, agent_development.md, README and CHANGELOG follow.

A2uiRendererCallsStore keeps building RendererFunctionCalls without
catalogs: the active catalogs are per session, so a store-wide setting
isn't the right shape.

Signed-off-by: Nan Yu <nanyu@google.com>
RendererFunctionCalls looks the target up in catalog.renderer_functions,
so an agent function, however it was built (including with from_api from
a rendererOrAgent declaration), is rejected with UNKNOWN_FUNCTION:
"Catalog 'c' has no renderer function 'f'." Tests, docs and the
CHANGELOG line follow.

Signed-off-by: Nan Yu <nanyu@google.com>
…all checks

Seven new call_renderer_function cases: an empty given functionCallId, a
name that isn't an identifier, a name starting with @, args that aren't an
object, an argument key that isn't an identifier, the name checked before
the catalogId, and the args checked before the catalogs. The call step
takes functionCallId, and its args may be any JSON value. The Python runner
passes functionCallId; the checks themselves come in the next commit.

Signed-off-by: Nan Yu <nanyu@google.com>
… shared order

RendererFunctionCalls.call follows the cross-SDK order: the timeout,
disposal, an empty given functionCallId (now INVALID_FUNCTION_CALL instead
of a generated id), a duplicate id, max_pending, an empty name, an empty
catalogId, a name that isn't a UAX #31 identifier or starts with @, args
that aren't a mapping with identifier keys and JSON values (NaN and
infinity refused, nested @call/@path objects left alone), and last the
catalogs. timeout and default_timeout must be finite positive numbers (no
bools); only default_timeout=None turns the limit off, and a per-call None
uses the default. A2uiRendererCallsStore checks its settings the same way.

Unit tests cover every rule and the order, including NaN and infinity in
args and timeouts. The conformance README and harness guide say how a
statically typed SDK meets the non-object args case. agent_development.md,
README and CHANGELOG follow.

Signed-off-by: Nan Yu <nanyu@google.com>
… calls

Signed-off-by: Nan Yu <nanyu@google.com>
…alls

- RendererFunctionCalls ignores responses and timeouts once disposed, even
  before an off-thread dispose has failed the calls on the loop, so the
  calls fail with CANCELLED. A malformed response is logged by error type
  at warning level and in full at debug level.
- a2ui_rpc_interceptor forgets a task's earlier RPC-only request when a new
  request on the task arrives, and moves a task it sees again to the end of
  the eviction order.
- a2ui_rpc_callback warns about passing an invalid message on to the agent
  only when RendererFunctionCalls.resolve doesn't take it, and logs the
  error by type.
- The renderer function calls suite gets closed expect_error shapes,
  RendererCallError ({code, message?}) on steps and RendererCallsBuildError
  ({category: CatalogError, message?}) on cases, and the Python runner
  rejects other keys. The docs say only the Python agent harness runs the
  suite so far.

Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu added a commit that referenced this pull request Nov 5, 2026
Audit the agent functions feature blueprint against the Python, TS and Dart
branches and fix the drift:
- The version check runs first for every message kind, so a
  callAgentFunction with a missing or other version raises even with a
  functionCallId; a schema-failing call with a readable id still gets an
  INVALID_FUNCTION_CALL reply in all three SDKs.
- The timeout option is agent_function_timeout (seconds) in Python,
  agentFunctionTimeoutMs in TypeScript and Duration agentFunctionTimeout in
  Dart; the sketch now uses a neutral name.
- The callback signatures per SDK, including Dart's FutureOr<void>
  callbacks that still decline with false.
- Only Python's a2ui.rpc re-exports the message classes; a validation error
  raised by a handler is INVALID_FUNCTION_CALL in Dart only.

Replace the follow-up note with section 5, the agent calling renderer
functions as implemented by #3939, #3940 and #3941: RendererFunctionCalls,
its checks and error codes, timeouts, resolve returning whether it owned
the response, malformed responses, disposal, the optional catalog checks,
the in-memory limitation on stateless servers, and the ADK integration.
Requirements, links, conformance (renderer_function_calls.yaml, 23 cases)
and implementation steps follow. The Python agent codebase blueprint says
renderer calls come in the follow-up #3939.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-renderer-function-calls branch from 8aa700d to cb9c59f Compare November 5, 2026 18:01
@nan-yu
nan-yu requested a review from gspencergoog November 5, 2026 18:32

@gspencergoog gspencergoog left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request implements the agent-initiated side of v1.0 RPC (callRendererFunction and rendererFunctionResponse) across the Python SDK stack (a2ui-core, a2ui-agent-sdk, a2ui-a2a, and a2ui-adk) and adds the shared conformance/agent/renderer_function_calls.yaml suite with extensions in a2a_integration.yaml and adk_extensions.yaml. All seven prior review threads from earlier rounds (including the off-thread dispose() race in RendererFunctionCalls, stale task retention and FIFO eviction ordering in _RpcTaskTracker, conformance documentation for TypeScript and Dart harnesses, schema-backed error key checking in test_renderer_function_calls_conformance.py, and warning suppression for claimed malformed responses in rpc_callback.py) have been verified resolved at cb9c59f96a8ed741f3063da86b6d1255b046bd0c.

🤖 Agent-generated

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants