Pre-flight Checklist
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.
Pre-flight Checklist
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_ENDevents). In this topology:GET /ag-ui/eventsSSE stream at boot.RunAgentInputon this path, so there is no place to attachtools[].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:tools[]on everyPOST /ag-uirun — 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:
The
Tooltype 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_CONNECTEDevent type on the SSE stream could acknowledge registration and signal the surface count to any subscribers.Surface flow (pseudo-code):
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
Tooltype 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
tools[]: Works for client-initiated runs but has no solution for the agent-push path. Also wasteful — re-sending identical schemas on every message.metadataonRunAgentInput: 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.