Skip to content

feat(python-core): declare agent functions on a catalog - #3792

Open
nan-yu wants to merge 8 commits into
mainfrom
feat/python-core-agent-functions
Open

nan-yu wants to merge 8 commits into
mainfrom
feat/python-core-agent-functions

Conversation

@nan-yu

@nan-yu nan-yu commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Based on main (the conformance PR #3742 has merged). The PR has seven commits: feat(python-core): declare agent functions on a catalog; fix(python-core): tighten agent function declarations after review, fix(python-core): address second review of agent functions, fix(python-core): check UAX #31 parameter names in union and shorthand schemas, fix(python-core): align RpcHandler.dispose and the agent call cache with the blueprint and fix(core): merge catalog and local $defs in function validation, which apply the review fixes across the stack; and docs(python-core): drop the stale agent functions CHANGELOG entry, which removes an entry the rebase brought back.
Part of #3536 (step 4 of the SDK-level fix plan).

Description

The renderer side of agent-hosted functions is in #3735 and #3739-#3741: a v1.0 renderer sends a call it can't run to the agent with callAgentFunction. The agent side had no way to declare such a function. To pass its own validation, an agent had to add a plain schema-only entry to its catalog, and keep the code that answers the call somewhere else.

This PR adds the declaration to a2ui_core, where Catalog lives, so the agent SDK can build on it. Answering callAgentFunction (#3793) and the A2A/ADK wiring (#3795) come in the next PRs of this stack.

  1. AgentFunction is a FunctionApi with a read-only handler. The handler takes the validated arguments, or the arguments and an AgentFunctionContext (catalog, surface and function call IDs, keyword-only). It returns the value, directly or as an awaitable. AgentFunction has no execute, so a renderer core given a catalog containing one treats it as declared but not implemented, and routes it. An AgentFunction built directly is always rendererOnly, and its constructor takes none of the catalog-document parameters (is_definition, from_document).
  2. The handler's signature is checked when the AgentFunction is built. The context is passed when the handler takes *args, or when its second positional parameter has no default or is named context or ctx; a defaulted second parameter with another name, such as an injected dependency, is left untouched. A handler that can be called neither as (args) nor as (args, context) raises TypeError.
  3. with_handler(handler) returns a copy bound to another handler, with the same name, schema, description and allowed_callers. A function built with from_api is rebuilt from its declaration, so a catalog still takes the copy in place of that declaration.
  4. AgentFunction.from_api(api, handler) builds an agent function from a function the catalog document already declares, so the definition stays as written; it copies the document fields from the declaration. It keeps the declaration's allowedCallers (rendererOnly or rendererOrAgent) and raises A2uiCatalogError for an agentOnly declaration, which renderers never call, and for one with requiresUserActivation: true that isn't rendererOnly, which the v1.0 catalog definition doesn't allow. It raises TypeError for something that isn't a FunctionApi, or that is already an AgentFunction.
  5. Catalog.copy_with(agent_functions=...) adds agent functions to a loaded catalog. With functions omitted, they replace the catalog's agent functions: one built with from_api goes back to its declaration, and any other is dropped. With functions given, its entries are kept as they are, agent functions included, and agent_functions are added to them. A catalog built in code can also list agent functions in functions; the constructor has no separate parameter. Agent functions are ordinary entries of catalog.functions, so validation, to_json, the prompt generators and the compilers treat them like any other declared function, with no changes there. The catalog document writes them like functions without an implementation and never includes the handler.
  6. Catalog.agent_functions and Catalog.renderer_functions split functions by whether an entry is an AgentFunction. Both are computed. renderer_functions keeps the catalog's function type (dict[str, TFunction]) and returns a new dict, so changing it leaves the catalog as it is.
  7. Names (_with_agent_functions). Each case has its own A2uiCatalogError message:
    1. An agent function whose name another function of the catalog already has is rejected: a renderer that implements the other function would run it and never call the agent. The exception is an agent function built with from_api from that entry, which replaces it in place.
    2. Two agent functions with one name are rejected.
    3. An agent function built with from_api from a function the catalog doesn't declare is rejected, instead of being added. Otherwise a declaration from another catalog could slip in through copy_with(agent_functions=...).
    4. An agent function built with from_api from a declaration of that name that isn't the catalog's own entry object, for example one from a reloaded or copied catalog, gets a message saying so, and to build it from this catalog's entry.
    5. On a v1.0 catalog, an agent function's parameter names must be UAX Initial commit for a2ui_restaurant_finder and a generic a2UI extension. #31 identifiers, as when a catalog document is parsed. This is checked at construction and in copy_with, for JSON Schema (including allOf, anyOf, oneOf, local and catalog #/$defs/ references, and shorthand maps) and Pydantic schemas.
  8. Versions. A catalog before v1.0 rejects agent functions, since callAgentFunction only exists from v1.0.
  9. derive and the pruning transformer keep agent functions consistent with functions without any special handling, because they carry the same entries.
  10. AgentFunction, AgentFunctionContext and AgentFunctionHandler are exported from a2ui.core. The CHANGELOG entry lists the main exports and methods.
  11. Blueprint parity (docs(blueprints): agent functions, renderer fallback routing and renderer calls #3798): RpcHandler.dispose() clears outbound_listener, and the settled agent call cache uses strict FIFO eviction (256 entries by default, configurable via SurfaceModel(..., max_agent_call_cache_entries=...) and MessageProcessorOptions.max_agent_call_cache_entries).
  12. Catalog and local $defs resolution (_resolved_schemas, _extract_raw_param_schema and _apply_object_defaults): merges local $defs over catalog $defs without discarding catalog definitions, validates parameter schemas defined through $ref/allOf/anyOf/oneOf without a top-level properties key, and populates schema defaults across properties, $ref and allOf.

Changes after the reviews:

  1. First review: the agent_functions constructor parameter and the allowed_callers parameter of AgentFunction are gone; from_api of an undeclared function, a duplicate name and a taken name are separate errors; renderer_functions, one-parameter handlers and the root exports are new.
  2. Second review: parameter names are checked against UAX Initial commit for a2ui_restaurant_finder and a generic a2UI extension. #31; copy_with(functions=...) keeps the agent functions it is given; the handler's arity is checked at construction; handler is read-only and with_handler is new; from_api of a different declaration object has its own error; renderer_functions keeps the catalog's function type; the constructor drops is_definition and from_document; from_api rejects requiresUserActivation unless the declaration is rendererOnly; AgentFunctionContext is keyword-only; the CHANGELOG entry is shorter.
  3. Cross-SDK review (feat(typescript): agent functions and RendererToAgentMessageProcessor #3862): merges local and catalog $defs during schema resolution, validation and default application, and supports top-level combinators ($ref, allOf, anyOf, oneOf) in PayloadValidator.validate_function.
  4. Review on feat(dart): agent functions and RendererToAgentMessageProcessor #3861: agent function parameter names follow a local #/$defs/... reference as a JSON Pointer, so #/$defs/Bundle/allOf/0 reaches only that member, and a whole function definition's own $defs are merged with its args' over the catalog's.

Tests

  1. tests/test_agent_functions.py: copy_with and functions (including agent functions given in functions), name collisions and their separate errors, from_api replacement (with a schema-only declaration), from_api of an undeclared, agentOnly, user-activation or reloaded declaration, from_api with a non-FunctionApi, rendererOnly for directly built functions, handler arity (one and two parameters, *args, defaulted parameters, bound methods, wrong arity), the read-only handler and with_handler, the keyword-only context, parameter names, renderer_functions and its type, pre-1.0 rejection, to_json output, derive and pruning, and strict validation of calls and components that use an agent function (including combined catalog and local $defs, schema defaults and rejected arguments).
  2. uv run pytest python/a2ui_core/tests: 2118 passed, 23 skipped, 5 xfailed.

@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 introduces AgentFunction, AgentFunctionContext, and AgentFunctionHandler to support catalog functions that run on the agent starting from protocol version 1.0. It updates Catalog and Catalog.copy_with to handle these agent functions, ensuring they are validated and merged correctly, and adds comprehensive unit tests. The feedback suggests improving the protocol version check using a version-at-least helper, simplifying a redundant fallback expression in copy_with, and adding a defensive type check in AgentFunction.from_api to prevent nested wrappers.

Comment thread python/a2ui_core/CHANGELOG.md Resolved
@nan-yu
nan-yu force-pushed the feat/python-core-agent-functions branch from 7fd36e5 to 5a4f8ce Compare November 3, 2026 16:46
Base automatically changed from test/conformance-agent-function-routing to main November 3, 2026 17:17
@nan-yu
nan-yu force-pushed the feat/python-core-agent-functions branch 2 times, most recently from 3e5ce21 to 787e4f6 Compare November 3, 2026 18:21
Comment thread python/a2ui_core/CHANGELOG.md Resolved
@nan-yu
nan-yu force-pushed the feat/python-core-agent-functions branch from 787e4f6 to 7e40330 Compare November 4, 2026 18:21
Signed-off-by: Nan Yu <nanyu@google.com>
- from_api for a function the catalog doesn't declare is a catalog error
- agent functions are added through copy_with or functions, not a
  constructor parameter
- AgentFunction is always rendererOnly; from_api keeps rendererOnly or
  rendererOrAgent and rejects agentOnly
- separate errors for duplicate agent functions and a taken name
- add Catalog.renderer_functions
- handlers may take (args) or (args, context); add AgentFunction.invoke
- from_api raises TypeError for a non-FunctionApi
- export AgentFunction, AgentFunctionContext and AgentFunctionHandler
  from a2ui.core

Signed-off-by: Nan Yu <nanyu@google.com>
- v1.0 catalogs check agent function parameter names against UAX #31
- copy_with keeps agent functions given in functions; existing ones go
  back to their declarations only when functions is omitted
- handler arity is checked at construction; a defaulted second
  parameter gets the context only if named context or ctx
- handler is read-only; add with_handler
- distinct error for from_api of a different declaration object
- renderer_functions keeps the catalog's function type
- AgentFunction.__init__ drops is_definition and from_document
- from_api rejects requiresUserActivation unless rendererOnly
- AgentFunctionContext is keyword-only
- shorter CHANGELOG entry

Signed-off-by: Nan Yu <nanyu@google.com>
The rebase onto main brought back the first version of the entry next to
the shorter one that replaced it.

Signed-off-by: Nan Yu <nanyu@google.com>
@nan-yu
nan-yu force-pushed the feat/python-core-agent-functions branch from 7e40330 to d1c83d1 Compare November 4, 2026 19:22
…d schemas

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

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

Follow a local $ref as a JSON Pointer, so #/$defs/Bundle/allOf/0 reaches
the node it names rather than the whole Bundle definition, and merge a
whole function definition's own $defs with its args' over the catalog's.
This matches the Dart fix from the #3861 review.

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

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