a2ui-project / a2ui-project/a2ui

Proposal: Server-Side Functions and Unified Function RPC (Reframing ActionResponses)

未关闭
#2,104 7 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
component: specification P2 status: first-line-handled type: feature/enhancement
主要语言
TypeScript
星标
16.4k
派生
1.3k
平均合并
2 天 13 小时
30 天内合并 PR
134

描述

> **Note**: This issue presents an architectural design document proposing to replace the draft A2UI v1.0 `wantResponse` and `actionResponse` event features with **Catalog-Defined Server-Side Functions**. The complete design proposal is included verbatim below for review and discussion.

---

### Issue Description

In draft A2UI v1.0, synchronous event responses were introduced by adding `"wantResponse": true` and `"responsePath"` to component `action.event` declarations. As identified during specification review, this approach introduces three architectural issues:
1. **The Informal Schema Contract Gap**: Setting `wantResponse: true` returns an untyped `any` payload on the wire. Components that bind `responsePath` to this return value rely on an unverified, out-of-band agreement between catalog designers and server implementers.
2. **Functional Redundancy**: Allowing an action response to mutate the data model via `responsePath` duplicates the capability of the standard `updateDataModel` message, creating ambiguity for implementers.
3. **Inexpressive Error Handling**: The draft `actionResponse.error` object accepts only a flat `code` and `message` string, making it impossible to return structured validation errors without treating failures as protocol-level successes.

This design proposal resolves these problems by:
* Preserving A2UI v0.9's asynchronous, fire-and-forget semantics for `action.event` and removing all draft v1.0 response properties (`wantResponse`, `responsePath`, and `actionResponse`).
* Formalizing **Server-Side Functions** in catalog definitions (`FunctionDefinition`) with an orthogonal separation between **implementation location** (`"executedAt": "client" | "server"`) and **invocation authorization** (`"callableFrom": "clientOnly" | "serverOnly" | "clientOrServer"`).
* Requiring explicit JSON Schema contracts for input arguments (`args`), return values (`returnType`), and structured errors (`errors`).
* Introducing short-circuit exception evaluation for nested function expressions and new client-to-server RPC messages (`callServerFunction` / `serverFunctionResponse`).

---

### Design Document: Server-Side Functions and Unified Function RPC

# A2UI v1.0 Proposal: Server-Side Functions and Unified Function RPC (Reframing ActionResponses)

This proposal replaces A2UI v1.0's draft `wantResponse` and `actionResponse` event features with **Catalog-Defined Server-Side Functions**.

By formalizing server-side computations as catalog-registered functions alongside client-side functions, we eliminate the informal schema contract gap, preserve A2UI v0.9's clear separation between fire-and-forget events and data-model mutations, and provide a unified, statically typed RPC mechanism for checks, queries, and synchronous interactions.

---

## 1. Executive Summary and Motivation

### Problems with Draft v1.0 `ActionResponses` (`wantResponse` / `responsePath`)
1. **The Informal Schema Contract Gap**: In draft A2UI v1.0, setting `"wantResponse": true` on an `event` action returns an untyped `"value": any` payload on the wire. Components that bind `responsePath` to this return value rely on an unverified, out-of-band agreement between the catalog designer and the server implementer.
2. **Functional Redundancy**: Allowing an action response to mutate the shared data model via `responsePath` duplicates the capability of the standard `updateDataModel` message, creating ambiguity for agent implementers.
3. **Inexpressive Error Handling**: The draft `actionResponse.error` object accepts only a flat `code` and `message`, making it impossible to return structured validation errors without treating failures as protocol-level successes.

### The Reframed Approach: Unified Catalog Functions
Instead of adding synchronous response properties to `event` actions, this proposal:
1. **Preserves v0.9 Action Semantics**: `action.event` remains an asynchronous, fire-and-forget notification sent from the renderer to the agent. All draft v1.0 fields (`wantResponse`, `responsePath`, and `actionResponse`) are removed.
2. **Distinguishes Implementation Location (`executedAt`) from Invocation Boundary (`callableFrom`)**:
- **`executedAt`** specifies where the function is **implemented and executed** (`"client"` or `"server"` — strictly one or the other; a single function cannot be executed on both). If an application requires similar logic on both client and server, it is modeled as two separate functions with distinct names.
- **`callableFrom`** specifies who is authorized to **invoke** the function (`"clientOnly"`, `"serverOnly"`, or `"clientOrServer"`).
3. **Establishes a Common Schema Contract**: Every catalog function definition declares explicit JSON Schema contracts for both **input arguments (`args`)** and **return types (`returnType`)**.
4. **Supports Errors and Exception-Like Short-Circuiting**: Both client and server functions can return structured errors instead of values. When functions are nested in expressions, an error returned at any point immediately short-circuits evaluation, propagating the error to the caller like an exception.
5. **Adds Explicit Client-to-Server RPC Messages**: Introduces a new `callServerFunction` message (renderer-to-agent) and a symmetrical `serverFunctionResponse` message (agent-to-renderer).

---

## 2. Architectural Design: Separation of Responsibilities

```mermaid
graph TD
subgraph Catalog / UI Component
ACT[Component Action Slot]
end

ACT -->|1. event| EVT[Fire-and-Forget Event]
ACT -->|2. functionCall| FUNC[Unified Function Invocation]

EVT -->|renderer_to_agent: action| A_SRV[Agent / Server]
A_SRV -->|agent_to_renderer: updateDataModel| R_DM[Renderer Data Model]

FUNC -->|executedAt: client| C_EXEC[Local Renderer Execution]
FUNC -->|executedAt: server| S_EXEC[callServerFunction RPC]
S_EXEC -->|renderer_to_agent: callServerFunction| A_SRV
A_SRV -->|agent_to_renderer: serverFunctionResponse| S_EXEC
```

This proposal establishes three clean, orthogonal communication channels:
* **Events (`action.event`)**: One-way, asynchronous user interaction notifications sent from renderer to agent. They do not block UI execution and do not return payloads.
* **State Mutations (`updateDataModel`)**: Authoritative, imperative state synchronization sent from agent to renderer.
* **Functions (`action.functionCall`)**: Synchronous computations, validations, queries, or checks triggered by interactive components. Whether a function runs locally on the renderer or remotely on the agent is determined transparently by the catalog's `executedAt` property (`"client"` vs. `"server"`), while authorization to invoke it is governed by `callableFrom`.

---

## 3. Formal Schema Definitions

### 3.1 Common Function Definition Schema (`catalog_definition.json`)
Every function declared in a catalog must conform to a common schema that defines its implementation location (`executedAt`), its invocation boundary (`callableFrom`), input arguments, return type, and optional error schemas:

```json
{
"FunctionDefinition": {
"type": "object",
"description": "Defines a client-side or server-side function in the catalog.",
"properties": {
"executedAt": {
"type": "string",
"enum": ["client", "server"],
"default": "client",
"description": "Indicates where the function is implemented and executed. Must be either 'client' or 'server'—a single function cannot be executed on both. If a system requires similar logic on both the client and server, it should be modeled as two separate functions with distinct names."
},
"callableFrom": {
"type": "string",
"enum": ["clientOnly", "serverOnly", "clientOrServer"],
"default": "clientOnly",
"description": "Indicates who is authorized to invoke this function—whether it can be called from the client, the server, or either."
},
"args": {
"$ref": "https://json-schema.org/draft/2020-12/schema",
"description": "JSON Schema defining the expected arguments for this function."
},
"returnType": {
"$ref": "https://json-schema.org/draft/2020-12/schema",
"description": "JSON Schema defining the return value of this function. MUST be specified for functions with executedAt: 'server'."
},
"errors": {
"type": "object",
"description": "Optional mapping of error codes to structured JSON Schemas representing error details.",
"additionalProperties": {
"$ref": "https://json-schema.org/draft/2020-12/schema"
}
}
},
"required": ["executedAt", "callableFrom", "returnType"],
"additionalProperties": false
}
}
```

#### Distinguishing `executedAt` and `callableFrom`
* **`executedAt` ("Where is it implemented?")**: Exclusively `"client"` or `"server"`. There is no need for a function that executes on either/both; if client-side and server-side checks share similar intent, they should be declared as two distinct functions (e.g., `validateEmailClient` and `validateEmailServer`).
* **`callableFrom` ("Who can call it?")**: Specifies caller authorization. For example:
* A function with `"executedAt": "server"` and `"callableFrom": "clientOnly"` is a server-side check or query that UI components can invoke.
* A function with `"executedAt": "client"` and `"callableFrom": "serverOnly"` is a local renderer utility that the agent can execute remotely via `callFunction`.

#### Example Catalog Declaration
```json
{
"functions": {
"validateEmailAddress": {
"executedAt": "server",
"callableFrom": "clientOnly",
"args": {
"type": "object",
"properties": {
"email": { "type": "string", "format": "email" }
},
"required": ["email"]
},
"returnType": {
"type": "object",
"properties": {
"isValid": { "type": "boolean" },
"normalizedEmail": { "type": "string" }
},
"required": ["isValid"]
},
"errors": {
"VALIDATION_ERROR": {
"type": "object",
"properties": {
"reason": { "type": "string" }
}
}
}
}
}
}
```

### 3.2 Component Action Binding Schema (`common_types.json`)
Interactive components bind their action slots to either an `event` (agent notification) or a `functionCall` (unified function invocation):

```json
{
"Action": {
"type": "object",
"properties": {
"event": {
"type": "object",
"description": "Dispatches an asynchronous, fire-and-forget event to the agent.",
"properties": {
"name": { "type": "string" },
"context": { "type": "object" }
},
"required": ["name"],
"additionalProperties": false
},
"functionCall": {
"type": "object",
"description": "Executes a catalog-defined function on the client or server.",
"properties": {
"call": { "type": "string" },
"args": { "type": "object" }
},
"required": ["call"],
"additionalProperties": false
}
},
"oneOf": [
{ "required": ["event"] },
{ "required": ["functionCall"] }
]
}
}
```

### 3.3 New Wire-Level Messages (`renderer_to_agent.json` and `agent_to_renderer.json`)
When a component triggers a `functionCall` whose catalog definition declares `"executedAt": "server"` (and whose `"callableFrom"` permits client invocation), the renderer transmits a `callServerFunction` message and awaits a `serverFunctionResponse` message.

#### Renderer-to-Agent: `callServerFunction`
```json
{
"version": "v1.0",
"callServerFunction": {
"functionCallId": "call_srv_8821",
"call": "validateEmailAddress",
"args": {
"email": "user@example.com"
}
}
}
```

- **`functionCallId`** *(string, required)*: A unique identifier generated by the renderer for this invocation.
- **`call`** *(string, required)*: The registered name of the server-side function.
- **`args`** *(object, optional)*: The arguments payload, validated against the function's catalog `args` schema.

#### Agent-to-Renderer: `serverFunctionResponse`
```json
{
"version": "v1.0",
"serverFunctionResponse": {
"functionCallId": "call_srv_8821",
"value": {
"isValid": true,
"normalizedEmail": "user@example.com"
}
}
}
```

- **`functionCallId`** *(string, required)*: Must match the invocation ID from `callServerFunction`.
- **`value`** *(any)*: Present when execution succeeds; validated against the catalog's `returnType` schema.
- **`error`** *(object)*: Present when execution fails; contains `code` (string), `message` (string), and optional structured `details` (any) matching the catalog's `errors` schema.
- Exactly one of `value` or `error` is required (`oneOf`).

---

## 4. Function Execution Semantics: Errors, Nesting, and Exception-Like Short-Circuiting

### 4.1 Error Returns vs. Value Returns
Both client-side and server-side functions can return structured error objects instead of values. Unlike draft v1.0's flat string errors, functions can return rich error payloads:

```json
{
"version": "v1.0",
"serverFunctionResponse": {
"functionCallId": "call_srv_8822",
"error": {
"code": "VALIDATION_ERROR",
"message": "The provided email address is already registered.",
"details": {
"field": "email",
"suggestion": "user+1@example.com"
}
}
}
}
```

### 4.2 Function Nesting and Exception-Like Short-Circuiting
A2UI supports composing functions inside argument expressions. A component can pass the return value of a client-side function into a server-side function, or vice versa:

```json
{
"action": {
"functionCall": {
"call": "checkDeviceAuthorization",
"args": {
"deviceId": {
"call": "getLocalDeviceId",
"args": {}
}
}
}
}
}
```

#### Exception-Like Short-Circuit Evaluation
When evaluating a nested function chain:
1. The renderer evaluates innermost functions first.
2. If any function in the chain (whether client-side or server-side) returns an **`error`**, evaluation of the entire expression is **immediately abandoned**.
3. Downstream functions are never invoked. The error short-circuits the call stack and propagates directly to the calling component or SDK handler, behaving like a thrown exception in traditional programming languages.

```mermaid
sequenceDiagram
participant C as Component
participant R as Renderer (Client Engine)
participant A as Agent (Server Engine)

C->>R: Execute checkDeviceAuthorization(getLocalDeviceId())
R->>R: Evaluate innermost: getLocalDeviceId()
Note over R: getLocalDeviceId() fails with error: "PERMISSION_DENIED"
Note over R,A: Short-circuit: checkDeviceAuthorization is NEVER called
R-->>C: Reject with error: "PERMISSION_DENIED"
```

### 4.3 SDK Ergonomics (`await action.execute(...)`)
Renderer SDKs present both client and server functions through an identical asynchronous Promise API. Whether a function executes locally or across the wire is determined by inspecting the catalog's `executedAt` property:

```typescript
async function handleActionClick() {
try {
// SDK automatically inspects catalog's executedAt property:
// If executedAt: "client" -> executes local handler
// If executedAt: "server" -> sends callServerFunction & awaits serverFunctionResponse
const result = await props.action.execute();
console.log("Function succeeded with validated returnType:", result);
} catch (error) {
// Catches short-circuited errors from either client or server functions
console.error("Function failed:", error.code, error.message, error.details);
}
}
```

---

## 5. Modeling the Four Real-World Use Cases

Below is an analysis of how each use case from the original analysis document is modeled using **Server-Side Functions**, highlighting how this proposal resolves the limitations of draft v1.0 `ActionResponses`.

### 5.1 Button with an Asynchronous Loading Spinner

A "Submit Order" button shows a spinner and disables itself while the server processes the order.

```json
{
"component": "Button",
"id": "submit_order_btn",
"action": {
"functionCall": {
"call": "submitOrder",
"args": {
"cartId": "${/cart/id}"
}
}
}
}
```

* **How it works**: When clicked, the button component's SDK wrapper sets `isSubmitting = true` and calls `submitOrder`. Because `submitOrder` is registered in the catalog with `"executedAt": "server"`, the SDK transmits a `callServerFunction` message. When the agent returns `serverFunctionResponse`, the Promise resolves, and the button resets `isSubmitting = false`.
* **Why this models it better**:
* **No wire-level event pollution**: `action.event` remains reserved for fire-and-forget notifications.
* **Formal return verification**: If `submitOrder` is declared in the catalog to return `{ "orderId": "string" }`, the SDK guarantees that the returned value matches this schema before resolving the Promise.
* **Handling Side Effects**: If submitting the order also clears the shopping cart, the agent transmits an authoritative `updateDataModel` message (`path: "/cart", value: null`) concurrently with or prior to `serverFunctionResponse`. State mutations remain imperative and decoupled from function return values.

---

### 5.2 Form Submission Returning Success Result or Validation Error

A registration form submits profile details. It can succeed or return field-level validation errors.

* **How it works**: The form's submit action invokes the server-side function `registerAccount(formPayload)`.
* **Success**: The server returns `"value": { "accountId": "acc_109" }`. The return value matches the catalog's `returnType` schema.
* **Validation Error**: The server returns an error payload matching the catalog's `errors` schema:
```json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid registration fields.",
"details": {
"fieldErrors": {
"email": "Email is already registered.",
"password": "Must be at least 8 characters."
}
}
}
}
```
* **Why this models it better**:
* **Solves the Informal Schema Contract Problem**: Both the success return value (`value`) and structured error payloads (`error.details`) are explicitly defined in the catalog's JSON Schemas (`returnType` and `errors`). The UI never relies on out-of-band assumptions about backend data shapes.
* **Rich Error Handling**: Because functions support structured error objects, forms can map field-level errors directly to UI input controls without abusing `message` strings or treating validation failures as HTTP 200 successes.

---

### 5.3 Autocomplete and Typeahead Suggestions

An input field queries the server for autocomplete suggestions as the user types.

```json
{
"component": "TextField",
"id": "search_input",
"action": {
"functionCall": {
"call": "fetchSuggestions",
"args": {
"query": "${/search/query}"
}
}
}
}
```

* **How it works**: As the user types `"app"`, the component calls the server-side function `fetchSuggestions({ query: "app" })`. The server returns `"value": ["apple", "application", "approved"]`. The component SDK receives the validated array and updates the dropdown suggestions list.
* **Why this models it better**:
* **Eliminates `responsePath` redundancy**: Rather than having the server automatically overwrite a path in the application's shared `dataModel`, the function returns a pure RPC result directly to the TextField component.
* **Schema Safety**: If `fetchSuggestions` is declared in the catalog to return an array of strings, any accidental backend change that returns incompatible data is caught immediately by catalog validation.
* **Clean Concurrency Control**: Because invocations are managed as Promises by the component SDK, the SDK can cancel or discard stale pending Promises when the user types a new character, preventing out-of-order race conditions without needing specification-level sequence tokens.

---

### 5.4 Synchronous Confirmation Gate (Modal or Dialog Completion)

A "Delete Project" modal requires server acknowledgment before closing or navigating away.

* **How it works**: Clicking "Confirm Delete" invokes the server-side function `deleteProject({ projectId: "prj_55" })`. The modal remains open while awaiting the Promise.
* If the Promise resolves (`value: true`), the modal closes.
* If the Promise rejects (`error`), an inline alert displays the error message inside the modal.
* **Why this models it better**:
* **Clean Separation of Control Flow and State Synchronization**: The function's return value (`true`) serves purely as a control-flow gate to close the dialog. To remove the project card from the background UI, the server transmits a standard `deleteSurface` or `updateDataModel` message. There is no ambiguity between an event `responsePath` side effect and explicit surface teardown messages.
* **Exception Short-Circuiting**: If `deleteProject` is nested within a validation chain (e.g., `deleteProject(verifyAdminToken())`), an error in token verification short-circuits the call before the deletion request ever leaves the client.

---

## 6. Comparison Table and Migration Guide

### 6.1 Architectural Comparison

| Dimension | A2UI v0.9 (Stable) | A2UI v1.0 (Draft ActionResponses) | New Proposal: Server-Side Functions |
| :--- | :--- | :--- | :--- |
| **Event Dispatch (`action.event`)** | Asynchronous, fire-and-forget (`name`, `context`). | Adds `wantResponse`, `actionId`, and `responsePath`. | **Preserves v0.9**: Asynchronous, fire-and-forget. Removes all draft v1.0 response properties. |
| **Execution vs. Call Boundary** | Implies client execution; `callFunction` implies server caller. | Confuses execution and invocation in event flags. | **Orthogonal**: `executedAt` (`client` \| `server`) vs. `callableFrom` (`clientOnly` \| `serverOnly` \| `clientOrServer`). |
| **RPC Mechanism** | None (client-initiated); `callFunction` (server-initiated). | `actionResponse` message keyed by `actionId`. | **`callServerFunction` / `serverFunctionResponse`** keyed by `functionCallId`. |
| **Schema Contract on Returns** | N/A | **Informal**: `value` is untyped `any` on the wire. | **Formal**: Explicit JSON Schema in catalog `returnType` and `errors`. |
| **State Mutation Model** | Explicit `updateDataModel` message. | Redundant: `updateDataModel` + automatic `responsePath` writes. | **Single Responsibility**: `updateDataModel` handles state; functions return pure RPC values. |
| **Error Expression** | Flat string `code` and `message`. | Flat string `code` and `message`. | **Structured**: Supports formal JSON Schema error `details` and short-circuit exception propagation. |

### 6.2 Migration Guide

#### From A2UI v0.9 to this Proposal
1. **Existing Actions Unchanged**: No changes are required for existing `action.event` or local `action.functionCall` components.
2. **Adding Server-Side Functions**: To define a server-side query or check, add a `FunctionDefinition` to the catalog with `"executedAt": "server"`, `"callableFrom": "clientOnly"` (or `"clientOrServer"`), an `args` schema, and a `returnType` schema.
3. **Binding Components**: Replace `action.event` with `action.functionCall` on buttons or inputs that require synchronous server returns or validation checks.

#### From Draft A2UI v1.0 (`wantResponse` / `responsePath`) to this Proposal
1. **Remove `wantResponse` and `responsePath`**: Delete `"wantResponse": true` and `"responsePath"` from all `action.event` blocks.
2. **Convert to Catalog Functions**: For each action that requested a response, declare a corresponding server-side function in the catalog:
- Move the event's `context` schema to the function's `args` schema.
- Define the expected return shape in the function's `returnType` schema.
- Specify `"executedAt": "server"`.
3. **Update Agent Handlers**: Replace agent-side `actionResponse` message generators with `serverFunctionResponse` handlers that respond to `callServerFunction` requests.
4. **Decouple Data Model Updates**: If an action previously relied on `responsePath` to mutate application state across multiple components, replace that side effect with an explicit `updateDataModel` message from the agent.

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。