Skip to content

[Feature]: Surface capability registration — connection-time tool handshake for persistent surface connections #2192

Description

@dudo

Pre-flight Checklist

  • I have searched existing issues and this hasn't been reported yet.
  • I am using the latest version AG-UI.

Problem or Motivation

RunAgentInput.tools[] lets a client declare frontend tool schemas per-request. This works well for the request/response path — the client initiates a run and advertises what it can execute.

But it breaks down for surfaces that maintain a persistent SSE connection to receive agent-initiated tool calls (TOOL_CALL_START / TOOL_CALL_ARGS / TOOL_CALL_END events). In this topology:

  • The surface opens a long-lived GET /ag-ui/events SSE stream at boot.
  • The agent can push tool calls at any time — not in response to a client run.
  • There is no RunAgentInput on this path, so there is no place to attach tools[].

The result: the agent has no standard way to know what tools the surface supports unless they are declared statically out-of-band (e.g. a config file that must be kept in sync with the surface code). This is fragile and defeats the purpose of a self-describing protocol.

Concrete example: We run a Slack surface that implements slack.sendMessage, slack.requestHumanApproval, slack.muteThread, etc. The surface connects to the agent at boot via SSE. The agent needs to know about these tools so the LLM can call them. With the current protocol, we had to either:

  • Re-declare the full JSON Schema for every tool in a YAML config file in every agent deployment repo (error-prone, duplicated).
  • Or send tools[] on every POST /ag-ui run — which doesn't cover the agent-push path at all.

Proposed Solution

Standardize a connection-time capability registration mechanism: when a surface establishes its persistent connection to an agent, it pushes its tool manifest once, and the agent registers those tools for the lifetime of the session.

Proposed endpoint convention:

POST /ag-ui/tools
Content-Type: application/json

{
  "tools": [
    { "name": "slack.sendMessage", "description": "...", "parameters": { ... } },
    { "name": "slack.muteThread",  "description": "...", "parameters": {} }
  ]
}

The Tool type already exists in the spec — this proposal adds no new types, only a convention for when and how surfaces push that payload.

Proposed response:

{ "registered": ["slack.sendMessage", "slack.muteThread"] }

Optionally, a SURFACE_CONNECTED event type on the SSE stream could acknowledge registration and signal the surface count to any subscribers.

Surface flow (pseudo-code):

// 1. Register tools with the agent before opening the event stream
await fetch(`${agentUrl}/ag-ui/tools`, {
  method: 'POST',
  body: JSON.stringify({ tools: manifest }),  // Tool[] — existing type
});

// 2. Open the persistent SSE connection
const stream = await fetch(`${agentUrl}/ag-ui/events`, {
  headers: { Accept: 'text/event-stream' },
});

// 3. Dispatch TOOL_CALL_* events to local handlers
for await (const event of stream) {
  dispatch(event);
}

Key Design Points

This is distinct from MCP. MCP tools are agent → tool-server (the agent calls out). Frontend tools here follow the opposite direction: the agent emits TOOL_CALL_* events and the surface executes them locally with credentials and state that never leave the surface process. These are not remotely callable services — they're surface-side capabilities the model can request, not invoke directly.

Registration is idempotent. If a surface reconnects after a drop, it re-registers the same manifest. The agent merges/overwrites.

The Tool type is already sufficient. { name, description, parameters, metadata? } covers everything needed. No new types required.

Scoped to session, not global. Registered tools are available for the lifetime of the surface connection. They don't persist across agent restarts (the surface re-registers on reconnect).

Reference Implementation

We've been running this pattern in production with a Slack surface + a custom agent runtime. The surface registers tools at connect time; the agent creates AI SDK stubs from the manifest and merges them into its live tool registry. Gated tools (human-in-the-loop) work correctly because the registration endpoint applies the same handoff-wrapping as statically configured tools.

The static config (YAML declarations of every tool schema, duplicated across every deployment) was completely eliminated. Adding a new surface tool now means writing one Zod schema in the surface codebase — nothing else changes.

Alternatives Considered

  • Per-request tools[]: Works for client-initiated runs but has no solution for the agent-push path. Also wasteful — re-sending identical schemas on every message.
  • Static config per deployment: What we replaced. Error-prone, duplicated, breaks the "surface is self-contained" principle.
  • MCP server on the surface: Wrong topology. Would require the surface to be publicly reachable from the agent, sharing credentials, and distributed state. The whole point of frontend tools is local execution with local state.
  • metadata on RunAgentInput: No path to cover agent-initiated calls.

Additional Context

Happy to contribute a reference implementation or docs PR once the shape is agreed on. The surface-side pattern is straightforward — the main ask is standardizing the endpoint path and acknowledgement semantics so different agent runtimes handle it consistently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions