# ag-ui (/docs/reference/agents/ag-ui)



```ts
import { createAgUiHandler, toNodeListener } from "@nylorun/agents/ag-ui";
```

Serves the Tenant's agents to any [AG-UI](https://docs.ag-ui.com) client:
`@ag-ui/client`'s `HttpAgent`, CopilotKit, or a desktop renderer. Your server
signs people in; the handler maps each AG-UI thread to one session per person,
agent, and thread. This entry point adds `@ag-ui/core`; nothing else imports
it. Guide: [App server → Serve people](/docs/deploy/app-server#serve-people).

## `createAgUiHandler` [#createaguihandler]

```ts
createAgUiHandler(options: AgUiHandlerOptions): AgUiHandler
```

| Option     | Type                                                      | Required | Description                                                                      |
| ---------- | --------------------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| `agents`   | `(BuiltAgent \| BuiltWorkflow \| string)[]`               | Yes      | Agents this endpoint may run. Nothing else is reachable.                         |
| `subject`  | `(request: Request) => string \| undefined \| Promise<…>` | Yes      | The signed-in person. `undefined` answers `401`; there is no anonymous mode.     |
| `basePath` | `string`                                                  | No       | Where the handler is mounted, e.g. `/api/agui`. Default `/`.                     |
| `client`   | `AgentsClient \| Promise<AgentsClient>`                   | No       | Application client. Default `createClient()`.                                    |
| `scopes`   | `SubjectScope[]`                                          | No       | Default `["sessions:own"]`. Add `"vaults:own"` when `session()` attaches vaults. |
| `session`  | `(subject, agentId) => AgUiSessionOptions`                | No       | Per-session `info`, `vaultIds`, or `credentialSelections`. Keep `info` stable.   |

**Returns** an `AgUiHandler`:

| Member                                 | Description                                               |
| -------------------------------------- | --------------------------------------------------------- |
| `fetch(request)`                       | One bound entry point that routes by method and path.     |
| `run(request, agentId)`                | `POST`: `RunAgentInput` in, AG-UI server-sent events out. |
| `history(request, agentId, threadId)`  | `GET`: the thread's messages as AG-UI `Message[]`.        |
| `reattach(request, agentId, threadId)` | `GET`: the rest of a run after a dropped connection.      |
| `cancel(request, agentId, threadId)`   | `POST`: cancel the thread's running turn.                 |

```ts
const agui = createAgUiHandler({
  basePath: "/api/agui",
  agents: [support],
  subject: async (request) => (await getSignedInUser(request))?.id,
});

createServer(toNodeListener(agui)).listen(3000);
// Next.js, Hono, Bun, Deno, Workers: export or mount agui.fetch directly.
```

## Routes [#routes]

| Method and path under `basePath`             | Operation                                                                  |
| -------------------------------------------- | -------------------------------------------------------------------------- |
| `POST /{agentId}`                            | Run: `RunAgentInput` in, server-sent events out.                           |
| `GET /{agentId}/threads/{threadId}/messages` | History as `Message[]`, usable as `HttpAgent`'s `initialMessages`.         |
| `GET /{agentId}/threads/{threadId}/events`   | Reattach from `Last-Event-ID` (or `?cursor=`); `204` when nothing is left. |
| `POST /{agentId}/threads/{threadId}/cancel`  | Cancel the running turn.                                                   |

## `toNodeListener` [#tonodelistener]

```ts
toNodeListener(handler: { fetch }): (req, res) => void
```

Adapts the handler to `node:http`. Closing the connection stops reading; it
never cancels the turn.

## Behavior [#behavior]

* Each AG-UI message id is the command's idempotency key, so a retried run
  replays the same turn.
* An approval ends the run with an AG-UI interrupt (`reason: "tool_approval"`).
  Resume with
  `runAgent({ resume: [{ interruptId, status: "resolved", payload: { approved: true } }] })`.
  Other interactions answer with `payload` as the response.
* Every event that ends a group carries the Runtime cursor as its SSE `id`.
  `HttpAgent` does not reconnect by itself; call the reattach route with the
  last id you received.
* The handler calls the Runtime as each person with `client.as()`, so the
  Runtime keeps one person out of another's threads.

## Errors [#errors]

| Status | Code                      | When                                                                                                 |
| ------ | ------------------------- | ---------------------------------------------------------------------------------------------------- |
| `401`  | none                      | `subject` returned `undefined`.                                                                      |
| `400`  | `invalid_request` or none | Invalid `RunAgentInput`, frontend tools, a non-user last message, or a request the Runtime rejected. |
| `404`  | none                      | Unknown agent, or a thread the person does not own.                                                  |
| `409`  | `session_busy`            | The thread is busy or waiting on an open interrupt.                                                  |
| `409`  | `session_conflict`        | The thread's session was opened with other `session()` parameters.                                   |
| `500`  | `subject_invalid`         | The subject is not a valid Runtime subject.                                                          |
| `502`  | `runtime_feature_missing` | The Runtime lacks `transcript-events` or `subject-headers`.                                          |
| `502`  | `runtime_incompatible`    | Protocol mismatch with the Runtime.                                                                  |
| `502`  | `runtime_error`           | Any other Runtime failure.                                                                           |

A busy or paused thread may also end a streaming run with `RUN_ERROR` code
`session_busy`.

## Limitations [#limitations]

* Assistant text arrives once per model step; there is no token streaming.
* No reasoning, state, activity, or subagent events. An agent used as a tool
  shows only its result.
* Frontend tools in `RunAgentInput.tools` are rejected with `400`.
* One text part per user message. Earlier messages cannot be edited or
  regenerated.
* Browsers always go through your server. Never let one call the Runtime.
