From 0275eabbd2e8230dfa713cf8be086004ca554c30 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Sat, 31 Oct 2026 00:20:40 +0000 Subject: [PATCH 1/3] docs(blueprints): specify agent functions and renderer fallback routing Add the agent_functions feature blueprint (agent functions on the catalog, AgentFunctionRegistry, RendererFunctionCalls, RendererMessageRouter and framework wiring), and describe renderer fallback routing as required v1.0 behavior in the a2ui_core module blueprint (RPC lifecycle, ValidationConfig, conformance plan). Signed-off-by: Nan Yu --- .../features/agent_functions.blueprint.md | 204 ++++++++++++++++++ blueprints/modules/a2ui_core.blueprint.md | 26 ++- 2 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 blueprints/features/agent_functions.blueprint.md diff --git a/blueprints/features/agent_functions.blueprint.md b/blueprints/features/agent_functions.blueprint.md new file mode 100644 index 0000000000..7451d58b2f --- /dev/null +++ b/blueprints/features/agent_functions.blueprint.md @@ -0,0 +1,204 @@ +--- +feature_name: agent_functions +module_blueprints: + - a2ui_core + - a2ui_agent +dependencies: [] +date_added: 2026-10-31 +--- + +# **Agent Functions Feature Blueprint** + +From v1.0 a renderer sends a function call it can't run locally to the agent with `callAgentFunction`, and the agent answers with `agentFunctionResponse` (see [renderer fallback routing](../modules/a2ui_core.blueprint.md#rpc-execution-lifecycle--safeguards) in the core module blueprint). The agent can also call renderer functions with `callRendererFunction`. This feature is the agent's side of both directions: + +- The agent declares the functions it hosts on its copy of a catalog, as a function definition plus a handler. +- The agent SDK answers `callAgentFunction` by running those handlers. +- The agent SDK sends `callRendererFunction` and matches the renderer's responses to the calls. +- One entry point takes every renderer-to-agent RPC message. + +Code identifiers below are the generic names used across SDKs. Each SDK spells them in its own style. + +--- + +## **Requirements** + +### **1. Declaration** + +- An agent declares an agent function on its copy of a catalog. Validation, prompt generation and the inference format compilers then treat it like any other function of the catalog, with no changes of their own. +- The model writes a call to an agent function exactly as it writes a call to a renderer function. Nothing in the prompt or the catalog document marks a function as running on the agent: where a call runs is the renderer's decision, made when it finds no local implementation. Usage notes, such as "the value is empty until the agent answers", belong in the function's `description` or the catalog's `instructions`. +- The renderer does not need the declaration. With default validation it accepts the call and routes it. A renderer that validates strictly declares the function schema-only (see [ยง3 of the details](#3-validation-on-both-sides)). + +### **2. Answering calls** + +- The agent answers every well-formed `callAgentFunction` with exactly one `agentFunctionResponse` that copies its `functionCallId`, unless the session is disposed while the call runs. +- Errors use the codes in the protocol: `UNKNOWN_FUNCTION`, `INVALID_FUNCTION_CALL` and `EXECUTION_ERROR`, or a code the handler chooses. +- The handler receives arguments validated against the function's schema, with the schema's defaults applied. + +### **3. Calling renderer functions** + +- The agent builds a `callRendererFunction` message and gets a pending result for it without blocking. The renderer's `rendererFunctionResponse` settles the result, and a timeout, an error response or disposal fails it. +- Responses come from the network, so a late, duplicate, unknown or malformed response is logged and ignored. It never raises. + +--- + +## **Detailed Description of Changes** + +### **1. Core: agent functions on the catalog** + +**Agent function entry.** An `AgentFunction` is a `FunctionApi` (name, `returnType`, argument schema, `allowedCallers`, `requiresUserActivation`, `description`) plus a handler. It has no `execute`, so a renderer core that is handed a catalog containing one treats it as declared without an implementation and routes calls to it. + +```typescript +interface AgentFunctionContext { + /** The catalog the renderer resolved the call to. */ + readonly catalogId: string; + /** The surface the call was made on. */ + readonly surfaceId: string; + /** The ID the response must copy. */ + readonly functionCallId: string; +} + +/** Receives validated arguments with defaults applied; returns the value, directly or asynchronously. */ +type AgentFunctionHandler = ( + args: Record, + context: AgentFunctionContext, +) => unknown | Promise; + +interface AgentFunction extends FunctionApi { + readonly handler: AgentFunctionHandler; +} +``` + +- `allowedCallers` defaults to `rendererOnly`, as for every function: the renderer is the caller. An `agentOnly` agent function is refused by renderers and is never routed, so declaring one is almost always a mistake. +- A function the catalog document already declares can become an agent function without restating its definition: build the agent function from the declared entry (for example `AgentFunction.from_api(entry, handler)` in Python). It keeps the entry's name, schema and metadata, and replaces that entry. + +**`Catalog.agentFunctions`.** The catalog constructor and `copyWith` take a list of agent functions. + +1. Each agent function is also in `functions`, so every consumer that reads `functions` (validation, `toJson`, prompt generators, compilers) handles it unchanged. `agentFunctions` maps names to the agent function entries. +2. The two stay consistent through every way of deriving a catalog: `copyWith`, `derive` and the function pruning transformer. Pruning a function also drops its agent entry. Deriving `agentFunctions` from `functions` (the entries that carry a handler) rather than storing a second map gives this for free. +3. `copyWith(agentFunctions: ...)` replaces the agent functions. An agent function that was built from a declared entry goes back to that entry. +4. **Name collision.** An agent function whose name is already a function of the catalog is an `A2uiCatalogError`, unless it was built from that very entry. Otherwise a renderer that implements the other function would run it and never route the call. +5. **Protocol version.** A catalog before v1.0 that has agent functions is an `A2uiCatalogError`: renderers only route calls from v1.0. +6. **Serialization.** `toJson` writes an agent function like any other declared function, without an implementation. Handlers are never serialized. A renderer that loads the document gets schema-only declarations, which it routes to the agent, so the agent's catalog document is also a valid strict renderer's declaration list. +7. Renderer code never sets agent functions, so renderer behavior is unchanged. + +### **2. Agent SDK: answering `callAgentFunction`** + +`AgentFunctionRegistry` is built from the agent's catalogs and indexes their agent functions by `(catalogId, name)`. `handle(message)` takes a `callAgentFunction` message, as a typed model or parsed JSON, and returns the `agentFunctionResponse` to send. + +**Error mapping**, checked in this order: + +| Condition | Result | +| :------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------- | +| Message fails the schema and has no usable `functionCallId` | Raise `A2uiValidationError`; nothing to answer. | +| Message fails the schema but has a `functionCallId` | `INVALID_FUNCTION_CALL` | +| No `catalogId`, and exactly one catalog has an agent function of that name | Run that function. | +| No `catalogId`, and no catalog has an agent function of that name | `UNKNOWN_FUNCTION` | +| No `catalogId`, and several catalogs have an agent function of that name | `INVALID_FUNCTION_CALL` (ambiguous) | +| `catalogId` is not one of the agent's catalogs, or the catalog has no agent function of that name | `UNKNOWN_FUNCTION`. This includes a function declared for the renderer. | +| The function's `allowedCallers` is `agentOnly` | `INVALID_FUNCTION_CALL` | +| Arguments fail the function's schema | `INVALID_FUNCTION_CALL`, with the validation message. | +| The handler raises the SDK's RPC error type with a code | That code and message, such as a domain code like `PROVIDER_NOT_FOUND`. | +| The handler raises anything else, or exceeds the registry timeout | `EXECUTION_ERROR` with the error message. The stack trace is logged. | +| The handler returns | `value`, which may be `null`. | + +Renderers set `catalogId` on every routed call, so the missing-`catalogId` rules only matter for other senders. Every SDK applies them the same way. + +**Arguments.** Validate with the core `PayloadValidator.validateFunction` under strict validation, against the catalog the call resolved to, and pass the coerced result to the handler, never the raw arguments. Defaults in the schema are applied before the handler runs. + +**Handler context.** The handler receives an `AgentFunctionContext` with the resolved `catalogId`, the call's `surfaceId` and its `functionCallId`. + +**Concurrency, timeouts and cancellation.** + +1. Each call is handled independently. The registry keeps no per-call state once it has answered, so one registry can serve many conversations. +2. A handler must not block the thread that runs the other conversations. Asynchronous handlers run on the caller's event loop or executor. Synchronous handlers run on a worker, as the language allows. +3. An optional concurrency limit bounds how many handlers run at once. Calls beyond it wait. +4. The timeout is a registry setting, optionally overridden per `handle` call, not a property of each function. It has no default, because the renderer already times out its call (30 seconds by default). When it expires, the handler is cancelled and the call gets `EXECUTION_ERROR`, so the agent stops working on an answer nobody is waiting for. +5. `dispose()` cancels running handlers and refuses later calls. A call whose handler is cancelled this way, or that arrives after disposal, gets no response; `handle` raises the RPC error with code `DISPOSED`, because the session is gone. The language's own cancellation signal is never turned into `EXECUTION_ERROR`. + +### **3. Validation on both sides** + +The agent and the renderer share a catalog ID but not the same function list: the agent's copy also has the agent functions. A payload that calls an agent function has to pass validation on both sides. + +- **Agent.** Model output is validated against the agent's catalogs, which include the agent functions. A declared agent function passes, and an undeclared name is still rejected, so the agent stays strict. The agent's model of the renderer's state validates against the same catalogs. +- **Renderer, default validation.** Every renderer core accepts a function that its catalog doesn't declare when the app passes no strict configuration, and routes the call. +- **Renderer, strict validation.** With `allowUnknownElements: false`, the renderer rejects the payload as an unrecognized function. It has two options, both described in the protocol and in the core module blueprint's `ValidationConfig` section: allow unknown elements, which keeps the other checks, or declare the agent functions it expects as schema-only entries, which keeps every check, including argument validation, and still routes the calls. A strict renderer can take those declarations from the agent's serialized catalog. + +### **4. Agent SDK: calling renderer functions** + +`RendererFunctionCalls` holds the agent's pending `callRendererFunction` calls for one conversation. + +```typescript +class RendererFunctionCalls { + constructor(options?: {defaultTimeoutSeconds?: number | null; maxPending?: number}); + /** Registers the call and returns its message and pending result at once. */ + call(options: { + catalogId: string; + name: string; + args?: Record; + surfaceId?: string; + timeoutSeconds?: number; + functionCallId?: string; + }): [CallRendererFunctionMessage, Promise]; + /** Settles the call a rendererFunctionResponse answers. Never throws for network input. */ + resolve(message: RendererFunctionResponseMessage | Record): void; + /** Fails every pending call and refuses later ones. */ + dispose(reason?: string): void; +} +``` + +1. **Message.** `call` generates the `functionCallId` unless one is given, always sets `catalogId` (required on `callRendererFunction`), and builds the message through the protocol schema. A missing `catalogId` or a message that fails the schema raises `INVALID_FUNCTION_CALL` before anything is registered. `surfaceId` is optional; without it the renderer runs the function against an empty data model, and the field is left out of the message rather than sent as `null`. +2. **Never blocking.** `call` returns at once. The caller sends the message, then awaits the result. There is no synchronous call-and-wait. +3. **One event loop or executor.** An instance belongs to the loop it is first used on, and `call` and its pending table run there. `resolve` and `dispose` may be called from any thread and hand the work to that loop. +4. **Timeouts.** Each call waits 30 seconds by default, configurable per instance or per call, or without limit. A timeout fails the result with code `TIMEOUT` and removes the entry. Settling a call cancels its timer. +5. **Error responses.** A response with `error` fails the result with the renderer's code and message. +6. **Cancellation.** If the caller cancels the pending result, its entry and timer are removed, and a later response is ignored. +7. **Late, duplicate, unknown or malformed responses** are logged (malformed at warning level, the others at debug level) and ignored. +8. **Disposal.** `dispose(reason)` fails every pending result with code `CANCELLED` and the reason, cancels their timers, and makes later `call`s raise with code `DISPOSED`. Where the language has scoped resources, the instance disposes itself at the end of the scope. +9. **Bounded.** An optional `maxPending` makes `call` raise with code `TOO_MANY_PENDING` instead of letting a looping model build up unbounded calls. Reusing a pending `functionCallId` raises with code `DUPLICATE`. + +### **5. Agent SDK: renderer message router** + +`RendererMessageRouter` is one entry point for renderer-to-agent RPC messages. It is built with an optional registry and an optional `RendererFunctionCalls`. + +- `handles(message)` tells whether `dispatch` accepts the message: `callAgentFunction` when it has a registry, `rendererFunctionResponse` when it has a `RendererFunctionCalls`. +- `dispatch(message)` returns the agent-to-renderer messages to send back: the `agentFunctionResponse` for a `callAgentFunction`, and none after settling a `rendererFunctionResponse`. +- Any other message, such as `action` or `error`, belongs to the agent's turn. `dispatch` raises `A2uiValidationError` for it, so callers check `handles` first. + +### **6. Serialization** + +An `agentFunctionResponse` must carry `value` even when it is `null`. Serialize RPC messages by omitting fields that were never set, not by omitting `null` fields (for example `exclude_unset`, not `exclude_none`, with Pydantic). Dropping `null` produces a response with neither `value` nor `error`, which fails the schema. + +--- + +## **Links** + +- [`a2ui_protocol.md` (v1.0)](../../specification/v1_0/docs/a2ui_protocol.md): `callAgentFunction` ("Function Location Resolution & Fallback Routing"), `agentFunctionResponse`, `callRendererFunction` and `rendererFunctionResponse`. +- [`a2ui_core.blueprint.md`](../modules/a2ui_core.blueprint.md): `FunctionApi`, `Catalog`, `ValidationConfig`, and the RPC layer, including renderer fallback routing. +- [`a2ui_agent.blueprint.md`](../modules/a2ui_agent.blueprint.md): catalog transformers, validation, and `A2uiRequestProcessor`. +- [#3536](https://github.com/a2ui-project/a2ui/issues/3536): agent functions declared by the agent and routed by the renderer. +- [#3535](https://github.com/a2ui-project/a2ui/issues/3535): RPC helpers for both directions on the agent. +- [#3415](https://github.com/a2ui-project/a2ui/issues/3415): renderer fallback routing. + +--- + +## **Test Cases & Conformance** + +- `conformance/agent/agent_functions.yaml`: declaring agent functions (listed with the catalog's functions; a taken name is a catalog error), every row of the error mapping table, defaults and the handler context, validating model output that calls a declared or an undeclared function, and `RendererFunctionCalls` (message shape, settling, error responses, unknown, late and duplicate responses, default and per-call timeouts, and disposal) on a fake clock. +- `conformance/core/message_processor_v1_0.yaml`, case `test_v10_strict_function_call_to_schema_only_function_of_surface_catalog`: under strict validation, a function the surface catalog declares schema-only passes. +- `conformance/core/agent_function_routing.yaml`: the renderer side of routing, which this feature relies on. + +An SDK without an agent function implementation lists `agent_functions.yaml` as a known gap in its agent conformance runner, naming this blueprint. + +--- + +## **Implementation Steps** + +1. **Core.** Add the agent function entry and its context type, `agentFunctions` on `Catalog` and `copyWith`, consistency through `derive` and pruning, the name-collision and pre-1.0 errors, and serialization without handlers. Run the declaration cases of `agent_functions.yaml`. +2. **Agent SDK.** Add `AgentFunctionRegistry`, `RendererFunctionCalls` and `RendererMessageRouter`, with no agent framework dependency. Run the rest of `agent_functions.yaml`, and add an end-to-end test where the agent validates model output that calls an agent function and a core `MessageProcessor` without the declaration accepts it under default validation, rejects it under strict validation, and accepts and routes it with a schema-only declaration. +3. **Framework integrations.** Wire the router into each agent framework the SDK supports: + 1. Read renderer-to-agent RPC messages out of the transport's A2UI parts, and wrap replies as A2UI parts. + 2. When a request holds only RPC messages (`callAgentFunction` and `rendererFunctionResponse`), dispatch them through the router and return the replies without running a model turn. Any other content goes to the agent as before. + 3. End a task that held only RPC messages as completed, even when the framework would leave it in progress because no model turn produced output. + 4. Keep RPC messages (`callAgentFunction`, `agentFunctionResponse`, `callRendererFunction` and `rendererFunctionResponse`) out of the contents sent to the model, while keeping them in the session history. + 5. Keep one `RendererFunctionCalls` per session, and dispose it when the session ends. +4. Add `agent_functions` to `implemented_features` in the codebase blueprints of the core and the agent SDK, pinned to the latest commit of this blueprint. diff --git a/blueprints/modules/a2ui_core.blueprint.md b/blueprints/modules/a2ui_core.blueprint.md index ccc0c5c509..2240a7fea5 100644 --- a/blueprints/modules/a2ui_core.blueprint.md +++ b/blueprints/modules/a2ui_core.blueprint.md @@ -852,6 +852,8 @@ export class PayloadValidator { } ``` +`allowUnknownElements` covers functions as well as components: under `false`, a call to a function that the catalog it resolves to doesn't declare is rejected as an unrecognized function. From v1.0 such a call would otherwise be [routed to the agent](#rpc-execution-lifecycle--safeguards), so a renderer that validates strictly and uses agent-side functions has two options. It can set `allowUnknownElements: true`, which keeps the other checks. Or it can declare the agent functions it expects as schema-only entries in the catalog the calls resolve to, which keeps every check, including argument validation, and still routes the calls, because a schema-only function has no local implementation. Supplying no `ValidationConfig` at all skips these checks, so calls to undeclared functions are accepted and routed. + ##### In-Memory Schema Resolution (Zero Network, Zero Disk I/O) When validating message payloads, catalogs, or function signatures against JSON Schema (e.g., using `Draft202012Validator` in Python or `Ajv` in TypeScript), validators must operate entirely independently of the host file system or network connectivity: @@ -1027,6 +1029,9 @@ export class RpcHandler { /** Correlates and settles an inbound agentFunctionResponse message with its pending call. */ handleAgentFunctionResponse(message: AgentFunctionResponseMessage): void; + /** Fails every pending outbound call made from the surface with `RpcErrorCode.CANCELLED`. */ + cancelSurfaceCalls(surfaceId: string): void; + /** Disposes the handler, cancelling all pending outbound requests. */ dispose(reason?: string): void; } @@ -1053,11 +1058,26 @@ export class RpcHandler { - **Listener Task Error Trapping**: Transport listener task completion callbacks must only remove or reject pending calls when the listener task itself fails or is cancelled; successful outbound delivery preserves the pending call to await the agent's correlated response. - **Timeout Enforcement**: If no correlated response is received before `timeoutSeconds` expires (default: 30 seconds), the pending call is rejected with `RpcErrorCode.TIMEOUT` and cleaned up. -3. **Synchronous & Asynchronous Loop Bridging**: +3. **Renderer Fallback Routing (v1.0+)**: + + A renderer sends a function call it cannot run locally to the agent, as "Function Location Resolution & Fallback Routing" under `callAgentFunction` in the [v1.0 protocol](../../specification/v1_0/docs/a2ui_protocol.md) requires. This is required behavior for every renderer core, not an optional feature. The agent's side is the optional [`agent_functions`](../features/agent_functions.blueprint.md) feature. + + - **When a call routes**: All of the following hold. The surface is v1.0 or later. The function is not a reserved `@` system function. The call's catalog resolves (its `catalogId`, else the surface default; see [Catalog Scope](#catalog-scope)); a call whose catalog doesn't resolve is a `CATALOG_ERROR` and is never sent. The resolved catalog has no executable implementation of the function: it either doesn't declare it, or declares it schema-only. The surface has an agent function caller. Otherwise nothing changes: an implemented function runs locally, and a missing one is the usual local `EXPRESSION_ERROR`. + - **The surface's agent function caller**: `SurfaceModel` holds an optional caller that sends a call to the agent and resolves with its result. `MessageProcessor` installs it on each v1.0 surface only when it has an outbound listener, and dispatches through `RpcHandler.callAgentFunction` with the processor's timeout. Without a listener there is no way to reach the agent, so calls keep failing locally rather than with `NO_LISTENER`. + - **Resolved `catalogId`**: Every outbound `callFunction` names the catalog the call resolved to, also when the payload left `catalogId` out and relied on the surface default, so the agent never tracks surface defaults. `args` carries resolved values. + - **Schema-only functions**: For a function the resolved catalog declares without an implementation, apply the same checks a local call gets before sending: refuse an `agentOnly` function and enforce `requiresUserActivation`, both as `EXPRESSION_ERROR`, then validate the arguments against the declared schema. A validation failure is an `EXPRESSION_ERROR` and nothing is sent. Send the parsed arguments, with the schema's defaults applied, which is what a local implementation would receive. A function the catalog doesn't declare is sent with its resolved arguments as they are. + - **Pending value and errors**: The value is pending (the language's null or undefined) until `agentFunctionResponse` arrives. An agent error response or a timeout is reported on the surface as `EXPRESSION_ERROR`, carrying the agent's message and the `functionCallId`, and the value stays null. Routing applies to every renderer call: property bindings, check rules, and `functionCall` actions. + - **Nested arguments**: Settle asynchronous argument values before the call runs or is sent, as for local calls. A call whose argument is a pending agent result waits for it instead of running with a null argument. If a nested argument failed, the enclosing call does not run or send, and the failure is reported once rather than again for each enclosing call. + - **Sharing and caching**: Calls are identified by resolved `catalogId`, function name and arguments, compared with object keys in canonical order. For reactive evaluation (bindings and checks), an identical call still awaiting its response shares that request, and the surface caches settled answers so that re-evaluating a binding, or a bound argument returning to an earlier value, does not send the request again. The cache is bounded (256 entries in the current SDKs, dropping the oldest first), and a failed call is not cached, so the next evaluation retries it. Imperative calls from actions never share a request or read the cache: each one sends a fresh call. + - **Surface deletion and disposal**: `deleteSurface` cancels the surface's pending calls through `RpcHandler.cancelSurfaceCalls` (code `CANCELLED`). Disposing the surface removes its caller and clears its shared calls and cache, so a disposed surface sends nothing more to the agent. + - **Inbound calls are unaffected**: Fallback routing applies only to calls the renderer evaluates. An agent's `callRendererFunction` for a function the resolved catalog doesn't have still gets `RpcErrorCode.INVALID_FUNCTION_CALL`. + - **Validation**: Fallback routing only applies to calls validation accepts. See `allowUnknownElements` under [`ValidationConfig`](#validationconfig--payloadvalidator) for what a strict renderer must do. Inside expression evaluation, a call to a function the catalog lacks is not rejected before routing when the surface has a caller. + +4. **Synchronous & Asynchronous Loop Bridging**: - In hybrid or synchronous runtime environments (e.g. Python synchronous SDK consumers), calling asynchronous RPC handlers while an event loop is already active must avoid re-entrant loop conflicts (such as `RuntimeError: This event loop is already running`). - Implementations bridge this by dispatching the asynchronous execution to a dedicated background single-worker thread (`ThreadPoolExecutor(max_workers=1)`) and waiting on the result synchronously. -4. **Lifecycle Disposal (`dispose`)**: +5. **Lifecycle Disposal (`dispose`)**: - Calling `dispose(reason)` cancels all outstanding pending calls with `RpcErrorCode.DISPOSED`, clears all active timeout timers, removes registered transport listeners, and cleans up internal executor threads. --- @@ -1540,3 +1560,5 @@ export class A2uiExpressionError extends A2uiError { See [Conformance README](../../conformance/README.md) for setup and schema definitions. `conformance/core/` covers this module. + +- `conformance/core/agent_function_routing.yaml` covers renderer fallback routing: when a call routes, the resolved `catalogId`, schema-only functions, nested and failed arguments, sharing and caching, check rules and actions. From e918d0c4ed5af82f7766392d88b58e0816fccad3 Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Sat, 31 Oct 2026 00:21:21 +0000 Subject: [PATCH 2/3] docs(blueprints): record agent functions and fallback routing in codebase blueprints Pin agent_functions in the python/a2ui_core and python/a2ui_agent codebase blueprints, and move typescript/core/web_core and python/a2ui_core to the a2ui_core module blueprint commit that specifies fallback routing. dart/a2ui_core (no schema-only functions) and swift/core (behind on earlier module changes) keep their pins, with a note. Signed-off-by: Nan Yu --- blueprints/codebases/dart/a2ui_core/codebase.blueprint.md | 1 + .../codebases/python/a2ui_agent/codebase.blueprint.md | 2 ++ blueprints/codebases/python/a2ui_core/codebase.blueprint.md | 6 ++++-- blueprints/codebases/swift/core/codebase.blueprint.md | 1 + .../typescript/core/web_core/codebase.blueprint.md | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/blueprints/codebases/dart/a2ui_core/codebase.blueprint.md b/blueprints/codebases/dart/a2ui_core/codebase.blueprint.md index dc9141ec71..c47d7c53e6 100644 --- a/blueprints/codebases/dart/a2ui_core/codebase.blueprint.md +++ b/blueprints/codebases/dart/a2ui_core/codebase.blueprint.md @@ -28,6 +28,7 @@ The official Dart implementation of the A2UI Core State Layer (`a2ui_core`), sha - **Idiomatic Dart Concurrency Primitives**: Uses `CancellationSignal` (`lib/src/primitives/cancellation.dart`) in place of Web `AbortSignal`, and Dart `Duration` for RPC call and heartbeat timeouts. - **Per-call validation config**: `processMessages` and `processMessagesAsync` take the config of one call as a `validationConfig` named parameter, beside `isUserActivated`, rather than in an execution context object. Validators are cached per catalog, protocol version and `allowUnknownElements`, the only part of the config a validator reads, so a call that passes a new config object reuses the validators already built. `processMessagesAsync` applies the config while it checks and applies each message, not across its waits for function calls. - **`Catalog.derive` entries**: Dart has no union of a name and an entry short of `Object`, so `derive` takes the names of the entries to keep in `components` and `functions`, and the new entries in `addComponents` and `addFunctions`, which follow the kept ones and replace a kept entry of the same name. +- **Renderer fallback routing, without schema-only functions**: Fallback routing to the agent is implemented as the module blueprint's RPC layer describes, except for schema-only functions: a Dart catalog holds `FunctionImplementation`s, so it cannot declare a function without an implementation, and the conformance cases that declare one are skipped as a known divergence. `module_blueprint_commit` therefore stays at the commit before routing was specified. - **Nested values in the binder**: `GenericBinder` resolves actions, bindings and child lists inside lists and objects, as the module blueprint requires. Its props are a `Map`, so there are no prop types to follow it. ## **Validation & Execution Recipes** diff --git a/blueprints/codebases/python/a2ui_agent/codebase.blueprint.md b/blueprints/codebases/python/a2ui_agent/codebase.blueprint.md index b4c50e29d6..f8ef5bfdb3 100644 --- a/blueprints/codebases/python/a2ui_agent/codebase.blueprint.md +++ b/blueprints/codebases/python/a2ui_agent/codebase.blueprint.md @@ -3,6 +3,7 @@ codebase_path: python/a2ui_agent associated_module: a2ui_agent module_blueprint_commit: ccf5dc20d0b0ed041b03e3913d807420ff45e178 implemented_features: + agent_functions: 0275eabbd2e8230dfa713cf8be086004ca554c30 skill_generator: c177173ca12a9d4739e16e56d6eb87b1e0608d85 typesafe_builder_api: ae0a5253b4e4d8aad06e1b97c236d426b8a401f0 local_development: @@ -33,6 +34,7 @@ The reference Python implementation of the A2UI Agent SDK (`a2ui_agent`). - **Streamed parts typing**: `ResponseStream` is generic in the parts it returns, and `open_stream` is overloaded on `after_rejected_block`, so only a stream opened with `"continue"` is typed `ResponseStream[StreamPart]` and returns `ErrorPart`s. Python 3.10 has no type parameter defaults, so every annotation names the part type, usually `ResponseStream[ResponsePart]`. - **Streaming without a chunk transaction**: `DirectJsonResponseStream` applies each block's messages as it emits them, so a failing chunk's earlier parts stay applied and are attached to the error as `partial_results`. A rejected block is still scanned for its closing tag, without being read, when the stream reads past rejected blocks. - **No renderer**: The Python SDK has no renderer, so it does not implement the optional `agent_renderer_app` feature. +- **Agent function RPC on asyncio**: `a2ui.rpc` is asyncio-native. `AgentFunctionRegistry` awaits async handlers on the caller's loop and runs sync handlers through `asyncio.to_thread`; `RendererFunctionCalls` returns an `asyncio.Future`, uses `loop.call_later` for timeouts, and accepts `resolve` and `dispose` from other threads through `loop.call_soon_threadsafe`. ## **Validation & Execution Recipes** diff --git a/blueprints/codebases/python/a2ui_core/codebase.blueprint.md b/blueprints/codebases/python/a2ui_core/codebase.blueprint.md index 9b151475be..6cb75561c7 100644 --- a/blueprints/codebases/python/a2ui_core/codebase.blueprint.md +++ b/blueprints/codebases/python/a2ui_core/codebase.blueprint.md @@ -1,8 +1,9 @@ --- codebase_path: python/a2ui_core associated_module: a2ui_core -module_blueprint_commit: ab3954fdef84d1bea723128ceac41cf88a5979cb -implemented_features: {} +module_blueprint_commit: 0275eabbd2e8230dfa713cf8be086004ca554c30 +implemented_features: + agent_functions: 0275eabbd2e8230dfa713cf8be086004ca554c30 local_development: test_command: 'uv run pytest' lint_command: 'uv run pyink --check .' @@ -24,6 +25,7 @@ The official Python implementation of the A2UI Core State Layer (`a2ui_core`). - **Auto-vivification Rules**: The JSON pointer implementation dynamically converts missing paths into dicts or list elements on setting nested undefined values. - **Derived catalogs**: `Catalog.derive` and `Catalog.copy_with` share one builder. `copy_with` keeps the source's id and descriptive fields and is typed as a schema-only `CatalogApi`; `derive` takes a new id, drops `$id`, title, description and instructions, and keeps the source's entry types. - **Nested resolution**: `GenericBinder` walks list and object properties at runtime, binding actions and bindings inside them as at the top level. Bound props are plain dictionaries, so there is no typed property set to update. +- **Agent functions on the catalog**: `Catalog.agent_functions` is computed from `functions` (the entries that are `AgentFunction`s) rather than stored, so `copy_with`, `derive` and the agent SDK's function pruning keep the two consistent without extra code. - **No renderer**: The Python SDK has no renderer, so it does not implement the optional `agent_renderer_app` feature. ## **Validation & Execution Recipes** diff --git a/blueprints/codebases/swift/core/codebase.blueprint.md b/blueprints/codebases/swift/core/codebase.blueprint.md index fd36503e90..ac56e5f144 100644 --- a/blueprints/codebases/swift/core/codebase.blueprint.md +++ b/blueprints/codebases/swift/core/codebase.blueprint.md @@ -35,6 +35,7 @@ The native Swift runtime processing engine and state model implementation for A2 name to keep (`.keep`, or a string literal) and a new entry (`.add`). A Swift catalog has no `$id`, title, description or instructions, so `derive` takes no instructions and has nothing else to drop. +- **Renderer fallback routing**: Fallback routing to the agent is implemented as the module blueprint's RPC layer describes, with `MessageProcessor.agentFunctionTimeoutSeconds` as the routed call timeout. `module_blueprint_commit` stays at its older pin because the codebase has not caught up with earlier module changes, such as the per-call `ExecutionContext` (`isUserActivated`) of `processMessages`. - **No agent**: The Swift SDK is a renderer only, so it does not implement the optional `agent_renderer_app` feature. diff --git a/blueprints/codebases/typescript/core/web_core/codebase.blueprint.md b/blueprints/codebases/typescript/core/web_core/codebase.blueprint.md index be9ee1ed2a..0345aed8c1 100644 --- a/blueprints/codebases/typescript/core/web_core/codebase.blueprint.md +++ b/blueprints/codebases/typescript/core/web_core/codebase.blueprint.md @@ -1,7 +1,7 @@ --- codebase_path: typescript/core/web_core associated_module: a2ui_core -module_blueprint_commit: 2e3d9bc434a34ba25498a11e0bfba079b52dcf29 +module_blueprint_commit: 0275eabbd2e8230dfa713cf8be086004ca554c30 implemented_features: agent_renderer_app: null node_resolution: b161f9bdc9395732b305b0adddfd182a36382f09 From eb21d104104229dbcaf2a3fdaa12798074ae97fe Mon Sep 17 00:00:00 2001 From: Nan Yu Date: Mon, 2 Nov 2026 18:37:15 +0000 Subject: [PATCH 3/3] docs(blueprints): match renderer fallback routing to the unified review fixes Describe the behaviors the four renderer cores now share: event action context and userMessage resolved in order and stopping at the first failure, user activation for event actions, the userMessage rule, arguments with no JSON form failing locally, silent cancellation, pending check rules, cache clearing when the caller changes, copies of shared answers, and when a failed call is sent again. Signed-off-by: Nan Yu --- blueprints/modules/a2ui_core.blueprint.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/blueprints/modules/a2ui_core.blueprint.md b/blueprints/modules/a2ui_core.blueprint.md index 2240a7fea5..aa244812d1 100644 --- a/blueprints/modules/a2ui_core.blueprint.md +++ b/blueprints/modules/a2ui_core.blueprint.md @@ -1066,10 +1066,12 @@ export class RpcHandler { - **The surface's agent function caller**: `SurfaceModel` holds an optional caller that sends a call to the agent and resolves with its result. `MessageProcessor` installs it on each v1.0 surface only when it has an outbound listener, and dispatches through `RpcHandler.callAgentFunction` with the processor's timeout. Without a listener there is no way to reach the agent, so calls keep failing locally rather than with `NO_LISTENER`. - **Resolved `catalogId`**: Every outbound `callFunction` names the catalog the call resolved to, also when the payload left `catalogId` out and relied on the surface default, so the agent never tracks surface defaults. `args` carries resolved values. - **Schema-only functions**: For a function the resolved catalog declares without an implementation, apply the same checks a local call gets before sending: refuse an `agentOnly` function and enforce `requiresUserActivation`, both as `EXPRESSION_ERROR`, then validate the arguments against the declared schema. A validation failure is an `EXPRESSION_ERROR` and nothing is sent. Send the parsed arguments, with the schema's defaults applied, which is what a local implementation would receive. A function the catalog doesn't declare is sent with its resolved arguments as they are. - - **Pending value and errors**: The value is pending (the language's null or undefined) until `agentFunctionResponse` arrives. An agent error response or a timeout is reported on the surface as `EXPRESSION_ERROR`, carrying the agent's message and the `functionCallId`, and the value stays null. Routing applies to every renderer call: property bindings, check rules, and `functionCall` actions. - - **Nested arguments**: Settle asynchronous argument values before the call runs or is sent, as for local calls. A call whose argument is a pending agent result waits for it instead of running with a null argument. If a nested argument failed, the enclosing call does not run or send, and the failure is reported once rather than again for each enclosing call. - - **Sharing and caching**: Calls are identified by resolved `catalogId`, function name and arguments, compared with object keys in canonical order. For reactive evaluation (bindings and checks), an identical call still awaiting its response shares that request, and the surface caches settled answers so that re-evaluating a binding, or a bound argument returning to an earlier value, does not send the request again. The cache is bounded (256 entries in the current SDKs, dropping the oldest first), and a failed call is not cached, so the next evaluation retries it. Imperative calls from actions never share a request or read the cache: each one sends a fresh call. - - **Surface deletion and disposal**: `deleteSurface` cancels the surface's pending calls through `RpcHandler.cancelSurfaceCalls` (code `CANCELLED`). Disposing the surface removes its caller and clears its shared calls and cache, so a disposed surface sends nothing more to the agent. + - **Pending value and errors**: The value is pending (the language's null or undefined) until `agentFunctionResponse` arrives. An agent error response or a timeout is always reported on the surface as `EXPRESSION_ERROR`, carrying the agent's message and the `functionCallId`, and the value stays null. A call that fails with `CANCELLED` (the surface was deleted or disposed) reports nothing. A check rule whose condition waits on the agent doesn't fail while it is pending, so the component isn't shown as invalid; when the call settles the rule is evaluated, and a failed call makes it invalid and notifies subscribers. Routing applies to every renderer call: property bindings, check rules, `functionCall` actions, and the `context` and `userMessage` of event actions. + - **Nested arguments**: Settle asynchronous argument values before the call runs or is sent, as for local calls. A call whose argument is a pending agent result waits for it instead of running with a null argument. If a nested argument failed, the enclosing call does not run or send, and the failure is reported once rather than again for each enclosing call. Sibling arguments may settle concurrently; when one fails, the others are abandoned without reporting their late failures. + - **Arguments must be JSON**: A call is sent only when its resolved arguments have a faithful JSON form. A value without one (NaN or an infinity in every language, and in JavaScript also a `Date`, function, symbol, bigint or other non-plain object) fails the call locally with `EXPRESSION_ERROR`, naming the function, and it is not sent, shared or cached. An object property whose value is absent (JavaScript `undefined`) is left out, as JSON does, so an optional argument bound to a missing path still works; an absent array element is an error. + - **Event actions**: An event action's `context` entries resolve in declared order, then its `userMessage`, one at a time: an entry that calls the agent is awaited before the next one is evaluated. The first entry that fails stops resolution, so no later entry is evaluated and no later call is sent, and the action is not dispatched; the failure is reported once. Event actions are user-activated by default, and an explicit user activation setting applies to them as it does to `functionCall` actions. A `userMessage` that resolves to a value that is neither a string nor null is an `INVALID_ACTION` and nothing is dispatched, whether it resolved synchronously or after an agent call; one that resolves to null, such as a binding to a missing path, is absent and the action is dispatched without it. This check belongs to action resolution; the low-level dispatch never validates and leaves out a `userMessage` that isn't a non-empty string. + - **Sharing and caching**: Calls are identified by resolved `catalogId`, function name and arguments, compared with object keys in canonical order and array order kept. For reactive evaluation (bindings and checks), an identical call still awaiting its response shares that request, and the surface caches settled answers so that re-evaluating a binding, or a bound argument returning to an earlier value, does not send the request again. Each caller gets its own copy of a shared or cached answer. The cache is bounded (256 entries in the current SDKs, dropping the oldest first). Replacing or clearing the surface's agent function caller drops the shared calls and the cache, and an answer that arrives for a replaced caller isn't cached. A failed call is evicted before subscribers are notified, so a new evaluation (such as a newly mounted component) sends it again, but a subscription that already saw it fail sends it again only when the call's own `catalogId`, name or arguments change, not when an unrelated path the binding reads changes. Imperative calls from actions never share a request or read the cache: each one sends a fresh call. + - **Surface deletion and disposal**: `deleteSurface` cancels the surface's pending calls through `RpcHandler.cancelSurfaceCalls` (code `CANCELLED`), and the cancellation is not reported. Disposing the surface removes its caller and clears its shared calls and cache. A call made on a deleted or disposed surface, including one from an action that resumes after `deleteSurface`, sends nothing and fails with `CANCELLED`, and an evaluation that was already aborted sends nothing. - **Inbound calls are unaffected**: Fallback routing applies only to calls the renderer evaluates. An agent's `callRendererFunction` for a function the resolved catalog doesn't have still gets `RpcErrorCode.INVALID_FUNCTION_CALL`. - **Validation**: Fallback routing only applies to calls validation accepts. See `allowUnknownElements` under [`ValidationConfig`](#validationconfig--payloadvalidator) for what a strict renderer must do. Inside expression evaluation, a call to a function the catalog lacks is not rejected before routing when the surface has a caller. @@ -1445,7 +1447,7 @@ _Per-call catalog dispatch._ A `FunctionCall` may carry a `catalogId`. `DataCont This depends on `catalogId` surviving deserialization. If the `FunctionCall` model used at runtime is the pre-v1.0 shape (`call`, `args`, `returnType`), a strict schema library strips `catalogId` before resolution ever sees it, and every call silently resolves against the default catalog. Version-specific schema models must be selected by the surface's protocol version rather than aliased back to a legacy definition. -_Renderer callers._ Every call `DataContext` evaluates is a renderer call: a property binding, a check, a `functionCall` action, or a call nested in the arguments of any of them. A function whose `allowedCallers` is `agentOnly` runs only for an agent-initiated `callRendererFunction`, which `RpcHandler` executes without going through `DataContext`'s function invocation. `DataContext` therefore refuses an `agentOnly` function with `A2uiExpressionError`, before the user activation check and before running it, and reports it as an `EXPRESSION_ERROR` on the surface. A function that declares no `allowedCallers` is `rendererOnly` and runs. +_Renderer callers._ Every call `DataContext` evaluates is a renderer call: a property binding, a check, a `functionCall` action, the `context` or `userMessage` of an event action, or a call nested in the arguments of any of them. A function whose `allowedCallers` is `agentOnly` runs only for an agent-initiated `callRendererFunction`, which `RpcHandler` executes without going through `DataContext`'s function invocation. `DataContext` therefore refuses an `agentOnly` function with `A2uiExpressionError`, before the user activation check and before running it, and reports it as an `EXPRESSION_ERROR` on the surface. A function that declares no `allowedCallers` is `rendererOnly` and runs. _Recursion into nested containers._ `resolveDynamicValue` recurses into plain objects and arrays, resolving bindings at any depth. Returning a plain object unresolved means a nested binding such as `{"style": {"color": {"@path": "/accent"}}}` reaches the renderer as a raw pointer object. Recovering that only through a higher-level schema walk leaves direct `DataContext` callers, including conformance harnesses, with different results from the framework path. @@ -1561,4 +1563,4 @@ See [Conformance README](../../conformance/README.md) for setup and schema defin `conformance/core/` covers this module. -- `conformance/core/agent_function_routing.yaml` covers renderer fallback routing: when a call routes, the resolved `catalogId`, schema-only functions, nested and failed arguments, sharing and caching, check rules and actions. +- `conformance/core/agent_function_routing.yaml` covers renderer fallback routing: when a call routes, the resolved `catalogId`, schema-only functions, nested and failed arguments, arguments with no JSON form, sharing and caching, check rules (including pending ones), actions, event actions (order, the first failure, and `userMessage`), `agentOnly` functions, cancellation on `deleteSurface`, and when a failed call is sent again.