Agents SDK
ag-ui
Serve agents to AG-UI clients from your own server with @nylorun/agents/ag-ui.
import { createAgUiHandler, toNodeListener } from "@nylorun/agents/ag-ui";Serves the Tenant's agents to any AG-UI 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.
createAgUiHandler
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. |
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
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(handler: { fetch }): (req, res) => voidAdapts the handler to node:http. Closing the connection stops reading; it
never cancels the turn.
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 withrunAgent({ resume: [{ interruptId, status: "resolved", payload: { approved: true } }] }). Other interactions answer withpayloadas the response. - Every event that ends a group carries the Runtime cursor as its SSE
id.HttpAgentdoes 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
| 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
- 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.toolsare rejected with400. - 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.