@nifrajs/ag-ui 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @nifrajs/ag-ui
2
+
3
+ Mount a nifra agent as an [AG-UI](https://docs.ag-ui.com) (Agent-User Interaction protocol) endpoint: one POST accepting `RunAgentInput`, a stream of AG-UI events out over SSE - run lifecycle, step and tool-call events projected from the runner's step evidence, text message events for the output, and a typed continuation for human-in-the-loop resume.
4
+
5
+ A protocol bridge over [`@nifrajs/agent`](../agent) - the request body reuses core's single bounded, prototype-guarded trust boundary, and the model, durable state store, and approval transport are injected per request through the same `ports` factory `mountAgent` uses. Dependency-free beyond the two nifra peers.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @nifrajs/ag-ui @nifrajs/agent
11
+ ```
12
+
13
+ ## Mount
14
+
15
+ ```ts
16
+ import { server } from "@nifrajs/core"
17
+ import { mountAgUI } from "@nifrajs/ag-ui"
18
+ import { agent } from "./agent" // an AgentDefinition
19
+
20
+ const app = server()
21
+ mountAgUI(app, {
22
+ agent,
23
+ ports: (c) => ({
24
+ model: myModelPort, // scope to the request subject
25
+ capabilities: ["search.read"],
26
+ state: myStateStore, // required for resume
27
+ }),
28
+ })
29
+ // POST /agui - RunAgentInput in, AG-UI events out (SSE)
30
+ ```
31
+
32
+ ## Event mapping
33
+
34
+ | Runner | AG-UI events |
35
+ | --- | --- |
36
+ | run starts | `RUN_STARTED`, then `CUSTOM { name: "nifra.turn" }` announcing the turn id |
37
+ | tool evidence | `TOOL_CALL_START` / `TOOL_CALL_END`, then `TOOL_CALL_RESULT` whose `content` is the token-only outcome `{ "outcome": "committed" \| "failed" \| "denied", "code"? }` |
38
+ | model, approval, budget, state evidence | `STEP_STARTED` / `STEP_FINISHED` |
39
+ | model text deltas | live `TEXT_MESSAGE_START` / `_CONTENT` (see streaming below) |
40
+ | model reasoning deltas | `REASONING_START` / `REASONING_MESSAGE_START` / `_CONTENT` / `_END` / `REASONING_END` |
41
+ | model tool-args deltas | provisional `TOOL_CALL_START` + `TOOL_CALL_ARGS`, closed by the call's tool evidence |
42
+ | shared state | `STATE_SNAPSHOT` on the seed and `STATE_DELTA` (RFC 6902 ops) per patch (see below) |
43
+ | model usage deltas | summed per `(provider, model)` into the `usage: TokenUsage[]` array on `RUN_FINISHED` |
44
+ | completed output | `TEXT_MESSAGE_START` / `_CONTENT` / `_END` (unless text was streamed), an optional `MESSAGES_SNAPSHOT` (see below), then `RUN_FINISHED` with `result` and `outcome: { "type": "success" }` |
45
+ | completed with a typed error | `RUN_ERROR` |
46
+ | suspended | `CUSTOM { name: "nifra.pending" }` with the continuation, then `RUN_FINISHED` with `outcome: { "type": "interrupt", "interrupts": [...] }` |
47
+
48
+ Evidence-derived events carry the evidence `timestamp`. The runtime is token-only by design - `TOOL_CALL_RESULT` reports the outcome and error code, never the tool's payload.
49
+
50
+ Agent input is `forwardedProps.input` when present, otherwise the content of the last `role: "user"` message.
51
+
52
+ ## Token streaming
53
+
54
+ A model port that calls the request's optional `onDelta` callback (see `@nifrajs/agent`'s `AgentModelDelta`) streams live:
55
+
56
+ - `{ kind: "text", text }` opens a `TEXT_MESSAGE` and streams each chunk as `TEXT_MESSAGE_CONTENT`. When any text was streamed, the terminal text block is suppressed - the streamed text IS the assistant message, so a streaming port must stream all user-visible text. A non-streaming port keeps the single terminal block; nothing changes for it.
57
+ - `{ kind: "reasoning", text }` streams a `REASONING_*` message.
58
+ - `{ kind: "tool-args", name?, argsText }` opens a provisional `TOOL_CALL_START` (id `<turnId>:call:<n>`) and streams `TOOL_CALL_ARGS`. The tool evidence that follows closes the same call - `TOOL_CALL_END` plus the token-only `TOOL_CALL_RESULT` - instead of opening a second one.
59
+ - `{ kind: "usage", provider?, model?, inputTokens?, ... }` never becomes a frame. The counts are summed per `(provider, model)` across the run's model decisions and stamped as the spec `usage: TokenUsage[]` array on the terminal `RUN_FINISHED` (success and interrupt alike); non-finite figures are ignored.
60
+
61
+ Each stream closes when a different delta kind starts, when tool evidence lands, or at the end of the run. Deltas are transient observer data: a `Last-Event-ID` replay carries evidence frames and the stored, unsuppressed terminal events only - the stored `RUN_FINISHED` keeps its `usage`.
62
+
63
+ ## Shared state
64
+
65
+ The `ports` factory receives `(c, run)`; `run.sharedState` is the run's `AgentSharedState` channel and `run.turnId` the resolved turn id. `body.state` seeds the document and is announced with an upfront `STATE_SNAPSHOT`; every `patch` from a port or tool executor streams as `STATE_DELTA` carrying the RFC 6902 ops. A first patch on an unseeded document announces itself as a `STATE_SNAPSHOT` instead of a delta against a base the client never saw. The channel is per-run and transient - nothing in it is persisted.
66
+
67
+ ```ts
68
+ mountAgUI(app, {
69
+ agent,
70
+ ports: (c, run) => ({
71
+ model: myStreamingModelPort(run.sharedState), // patch progress as the run advances
72
+ capabilities: ["search.read"],
73
+ }),
74
+ })
75
+ ```
76
+
77
+ With `emitMessagesSnapshot: true`, a successful run also emits `MESSAGES_SNAPSHOT` - the request's `messages` echoed back with the assistant output message appended - before `RUN_FINISHED`. It is off by default: the snapshot echoes client message payloads, and terminal events persist to the evidence log when one is configured.
78
+
79
+ ## Human-in-the-loop resume
80
+
81
+ A suspended run finishes with `outcome: { "type": "interrupt", "interrupts": [interrupt] }` where the interrupt is:
82
+
83
+ ```jsonc
84
+ {
85
+ "id": "<turnId>",
86
+ "reason": "approval", // approval | budget | model | cancelled | max_turns
87
+ "toolCallId": "<effectId>", // present for tool suspensions
88
+ "responseSchema": { /* JSON Schema for the resume payload */ },
89
+ "metadata": { "turnId": "…", "continuation": { "kind": "approval", "tool": "…", "effectId": "…" } }
90
+ }
91
+ ```
92
+
93
+ Resume with the AG-UI `resume` array. The runtime keeps state token-only - it holds no interrupt registry - so the payload must echo `metadata.continuation`, with the suspended tool's input replayed in `continuation.input`:
94
+
95
+ ```jsonc
96
+ {
97
+ "threadId": "thread-1", "runId": "run-2", "messages": [],
98
+ "forwardedProps": { "input": { "prompt": "go" } },
99
+ "resume": [{
100
+ "interruptId": "<interrupt id>",
101
+ "status": "resolved", // "cancelled" without an approval resumes as a denial
102
+ "payload": {
103
+ "continuation": { "kind": "approval", "tool": "search.read", "effectId": "…", "input": { "q": "…" } },
104
+ "approval": { "granted": true }
105
+ }
106
+ }]
107
+ }
108
+ ```
109
+
110
+ The pre-interrupt form - the same `{ continuation, approval? }` object in `forwardedProps.resume` plus `forwardedProps.turnId` - keeps working. A resume that fails validation is ignored and the POST starts a fresh run.
111
+
112
+ ## Resumable streams
113
+
114
+ Pass an `evidenceLog` to make a dropped SSE connection resumable. Evidence-derived frames then
115
+ carry SSE `id: <seq>`; a client reconnects by re-POSTing the same body with a `Last-Event-ID`
116
+ header and receives the missed events, rejoining a still-running turn live or replaying the stored
117
+ terminal events - the run is never re-executed.
118
+
119
+ ```ts
120
+ import { createMemoryAgentEvidenceLog } from "@nifrajs/agent/events"
121
+
122
+ mountAgUI(app, { agent, ports, evidenceLog: createMemoryAgentEvidenceLog() })
123
+ ```
124
+
125
+ The in-memory log is the single-process dev/test reference; a durable, multi-process log is an
126
+ adapter implementing the same `AgentEvidenceLog` interface.
127
+
128
+ The seam performs no authentication or authorization - wrap it with your app's route guards and scope every port in `ports(c)` to the caller.
129
+
130
+ ## Docs
131
+
132
+ Part of the **[nifra](https://nifra.dev)** full-stack TypeScript framework - one core, five UI libraries, every runtime. Scaffold a new app with `bun create nifra`.
133
+
134
+ For AI agents, see [`LLM.md`](./LLM.md) and the full corpus
135
+ [`../../llms-full.txt`](../../llms-full.txt).
@@ -0,0 +1,125 @@
1
+ /**
2
+ * `@nifrajs/ag-ui` - mount a nifra agent as an AG-UI (Agent-User Interaction protocol) endpoint.
3
+ *
4
+ * The package is a protocol bridge over the `@nifrajs/agent` runner, mirroring the `mountAgent`
5
+ * seam: one HTTP POST accepting the AG-UI `RunAgentInput` body, the bounded runner in the middle,
6
+ * a stream of AG-UI events out. The request body is read through core's single bounded,
7
+ * proto-guarded framing lane (`@nifrajs/core/edge-kit`); the model, durable state store, and
8
+ * approval transport arrive through a per-request `ports` factory and never live in this package.
9
+ *
10
+ * Wire contract (default path `/agui`):
11
+ * - `POST {path}` body `RunAgentInput` (`threadId` and `runId` required) -> SSE where each `data:`
12
+ * field is one AG-UI event:
13
+ * - `RUN_STARTED`, then `CUSTOM { name: "nifra.turn", value: { turnId } }` announcing the
14
+ * runner's turn id.
15
+ * - Step evidence maps to `TOOL_CALL_START`/`TOOL_CALL_END` for tool effects, followed by a
16
+ * `TOOL_CALL_RESULT` whose `content` is the token-only outcome (`{ outcome, code? }` - the
17
+ * runtime never carries tool payloads), and `STEP_STARTED`/`STEP_FINISHED` for everything
18
+ * else. Evidence-derived events carry the evidence `timestamp`.
19
+ * - A model port that streams (`request.onDelta`) turns into live token frames:
20
+ * `TEXT_MESSAGE_START`/`_CONTENT` for text deltas, `REASONING_*` for reasoning deltas, and
21
+ * `TOOL_CALL_START` + `TOOL_CALL_ARGS` for the tool call being formed - the following tool
22
+ * evidence closes that same call (`TOOL_CALL_END` + `TOOL_CALL_RESULT`) instead of opening a
23
+ * second one. When any text was streamed, the terminal `TEXT_MESSAGE_*` block is suppressed -
24
+ * the streamed text IS the assistant message, so a streaming port must stream all
25
+ * user-visible text. A non-streaming port keeps the single terminal
26
+ * `TEXT_MESSAGE_START`/`_CONTENT`/`_END` block. `usage` deltas are summed per
27
+ * `(provider, model)` across the run's model decisions and stamped as the spec's
28
+ * `usage: TokenUsage[]` array on the terminal `RUN_FINISHED` event (success and interrupt
29
+ * alike) - they never produce a frame of their own. Deltas are transient: a `Last-Event-ID`
30
+ * replay carries evidence frames and the stored (unsuppressed) terminal events only - the
31
+ * stored `RUN_FINISHED` keeps its `usage`.
32
+ * - The `ports` factory receives `(c, run)` where `run.sharedState` is the run's
33
+ * `AgentSharedState` channel: `body.state` seeds the document (announced as an upfront
34
+ * `STATE_SNAPSHOT` when present), every `patch` streams as `STATE_DELTA` (RFC 6902 ops), and
35
+ * a first patch without a seeded document announces itself as a `STATE_SNAPSHOT`.
36
+ * - A completed run ends `RUN_FINISHED` with `result` and `outcome: { type: "success" }`, after
37
+ * an optional `MESSAGES_SNAPSHOT` (see `emitMessagesSnapshot`); a failed one ends `RUN_ERROR`.
38
+ * - A suspended run (approval, budget, model retry, max turns) emits
39
+ * `CUSTOM { name: "nifra.pending", value: { turnId, reason, continuation } }` and then
40
+ * `RUN_FINISHED` with `outcome: { type: "interrupt", interrupts: [{ id: turnId, reason,
41
+ * toolCallId?, responseSchema, metadata: { turnId, continuation } }] }`. Resume with the
42
+ * spec `resume` array - `[{ interruptId: turnId, status, payload: { continuation,
43
+ * approval? } }]` where `payload.continuation` echoes `metadata.continuation` (the runtime is
44
+ * stateless, so the client must send it back) - or with the legacy
45
+ * `forwardedProps.resume` (`{ continuation, approval? }`) plus `forwardedProps.turnId`. A
46
+ * `status: "cancelled"` entry without an explicit approval resumes as a denial. The runtime
47
+ * keeps state token-only, so a suspended tool's input must be replayed in
48
+ * `continuation.input`. A resume that fails validation is ignored and the POST starts a
49
+ * fresh run.
50
+ * - Agent input is `forwardedProps.input` when present, otherwise the content of the last
51
+ * `role: "user"` message.
52
+ * - With an `evidenceLog` configured, evidence-derived frames carry SSE `id: <seq>` and a dropped
53
+ * connection is resumable: re-POST the same body with a `Last-Event-ID` header to replay the
54
+ * missed events and rejoin the still-running turn - the run is never re-executed. Without the
55
+ * log the header is ignored and every POST starts a run.
56
+ * - The seam performs no authentication or authorization. Wrap it with the app's own route guards,
57
+ * and scope the store/model returned by `ports` to the caller.
58
+ */
59
+ import { type AgentDefinition, type AgentPorts, type AgentSharedState } from "@nifrajs/agent";
60
+ import { type AgentEvidenceLog } from "@nifrajs/agent/events";
61
+ import { type ProtoPoisoning } from "@nifrajs/core/edge-kit";
62
+ import type { StandardSchemaV1 } from "@nifrajs/core/schema";
63
+ /** The structural slice of a route context the seam needs. */
64
+ export interface AgUIRouteContext {
65
+ readonly req: Request;
66
+ }
67
+ /** The structural slice of a nifra server `mountAgUI` needs. */
68
+ export interface AgUIMountableApp {
69
+ post(path: string, handler: (c: AgUIRouteContext) => Response | Promise<Response>): unknown;
70
+ }
71
+ /** Per-run context handed to the `ports` factory alongside the route context. */
72
+ export interface AgUIRunContext {
73
+ /** The runner turn id this request resolves to (fresh run or resume target). */
74
+ readonly turnId: string;
75
+ /**
76
+ * The run's shared UI state channel. `body.state` seeds the document; patches applied by any
77
+ * port stream to the client as `STATE_SNAPSHOT`/`STATE_DELTA`. Transient - never persisted.
78
+ */
79
+ readonly sharedState: AgentSharedState;
80
+ }
81
+ export interface MountAgUIOptions<InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1> {
82
+ readonly agent: AgentDefinition<InputSchema, OutputSchema>;
83
+ /** Endpoint path. Default `/agui`. */
84
+ readonly path?: string;
85
+ /** Bounded run length passed to the runner. */
86
+ readonly maxTurns?: number;
87
+ /** Maximum request body size in bytes. Default 1_000_000. */
88
+ readonly maxBodyBytes?: number;
89
+ /** Prototype-poisoning policy for the framing lane. Default `"reject"`. */
90
+ readonly protoPoisoning?: ProtoPoisoning;
91
+ /**
92
+ * Build the ports for one request - the model, durable state store, approval transport,
93
+ * capabilities, and budgets. Receives the route context so the caller can scope every port to
94
+ * the request subject, and the run context carrying the turn id and the shared UI state channel.
95
+ */
96
+ readonly ports: (c: AgUIRouteContext, run: AgUIRunContext) => AgentPorts | Promise<AgentPorts>;
97
+ /**
98
+ * Evidence log making the SSE stream resumable via `Last-Event-ID`. The in-memory reference
99
+ * (`createMemoryAgentEvidenceLog`) is single-process; a durable log is an adapter concern.
100
+ */
101
+ readonly evidenceLog?: AgentEvidenceLog;
102
+ /**
103
+ * Emit a `MESSAGES_SNAPSHOT` (the request's `messages` plus the assistant output message)
104
+ * before `RUN_FINISHED` on a successful completion. Default `false`: the snapshot echoes
105
+ * client-sent message payloads, and terminal events are persisted to the evidence log when one
106
+ * is configured - leave it off unless the client relies on an authoritative snapshot.
107
+ */
108
+ readonly emitMessagesSnapshot?: boolean;
109
+ }
110
+ /** Mount a single agent as an AG-UI `POST {path}` SSE endpoint. */
111
+ export declare function mountAgUI<InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1>(app: AgUIMountableApp, options: MountAgUIOptions<InputSchema, OutputSchema>): void;
112
+ /**
113
+ * One aggregated token-usage entry in the spec's `RUN_FINISHED.usage` array: the model port's
114
+ * `usage` deltas summed per `(provider, model)` pair, matching AG-UI's `TokenUsage` shape.
115
+ */
116
+ export interface AgUIRunUsage {
117
+ readonly provider?: string;
118
+ readonly model?: string;
119
+ readonly inputTokens?: number;
120
+ readonly outputTokens?: number;
121
+ readonly totalTokens?: number;
122
+ readonly reasoningTokens?: number;
123
+ readonly cachedInputTokens?: number;
124
+ }
125
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,EACL,KAAK,eAAe,EAIpB,KAAK,UAAU,EAEf,KAAK,gBAAgB,EAStB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,gBAAgB,EAGtB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAEL,KAAK,cAAc,EAKpB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAQ5D,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACtB;AAED,gEAAgE;AAChE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,EAAE,gBAAgB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAA;CAC5F;AAED,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAA;CACvC;AAED,MAAM,WAAW,gBAAgB,CAC/B,WAAW,SAAS,gBAAgB,EACpC,YAAY,SAAS,gBAAgB;IAErC,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAC,WAAW,EAAE,YAAY,CAAC,CAAA;IAC1D,sCAAsC;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,2EAA2E;IAC3E,QAAQ,CAAC,cAAc,CAAC,EAAE,cAAc,CAAA;IACxC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,cAAc,KAAK,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;IAC9F;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,gBAAgB,CAAA;IACvC;;;;;OAKG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAA;CACxC;AAED,mEAAmE;AACnE,wBAAgB,SAAS,CACvB,WAAW,SAAS,gBAAgB,EACpC,YAAY,SAAS,gBAAgB,EACrC,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,gBAAgB,CAAC,WAAW,EAAE,YAAY,CAAC,GAAG,IAAI,CAgBnF;AAyND;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CACpC"}