@truefoundry/assistant-ui-runtime 0.1.6-rc.0 → 0.1.7

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.
Files changed (38) hide show
  1. package/README.md +193 -579
  2. package/dist/chunk-3A2EPLQG.js +93 -0
  3. package/dist/chunk-3A2EPLQG.js.map +1 -0
  4. package/dist/chunk-SQDOTGP2.js +292 -0
  5. package/dist/chunk-SQDOTGP2.js.map +1 -0
  6. package/dist/index.d.ts +24 -36
  7. package/dist/index.js +276 -249
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
  10. package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
  11. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
  12. package/dist/server/index.d.ts +17 -0
  13. package/dist/server/index.js +9 -0
  14. package/dist/server/index.js.map +1 -0
  15. package/dist/{types-VUBzoJT2.d.ts → types-DbNsU075.d.ts} +212 -19
  16. package/package.json +10 -5
  17. package/src/{private → draft}/agentSpec.ts +14 -17
  18. package/src/{private → draft}/draftSessionBridge.ts +1 -2
  19. package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
  20. package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
  21. package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
  22. package/src/draftAgentConfig.test.ts +2 -1
  23. package/src/index.ts +71 -7
  24. package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
  25. package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
  26. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
  27. package/src/plugins/truefoundry-agent-server-adapter/index.ts +154 -40
  28. package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
  29. package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
  30. package/src/server/index.ts +23 -0
  31. package/src/server/types.ts +272 -21
  32. package/src/truefoundryExtras.ts +4 -1
  33. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
  34. package/src/types.ts +1 -2
  35. package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
  36. package/src/useTrueFoundryAgentMessages.ts +284 -176
  37. package/src/useTrueFoundryAgentRuntime.ts +31 -21
  38. /package/src/{private → draft}/useDraftAgentSpec.test.tsx +0 -0
package/README.md CHANGED
@@ -1,72 +1,67 @@
1
1
  # @truefoundry/assistant-ui-runtime
2
2
 
3
- TrueFoundry agent runtime adapter for [assistant-ui](https://www.assistant-ui.com/).
3
+ A headless React runtime that connects [assistant-ui](https://www.assistant-ui.com/) to TrueFoundry agent sessions. Bring your own UI and server — the adapter maps sessions, turns, and streaming events onto assistant-ui's external-store runtime.
4
4
 
5
- Connect assistant-ui components (`Thread`, `Composer`, tool UIs, `ThreadList`) to TrueFoundry agent sessions via `useTrueFoundryAgentRuntime`. The adapter maps gateway turns and streaming events onto assistant-ui's external-store runtime, including multi-agent nesting, tool approvals, ask-user tool responses, MCP auth, batched resume, resumable streams, and composer attachment forwarding on send.
5
+ Built on top of [`@assistant-ui/react`](https://www.assistant-ui.com/), so Thread, Composer, ThreadList, and tool UIs work against a familiar contract out of the box.
6
6
 
7
- ## Requirements
7
+ ### Checkout the Demo [here](../../examples/assistant-ui-vite)
8
8
 
9
- - **React** `^18 || ^19` (peer dependency)
10
- - **`@assistant-ui/react`** in the host app for the UI primitives
11
- - An **`AgentChatServer`** implementation — either use the built-in TrueFoundry gateway plugin (see below) or bring your own
9
+ ---
12
10
 
13
- Bundled deps `@assistant-ui/core` and `@assistant-ui/store` are pulled in automatically.
11
+ ## Table of contents
12
+
13
+ - [Installation](#installation)
14
+ - [Quick start](#quick-start)
15
+ - [`useTrueFoundryAgentRuntime` options](#usetruefoundryagentruntime-options)
16
+ - [Agent modes](#agent-modes)
17
+ - [Attachments](#attachments)
18
+ - [Runtime extras](#runtime-extras)
19
+ - [Server port (`AgentChatServer`)](#server-port-agentchatserver)
20
+ - [TrueFoundry gateway plugin](#truefoundry-gateway-plugin)
21
+ - [Exports](#exports)
22
+ - [Architecture](#architecture-source-map)
23
+ - [License](#license)
24
+
25
+ ---
14
26
 
15
27
  ## Installation
16
28
 
17
29
  ```bash
18
30
  npm install @truefoundry/assistant-ui-runtime @assistant-ui/react
31
+ # or
32
+ pnpm add @truefoundry/assistant-ui-runtime @assistant-ui/react
33
+ # or
34
+ yarn add @truefoundry/assistant-ui-runtime @assistant-ui/react
19
35
  ```
20
36
 
21
- If using the built-in TrueFoundry gateway adapter plugin, also install the gateway SDK:
37
+ Using the built-in TrueFoundry gateway plugin? Also install the gateway SDK:
22
38
 
23
39
  ```bash
24
40
  npm install truefoundry-gateway-sdk
25
41
  ```
26
42
 
27
- ## Quickstart
43
+ **Peers:** React `^18 || ^19`, `@assistant-ui/react` in the host app, and an `AgentChatServer` implementation (plugin or your own). Bundled deps `@assistant-ui/core` and `@assistant-ui/store` are pulled in automatically.
28
44
 
29
- ### 1. Create an `AgentChatServer`
45
+ ---
30
46
 
31
- The runtime accepts any object implementing the `AgentChatServer` interface — a flat, stateless port with methods like `createSession`, `listSessions`, `prepareAndExecuteTurn`, etc. It never reads credentials itself.
47
+ ## Quick start
32
48
 
33
- Using the built-in TrueFoundry gateway plugin (requires `truefoundry-gateway-sdk`):
49
+ The fastest path is a TrueFoundry gateway server + the runtime hook + your Thread UI.
34
50
 
35
51
  ```tsx
36
- import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime/plugins/truefoundry-agent-server-adapter";
52
+ "use client";
53
+
54
+ import { AssistantRuntimeProvider } from "@assistant-ui/react";
55
+ import {
56
+ createTrueFoundryChatServer,
57
+ useTrueFoundryAgentRuntime,
58
+ } from "@truefoundry/assistant-ui-runtime";
59
+ import { Thread } from "@/components/assistant-ui/thread";
37
60
 
38
61
  const server = createTrueFoundryChatServer({
39
62
  apiKey: process.env.TFY_API_KEY!,
40
63
  baseUrl: process.env.TFY_GATEWAY_URL!,
41
64
  });
42
- ```
43
-
44
- Or bring your own implementation against any backend:
45
-
46
- ```tsx
47
- import type { AgentChatServer } from "@truefoundry/assistant-ui-runtime";
48
-
49
- const server: AgentChatServer = {
50
- createSession: async (req) => { /* ... */ },
51
- listSessions: async (req) => { /* ... */ },
52
- getSession: async (req) => { /* ... */ },
53
- updateSession: async (req) => { /* ... */ },
54
- prepareAndExecuteTurn: (req) => { /* return AsyncIterable<TurnStreamData> */ },
55
- cancelSession: async (req) => { /* ... */ },
56
- listTurns: async (req) => { /* ... */ },
57
- getTurn: async (req) => { /* ... */ },
58
- listEvents: async (req) => { /* ... */ },
59
- };
60
- ```
61
-
62
- ### 2. Set up the runtime
63
-
64
- ```tsx
65
- "use client";
66
-
67
- import { AssistantRuntimeProvider } from "@assistant-ui/react";
68
- import { useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
69
- import { Thread } from "@/components/assistant-ui/thread";
70
65
 
71
66
  export function MyAssistant() {
72
67
  const runtime = useTrueFoundryAgentRuntime({
@@ -82,452 +77,94 @@ export function MyAssistant() {
82
77
  }
83
78
  ```
84
79
 
85
- ### 3. Use the component
80
+ That wires streaming turns, tool approvals, ask-user prompts, MCP auth, and sub-agent nesting through the runtime.
86
81
 
87
- ```tsx
88
- import { MyAssistant } from "@/components/MyAssistant";
82
+ > Prefer a drop-in chat UI? Pair with [`@truefoundry/agent-ui-sdk`](https://www.npmjs.com/package/@truefoundry/agent-ui-sdk) (`AgentChat`) instead of a custom Thread.
89
83
 
90
- export default function Home() {
91
- return (
92
- <main className="h-dvh">
93
- <MyAssistant />
94
- </main>
95
- );
96
- }
97
- ```
98
-
99
- ### 4. Set up UI components
100
-
101
- See the assistant-ui [Thread UI guide](https://www.assistant-ui.com/docs/ui/thread) for wiring Thread, composer, and primitives.
84
+ ---
102
85
 
103
86
  ## `useTrueFoundryAgentRuntime` options
104
87
 
105
- `UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. The adapter-specific fields are:
88
+ `UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. Adapter-specific fields:
106
89
 
107
90
  | Option | Type | Required | Description |
108
- |--------|------|----------|-------------|
109
- | `server` | `AgentChatServer` | Yes | Server implementation. The runtime never reads credentials itself. |
110
- | `agentName` | `string` | Yes* | Saved agent to run. *Or use `agent` for draft mode. |
111
- | `agent` | `NamedAgentConfig \| DraftAgentConfig` | No | Discriminated agent source. Overrides `agentName` when set. |
112
- | `initialSessionId` | `string` | No | Pin an existing session once on mount (uncontrolled). |
113
- | `threadId` | `string` | No | Controlled active session id; reactive and URL-syncable. |
114
- | `onThreadIdChange` | `(threadId: string \| undefined) => void` | No | Fires when the active session changes. |
115
- | `onError` | `(error: unknown) => void` | No | Invoked on stream/load/turn errors. |
116
- | `adapters` | `{ attachments?, speech?, dictation?, voice?, feedback? }` | No | Optional assistant-ui adapters forwarded to the runtime. |
117
-
118
- ### Specifying the agent
119
-
120
- Named agent (saved on the gateway):
91
+ | ------ | ---- | -------- | ----------- |
92
+ | `server` | `AgentChatServer` | | Server implementation. The runtime never reads credentials itself. |
93
+ | `agentName` | `string` | ✅\* | Saved agent to run. \*Or use `agent` for draft / explicit named mode. |
94
+ | `agent` | `NamedAgentConfig \| DraftAgentConfig` | | Discriminated agent source. Overrides `agentName` when set. |
95
+ | `initialSessionId` | `string` | | Pin an existing session once on mount (uncontrolled). |
96
+ | `threadId` | `string` | | Controlled active session id; reactive and URL-syncable. |
97
+ | `onThreadIdChange` | `(threadId: string \| undefined) => void` | | Fires when the active session changes. |
98
+ | `onError` | `(error: unknown) => void` | | Invoked on stream / load / turn errors. |
99
+ | `adapters` | `{ attachments?, speech?, dictation?, voice?, feedback? }` | | Optional assistant-ui adapters forwarded to the runtime. |
100
+
101
+ ### Resume / pin a session
121
102
 
122
103
  ```tsx
123
104
  const runtime = useTrueFoundryAgentRuntime({
124
105
  server,
125
106
  agentName: "support-bot",
107
+ initialSessionId: "ses_abc123",
126
108
  });
127
109
  ```
128
110
 
129
- Draft agent (inline spec, mutable):
130
-
131
- ```tsx
132
- const runtime = useTrueFoundryAgentRuntime({
133
- server,
134
- agent: {
135
- mode: "draft",
136
- defaultAgentSpec: { model: { name: "gpt-4o" } },
137
- onAgentSpecChange: (spec) => console.log("spec updated", spec),
138
- },
139
- });
140
- ```
141
-
142
- ### Adding adapters
111
+ Omit `<ThreadList>` if you manage session ids yourself — the session-list adapter only powers that UI. Each gateway session corresponds to one assistant-ui thread.
143
112
 
144
- Attachments are **opt-in**: wire the built-in adapter when you want composer file pick / previews and gateway forwarding on send.
113
+ ---
145
114
 
146
- ```tsx
147
- import { trueFoundryAttachmentAdapter, useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
115
+ ## Agent modes
148
116
 
149
- const runtime = useTrueFoundryAgentRuntime({
150
- server,
151
- agentName,
152
- adapters: { attachments: trueFoundryAttachmentAdapter },
153
- });
154
- ```
117
+ `agent` / `agentName` control how the runtime sources the agent.
155
118
 
156
- ### Resuming a session
119
+ | Mode | Config | Behavior |
120
+ | ---- | ------ | -------- |
121
+ | `named` _(default)_ | `agentName` or `agent: { mode: "named", agentName }` | Runs a saved gateway agent |
122
+ | `draft` | `agent: { mode: "draft", defaultAgentSpec }` | Inline mutable `AgentSpec`, synced via draft sessions |
157
123
 
158
124
  ```tsx
125
+ // Named
159
126
  const runtime = useTrueFoundryAgentRuntime({
160
127
  server,
161
- agentName,
162
- initialSessionId: "ses_abc123",
128
+ agentName: "support-bot",
163
129
  });
164
- ```
165
-
166
- ### Bring your own session ID (no session list)
167
-
168
- Pin the active session with `initialSessionId` (one-time) or controlled `threadId` (reactive, URL-syncable). Omit `<ThreadList>` — the session list adapter only powers that UI.
169
130
 
170
- ```tsx
131
+ // Draft
171
132
  const runtime = useTrueFoundryAgentRuntime({
172
133
  server,
173
- agentName,
174
- initialSessionId: "ses_abc123",
134
+ agent: {
135
+ mode: "draft",
136
+ defaultAgentSpec: { model: { name: "gpt-4o" } },
137
+ onAgentSpecChange: (spec) => console.log("spec updated", spec),
138
+ },
175
139
  });
176
-
177
- return (
178
- <AssistantRuntimeProvider runtime={runtime}>
179
- <Thread />
180
- </AssistantRuntimeProvider>
181
- );
182
140
  ```
183
141
 
184
- Each gateway session corresponds to one assistant-ui thread.
142
+ ---
185
143
 
186
- ## `AgentChatServer` interface
144
+ ## Attachments
187
145
 
188
- The runtime operates against a flat server port no session-with-methods objects, no SDK dependency. Any backend can implement this interface:
146
+ Attachments are **opt-in**. Wire the built-in adapter for composer file pick / previews and gateway forwarding on send.
189
147
 
190
148
  ```tsx
191
- interface AgentChatServer {
192
- createSession(req: CreateSessionRequest): Promise<Session>;
193
- listSessions(req?: ListSessionsParams): Promise<ListResult<Session>>;
194
- getSession(req: { sessionId: string }): Promise<Session>;
195
- updateSession(req: UpdateSessionRequest): Promise<Session>;
196
-
197
- prepareAndExecuteTurn(req: {
198
- sessionId: string;
199
- input?: TurnInputItem[];
200
- previousTurnId?: PreviousTurnIdInput;
201
- abortSignal?: AbortSignal;
202
- headers?: Record<string, string>;
203
- }): AsyncIterable<TurnStreamData>;
204
-
205
- cancelSession(req: { sessionId: string }): Promise<void>;
206
- deleteSession?(req: { sessionId: string }): Promise<void>;
207
-
208
- listTurns(req: { sessionId: string; limit?: number; pageToken?: string; order?: "asc" | "desc" }): Promise<ListResult<Turn>>;
209
- getTurn(req: { sessionId: string; turnId: string }): Promise<Turn>;
210
- listEvents(req: { sessionId: string; pageToken?: string; lastTurnId?: string; limit?: number }): Promise<ListResult<SessionEventItem>>;
211
-
212
- listTurnEvents?(req: { sessionId: string; turnId: string; limit?: number; pageToken?: string; order?: "asc" | "desc" }): Promise<ListResult<TurnEvent>>;
213
- subscribeToTurn?(req: { sessionId: string; turnId: string; afterSequenceNumber?: number; abortSignal?: AbortSignal }): AsyncIterable<TurnStreamData>;
214
- downloadSandboxFile?(sandboxId: string, req: { path: string }): Promise<Blob>;
215
- }
216
- ```
217
-
218
- `ListResult<T>` is `{ data: T[]; nextPageToken?: string }` — flat token-based pagination.
219
-
220
- ### Implementing your own backend
221
-
222
- Below is a fully-typed class implementing `AgentChatServer` against a custom REST API. Use this as a starting point when integrating your own agent backend:
223
-
224
- ```typescript
225
- import type {
226
- AgentChatServer,
227
- CreateSessionRequest,
228
- ListResult,
229
- ListSessionsParams,
230
- Session,
231
- SessionEventItem,
232
- Turn,
233
- TurnEvent,
234
- TurnInputItem,
235
- TurnStreamData,
236
- UpdateSessionRequest,
149
+ import {
150
+ trueFoundryAttachmentAdapter,
151
+ useTrueFoundryAgentRuntime,
237
152
  } from "@truefoundry/assistant-ui-runtime";
238
153
 
239
- class MyAgentChatServer implements AgentChatServer {
240
- constructor(private baseUrl: string, private authToken: string) {}
241
-
242
- private async request<T>(path: string, init?: RequestInit): Promise<T> {
243
- const res = await fetch(`${this.baseUrl}${path}`, {
244
- ...init,
245
- headers: {
246
- "Content-Type": "application/json",
247
- Authorization: `Bearer ${this.authToken}`,
248
- ...init?.headers,
249
- },
250
- });
251
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
252
- return res.json() as Promise<T>;
253
- }
254
-
255
- async createSession(req: CreateSessionRequest): Promise<Session> {
256
- return this.request("/sessions", {
257
- method: "POST",
258
- body: JSON.stringify(req),
259
- });
260
- }
261
-
262
- async listSessions(req?: ListSessionsParams): Promise<ListResult<Session>> {
263
- const params = new URLSearchParams();
264
- if (req?.limit) params.set("limit", String(req.limit));
265
- if (req?.pageToken) params.set("pageToken", req.pageToken);
266
- if (req?.agentName) params.set("agentName", req.agentName);
267
- return this.request(`/sessions?${params}`);
268
- }
269
-
270
- async getSession(req: { sessionId: string }): Promise<Session> {
271
- return this.request(`/sessions/${req.sessionId}`);
272
- }
273
-
274
- async updateSession(req: UpdateSessionRequest): Promise<Session> {
275
- return this.request(`/sessions/${req.sessionId}`, {
276
- method: "PATCH",
277
- body: JSON.stringify({ agentSpec: req.agentSpec, title: req.title }),
278
- });
279
- }
280
-
281
- prepareAndExecuteTurn(req: {
282
- sessionId: string;
283
- input?: TurnInputItem[];
284
- previousTurnId?: string;
285
- abortSignal?: AbortSignal;
286
- }): AsyncIterable<TurnStreamData> {
287
- const self = this;
288
- return {
289
- [Symbol.asyncIterator]() {
290
- return self.streamTurn(req);
291
- },
292
- };
293
- }
294
-
295
- private async *streamTurn(req: {
296
- sessionId: string;
297
- input?: TurnInputItem[];
298
- previousTurnId?: string;
299
- abortSignal?: AbortSignal;
300
- }): AsyncGenerator<TurnStreamData> {
301
- const res = await fetch(`${this.baseUrl}/sessions/${req.sessionId}/turns`, {
302
- method: "POST",
303
- headers: {
304
- "Content-Type": "application/json",
305
- Authorization: `Bearer ${this.authToken}`,
306
- Accept: "text/event-stream",
307
- },
308
- body: JSON.stringify({
309
- input: req.input,
310
- previousTurnId: req.previousTurnId,
311
- }),
312
- signal: req.abortSignal,
313
- });
314
-
315
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
316
- const reader = res.body!.getReader();
317
- const decoder = new TextDecoder();
318
- let buffer = "";
319
-
320
- while (true) {
321
- const { done, value } = await reader.read();
322
- if (done) break;
323
-
324
- buffer += decoder.decode(value, { stream: true });
325
- const lines = buffer.split("\n");
326
- buffer = lines.pop() ?? "";
327
-
328
- for (const line of lines) {
329
- if (!line.startsWith("data: ")) continue;
330
- const json = line.slice(6);
331
- if (json === "[DONE]") return;
332
- yield JSON.parse(json) as TurnStreamData;
333
- }
334
- }
335
- }
336
-
337
- async cancelSession(req: { sessionId: string }): Promise<void> {
338
- await this.request(`/sessions/${req.sessionId}/cancel`, { method: "POST" });
339
- }
340
-
341
- async listTurns(req: {
342
- sessionId: string;
343
- limit?: number;
344
- pageToken?: string;
345
- }): Promise<ListResult<Turn>> {
346
- const params = new URLSearchParams();
347
- if (req.limit) params.set("limit", String(req.limit));
348
- if (req.pageToken) params.set("pageToken", req.pageToken);
349
- return this.request(`/sessions/${req.sessionId}/turns?${params}`);
350
- }
351
-
352
- async getTurn(req: { sessionId: string; turnId: string }): Promise<Turn> {
353
- return this.request(`/sessions/${req.sessionId}/turns/${req.turnId}`);
354
- }
355
-
356
- async listEvents(req: {
357
- sessionId: string;
358
- pageToken?: string;
359
- lastTurnId?: string;
360
- limit?: number;
361
- }): Promise<ListResult<SessionEventItem>> {
362
- const params = new URLSearchParams();
363
- if (req.limit) params.set("limit", String(req.limit));
364
- if (req.pageToken) params.set("pageToken", req.pageToken);
365
- if (req.lastTurnId) params.set("lastTurnId", req.lastTurnId);
366
- return this.request(`/sessions/${req.sessionId}/events?${params}`);
367
- }
368
-
369
- async listTurnEvents(req: {
370
- sessionId: string;
371
- turnId: string;
372
- limit?: number;
373
- pageToken?: string;
374
- }): Promise<ListResult<TurnEvent>> {
375
- const params = new URLSearchParams();
376
- if (req.limit) params.set("limit", String(req.limit));
377
- if (req.pageToken) params.set("pageToken", req.pageToken);
378
- return this.request(
379
- `/sessions/${req.sessionId}/turns/${req.turnId}/events?${params}`,
380
- );
381
- }
382
-
383
- subscribeToTurn(req: {
384
- sessionId: string;
385
- turnId: string;
386
- afterSequenceNumber?: number;
387
- abortSignal?: AbortSignal;
388
- }): AsyncIterable<TurnStreamData> {
389
- const self = this;
390
- return {
391
- [Symbol.asyncIterator]() {
392
- return self.streamSubscribe(req);
393
- },
394
- };
395
- }
396
-
397
- private async *streamSubscribe(req: {
398
- sessionId: string;
399
- turnId: string;
400
- afterSequenceNumber?: number;
401
- abortSignal?: AbortSignal;
402
- }): AsyncGenerator<TurnStreamData> {
403
- const params = new URLSearchParams();
404
- if (req.afterSequenceNumber != null) {
405
- params.set("after", String(req.afterSequenceNumber));
406
- }
407
- const res = await fetch(
408
- `${this.baseUrl}/sessions/${req.sessionId}/turns/${req.turnId}/stream?${params}`,
409
- {
410
- headers: {
411
- Authorization: `Bearer ${this.authToken}`,
412
- Accept: "text/event-stream",
413
- },
414
- signal: req.abortSignal,
415
- },
416
- );
417
-
418
- if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
419
- const reader = res.body!.getReader();
420
- const decoder = new TextDecoder();
421
- let buffer = "";
422
-
423
- while (true) {
424
- const { done, value } = await reader.read();
425
- if (done) break;
426
-
427
- buffer += decoder.decode(value, { stream: true });
428
- const lines = buffer.split("\n");
429
- buffer = lines.pop() ?? "";
430
-
431
- for (const line of lines) {
432
- if (!line.startsWith("data: ")) continue;
433
- const json = line.slice(6);
434
- if (json === "[DONE]") return;
435
- yield JSON.parse(json) as TurnStreamData;
436
- }
437
- }
438
- }
439
- }
440
- ```
441
-
442
- Then use it with the runtime:
443
-
444
- ```tsx
445
- const server = new MyAgentChatServer("https://api.example.com", authToken);
446
-
447
- function App() {
448
- const runtime = useTrueFoundryAgentRuntime({
449
- server,
450
- agentName: "my-agent",
451
- });
452
-
453
- return (
454
- <AssistantRuntimeProvider runtime={runtime}>
455
- <Thread />
456
- </AssistantRuntimeProvider>
457
- );
458
- }
459
- ```
460
-
461
- ## Multi-agent (nested sub-agents)
462
-
463
- TrueFoundry sub-agents are discovered at runtime via `thread.created` and nested under `ToolCallMessagePart.messages`. The gateway sends `title` on `thread.created` / `thread.done`. This runtime copies it onto `metadata.custom.subAgent.title` for the first nested message of each child thread; `name` comes from `agentInfo.name` on the same event.
464
-
465
- Render nested threads with `MessagePartPrimitive.Messages` inside your tool fallback:
466
-
467
- ```tsx
468
- import { MessagePartPrimitive, MessagePrimitive } from "@assistant-ui/react";
469
- import { useAuiState } from "@assistant-ui/store";
470
- import type { TrueFoundryMessageCustomMetadata } from "@truefoundry/assistant-ui-runtime";
471
-
472
- function NestedSubAgentAssistantMessage() {
473
- const custom = useAuiState(
474
- (s) => s.message.metadata.custom as TrueFoundryMessageCustomMetadata,
475
- );
476
- const heading = custom.subAgent?.title ?? custom.subAgent?.name;
477
-
478
- return (
479
- <>
480
- {heading != null && (
481
- <div className="text-sm text-muted-foreground">{heading}</div>
482
- )}
483
- <MessagePrimitive.Root data-role="assistant">
484
- <MessagePrimitive.Parts />
485
- </MessagePrimitive.Root>
486
- </>
487
- );
488
- }
489
-
490
- <MessagePartPrimitive.Messages
491
- components={{
492
- AssistantMessage: NestedSubAgentAssistantMessage,
493
- UserMessage: () => null,
494
- }}
495
- />
154
+ const runtime = useTrueFoundryAgentRuntime({
155
+ server,
156
+ agentName,
157
+ adapters: { attachments: trueFoundryAttachmentAdapter },
158
+ });
496
159
  ```
497
160
 
498
- See the [Multi-Agent Chat UI guide](https://www.assistant-ui.com/docs/tools/multi-agent).
499
-
500
- ## Tool approvals
501
-
502
- When the agent requests approval, the assistant message carries a `requires-action` status and the tool-call part exposes an `approval`. Respond through assistant-ui's tool-approval UI; the adapter converts the decision back into a gateway `user.tool_approval` input and resumes the turn. Approvals on nested sub-agent threads are scoped to the correct `threadId` automatically.
503
-
504
- For custom approval chrome (a thread-level bar instead of per-part buttons), use the extras hooks — see [Runtime extras](#runtime-extras) below.
505
-
506
- ## Ask-user tool responses (`tool.response_required`)
507
-
508
- When the agent calls the client-side `ask_user_question` system tool, the turn ends with `tool.response_required`. The adapter marks the tool call with a human `interrupt` payload (`question`, `options`) resolved from the originating `model.message` via `ToolCallRef.sourceEventId`.
509
-
510
- Collect the user's answer and call `respondToToolResponse({ toolCallId, content })`. The `content` string is free-form (chosen option text, typed answer, etc.).
511
-
512
- ## Batched resume (approvals + responses)
513
-
514
- The gateway requires **every** pending `user.tool_approval` and `user.tool_response` across all threads (root + sub-agents) in a **single** `prepareTurn({ input })` call. The adapter stages decisions locally and only sends when nothing is pending anywhere:
515
-
516
- 1. User resolves all tool approvals (`respondToToolApproval`).
517
- 2. User answers all ask-user prompts (`respondToToolResponse`).
518
- 3. Runtime collects `collectRequiredActionInputs(message)` → one mixed `TurnInputItem[]` → `sendTurn({ inputs })`.
519
-
520
- Do not send partial resumes; the backend rejects incomplete input sets.
521
-
522
- ## MCP auth
523
-
524
- When MCP OAuth is required, the paused assistant message has `metadata.custom.pendingMcpAuth === true` and structured `metadata.custom.mcpServers` (`{ id, name, authUrl }[]`) — both fields are on `TrueFoundryMessageCustomMetadata`. After the user completes OAuth in the browser, call `resumeMcpAuth()` from extras (or `startRun` with `runConfig.custom.resumeMcpAuth: true`).
161
+ ---
525
162
 
526
163
  ## Runtime extras
527
164
 
528
- Typed escape hatch for adapter-specific state and actions same pattern as `@assistant-ui/react-google-adk`. Read pending state with selector hooks; call actions via `trueFoundryExtras.get(aui)` when rendering inside nested sub-agent threads (readonly context).
165
+ Typed escape hatch for adapter-specific state and actions (same pattern as `@assistant-ui/react-google-adk`). Use selector hooks for thread-level UI; use action hooks / `trueFoundryExtras.get(aui)` inside nested sub-agent renderers.
529
166
 
530
- ### Read hooks (thread-level UI)
167
+ ### Approvals, ask-user, MCP auth
531
168
 
532
169
  ```tsx
533
170
  import {
@@ -536,166 +173,142 @@ import {
536
173
  useTrueFoundryMcpAuth,
537
174
  } from "@truefoundry/assistant-ui-runtime";
538
175
 
539
- function ApprovalBar() {
540
- const { pending, respond } = useTrueFoundryApprovals();
541
- if (pending.length === 0) return null;
542
-
543
- const item = pending[0]!;
544
- return (
545
- <div>
546
- <p>Allow {item.toolName}?</p>
547
- <button onClick={() => respond({ approvalId: item.approvalId, approved: true })}>
548
- Allow
549
- </button>
550
- <button onClick={() => respond({ approvalId: item.approvalId, approved: false })}>
551
- Deny
552
- </button>
553
- </div>
554
- );
555
- }
556
-
557
- function AskUserBar() {
558
- const { pending, respond } = useTrueFoundryToolResponses();
559
- if (pending.length === 0) return null;
560
-
561
- const item = pending[0]!;
562
- return (
563
- <div>
564
- <p>{item.question ?? "Answer required"}</p>
565
- {(item.options ?? []).map((option) => (
566
- <button key={option} onClick={() => respond({ toolCallId: item.toolCallId, content: option })}>
567
- {option}
568
- </button>
569
- ))}
570
- </div>
571
- );
572
- }
573
-
574
- function McpAuthContinue() {
575
- const { pending, resume } = useTrueFoundryMcpAuth();
576
- if (pending == null) return null;
577
-
578
- return (
579
- <div>
580
- {pending.mcpServers.map((server) => (
581
- <a key={server.id} href={server.authUrl} target="_blank" rel="noreferrer">
582
- Authorize {server.name}
583
- </a>
584
- ))}
585
- <button onClick={() => void resume()}>Continue</button>
586
- </div>
587
- );
588
- }
176
+ const { pending, respond } = useTrueFoundryApprovals();
177
+ const { pending: asks, respond: answer } = useTrueFoundryToolResponses();
178
+ const { pending: mcp, resume } = useTrueFoundryMcpAuth();
589
179
  ```
590
180
 
591
- ### Action hooks (any render context, including nested sub-agents)
592
-
593
- ```tsx
594
- import {
595
- useTrueFoundryRespondToToolApproval,
596
- useTrueFoundryRespondToToolResponse,
597
- useTrueFoundryResumeMcpAuth,
598
- useTrueFoundryCancel,
599
- } from "@truefoundry/assistant-ui-runtime";
600
-
601
- const respondToApproval = useTrueFoundryRespondToToolApproval();
602
- const respondToResponse = useTrueFoundryRespondToToolResponse();
603
- const resumeMcpAuth = useTrueFoundryResumeMcpAuth();
604
- const cancel = useTrueFoundryCancel();
605
-
606
- respondToApproval({ approvalId, approved: true });
607
- respondToResponse({ toolCallId, content: "Option A" });
608
- void resumeMcpAuth();
609
- void cancel();
610
- ```
181
+ **Batched resume:** the gateway requires **every** pending `user.tool_approval` and `user.tool_response` across all threads (root + sub-agents) in a **single** resume call. The adapter stages decisions locally and only sends when nothing is pending anywhere — partial resumes are rejected.
611
182
 
612
183
  ### Hooks reference
613
184
 
614
185
  | Hook | Returns | Description |
615
- |------|---------|-------------|
616
- | `useTrueFoundryApprovals()` | `{ pending, respond }` | Pending tool approvals plus a respond action. |
617
- | `useTrueFoundryToolResponses()` | `{ pending, respond }` | Pending ask-user prompts plus a respond action. |
618
- | `useTrueFoundryMcpAuth()` | `{ pending, resume }` | Pending MCP OAuth pause state plus a resume action. |
619
- | `useTrueFoundryRespondToToolApproval()` | `(r) => void` | Respond to a tool approval from any render context. |
620
- | `useTrueFoundryRespondToToolResponse()` | `(r) => void` | Respond to an ask-user prompt from any render context. |
621
- | `useTrueFoundryResumeMcpAuth()` | `() => Promise<void>` | Resume after MCP OAuth. |
622
- | `useTrueFoundryCancel()` | `() => Promise<void>` | Cancel the active turn. |
623
- | `useTrueFoundryResetFromTurn()` | n/a | Re-submit a user turn (branch/reset). |
624
- | `useTrueFoundryReload()` | n/a | Retry the current session load. |
625
- | `useTrueFoundryHistoryPagination()` | `{ hasOlderHistory, isLoadingOlderHistory, loadOlderHistory }` | Scroll-up older history. |
186
+ | ---- | ------- | ----------- |
187
+ | `useTrueFoundryApprovals()` | `{ pending, respond }` | Pending tool approvals + respond |
188
+ | `useTrueFoundryToolResponses()` | `{ pending, respond }` | Pending ask-user prompts + respond |
189
+ | `useTrueFoundryMcpAuth()` | `{ pending, resume }` | Pending MCP OAuth + resume |
190
+ | `useTrueFoundryRespondToToolApproval()` | `(r) => void` | Respond from any render context |
191
+ | `useTrueFoundryRespondToToolResponse()` | `(r) => void` | Answer ask-user from any render context |
192
+ | `useTrueFoundryResumeMcpAuth()` | `() => Promise<void>` | Resume after MCP OAuth |
193
+ | `useTrueFoundryCancel()` | `() => Promise<void>` | Cancel the active turn |
194
+ | `useTrueFoundryHistoryPagination()` | `{ hasOlderHistory, isLoadingOlderHistory, loadOlderHistory }` | Scroll-up older history |
626
195
 
627
196
  ### Low-level namespace
628
197
 
629
198
  ```tsx
630
- import { trueFoundryExtras, type TrueFoundryRuntimeExtras } from "@truefoundry/assistant-ui-runtime";
199
+ import { trueFoundryExtras } from "@truefoundry/assistant-ui-runtime";
631
200
 
632
201
  const extras = trueFoundryExtras.use();
633
202
  const pending = trueFoundryExtras.use((e) => e.pendingApprovals, []);
634
203
  ```
635
204
 
636
- ## Cancellation
205
+ ---
637
206
 
638
- `cancel()` calls `server.cancelSession()` and then keeps consuming the active stream: the backend closes the SSE gracefully by emitting a terminal `turn.done` event before ending the stream. No explicit reconcile is performed — the cancelled turn is terminal, and local state reconciles against the authoritative event log on the next session load.
207
+ ## Server port (`AgentChatServer`)
639
208
 
640
- ## Resumable streams
209
+ The runtime never holds credentials. It accepts any object implementing `AgentChatServer` — a flat, stateless port with methods like `createSession`, `listSessions`, `prepareAndExecuteTurn`, etc.
641
210
 
642
- Works out of the box — no server route or Redis store. TrueFoundry persists every turn server-side; on reload or reconnect the runtime calls `subscribeToTurn` and replays events into the fold (idempotent). Running turns are detected on session load and resumed automatically.
211
+ **First-party:** use [`createTrueFoundryChatServer`](#truefoundry-gateway-plugin) (requires `truefoundry-gateway-sdk`).
643
212
 
644
- ## History pagination
213
+ **Your own backend:**
645
214
 
646
- Thread open no longer drains every turn. Initial load:
215
+ ```tsx
216
+ import type { AgentChatServer } from "@truefoundry/assistant-ui-runtime";
647
217
 
648
- 1. `listTurns({ limit: 1 })` once — detect a running turn.
649
- 2. One (or a few) `listEvents` page(s) for the newest complete user-message group.
650
- 3. Clears `isLoading`, then resumes a running turn via subscribe if needed.
218
+ const server: AgentChatServer = {
219
+ createSession: async (req) => {
220
+ /* */
221
+ },
222
+ listSessions: async (req) => {
223
+ /* … */
224
+ },
225
+ getSession: async (req) => {
226
+ /* … */
227
+ },
228
+ updateSession: async (req) => {
229
+ /* … */
230
+ },
231
+ prepareAndExecuteTurn: (req) => {
232
+ /* return AsyncIterable<TurnStreamData> */
233
+ },
234
+ cancelSession: async (req) => {
235
+ /* … */
236
+ },
237
+ listTurns: async (req) => {
238
+ /* … */
239
+ },
240
+ getTurn: async (req) => {
241
+ /* … */
242
+ },
243
+ listEvents: async (req) => {
244
+ /* … */
245
+ },
246
+ };
247
+ ```
248
+
249
+ `ListResult<T>` is `{ data: T[]; nextPageToken?: string }` — flat token-based pagination. Optional methods: `deleteSession`, `listTurnEvents`, `subscribeToTurn`, `downloadSandboxFile`.
250
+
251
+ ---
252
+
253
+ ## TrueFoundry gateway plugin
651
254
 
652
- Older history is opt-in via `useTrueFoundryHistoryPagination()`:
255
+ `createTrueFoundryChatServer` wraps `truefoundry-gateway-sdk` into an `AgentChatServer`. Import from the main entry or the isolated subpath (no React):
653
256
 
654
257
  ```tsx
655
- const { hasOlderHistory, isLoadingOlderHistory, loadOlderHistory } =
656
- useTrueFoundryHistoryPagination();
258
+ import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime";
259
+ // or
260
+ import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime/plugins/truefoundry-agent-server-adapter";
657
261
 
658
- if (hasOlderHistory && !isLoadingOlderHistory) {
659
- void loadOlderHistory();
660
- }
262
+ const server = createTrueFoundryChatServer({
263
+ apiKey: process.env.TFY_API_KEY!,
264
+ baseUrl: process.env.TFY_GATEWAY_URL!,
265
+ });
661
266
  ```
662
267
 
268
+ See the [plugin README](./src/plugins/truefoundry-agent-server-adapter/README.md) for options, named vs draft routing, `Tfy*` types, type guards, and host-spec extension.
269
+
270
+ ---
271
+
272
+ ## Exports
273
+
274
+ | Export | Kind | Purpose |
275
+ | ------ | ---- | ------- |
276
+ | `useTrueFoundryAgentRuntime` | Hook | Root runtime — wires external-store + thread list |
277
+ | `createTrueFoundryChatServer` | Function | TrueFoundry gateway → `AgentChatServer` (also via plugin subpath) |
278
+ | `trueFoundryAttachmentAdapter` | Adapter | Opt-in composer attachments |
279
+ | `trueFoundryExtras` | Namespace | Low-level extras access |
280
+ | `useTrueFoundryApprovals` / `ToolResponses` / `McpAuth` / … | Hooks | Pending state + actions |
281
+ | `AgentChatServer`, `AgentBuilderServer`, `CatalogServer`, `Session`, `Turn`, … | Types | Server ports + DTOs |
282
+ | `TfyAgentSpec`, `TfySession`, `isTfyToolInfo`, … | Types / guards | Gateway-concrete types from the plugin |
283
+ | `NamedAgentConfig`, `DraftAgentConfig` | Types | Agent source discriminants |
284
+
285
+ ---
286
+
663
287
  ## Architecture (source map)
664
288
 
665
289
  For contributors working inside this package. Source lives in `src/`; the published entry point is `dist/index.js` (built by `tsup`).
666
290
 
667
291
  | File | Responsibility |
668
- |------|----------------|
669
- | `server/types.ts` | `AgentChatServer` + `AgentBuilderServer` interfaces, `Session`, `Turn`, `AgentSpec`, pagination types. |
670
- | `server/events.ts` | Concrete turn/stream event types (`ModelMessageEvent`, `TurnCreatedEvent`, etc.). |
671
- | `server/eventUtils.ts` | `isEventDelta()` + `mergeEventDelta()` streaming delta merge logic. |
672
- | `useTrueFoundryAgentRuntime.ts` | Public hook. Wires the external-store runtime, thread-list runtime, adapters, and extras. |
673
- | `useTrueFoundryAgentMessages.ts` | Reactive `SessionSnapshot` store: load, stream ingestion, cancel, resume; derives `messages` via pure projection. |
674
- | `sessionSnapshot.ts` | `SessionSnapshot` shape, required-actions overlay, and immutable wrapper helpers. |
675
- | `truefoundryExtras.ts` | `createRuntimeExtras` namespace and `TrueFoundryRuntimeExtras` type. |
676
- | `hooks.ts` | Consumer hooks read selectors + action callbacks via `.get(aui)`. |
677
- | `collectPending.ts` | Derives `pendingApprovals`, `pendingToolResponses`, `pendingMcpAuth` from messages. |
678
- | `requiredActionInputs.ts` | Combined gate + `collectRequiredActionInputs` for batched resume. |
679
- | `truefoundryThreadListAdapter.ts` | `RemoteThreadListAdapter` — cursor-paginated session list. |
680
- | `convertTurnMessages.ts` | `projectSessionMessages` pure projector; `buildSnapshotFromSessionEvents` history ingest; stream-event aggregation. |
681
- | `foldPeerThreads.ts` | `PeerThreadFoldState` — folds peer/sub-agent threads under their spawning tool call. |
682
- | `streamTurn.ts` | `streamTurnContent` / `resumeTurnStream` generators over `AgentChatServer`. |
683
- | `toolApproval.ts` | Approval state, decision mapping, and `user.tool_approval` input collection. |
684
- | `toolResponse.ts` | Ask-user response state, staging, and `user.tool_response` input collection. |
685
- | `listPages.ts` | `drainListPages` utility for exhausting token-paginated `ListResult` APIs. |
292
+ | ---- | -------------- |
293
+ | `server/types.ts` | `AgentChatServer` + `AgentBuilderServer` + `CatalogServer` (modelCatalog/connectorCatalog/skillCatalog, optional via `AgentUIServerPort.catalog`), `AgentSpec`, session/turn/pagination types |
294
+ | `server/events.ts` | Concrete turn/stream event types |
295
+ | `draft/` | Draft-mode helpers (`mergeAgentSpec`, session bridge, draft thread-list, `useDraftAgentSpec`) |
296
+ | `useTrueFoundryAgentRuntime.ts` | Public hook external-store + thread-list + extras |
297
+ | `useTrueFoundryAgentMessages.ts` | Reactive session snapshot: load, stream, cancel, resume |
298
+ | `truefoundryExtras.ts` / `hooks.ts` | Extras namespace + consumer hooks |
299
+ | `convertTurnMessages.ts` | Pure projection from snapshot thread messages |
300
+ | `foldPeerThreads.ts` | Nest peer/sub-agent threads under spawning tool calls |
301
+ | `plugins/truefoundry-agent-server-adapter/` | Gateway SDK `AgentChatServer` |
686
302
 
687
303
  ### Invariants
688
304
 
689
305
  - One gateway **session** ⇄ one assistant-ui **thread** (`session.id` = thread `remoteId`).
690
- - The root thread id is always `"main"` (`ROOT_THREAD_ID`); sub-agent threads nest beneath their `create_sub_agent` tool call.
691
- - The runtime never holds credentials — it only accepts a pre-built `AgentChatServer`.
692
- - Event/turn types are defined in `src/server/events.ts` (first-party, no external SDK dependency).
693
- - A paused turn's resume `input` must include **all** pending `user.tool_approval` and `user.tool_response` events across every thread in one batch.
694
- - Two agent modes: **named** (`agentName`) and **draft** (`agent: { mode: "draft", defaultAgentSpec }`).
695
-
696
- ## Local development
306
+ - Root thread id is always `"main"` (`ROOT_THREAD_ID`); sub-agents nest under their `create_sub_agent` tool call.
307
+ - The runtime never holds credentials — only a pre-built `AgentChatServer`.
308
+ - A paused turn's resume `input` must include **all** pending approvals + tool responses across every thread in one batch.
309
+ - Two agent modes: **named** (`agentName`) and **draft** (`agent: { mode: "draft", }`).
697
310
 
698
- From this package directory:
311
+ ### Local development
699
312
 
700
313
  ```bash
701
314
  pnpm build # tsup → dist/
@@ -703,17 +316,18 @@ pnpm test # vitest run
703
316
  pnpm typecheck # tsc --noEmit
704
317
  ```
705
318
 
706
- `dist/` is generated output and is gitignored. From the repo root, `pnpm build` builds this package.
319
+ ### Unsupported assistant-ui features
707
320
 
708
- ## Unsupported assistant-ui features
321
+ | Feature | Notes |
322
+ | ------- | ----- |
323
+ | Attachment rendering | Forwarded on send; user bubbles show text only today |
324
+ | Speech / Dictation / Voice | Pass-through only |
325
+ | Feedback | Pass-through only; not persisted to the gateway |
326
+ | Thread rename / archive / delete | Thread-list adapter no-ops |
327
+ | Thread title generation | Returns an empty stream |
709
328
 
710
- Features below are not implemented in this adapter today. Other assistant-ui capabilities (streaming, cancel, tool approval, ask-user responses, MCP auth, sub-agent nesting, resumable streams, reasoning parts) are supported.
329
+ ---
711
330
 
712
- | Feature | Notes |
713
- |---------|-------|
714
- | Attachment rendering | Attachments are forwarded to the gateway on send, but user message bubbles show text only. |
715
- | Built-in `AttachmentAdapter` | Ships as `trueFoundryAttachmentAdapter` (opt-in via `adapters.attachments`). Not applied by default. |
716
- | Speech / Dictation / Voice | Pass-through only. Not shipped. |
717
- | Feedback | Pass-through only. Ratings are not persisted to the gateway. |
718
- | Thread rename / archive / delete | Thread-list adapter no-ops. |
719
- | Thread title generation | Returns an empty stream. |
331
+ ## License
332
+
333
+ See [LICENSE](../../LICENSE).