Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b84b83f
docs(spec): @dawn-ai/ag-ui transport-agnostic adapter design
blove Jul 7, 2026
c3de943
docs(plan): @dawn-ai/ag-ui adapter implementation plan
blove Jul 7, 2026
026c373
feat(ag-ui): id factories (deterministic + default)
blove Jul 8, 2026
813a3a1
feat(ag-ui): Dawn chunk input types + guards
blove Jul 8, 2026
c30193e
feat(ag-ui): interrupt/resume translation
blove Jul 8, 2026
fb9acaa
feat(ag-ui): toAguiEvents outbound stream mapper
blove Jul 8, 2026
82b4ef4
feat(ag-ui): fromRunAgentInput inbound mapper
blove Jul 8, 2026
01dae2d
feat(ag-ui): public barrel + interrupt round-trip test
blove Jul 9, 2026
0866b5e
feat(langchain): surface tool invocation run_id on tool_call/tool_res…
blove Jul 9, 2026
aae7a9b
docs(ag-ui): changeset + package README
blove Jul 9, 2026
6bc15b8
fix(cli): support AG-UI smoke integration
blove Jul 9, 2026
f9e23dc
fix(ag-ui): preserve upstream tool ids in translator
blove Jul 9, 2026
8fd4296
docs(spec): canonicalize AG-UI adapter API
blove Jul 10, 2026
2609bed
docs(spec): approve canonical AG-UI design
blove Jul 11, 2026
d210ce1
docs(plan): canonical AG-UI adapter cutover
blove Jul 11, 2026
2933cc3
feat(ag-ui): complete canonical outbound mapping
blove Jul 11, 2026
7732cec
feat(ag-ui): add focused SSE encoder subpath
blove Jul 11, 2026
e43435c
refactor(cli): centralize pending interrupt resolution
blove Jul 12, 2026
263b389
fix(cli): support addressed resume maps in route streams
blove Jul 12, 2026
0bbafd0
refactor(cli): share middleware request projection
blove Jul 13, 2026
1ce0f6b
refactor(cli): route AG-UI through canonical adapter
blove Jul 13, 2026
b9a9bb1
fix(cli): enforce AG-UI runtime boundaries
blove Jul 13, 2026
e29a0fa
docs(ag-ui): document canonical adapter API
blove Jul 13, 2026
d1136bc
test(ag-ui): verify published adapter entrypoints
blove Jul 13, 2026
59c77a1
chore(ag-ui): finalize canonical adapter release
blove Jul 13, 2026
dc802f0
fix(ag-ui): close release audit gaps
blove Jul 13, 2026
59f4999
Merge remote-tracking branch 'origin/main' into blove/ag-ui-adapter
blove Jul 14, 2026
97ab104
test(langchain): satisfy tool id lint
blove Jul 14, 2026
4734c08
fix(chat-web): clean stale Next build output
blove Jul 14, 2026
1410487
fix(testing): pass canonical route resume input
blove Jul 14, 2026
33527d0
fix(pack-check): replace export wildcards safely
blove Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .changeset/ag-ui-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@dawn-ai/ag-ui": patch
"@dawn-ai/cli": patch
"@dawn-ai/langchain": patch
---

Consolidate the existing `@dawn-ai/ag-ui` package as Dawn's pure canonical AG-UI
adapter. Its root API now maps standard `RunAgentInput` requests and Dawn stream
chunks, including standard interrupt outcomes and addressed resume decisions,
while the focused `@dawn-ai/ag-ui/sse` subpath provides event-stream encoding
without taking ownership of a server or runtime transport.

The CLI AG-UI endpoint now uses the canonical adapter, applies the same request
projection as other runtime middleware, and emits canonical events without the
former custom state event shapes. Pending checkpoint interrupts are resolved
through the standard resume contract.

The langchain adapter surfaces each tool invocation's `run_id` on its
`tool_call` and `tool_result` chunks, and the CLI preserves those IDs through
Dawn and AG-UI streams for reliable `toolCallId` correlation. Local in-process
`dawn run` also assigns agent routes a one-shot thread ID so the default SQLite
checkpointer can execute the same route shape supported by `dawn dev`.
136 changes: 92 additions & 44 deletions apps/web/content/docs/ag-ui.mdx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
# AG-UI & Web Clients

`dawn dev` (and the `node` build target in production) serves a second protocol alongside Agent Protocol: [AG-UI](https://github.com/ag-ui-protocol/ag-ui), a wire format built for streaming a run to a browser. `@dawn-ai/ag-ui` is the translation layer between the two — it maps Dawn's internal stream chunks to AG-UI events, and maps an AG-UI `RunAgentInput` back to a Dawn route input. This page covers what that translation guarantees; for the endpoint's request/response shape, see [Dev Server](/docs/dev-server#ag-ui-endpoint).
Dawn exposes [AG-UI](https://github.com/ag-ui-protocol/ag-ui) for browser clients
alongside Agent Protocol. `@dawn-ai/ag-ui` is the pure translation library: it
maps Dawn stream chunks to AG-UI events and maps `RunAgentInput` back to a
Dawn-shaped run input. The package owns no server, transport, or agent runtime.

## Why a second protocol

Agent Protocol (`/threads/:id/runs/wait`, `/runs/stream`) is the durable, thread-oriented interface Dawn harnesses and the CLI use. AG-UI is what UI component libraries — CopilotKit chief among them — expect: a single `POST` that opens an SSE stream of typed UI events (message deltas, tool call lifecycle, state snapshots). Both sit on top of the same route runtime and the same thread; a request through either protocol reads and writes the same checkpointed state in `.dawn/threads.sqlite` and `.dawn/checkpoints.sqlite`. AG-UI is not a separate agent runtime — it's a client-facing view onto the one Dawn already runs.
`dawn dev` and the production runtime started by `dawn start` use that adapter to
serve an SSE endpoint. For its complete request and middleware behavior, see
[Dev Server](/docs/dev-server#ag-ui-endpoint).

## The endpoint

Expand All @@ -14,70 +17,115 @@ content-type: application/json
accept: text/event-stream
```

The URL segment is the URL-encoded `<routeId>#<kind>` assistant id (`/chat#agent` → `/agui/%2Fchat%23agent`). The request body is an AG-UI `RunAgentInput`; Dawn creates the thread when the incoming `threadId` is new, maps the newest user message to the route's input, and streams the translated event sequence back. Full request/response details, including how HITL resume rides on `forwardedProps.command.resume`, live on [Dev Server](/docs/dev-server#ag-ui-endpoint) — this page does not repeat that reference.
The URL segment is the URL-encoded `<routeId>#<kind>` assistant id. For example,
`/chat#agent` becomes `/agui/%2Fchat%23agent`. The request body is an AG-UI
`RunAgentInput`. Dawn creates an unknown thread, invokes the same route runtime
used by Agent Protocol, and streams translated events back as SSE.

## Consuming it from a web UI

`examples/chat/web` is the canonical reference client: a CopilotKit v2 app (`@copilotkit/react-core/v2` + `@copilotkit/runtime`) whose runtime route registers an `HttpAgent` pointed at Dawn's `/agui/{routeId}` endpoint. The browser never talks to Dawn directly — CopilotKit's Next.js runtime route proxies to it, and only the Dawn server holds model credentials.
`examples/chat/web` is the canonical reference client. Its CopilotKit runtime
registers an `HttpAgent` pointed at Dawn's encoded route URL. The browser talks
to the Next.js runtime route, while only the Dawn server holds model credentials.

```text
browser
-> CopilotKit runtime (Next.js route, no API key)
-> HttpAgent -> POST /agui/%2Fchat%23agent (Dawn dev server, holds the model API key)
-> the live route (workspace tools, planning, HITL permissions)
-> AG-UI event stream back to the browser
-> CopilotKit runtime
-> HttpAgent -> POST /agui/%2Fchat%23agent
-> Dawn route runtime
-> AG-UI event stream
```

CopilotKit's hooks (`useAgent`, `useInterrupt`, `CopilotSidebar`) fall back to the literal agent id `"default"` when none is given — the example registers the Dawn route under that key so the components bind without per-component wiring. See `examples/chat/web/README.md` for the full architecture and a live smoke checklist.
CopilotKit's sidebar uses the literal agent id `default` when none is supplied,
so the example registers the Dawn route under that key. See
`examples/chat/web/README.md` for the complete setup and smoke checklist.

## Event contract
## Adapter API

`createAgUiTranslator({ threadId, runId })` from `@dawn-ai/ag-ui` (verified against `packages/ag-ui/src/translate.ts`) emits these AG-UI event types, one Dawn stream chunk at a time:
The root package exports the transport-independent mapping surface:

| Dawn chunk | AG-UI event(s) |
|---|---|
| (stream start) | `RUN_STARTED` |
| `token` / `chunk` | `TEXT_MESSAGE_START` (once) → `TEXT_MESSAGE_CONTENT` per delta |
| `tool_call` | `TEXT_MESSAGE_END` (if a message was open) → `TOOL_CALL_START` → `TOOL_CALL_ARGS` → `TOOL_CALL_END` |
| `tool_result` | `TEXT_MESSAGE_END` (if open) → `TOOL_CALL_RESULT` |
| `plan_update` | `STATE_SNAPSHOT` (merges into a running state object the translator accumulates for the run) |
| `interrupt` | `CUSTOM` with `name: "on_interrupt"` |
| `subagent.*` | `CUSTOM` with `name: "dawn.<chunk.type>"` |
| any other object-shaped chunk | `STATE_SNAPSHOT` (merged into the same running state) |
| `done` (success) | `RUN_FINISHED` with `{ threadId, runId, result }` |
| `done` (chunk output has an `error` field) | `RUN_ERROR` |
| stream ends without a `done` chunk | `RUN_FINISHED` (no result) |
| stream throws | `RUN_ERROR` |
```ts
import { fromRunAgentInput, toAguiEvents } from "@dawn-ai/ag-ui"

A text message is always closed (`TEXT_MESSAGE_END`) before a tool call, tool result, state snapshot, or the terminal event is emitted — the translator never interleaves an open text message with another event type.
const dawnInput = fromRunAgentInput(runAgentInput)

### Threading
for await (const event of toAguiEvents(dawnChunks, { threadId, runId })) {
// Send `event` through the transport chosen by the application.
}
```

AG-UI's `threadId` on `RunAgentInput` **is** the Dawn thread id — there's no separate mapping table. `mapRunInput` (in `packages/ag-ui/src/run-input.ts`) takes the newest `user`-role message from `RunAgentInput.messages` as the turn's input; Dawn keeps the rest of the conversation in its own checkpoint keyed by that thread id, so only the newest turn is forwarded on the wire.
Transport helpers are isolated behind subpaths. An SSE server can encode an
event without expanding the root API:

```ts
import { encodeAgUiSse } from "@dawn-ai/ag-ui/sse"

response.write(encodeAgUiSse(event, request.headers.accept))
```

### Human-in-the-loop resume
### Outbound events

A `kind: "on_interrupt"` `CUSTOM` event carries Dawn's permission or memory interrupt payload (the same shapes documented in [Permissions](/docs/permissions)). The client resumes by sending `forwardedProps.command.resume` on the next `RunAgentInput` — either a bare decision string or `{ decision, interruptId }`:
`toAguiEvents(chunks, context)` is a stateful async generator. It frames Dawn's
implicit assistant text and preserves upstream tool-call ids for result
correlation.

```json
{ "forwardedProps": { "command": { "resume": { "decision": "once", "interruptId": "perm-abc123" } } } }
| Dawn chunk | AG-UI event(s) |
|---|---|
| stream start | `RUN_STARTED` |
| `token` | `TEXT_MESSAGE_START` once, then `TEXT_MESSAGE_CONTENT` per delta |
| `tool_call` | close open text, then `TOOL_CALL_START`, `TOOL_CALL_ARGS`, `TOOL_CALL_END` |
| `tool_result` | close open text, then `TOOL_CALL_RESULT` |
| `interrupt` | terminal `RUN_FINISHED` with `outcome.type: "interrupt"` |
| `done` | terminal `RUN_FINISHED` with `outcome.type: "success"` |
| upstream error | terminal `RUN_ERROR` |

Planning updates, subagent capability events, and unknown chunk types have no v1
mapping and are ignored. The adapter does not invent application state or
protocol-specific custom events.

### Inbound input

`fromRunAgentInput(input)` translates every AG-UI message and preserves the
original request as `raw`, where consumers can inspect tools, state, and context.
The CLI endpoint forwards only the newest user message because Dawn owns the
checkpointed conversation for the thread.

The standard top-level `RunAgentInput.resume` array is preserved as
vocabulary-neutral Dawn resume requests:

```ts
resume?: Array<{
interruptId: string
status: "resolved" | "cancelled"
payload?: unknown
}>
```

### Disconnect and reconnect
For a permission prompt, a resolved payload can carry a Dawn decision such as
`"once"` or `"always"`; cancelling the interrupt maps to denial at the runtime
boundary. Every answer must address one currently pending interrupt.

<Callout type="warn" title="No reconnect or replay support">
The AG-UI handler (`packages/cli/src/lib/dev/agui-handler.ts`) opens one SSE response per `POST` and writes translated events to it as the route streams. There is no server-side buffer, no last-event-id tracking, and no replay mechanism — if the connection drops mid-run, the events already written are gone and the handler has no way to resume that specific stream. This is not a documented guarantee Dawn makes; do not build reconnect-and-catch-up logic that assumes one exists. The route's underlying execution and checkpoint are unaffected by a dropped SSE connection (the run keeps going and the thread state still lands in `.dawn/checkpoints.sqlite`), but the *client* has no protocol-level way to reattach to that in-flight stream — a fresh request starts a new run against the same thread, picking up from the last checkpoint rather than the interrupted stream.
</Callout>
## Threading

## Relation to Agent Protocol
AG-UI's `threadId` is the Dawn thread id; there is no separate mapping table.
AG-UI and Agent Protocol requests execute the same route code and use the same
thread store and checkpointer. A caller can inspect state through Agent Protocol
while a UI drives later turns through AG-UI.

AG-UI and Agent Protocol are two protocols in front of the same runtime, not two runtimes. A route invoked via `/agui/{routeId}` and one invoked via `/threads/:id/runs/stream` execute the same route code, share the same thread store and checkpointer, and can be mixed against the same `threadId` — an operator could inspect a thread's state with `GET /threads/:id/state` (Agent Protocol) while a web client drives it over AG-UI. Choose Agent Protocol for harnesses, CLIs, and server-to-server calls; choose AG-UI when the caller is a UI component library that already speaks it.
## Disconnect and reconnect

<Callout type="warn" title="No reconnect or replay support">
Each POST owns one SSE response. Dawn does not buffer emitted AG-UI events or
support last-event-id replay. If the connection drops, the route can finish
and checkpoint its state, but the client cannot reattach to that in-flight
event stream. A later request against the same thread starts a new run from
the latest checkpoint.
</Callout>

## Related

<RelatedCards items={[
{ href: "/docs/dev-server", title: "Dev Server", subtitle: "the /agui endpoint's request/response reference and Agent Protocol thread endpoints" },
{ href: "/docs/permissions", title: "Permissions", subtitle: "the interrupt payloads carried over CUSTOM{on_interrupt}" },
{ href: "/docs/planning", title: "Planning", subtitle: "the plan_update chunks translated into STATE_SNAPSHOT" },
{ href: "/docs/deployment", title: "Deployment", subtitle: "the node build target, the only one that serves AG-UI in production" },
{ href: "/docs/dev-server", title: "Dev Server", subtitle: "the AG-UI endpoint, resume validation, and middleware behavior" },
{ href: "/docs/permissions", title: "Permissions", subtitle: "the Dawn decisions carried by resolved interrupt payloads" },
{ href: "/docs/deployment", title: "Deployment", subtitle: "building and starting the production runtime" },
]} />
98 changes: 52 additions & 46 deletions apps/web/content/docs/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -705,87 +705,93 @@ export type { ThreadsStore } from "@dawn-ai/sqlite-storage"

## @dawn-ai/ag-ui

AG-UI translation utilities used by `dawn dev` to serve `POST /agui/{routeId}` for CopilotKit and other AG-UI clients. The `{routeId}` URL segment is the URL-encoded Dawn assistant id, such as `%2Fchat%23agent` for `/chat#agent`. Most apps use the endpoint through the CLI, not by importing this package directly.
Pure, transport-agnostic AG-UI translation utilities. The CLI uses the same adapter when `dawn dev` or `dawn start` serves `POST /agui/{routeId}`; generated production entrypoints invoke the exported `serveRuntime()` function directly. The `{routeId}` URL segment is the URL-encoded Dawn assistant id, such as `%2Fchat%23agent` for `/chat#agent`.

```ts
import {
createAgUiTranslator,
encodeAgUiSse,
mapRunInput,
type AgUiEvent,
type DawnStreamChunk,
type MappedRunInput,
type ResumeDecision,
type TranslatorOptions,
createCounterIdFactory,
createDefaultIdFactory,
fromRunAgentInput,
toAguiEvents,
type AguiOutboundEvent,
type DawnAgentStreamChunk,
type DawnInterruptEnvelope,
type DawnMessage,
type DawnResumeRequest,
type DawnRunInput,
type IdFactory,
type RunContext,
type ToAguiOptions,
} from "@dawn-ai/ag-ui"
```

See [Dev Server](/docs/dev-server#ag-ui-endpoint).

### `mapRunInput(input)`
### ID factories

`toAguiEvents` uses `createDefaultIdFactory()` to generate prefixed UUID-based message, tool-call, and tool-result ids when it must synthesize them. `createCounterIdFactory()` produces deterministic counters for tests. Inject either factory, or a custom `IdFactory`, through `options.idFactory`:

```ts
export function mapRunInput(input: RunAgentInput): MappedRunInput
const events = toAguiEvents(chunks, context, {
idFactory: createCounterIdFactory(),
})
```

Maps an AG-UI `RunAgentInput` to Dawn route input. The newest user message becomes Dawn's `{ messages: [...] }` input because Dawn stores conversation history in the checkpoint keyed by `threadId`. Human-in-the-loop resume decisions are read from `forwardedProps.command.resume`.

```ts
export type ResumeDecision = "once" | "always" | "deny"
export type IdFactory = (kind: "message" | "toolCall" | "toolResult") => string

export interface MappedRunInput {
readonly dawnInput: { readonly messages: ReadonlyArray<{ role: string; content: string }> }
readonly resumeDecision?: ResumeDecision
readonly interruptId?: string
export interface ToAguiOptions {
readonly idFactory?: IdFactory
}
```

### `createAgUiTranslator(options)`
### `toAguiEvents(chunks, context)`

```ts
export function createAgUiTranslator(options: TranslatorOptions): AgUiTranslator
export function toAguiEvents(
chunks: AsyncIterable<DawnAgentStreamChunk>,
context: RunContext,
options?: ToAguiOptions,
): AsyncGenerator<AguiOutboundEvent>
```

Creates a per-run translator. Call `begin()` once, pass each Dawn stream chunk to `translate(chunk)`, and call `end()` if the stream completes without a Dawn `done` chunk.
Maps Dawn `token`, `tool_call`, `tool_result`, `interrupt`, and `done` chunks to canonical AG-UI lifecycle, text, and tool events. Upstream tool invocation ids are preserved. Interrupts produce a standard `RUN_FINISHED` interrupt outcome, successful runs produce a success outcome, and upstream failures produce one `RUN_ERROR`.

```ts
export interface TranslatorOptions {
readonly threadId: string
readonly runId: string
}
Planning updates, subagent capability events, and other unknown chunk types have no v1 mapping and are ignored.

export interface AgUiTranslator {
begin(): AgUiEvent[]
translate(chunk: DawnStreamChunk): AgUiEvent[]
end(): AgUiEvent[]
}
```
### `fromRunAgentInput(input)`

The translator maps Dawn token/chunk events to AG-UI text events, tool calls/results to AG-UI tool events, `plan_update` chunks to `STATE_SNAPSHOT`, interrupts to `CUSTOM{name:"on_interrupt"}`, and `subagent.*` chunks to `CUSTOM{name:"dawn.<event>"}`.
```ts
export function fromRunAgentInput(input: RunAgentInput): DawnRunInput
```

### `encodeAgUiSse(event, accept?)`
Maps every AG-UI message structurally and preserves the original input as `raw`. A non-empty top-level `RunAgentInput.resume` array becomes `DawnRunInput.resume` with the same `interruptId`, `status`, and optional `payload` fields:

```ts
export function encodeAgUiSse(event: AgUiEvent, accept?: string): string
export interface DawnRunInput {
readonly messages: DawnMessage[]
readonly resume?: Array<{
readonly interruptId: string
readonly status: "resolved" | "cancelled"
readonly payload?: unknown
}>
readonly raw: RunAgentInput
}
```

Encodes one AG-UI event as an SSE frame using `@ag-ui/encoder`.
The adapter leaves AG-UI `tools`, `state`, and `context` uninterpreted in v1; consumers can read them from `raw`.

### Types
### SSE subpath: `encodeAgUiSse(event, accept?)`

```ts
export type AgUiEvent = BaseEvent
import { encodeAgUiSse } from "@dawn-ai/ag-ui/sse"
```

export interface DawnStreamChunk {
readonly type: string
readonly data?: unknown
readonly name?: string
readonly input?: unknown
readonly output?: unknown
}
```ts
export function encodeAgUiSse(event: BaseEvent, accept?: string): string
```

`DawnStreamChunk` is structural so `@dawn-ai/ag-ui` does not depend on `@dawn-ai/cli`.
Encodes one AG-UI event as an SSE frame using `@ag-ui/encoder`. This transport helper is available only from `@dawn-ai/ag-ui/sse`; the root package remains transport-agnostic.

---

Expand Down
2 changes: 2 additions & 0 deletions apps/web/content/docs/deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ dawn start --host 127.0.0.1 --port 3000

Because it runs the actual Dawn runtime, this is the only deployment path that engages the [execution sandbox](/docs/sandbox), [tool scoping](/docs/tools), and [permissions](/docs/permissions) exactly as configured in `dawn.config.ts`. The `langsmith` target does not run the Dawn runtime and does not engage any of the three.

The built runtime and `dawn start` serve the same Agent Protocol and AG-UI routes as `dawn dev`; generated server entrypoints call the exported `serveRuntime()` function directly. In both modes, `src/middleware.ts` gates Agent Protocol run, wait, and resume execution plus AG-UI route execution, but not thread create, read, delete, or state endpoints. Allowed middleware context reaches tools as `ctx.middleware`.

Select targets explicitly in `dawn.config.ts` if you only want one:

```ts title="dawn.config.ts"
Expand Down
Loading
Loading