ag-ui-protocol / ag-ui-protocol/ag-ui
[Feature] Client→server custom action channel (symmetric to server CUSTOM events)
- Langage dominant
- Python
- Étoiles
- 15.9k
- Forks
- 1.4k
- Merge moyen
- 1 j 17 h
- PR mergées (30 j)
- 163
Description
## Summary
AG-UI has a **server→client** escape hatch — `CustomEvent` (`{ name, value }`) emitted from the agent's run stream — but there is **no client→server counterpart**. A UI cannot send an app-specific, out-of-band **action/signal** to the server without going through `RunAgentInput` (i.e. starting an agent run). There's no way to say *"the user just favorited message X"* or *"thumbs-up on this response"* without either invoking the model or bolting on a bespoke side-channel.
## Why this matters
### Many UI actions are out-of-band and must NOT trigger the LLM
Chat UIs are full of side actions that mutate app/session state but have nothing to do with a model turn:
- **Favorite / bookmark** a message
- **Thumbs up / down** (feedback) on a response
- **Pin** / **mark as read** / **archive** a message or conversation
- **Rename** or **close** a conversation
None of these should spin up an agent run, call the model, or append to the transcript. They're small, named, side-effecting signals from the client to the server.
### Today the client has only one channel, forcing bad trade-offs
The only client→server payload is `RunAgentInput` (`messages`, `state`, `context`, `forwardedProps`, `tools`). So an integrator must pick one of:
1. **Carry the action in `forwardedProps`/`state` on a run** — but that **invokes the run pipeline / the model** just to record a favorite, and pollutes the conversation. Wasteful and surprising.
2. **Build a bespoke side-channel** — a custom REST endpoint or a raw WebSocket frame the integrator invents. It works, but it's **off-spec**: every server and every client reinvents the shape, nothing is portable, and it defeats the point of a shared protocol.
### The asymmetry
The server can already push arbitrary named events to the client (`CUSTOM`), and integrations rely on it. The reverse — the client pushing an arbitrary named action to the server — simply doesn't exist. Making the protocol **symmetric** would close the gap once, instead of N bespoke channels.
## Concrete example: "favorite a message"
The user clicks the star on an assistant message. The UI wants to persist that as app/session state (e.g. a `favorite_keys` set keyed by message id) so it survives reloads and can be listed later. There is **no LLM involvement** — it's pure state.
Desired: the client emits something like
```json
{ "name": "favorite_add", "value": { "messageId": "msg_123" } }
```
and the server handles it (updates state, maybe emits a `STATE_DELTA` back) **without** starting a run. Today this has no home in AG-UI.
## Questions / possible directions (for discussion)
1. **A client→server custom action, symmetric to `CustomEvent`** — a lightweight, transport-agnostic `{ name, value }` "action"/"signal" the client can send **outside a run** (a small `POST`, or a frame on the persistent transport), which the server dispatches without invoking the agent. UIs get one portable mechanism for favorite/feedback/pin/etc.
2. **A documented `forwardedProps` action convention on a "no-op run"** — reserved keys + the semantics that such a run only applies the action and returns immediately (no model call). Cheaper spec-wise, but still rides the run pipeline.
3. **Bidirectional custom frames on stateful transports** — when a persistent connection exists (e.g. WebSocket), allow named client→server frames alongside the server→client `CUSTOM` events, so the channel is truly symmetric.
Each has trade-offs (portability vs. schema change vs. transport-specificity). I'd value a maintainer's take on the intended direction.
## Related
- #1915 — the *server-side* counterpart (agent emitting named `CustomEvent`s). This issue is the **opposite direction**: the *client* emitting a named action to the server.
## Offer
Happy to prototype the chosen shape end to end in the ADK middleware (`ag-ui-adk`) and `@ag-ui/client` — e.g. a `sendAction({name, value})` on the client that maps to a documented server handler which mutates state and can answer with a `STATE_DELTA`. Flagging it as design-first since option 1 touches the client API and (potentially) the transport contract.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.