ai-core/ag-ui-protocol
The AG-UI Protocol skill establishes server-side integration between TanStack AI and AG-UI client applications, handling bidirectional streaming via Server-Sent Events. It provides utilities to convert chat streams into SSE responses and to parse and validate incoming AG-UI RunAgentInput requests, including tool merging and message deduplication for agent-based interactions.
git clone --depth 1 https://github.com/TanStack/ai /tmp/ai-core-ag-ui-protocol && cp -r /tmp/ai-core-ag-ui-protocol/packages/ai/skills/ai-core/ag-ui-protocol ~/.claude/skills/ai-core-ag-ui-protocolSKILL.md
# AG-UI Protocol
This skill builds on ai-core. Read it first for critical rules.
## Setup — Server Endpoint Producing AG-UI Events via SSE
```typescript
import { chat, toServerSentEventsResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
export async function POST(request: Request) {
const { messages } = await request.json()
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages,
})
return toServerSentEventsResponse(stream)
}
```
`chat()` returns an `AsyncIterable<StreamChunk>`. Each `StreamChunk` is a
typed AG-UI event (discriminated union on `type`). The `toServerSentEventsResponse()`
helper encodes that iterable into an SSE-formatted `Response` with correct headers.
## Setup — Receiving AG-UI RunAgentInput on the Server
```typescript
import {
chat,
chatParamsFromRequestBody,
mergeAgentTools,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai/adapters'
import { serverTools } from './tools'
export async function POST(req: Request) {
let params
try {
params = await chatParamsFromRequestBody(await req.json())
} catch (error) {
return new Response(
error instanceof Error ? error.message : 'Bad request',
{ status: 400 },
)
}
const stream = chat({
adapter: openaiText('gpt-4o'),
messages: params.messages,
tools: mergeAgentTools(serverTools, params.tools),
})
return toServerSentEventsResponse(stream)
}
```
`chatParamsFromRequestBody` validates the body against `RunAgentInputSchema` from `@ag-ui/core`. `mergeAgentTools` merges the server's tool registry with client-declared tools (server wins on collision; client-only tools become no-execute stubs that flow through the runtime's `ClientToolRequest` path).
`params.messages` is a mixed array of TanStack `UIMessage` anchors (with `parts`) and AG-UI fan-out duplicates (`{role:'tool',...}`, `{role:'reasoning',...}`). The existing `convertMessagesToModelMessages` (called inside `chat()`) handles dedup automatically.
**Wire shape (POST body):** AG-UI `RunAgentInput` — `{threadId, runId, parentRunId?, state, messages, tools, context, forwardedProps}`. The `messages` array carries TanStack `UIMessage` anchors with their canonical `parts` plus AG-UI mirror fields (`content`, `toolCalls`) inline; tool results and thinking parts are additionally emitted as fan-out `{role:'tool',...}` and `{role:'reasoning',...}` entries.
**`forwardedProps` security:** Don't spread it directly into `chat()` — clients could override `adapter`, `model`, `tools`, etc. Always allowlist specific fields.
## Core Patterns
### 1. SSE Format — toServerSentEventsStream / toServerSentEventsResponse
**Wire format:** Each event is `data: <JSON>\n\n`. Stream ends with `data: [DONE]\n\n`.
```typescript
import {
chat,
toServerSentEventsStream,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
// Option A: Get a ReadableStream (manual Response construction)
const abortController = new AbortController()
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages,
abortController,
})
const sseStream = toServerSentEventsStream(stream, abortController)
const response = new Response(sseStream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
},
})
// Option B: Use the helper (sets headers automatically)
const response2 = toServerSentEventsResponse(stream, { abortController })
// Default headers: Content-Type: text/event-stream, Cache-Control: no-cache, Connection: keep-alive
```
**Default response headers set by `toServerSentEventsResponse()`:**
| Header | Value |
| --------------- | ------------------- |
| `Content-Type` | `text/event-stream` |
| `Cache-Control` | `no-cache` |
| `Connection` | `keep-alive` |
Custom headers merge on top (user headers override defaults):
```typescript
toServerSentEventsResponse(stream, {
headers: {
'X-Accel-Buffering': 'no', // Disable nginx buffering
'Cache-Control': 'no-store', // Override default
},
abortController,
})
```
**Error handling:** If the stream throws, a `RUN_ERROR` event is emitted
automatically before the stream closes. If the `abortController` is already
aborted, the error event is suppressed and the stream closes silently.
### 2. HTTP Stream (NDJSON) — toHttpStream / toHttpResponse
**Wire format:** Each event is `<JSON>\n` (newline-delimited JSON, no SSE prefix, no `[DONE]` marker).
```typescript
import { chat, toHttpStream, toHttpResponse } from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
// Option A: Get a ReadableStream
const abortController = new AbortController()
const stream = chat({
adapter: openaiText('gpt-5.2'),
messages,
abortController,
})
const ndjsonStream = toHttpStream(stream, abortController)
const response = new Response(ndjsonStream, {
headers: {
'Content-Type': 'application/x-ndjson',
},
})
// Option B: Use the helper (does NOT set headers automatically)
const response2 = toHttpResponse(stream, { abortController })
// Note: toHttpResponse does NOT set Content-Type automatically.
// You should pass headers explicitly:
const response3 = toHttpResponse(stream, {
headers: { 'Content-Type': 'application/x-ndjson' },
abortController,
})
```
**Client-side pairing:** SSE endpoints are consumed by `fetchServerSentEvents()`.
HTTP stream endpoints are consumed by `fetchHttpStream()`. Both are connection
adapters from `@tanstack/ai-react` (or the framework-specific package).
### 3. AG-UI Event Types Reference
All events extend `BaseAGUIEvent` which carries `type`, `timestamp`, optional
`model`, and optional `rawEvent`.
| Event Type | Description |
| ---------------------- | ------------------------->
Triage all open GitHub issues, PRs, and discussions in the current repository by fanning out up to 100 parallel subagents (one per item), then produce a single prioritized report ranking which PRs to review first, which issues to address first, and which discussions need maintainer attention. Use when the user asks to "triage open issues/PRs", "triage discussions", "prioritize the backlog", "what should I review first", "sweep the repo", or any request to bulk-evaluate open GitHub work and recommend an order.
>
>
>
>
>
>