NylorunDocsBeta
Get startedBuildRunDeployReferenceMore
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
OptionTypeRequiredDescription
agents(BuiltAgent | BuiltWorkflow | string)[]YesAgents this endpoint may run. Nothing else is reachable.
subject(request: Request) => string | undefined | Promise<…>YesThe signed-in person. undefined answers 401; there is no anonymous mode.
basePathstringNoWhere the handler is mounted, e.g. /api/agui. Default /.
clientAgentsClient | Promise<AgentsClient>NoApplication client. Default createClient().
scopesSubjectScope[]NoDefault ["sessions:own"]. Add "vaults:own" when session() attaches vaults.
session(subject, agentId) => AgUiSessionOptionsNoPer-session info, vaultIds, or credentialSelections. Keep info stable.

Returns an AgUiHandler:

MemberDescription
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 basePathOperation
POST /{agentId}Run: RunAgentInput in, server-sent events out.
GET /{agentId}/threads/{threadId}/messagesHistory as Message[], usable as HttpAgent's initialMessages.
GET /{agentId}/threads/{threadId}/eventsReattach from Last-Event-ID (or ?cursor=); 204 when nothing is left.
POST /{agentId}/threads/{threadId}/cancelCancel the running turn.

toNodeListener

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

Adapts 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 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

StatusCodeWhen
401nonesubject returned undefined.
400invalid_request or noneInvalid RunAgentInput, frontend tools, a non-user last message, or a request the Runtime rejected.
404noneUnknown agent, or a thread the person does not own.
409session_busyThe thread is busy or waiting on an open interrupt.
409session_conflictThe thread's session was opened with other session() parameters.
500subject_invalidThe subject is not a valid Runtime subject.
502runtime_feature_missingThe Runtime lacks transcript-events or subject-headers.
502runtime_incompatibleProtocol mismatch with the Runtime.
502runtime_errorAny 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.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.

On this page