Skip to content

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

Description

@jacobsimionato

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

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
Loading

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:

{
  "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

{
  "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):

{
  "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

{
  "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

{
  "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:

{
  "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:

{
  "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.
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"
Loading

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:

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.

{
  "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:
      {
        "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.

{
  "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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions