@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.
- package/README.md +193 -579
- package/dist/chunk-3A2EPLQG.js +93 -0
- package/dist/chunk-3A2EPLQG.js.map +1 -0
- package/dist/chunk-SQDOTGP2.js +292 -0
- package/dist/chunk-SQDOTGP2.js.map +1 -0
- package/dist/index.d.ts +24 -36
- package/dist/index.js +276 -249
- package/dist/index.js.map +1 -1
- package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +134 -5
- package/dist/plugins/truefoundry-agent-server-adapter/index.js +16 -195
- package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -1
- package/dist/server/index.d.ts +17 -0
- package/dist/server/index.js +9 -0
- package/dist/server/index.js.map +1 -0
- package/dist/{types-VUBzoJT2.d.ts → types-DbNsU075.d.ts} +212 -19
- package/package.json +10 -5
- package/src/{private → draft}/agentSpec.ts +14 -17
- package/src/{private → draft}/draftSessionBridge.ts +1 -2
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.test.ts +1 -1
- package/src/{private → draft}/truefoundryDraftThreadListAdapter.ts +2 -1
- package/src/{private → draft}/useDraftAgentSpec.ts +16 -5
- package/src/draftAgentConfig.test.ts +2 -1
- package/src/index.ts +71 -7
- package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
- package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
- package/src/plugins/truefoundry-agent-server-adapter/index.ts +154 -40
- package/src/plugins/truefoundry-agent-server-adapter/types.ts +137 -0
- package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
- package/src/server/index.ts +23 -0
- package/src/server/types.ts +272 -21
- package/src/truefoundryExtras.ts +4 -1
- package/src/truefoundryOwnedSessionsThreadListAdapter.ts +1 -1
- package/src/types.ts +1 -2
- package/src/useTrueFoundryAgentMessages.test.tsx +261 -1
- package/src/useTrueFoundryAgentMessages.ts +284 -176
- package/src/useTrueFoundryAgentRuntime.ts +31 -21
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
### Checkout the Demo [here](../../examples/assistant-ui-vite)
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
+
---
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
## Quick start
|
|
32
48
|
|
|
33
|
-
|
|
49
|
+
The fastest path is a TrueFoundry gateway server + the runtime hook + your Thread UI.
|
|
34
50
|
|
|
35
51
|
```tsx
|
|
36
|
-
|
|
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
|
-
|
|
80
|
+
That wires streaming turns, tool approvals, ask-user prompts, MCP auth, and sub-agent nesting through the runtime.
|
|
86
81
|
|
|
87
|
-
|
|
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
|
-
|
|
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`.
|
|
88
|
+
`UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. Adapter-specific fields:
|
|
106
89
|
|
|
107
90
|
| Option | Type | Required | Description |
|
|
108
|
-
|
|
109
|
-
| `server` | `AgentChatServer` |
|
|
110
|
-
| `agentName` | `string` |
|
|
111
|
-
| `agent` | `NamedAgentConfig \| DraftAgentConfig` |
|
|
112
|
-
| `initialSessionId` | `string` |
|
|
113
|
-
| `threadId` | `string` |
|
|
114
|
-
| `onThreadIdChange` | `(threadId: string \| undefined) => void` |
|
|
115
|
-
| `onError` | `(error: unknown) => void` |
|
|
116
|
-
| `adapters` | `{ attachments?, speech?, dictation?, voice?, feedback? }` |
|
|
117
|
-
|
|
118
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
---
|
|
145
114
|
|
|
146
|
-
|
|
147
|
-
import { trueFoundryAttachmentAdapter, useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
|
|
115
|
+
## Agent modes
|
|
148
116
|
|
|
149
|
-
|
|
150
|
-
server,
|
|
151
|
-
agentName,
|
|
152
|
-
adapters: { attachments: trueFoundryAttachmentAdapter },
|
|
153
|
-
});
|
|
154
|
-
```
|
|
117
|
+
`agent` / `agentName` control how the runtime sources the agent.
|
|
155
118
|
|
|
156
|
-
|
|
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
|
-
|
|
131
|
+
// Draft
|
|
171
132
|
const runtime = useTrueFoundryAgentRuntime({
|
|
172
133
|
server,
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
142
|
+
---
|
|
185
143
|
|
|
186
|
-
##
|
|
144
|
+
## Attachments
|
|
187
145
|
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
|
|
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
|
|
617
|
-
| `useTrueFoundryToolResponses()` | `{ pending, respond }` | Pending ask-user prompts
|
|
618
|
-
| `useTrueFoundryMcpAuth()` | `{ pending, resume }` | Pending MCP OAuth
|
|
619
|
-
| `useTrueFoundryRespondToToolApproval()` | `(r) => void` | Respond
|
|
620
|
-
| `useTrueFoundryRespondToToolResponse()` | `(r) => void` |
|
|
621
|
-
| `useTrueFoundryResumeMcpAuth()` | `() => Promise<void>` | Resume after MCP OAuth
|
|
622
|
-
| `useTrueFoundryCancel()` | `() => Promise<void>` | Cancel the active turn
|
|
623
|
-
| `
|
|
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
|
|
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
|
-
|
|
205
|
+
---
|
|
637
206
|
|
|
638
|
-
|
|
207
|
+
## Server port (`AgentChatServer`)
|
|
639
208
|
|
|
640
|
-
|
|
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
|
-
|
|
211
|
+
**First-party:** use [`createTrueFoundryChatServer`](#truefoundry-gateway-plugin) (requires `truefoundry-gateway-sdk`).
|
|
643
212
|
|
|
644
|
-
|
|
213
|
+
**Your own backend:**
|
|
645
214
|
|
|
646
|
-
|
|
215
|
+
```tsx
|
|
216
|
+
import type { AgentChatServer } from "@truefoundry/assistant-ui-runtime";
|
|
647
217
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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`
|
|
670
|
-
| `server/events.ts` | Concrete turn/stream event types
|
|
671
|
-
| `
|
|
672
|
-
| `useTrueFoundryAgentRuntime.ts` | Public hook
|
|
673
|
-
| `useTrueFoundryAgentMessages.ts` | Reactive
|
|
674
|
-
| `
|
|
675
|
-
| `
|
|
676
|
-
| `
|
|
677
|
-
| `
|
|
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
|
-
-
|
|
691
|
-
- The runtime never holds credentials —
|
|
692
|
-
-
|
|
693
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
+
### Unsupported assistant-ui features
|
|
707
320
|
|
|
708
|
-
|
|
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
|
-
|
|
329
|
+
---
|
|
711
330
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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).
|