pkg/agui package serves your agents over the AG-UI protocol — the same wire format CopilotKit and @ag-ui/client speak. You get streaming assistant text, reasoning, tool calls, conversation threads, and human-in-the-loop approvals over a single SSE endpoint, plus an optional embedded chat UI so you can talk to an agent in the browser without building a frontend.
There are three layers, from highest to lowest level:
*hastekit.AgentRegistry satisfies the agui.Registry interface (Agent(name) + AgentNames()), so you pass one to any of these. Agents register into a package-global registry when you create them with hastekit.NewAgent, so a zero-value &hastekit.AgentRegistry{} already sees every agent.
Embedded chat UI
The fastest way to interact with an agent.web.Serve mounts a ready-made CopilotKit chat client and the AG-UI protocol endpoints on the same address:
http://localhost:8080. The default UI lists registered agents, shows a sidebar of prior conversations to resume, streams assistant text / reasoning / tool calls live, and renders approval cards inline for human-in-the-loop pauses.
web.Handler(registry, opts...) returns the same surface as an http.Handler for mounting into an existing server. It serves:
The protocol endpoints are mounted under
/api/agui (exported as web.APIPrefix), so external AG-UI clients can target them too — the embedded UIs are just two more consumers.
Protocol handler
If you have your own frontend (CopilotKit’sHttpAgent, raw @ag-ui/client, or a custom SSE consumer), serve just the protocol with agui.NewHandler:
Routes
The threads endpoints power conversation pickers. Listing requires the agent’s persistence adapter to implement
history.ThreadLister (the SDK’s in-memory and file adapters do); when it doesn’t, the listing endpoint answers 501 so clients can hide the picker. See Conversation History for persistence setup.
Single agent
To mount one agent’s run endpoint on an existing mux, useagui.AgentHandler, which runs the agent for every POST regardless of path:
Request body
The run endpoint accepts the canonical AG-UIRunAgentInput body (camelCase field names):
Message follows the AG-UI message shape:
threadId and at least one of messages or an approval decision (see Human-in-the-loop below).
messages and relies on the SDK’s conversation persistence for prior history. Use WithFullHistory if the client owns history and the agent has no persistence.
Grounding context
context entries are flattened into the run’s RunContext so prompt templates can reference them. A {description, value} pair becomes {{Context.<description>}}, while state and forwardedProps are available as {{State.x}} and {{ForwardedProps.y}}.
Event stream
The run endpoint responds withContent-Type: text/event-stream and emits canonical AG-UI events. It always starts with RUN_STARTED and always ends with RUN_FINISHED (or RUN_ERROR), synthesising the terminal event if the stream closes early so clients never hang. The main event types:
Right after
RUN_STARTED, the handler also emits a CUSTOM event carrying the broker streamId, runId, and threadId so a client can correlate the AG-UI run with the SDK’s streaming surface.
Correlation headers
The response sets these headers before the first event:X-Stream-Id— the broker stream id (matcheshandle.StreamID)X-Agui-Run-Id— the AG-UI run id (echoed fromrunId, or generated)X-Agui-Thread-Id— the thread id from the request
Options
All three entry points (web.Serve, web.Handler, agui.NewHandler, agui.AgentHandler) accept the same options:
agui.WithNamespace(ns)— Conversation namespace (default"default").agui.WithSenderID(id)— Sender attribution for messages POSTed by AG-UI clients (default"user").agui.WithFullHistory()— Forward the client’s complete message list into the run instead of only the trailing turn. Use this when the agent has no conversation persistence and the client owns history; with persistence enabled (the SDK default) it would duplicate prior turns in the thread on every POST.agui.WithKeepalive(d)— SSE keep-alive comment interval (default15s).
Human-in-the-loop
When an agent pauses on an approval-gated tool, the run ends with aRUN_FINISHED in a paused state and the client renders an approval card. To resume, the client POSTs back to the same threadId with the decision under forwardedProps.command.resume (CopilotKit’s useInterrupt shape) — no new messages are required:
forwardedProps.hastekitApprovals array with the same {toolCallId, approved} entries is also accepted for simpler clients. The handler maps these decisions onto the SDK’s approval resume flow and continues the paused run.