@truefoundry/assistant-ui-runtime 0.1.5 → 0.1.6

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 (70) hide show
  1. package/README.md +198 -401
  2. package/dist/chunk-3A2EPLQG.js +93 -0
  3. package/dist/chunk-3A2EPLQG.js.map +1 -0
  4. package/dist/chunk-Q2SHKMLM.js +270 -0
  5. package/dist/chunk-Q2SHKMLM.js.map +1 -0
  6. package/dist/index.d.ts +24 -29
  7. package/dist/index.js +264 -237
  8. package/dist/index.js.map +1 -1
  9. package/dist/plugins/truefoundry-agent-server-adapter/index.d.ts +162 -0
  10. package/dist/plugins/truefoundry-agent-server-adapter/index.js +19 -0
  11. package/dist/plugins/truefoundry-agent-server-adapter/index.js.map +1 -0
  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-BfiFf8O1.d.ts +468 -0
  16. package/package.json +19 -6
  17. package/src/askUserQuestion.ts +3 -3
  18. package/src/buildEditedUserMessageContent.test.ts +2 -2
  19. package/src/collectPending.ts +1 -1
  20. package/src/convertTurnMessages.test.ts +141 -196
  21. package/src/convertTurnMessages.ts +130 -76
  22. package/src/createSubAgent.ts +1 -1
  23. package/src/draftAgentConfig.test.ts +26 -29
  24. package/src/extractTurnUserText.ts +1 -1
  25. package/src/foldPeerThreads.test.ts +1 -1
  26. package/src/foldPeerThreads.ts +3 -2
  27. package/src/index.ts +80 -4
  28. package/src/listPages.ts +21 -0
  29. package/src/loadSessionSnapshot.test.ts +9 -8
  30. package/src/loadSessionSnapshot.ts +9 -14
  31. package/src/mcpAuth.ts +6 -3
  32. package/src/messageCustomMetadata.ts +1 -1
  33. package/src/modelMessageContent.ts +1 -1
  34. package/src/modelMessageImageContent.test.ts +1 -1
  35. package/src/modelMessageImageContent.ts +7 -6
  36. package/src/plugins/truefoundry-agent-server-adapter/README.md +178 -0
  37. package/src/plugins/truefoundry-agent-server-adapter/guards.test.ts +113 -0
  38. package/src/plugins/truefoundry-agent-server-adapter/guards.ts +130 -0
  39. package/src/plugins/truefoundry-agent-server-adapter/index.ts +359 -0
  40. package/src/plugins/truefoundry-agent-server-adapter/types.ts +135 -0
  41. package/src/plugins/truefoundry-agent-server-adapter/types.typecheck.ts +164 -0
  42. package/src/private/agentSpec.ts +8 -3
  43. package/src/private/draftSessionBridge.ts +14 -13
  44. package/src/private/truefoundryDraftThreadListAdapter.test.ts +44 -49
  45. package/src/private/truefoundryDraftThreadListAdapter.ts +22 -16
  46. package/src/requiredActionInputs.ts +1 -1
  47. package/src/requiredActionsFromActiveUpdate.test.ts +1 -1
  48. package/src/server/eventUtils.ts +120 -0
  49. package/src/server/events.ts +246 -0
  50. package/src/server/index.ts +66 -0
  51. package/src/server/types.ts +319 -0
  52. package/src/sessionSnapshot.ts +1 -1
  53. package/src/sessions.ts +5 -21
  54. package/src/streamTurn.test.ts +172 -155
  55. package/src/streamTurn.ts +51 -48
  56. package/src/toolApproval.ts +4 -4
  57. package/src/toolResponse.ts +4 -4
  58. package/src/truefoundryExtras.ts +1 -1
  59. package/src/truefoundryOwnedSessionsThreadListAdapter.test.ts +26 -29
  60. package/src/truefoundryOwnedSessionsThreadListAdapter.ts +18 -23
  61. package/src/truefoundryThreadListAdapter.test.ts +16 -18
  62. package/src/truefoundryThreadListAdapter.ts +7 -7
  63. package/src/turnEventHelpers.ts +1 -1
  64. package/src/types.ts +2 -16
  65. package/src/useTrueFoundryAgentMessages.test.tsx +38 -70
  66. package/src/useTrueFoundryAgentMessages.ts +32 -44
  67. package/src/useTrueFoundryAgentRuntime.ts +11 -28
  68. package/src/private/bindDraftAgentSession.test.ts +0 -54
  69. package/src/private/bindDraftAgentSession.ts +0 -28
  70. package/src/private/getGatewayFromPrivateClient.ts +0 -13
package/README.md CHANGED
@@ -1,55 +1,72 @@
1
- # truefoundry-agents-assistant-ui-runtime
1
+ # @truefoundry/assistant-ui-runtime
2
2
 
3
- TrueFoundry Gateway 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
- - **`truefoundry-gateway-sdk`** (peer dependency) — provides `AgentSessionClient` and agent types
11
- - **`@assistant-ui/react`** in the host app for the UI primitives
12
- - Bundled deps `@assistant-ui/core` and `@assistant-ui/store` are pulled in automatically
9
+ ---
10
+
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
+ ---
13
26
 
14
27
  ## Installation
15
28
 
16
29
  ```bash
17
- npm install @assistant-ui/react truefoundry-agents-assistant-ui-runtime truefoundry-gateway-sdk@^0.1.0-rc.1
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
18
35
  ```
19
36
 
20
- ## Quickstart
37
+ Using the built-in TrueFoundry gateway plugin? Also install the gateway SDK:
21
38
 
22
- ### 1. Create an `AgentSessionClient`
39
+ ```bash
40
+ npm install truefoundry-gateway-sdk
41
+ ```
23
42
 
24
- Construct the client in your app (server module, proxy route, or demo). The runtime only accepts a pre-built client it does **not** read API keys or gateway URLs itself.
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.
25
44
 
26
- ```tsx
27
- import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
45
+ ---
28
46
 
29
- const client = new AgentSessionClient({
30
- apiKey: process.env.TFY_API_KEY!,
31
- environment: process.env.TFY_GATEWAY_URL!, // https://gateway.truefoundry.ai/<tenant>
32
- });
33
- ```
34
-
35
- For production, point `fetch` or `auth` at your own backend proxy so secrets never reach the browser.
47
+ ## Quick start
36
48
 
37
- ### 2. Set up the client runtime
49
+ The fastest path is a TrueFoundry gateway server + the runtime hook + your Thread UI.
38
50
 
39
51
  ```tsx
40
52
  "use client";
41
53
 
42
54
  import { AssistantRuntimeProvider } from "@assistant-ui/react";
43
- import { useTrueFoundryAgentRuntime } from "truefoundry-agents-assistant-ui-runtime";
55
+ import {
56
+ createTrueFoundryChatServer,
57
+ useTrueFoundryAgentRuntime,
58
+ } from "@truefoundry/assistant-ui-runtime";
44
59
  import { Thread } from "@/components/assistant-ui/thread";
45
60
 
46
- const AGENT_NAME = process.env.TFY_AGENT_NAME!;
47
- const client = new AgentSessionClient({ /* ... */ });
61
+ const server = createTrueFoundryChatServer({
62
+ apiKey: process.env.TFY_API_KEY!,
63
+ baseUrl: process.env.TFY_GATEWAY_URL!,
64
+ });
48
65
 
49
66
  export function MyAssistant() {
50
67
  const runtime = useTrueFoundryAgentRuntime({
51
- client,
52
- agentName: AGENT_NAME,
68
+ server,
69
+ agentName: "support-bot",
53
70
  });
54
71
 
55
72
  return (
@@ -60,445 +77,237 @@ export function MyAssistant() {
60
77
  }
61
78
  ```
62
79
 
63
- ### 3. Use the component
80
+ That wires streaming turns, tool approvals, ask-user prompts, MCP auth, and sub-agent nesting through the runtime.
64
81
 
65
- ```tsx
66
- 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.
67
83
 
68
- export default function Home() {
69
- return (
70
- <main className="h-dvh">
71
- <MyAssistant />
72
- </main>
73
- );
74
- }
75
- ```
76
-
77
- ### 4. Set up UI components
78
-
79
- See the assistant-ui [Thread UI guide](https://www.assistant-ui.com/docs/ui/thread) for wiring Thread, composer, and primitives.
84
+ ---
80
85
 
81
86
  ## `useTrueFoundryAgentRuntime` options
82
87
 
83
- `UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. The adapter-specific fields are:
88
+ `UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. Adapter-specific fields:
84
89
 
85
90
  | Option | Type | Required | Description |
86
- |--------|------|----------|-------------|
87
- | `client` | `AgentSessionClient` | Yes | Pre-built gateway client. The runtime never reads credentials itself. |
88
- | `agentName` | `string` | Yes | Saved agent to run (gateway agent name). |
89
- | `initialSessionId` | `string` | No | Pin an existing session once on mount (uncontrolled). |
90
- | `threadId` | `string` | No | Controlled active session id; reactive and URL-syncable. |
91
- | `onThreadIdChange` | `(threadId: string \| undefined) => void` | No | Fires when the active session changes. |
92
- | `onError` | `(error: unknown) => void` | No | Invoked on stream/load/turn errors. |
93
- | `adapters` | `{ attachments?, speech?, dictation?, voice?, feedback? }` | No | Optional assistant-ui adapters forwarded to the runtime. See [Unsupported assistant-ui features](#unsupported-assistant-ui-features). |
94
-
95
- ### Specifying the agent
96
-
97
- Pass the saved agent name as `agentName`:
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
98
102
 
99
103
  ```tsx
100
104
  const runtime = useTrueFoundryAgentRuntime({
101
- client,
105
+ server,
102
106
  agentName: "support-bot",
107
+ initialSessionId: "ses_abc123",
103
108
  });
104
109
  ```
105
110
 
106
- ### 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.
107
112
 
108
- Pass optional assistant-ui adapters through `adapters`. Attachments are **opt-in**: wire the built-in adapter when you want composer file pick / previews and gateway forwarding on send.
113
+ ---
109
114
 
110
- ```tsx
111
- import { trueFoundryAttachmentAdapter, useTrueFoundryAgentRuntime } from "truefoundry-agents-assistant-ui-runtime";
115
+ ## Agent modes
112
116
 
113
- const runtime = useTrueFoundryAgentRuntime({
114
- client,
115
- agentName,
116
- adapters: { attachments: trueFoundryAttachmentAdapter },
117
- });
118
- ```
117
+ `agent` / `agentName` control how the runtime sources the agent.
119
118
 
120
- Other adapters (speech, feedback, etc.) follow the same pattern:
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 |
121
123
 
122
124
  ```tsx
125
+ // Named
123
126
  const runtime = useTrueFoundryAgentRuntime({
124
- client,
125
- agentName,
126
- adapters: { attachments: trueFoundryAttachmentAdapter, speech, feedback },
127
+ server,
128
+ agentName: "support-bot",
127
129
  });
128
- ```
129
-
130
- ### Resuming a session
131
130
 
132
- ```tsx
131
+ // Draft
133
132
  const runtime = useTrueFoundryAgentRuntime({
134
- client,
135
- agentName,
136
- initialSessionId: "ses_abc123",
133
+ server,
134
+ agent: {
135
+ mode: "draft",
136
+ defaultAgentSpec: { model: { name: "gpt-4o" } },
137
+ onAgentSpecChange: (spec) => console.log("spec updated", spec),
138
+ },
137
139
  });
138
140
  ```
139
141
 
140
- ### Bring your own session ID (no session list)
142
+ ---
141
143
 
142
- You can drive a single, externally-owned session without rendering `<ThreadList>`. Pin the active session with `initialSessionId` (one-time) or controlled `threadId` (reactive, URL-syncable). Omit `<ThreadList>` — the session list adapter only powers that UI.
144
+ ## Attachments
145
+
146
+ Attachments are **opt-in**. Wire the built-in adapter for composer file pick / previews and gateway forwarding on send.
143
147
 
144
148
  ```tsx
149
+ import {
150
+ trueFoundryAttachmentAdapter,
151
+ useTrueFoundryAgentRuntime,
152
+ } from "@truefoundry/assistant-ui-runtime";
153
+
145
154
  const runtime = useTrueFoundryAgentRuntime({
146
- client,
155
+ server,
147
156
  agentName,
148
- initialSessionId: "ses_abc123",
157
+ adapters: { attachments: trueFoundryAttachmentAdapter },
149
158
  });
150
-
151
- return (
152
- <AssistantRuntimeProvider runtime={runtime}>
153
- <Thread />
154
- </AssistantRuntimeProvider>
155
- );
156
- ```
157
-
158
- Each gateway session corresponds to one assistant-ui thread.
159
-
160
- ## Multi-agent (nested sub-agents)
161
-
162
- TrueFoundry sub-agents are discovered at runtime via `thread.created` and nested under `ToolCallMessagePart.messages`. The gateway always sends `title` as a required `string` on `thread.created` / `thread.done` (never `null`). 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.
163
-
164
- Render nested threads with `MessagePartPrimitive.Messages` inside your tool fallback (recommended — no per-tool registration):
165
-
166
- ```tsx
167
- import { MessagePartPrimitive, MessagePrimitive } from "@assistant-ui/react";
168
- import { useAuiState } from "@assistant-ui/store";
169
- import type { TrueFoundryMessageCustomMetadata } from "truefoundry-agents-assistant-ui-runtime";
170
-
171
- function NestedSubAgentAssistantMessage() {
172
- const custom = useAuiState(
173
- (s) => s.message.metadata.custom as TrueFoundryMessageCustomMetadata,
174
- );
175
- const heading = custom.subAgent?.title ?? custom.subAgent?.name;
176
-
177
- return (
178
- <>
179
- {heading != null && (
180
- <div className="text-sm text-muted-foreground">{heading}</div>
181
- )}
182
- <MessagePrimitive.Root data-role="assistant">
183
- <MessagePrimitive.Parts />
184
- </MessagePrimitive.Root>
185
- </>
186
- );
187
- }
188
-
189
- <MessagePartPrimitive.Messages
190
- components={{
191
- AssistantMessage: NestedSubAgentAssistantMessage,
192
- UserMessage: () => null,
193
- }}
194
- />
195
159
  ```
196
160
 
197
- For a collapsed tool-row header, cast the spawning `create_sub_agent` tool part’s `artifact` to `SubAgentArtifact` and read `subAgents[].title` (or `agentInfo.name`):
198
-
199
- Alternative: register `defineToolkit({ create_sub_agent: ... })` — all sub-agents share that one system tool name.
200
-
201
- See the [Multi-Agent Chat UI guide](https://www.assistant-ui.com/docs/tools/multi-agent).
202
-
203
- ## Tool approvals
204
-
205
- 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.
206
-
207
- For custom approval chrome (a thread-level bar instead of per-part buttons), use the extras hooks — see [Runtime extras](#runtime-extras) below.
208
-
209
- ## Ask-user tool responses (`tool.response_required`)
210
-
211
- 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`.
212
-
213
- Collect the user's answer and call `respondToToolResponse({ toolCallId, content })`. The `content` string is free-form (chosen option text, typed answer, etc.).
214
-
215
- ## Batched resume (approvals + responses)
216
-
217
- 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:
218
-
219
- 1. User resolves all tool approvals (`respondToToolApproval`).
220
- 2. User answers all ask-user prompts (`respondToToolResponse`).
221
- 3. Runtime collects `collectRequiredActionInputs(message)` → one mixed `TurnInputItem[]` → `sendTurn({ inputs })`.
222
-
223
- Do not send partial resumes; the backend rejects incomplete input sets.
224
-
225
- ## MCP auth
226
-
227
- 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
+ ---
228
162
 
229
163
  ## Runtime extras
230
164
 
231
- 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.
232
166
 
233
- ### Read hooks (thread-level UI)
167
+ ### Approvals, ask-user, MCP auth
234
168
 
235
169
  ```tsx
236
170
  import {
237
171
  useTrueFoundryApprovals,
238
172
  useTrueFoundryToolResponses,
239
173
  useTrueFoundryMcpAuth,
240
- } from "truefoundry-agents-assistant-ui-runtime";
241
-
242
- function ApprovalBar() {
243
- const { pending, respond } = useTrueFoundryApprovals();
244
- if (pending.length === 0) return null;
245
-
246
- const item = pending[0]!;
247
- return (
248
- <div>
249
- <p>Allow {item.toolName}?</p>
250
- <button onClick={() => respond({ approvalId: item.approvalId, approved: true })}>
251
- Allow
252
- </button>
253
- <button onClick={() => respond({ approvalId: item.approvalId, approved: false })}>
254
- Deny
255
- </button>
256
- </div>
257
- );
258
- }
259
-
260
- function AskUserBar() {
261
- const { pending, respond } = useTrueFoundryToolResponses();
262
- if (pending.length === 0) return null;
263
-
264
- const item = pending[0]!;
265
- return (
266
- <div>
267
- <p>{item.question ?? "Answer required"}</p>
268
- {(item.options ?? []).map((option) => (
269
- <button key={option} onClick={() => respond({ toolCallId: item.toolCallId, content: option })}>
270
- {option}
271
- </button>
272
- ))}
273
- </div>
274
- );
275
- }
276
-
277
- function McpAuthContinue() {
278
- const { pending, resume } = useTrueFoundryMcpAuth();
279
- if (pending == null) return null;
280
-
281
- return (
282
- <div>
283
- {pending.mcpServers.map((server) => (
284
- <a key={server.id} href={server.authUrl} target="_blank" rel="noreferrer">
285
- Authorize {server.name}
286
- </a>
287
- ))}
288
- <button onClick={() => void resume()}>Continue</button>
289
- </div>
290
- );
291
- }
292
- ```
293
-
294
- ### Action hooks (any render context, including nested sub-agents)
295
-
296
- ```tsx
297
- import { useTrueFoundryRespondToToolApproval } from "truefoundry-agents-assistant-ui-runtime";
174
+ } from "@truefoundry/assistant-ui-runtime";
298
175
 
299
- function NestedToolApprovalButton({ approvalId }: { approvalId: string }) {
300
- const respond = useTrueFoundryRespondToToolApproval();
301
- return (
302
- <button onClick={() => respond({ approvalId, approved: true })}>
303
- Allow
304
- </button>
305
- );
306
- }
176
+ const { pending, respond } = useTrueFoundryApprovals();
177
+ const { pending: asks, respond: answer } = useTrueFoundryToolResponses();
178
+ const { pending: mcp, resume } = useTrueFoundryMcpAuth();
307
179
  ```
308
180
 
309
- The action-only hooks return a single callback you can call from any render context (root or nested sub-agent thread). All four follow the same pattern:
310
-
311
- ```tsx
312
- import {
313
- useTrueFoundryRespondToToolApproval,
314
- useTrueFoundryRespondToToolResponse,
315
- useTrueFoundryResumeMcpAuth,
316
- useTrueFoundryCancel,
317
- } from "truefoundry-agents-assistant-ui-runtime";
318
-
319
- const respondToApproval = useTrueFoundryRespondToToolApproval();
320
- const respondToResponse = useTrueFoundryRespondToToolResponse();
321
- const resumeMcpAuth = useTrueFoundryResumeMcpAuth();
322
- const cancel = useTrueFoundryCancel();
323
-
324
- respondToApproval({ approvalId, approved: true });
325
- respondToResponse({ toolCallId, content: "Option A" });
326
- void resumeMcpAuth();
327
- void cancel();
328
- ```
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.
329
182
 
330
183
  ### Hooks reference
331
184
 
332
185
  | Hook | Returns | Description |
333
- |------|---------|-------------|
334
- | `useTrueFoundryApprovals()` | `{ pending: PendingApproval[]; respond: (r: RespondToToolApprovalOptions) => void }` | Pending tool approvals (across all threads) plus a respond action. Use for thread-level approval chrome. |
335
- | `useTrueFoundryToolResponses()` | `{ pending: PendingToolResponse[]; respond: (r: RespondToToolResponseOptions) => void }` | Pending ask-user / `tool.response_required` prompts plus a respond action. |
336
- | `useTrueFoundryMcpAuth()` | `{ pending: { mcpServers } \| null; resume: () => Promise<void> }` | Pending MCP OAuth pause state plus a resume action. |
337
- | `useTrueFoundryRespondToToolApproval()` | `(r: RespondToToolApprovalOptions) => void` | Respond to a tool approval from any render context, including nested sub-agent (readonly) renderers. |
338
- | `useTrueFoundryRespondToToolResponse()` | `(r: RespondToToolResponseOptions) => void` | Respond to an ask-user / `tool.response_required` prompt from any render context. |
339
- | `useTrueFoundryResumeMcpAuth()` | `() => Promise<void>` | Resume the paused turn after the user completes MCP OAuth in the browser. |
340
- | `useTrueFoundryCancel()` | `() => Promise<void>` | Cancel the active turn. Calls `session.cancel()` and drains the stream to its terminal `turn.done`. |
341
- | `useTrueFoundryHistoryPagination()` | `{ hasOlderHistory, isLoadingOlderHistory, loadOlderHistory }` | Scroll-up older history: call `loadOlderHistory()` when the user nears the top of the thread. |
342
-
343
- Where:
344
-
345
- - `PendingApproval` = `{ approvalId, threadId, toolName, args, argsText }`
346
- - `PendingToolResponse` = `{ toolCallId, threadId, toolName, args, argsText, question?, options? }`
347
- - `RespondToToolApprovalOptions` = `{ approvalId, approved, optionId?, reason? }`
348
- - `RespondToToolResponseOptions` = `{ toolCallId, content }`
349
-
350
- > **Read vs. action hooks.** The three `use*Approvals` / `use*ToolResponses` / `use*McpAuth` read hooks subscribe to extras state and re-render when pending items change — use them in thread-level UI (e.g. an approval bar). The four action-only hooks (`useTrueFoundryRespondTo*`, `useTrueFoundryResumeMcpAuth`, `useTrueFoundryCancel`) read the action via `trueFoundryExtras.get(aui)` and do **not** subscribe to state, so they are safe to call from nested sub-agent renderers where only a readonly context is available.
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 |
351
195
 
352
196
  ### Low-level namespace
353
197
 
354
198
  ```tsx
355
- import { trueFoundryExtras, type TrueFoundryRuntimeExtras } from "truefoundry-agents-assistant-ui-runtime";
199
+ import { trueFoundryExtras } from "@truefoundry/assistant-ui-runtime";
356
200
 
357
- // Throws outside useTrueFoundryAgentRuntime:
358
201
  const extras = trueFoundryExtras.use();
359
-
360
- // Safe with fallback (returns default outside runtime):
361
202
  const pending = trueFoundryExtras.use((e) => e.pendingApprovals, []);
362
203
  ```
363
204
 
364
- `TrueFoundryRuntimeExtras` fields:
365
-
366
- | Field | Type | Purpose |
367
- |-------|------|---------|
368
- | `pendingApprovals` | `PendingApproval[]` | Undecided tool approvals across all threads |
369
- | `pendingToolResponses` | `PendingToolResponse[]` | Unanswered ask-user / client-side tool prompts |
370
- | `pendingMcpAuth` | `{ mcpServers } \| null` | MCP OAuth pause state |
371
- | `respondToToolApproval` | `(r: { approvalId, approved, reason? }) => void` | Stage approval; batch-send when complete |
372
- | `respondToToolResponse` | `(r: { toolCallId, content }) => void` | Stage answer; batch-send when complete |
373
- | `resumeMcpAuth` | `() => Promise<void>` | Resume after OAuth |
374
- | `cancel` | `() => Promise<void>` | Cancel the active turn: calls `session.cancel()` and lets the stream drain to its terminal `turn.done` (reconciles on next session load) |
375
- | `resetFromTurn` | `(turnId: string) => Promise<void>` | Re-submit a user turn (branch/reset) |
376
- | `reload` | `() => void` | Retry the current session load |
377
- | `downloadSandboxFile` | `(path: string) => Promise<Blob>` | Download a file from the session sandbox |
378
- | `hasOlderHistory` | `boolean` | True when another older `listEvents` page is available |
379
- | `isLoadingOlderHistory` | `boolean` | True while `loadOlderHistory` is in flight |
380
- | `loadOlderHistory` | `() => Promise<void>` | Prepend the next older history window (scroll-up) |
381
- | `draft` | `TrueFoundryDraftRuntimeExtras \| null` | Draft-mode agent spec sync extras |
382
-
383
- Per-part `respondToApproval` from assistant-ui still works for root-thread tool UIs; extras complements that for global chrome and nested renderers.
384
-
385
- ## Cancellation
205
+ ---
386
206
 
387
- `cancel()` does **not** tear down the stream mid-flight. It calls `session.cancel()` and then keeps consuming the active stream: the backend closes the SSE gracefully by emitting a terminal `turn.done` event before ending the stream, so the in-flight run drains to completion on its own. No explicit reconcile is performed — the cancelled turn is terminal, and local state reconciles against the authoritative event log on the next session load (e.g. page reload). A subsequent `sendTurn` chains on the cancelled turn's history via `previousTurnId: "auto"`.
207
+ ## Server port (`AgentChatServer`)
388
208
 
389
- (Hard aborts still happen when *switching away*starting a new run or changing sessions abandons the previous turn.)
209
+ The runtime never holds credentials. It accepts any object implementing `AgentChatServer` — a flat, stateless port with methods like `createSession`, `listSessions`, `prepareAndExecuteTurn`, etc.
390
210
 
391
- ## Resumable streams
211
+ **First-party:** use [`createTrueFoundryChatServer`](#truefoundry-gateway-plugin) (requires `truefoundry-gateway-sdk`).
392
212
 
393
- Works out of the box — no server route or Redis store. TrueFoundry persists every turn server-side; on reload or reconnect the runtime calls `turn.stream({})` and replays events into the fold (idempotent). Running turns are detected on session load and resumed automatically.
213
+ **Your own backend:**
394
214
 
395
- > **TODO:** Track the last ingested `sequenceNumber` and pass `afterSequenceNumber` on reconnect to avoid replaying already-seen events.
396
-
397
- Contrast with the [AI SDK resumable streams guide](https://www.assistant-ui.com/docs/guides/resumable-streams), which requires a separate encoded-byte store.
215
+ ```tsx
216
+ import type { AgentChatServer } from "@truefoundry/assistant-ui-runtime";
217
+
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
+ ```
398
248
 
399
- ## History pagination
249
+ `ListResult<T>` is `{ data: T[]; nextPageToken?: string }` — flat token-based pagination. Optional methods: `deleteSession`, `listTurnEvents`, `subscribeToTurn`, `downloadSandboxFile`.
400
250
 
401
- Thread open no longer drains every turn. Initial load:
251
+ ---
402
252
 
403
- 1. `listTurns({ limit: 1 })` once — detect a running turn (does **not** walk `page_token`).
404
- 2. One (or a few) `listEvents` page(s) for the newest complete user-message group.
405
- 3. Clears `isLoading`, then resumes a running turn via subscribe if needed.
253
+ ## TrueFoundry gateway plugin
406
254
 
407
- Older history is opt-in via extras / `useTrueFoundryHistoryPagination()`:
255
+ `createTrueFoundryChatServer` wraps `truefoundry-gateway-sdk` into an `AgentChatServer`. Import from the main entry or the isolated subpath (no React):
408
256
 
409
257
  ```tsx
410
- const { hasOlderHistory, isLoadingOlderHistory, loadOlderHistory } =
411
- useTrueFoundryHistoryPagination();
258
+ import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime";
259
+ // or
260
+ import { createTrueFoundryChatServer } from "@truefoundry/assistant-ui-runtime/plugins/truefoundry-agent-server-adapter";
412
261
 
413
- // e.g. IntersectionObserver at the top of the message list
414
- if (hasOlderHistory && !isLoadingOlderHistory) {
415
- void loadOlderHistory();
416
- }
262
+ const server = createTrueFoundryChatServer({
263
+ apiKey: process.env.TFY_API_KEY!,
264
+ baseUrl: process.env.TFY_GATEWAY_URL!,
265
+ });
417
266
  ```
418
267
 
419
- `loadOlderHistory` prepends older turns without aborting an active stream.
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.
420
269
 
421
- ## Public API
270
+ ---
422
271
 
423
- Everything below is exported from the package root (`truefoundry-agents-assistant-ui-runtime`).
272
+ ## Exports
424
273
 
425
274
  | Export | Kind | Purpose |
426
- |--------|------|---------|
427
- | `useTrueFoundryAgentRuntime` | hook | Main entry point. Returns an assistant-ui runtime bound to gateway sessions. |
428
- | `UseTrueFoundryAgentRuntimeOptions` | type | Options for the hook (see table above). |
429
- | `useTrueFoundryApprovals` | hook | `{ pending, respond }` for tool approvals via extras. |
430
- | `useTrueFoundryToolResponses` | hook | `{ pending, respond }` for ask-user / `tool.response_required` prompts. |
431
- | `useTrueFoundryMcpAuth` | hook | `{ pending, resume }` for MCP OAuth pause/resume. |
432
- | `useTrueFoundryRespondToToolApproval` | hook | Action callback via `trueFoundryExtras.get(aui)` works in nested renderers. |
433
- | `useTrueFoundryRespondToToolResponse` | hook | Same pattern for tool responses. |
434
- | `useTrueFoundryResumeMcpAuth` | hook | Same pattern for MCP resume. |
435
- | `useTrueFoundryCancel` | hook | Same pattern for cancel. |
436
- | `useTrueFoundryHistoryPagination` | hook | `{ hasOlderHistory, isLoadingOlderHistory, loadOlderHistory }` for scroll-up history. |
437
- | `trueFoundryExtras` | namespace | `createRuntimeExtras` channel — `.use()`, `.get(aui)`, `.provide()`. |
438
- | `TrueFoundryRuntimeExtras` | type | Shape provided into the runtime extras slot. |
439
- | `PendingApproval`, `PendingToolResponse` | types | Derived pending items for UI rendering. |
440
- | `createTrueFoundryThreadListAdapter` | fn | Builds the cursor-paginated `RemoteThreadListAdapter` powering `<ThreadList>` (`list({ after })` → `nextCursor`). Used internally; exported for custom wiring. |
441
- | `getSession` | fn | `(client, sessionId) => Promise<AgentSession>` convenience wrapper. |
442
- | `convertTurnsToThreadMessages` | fn | Loads a session's turns and folds them into assistant-ui `ThreadMessage[]` (`ConvertTurnsResult`). |
443
- | `buildTurnAssistantContent` | fn | Folds a single turn's events into assistant content parts. |
444
- | `repositoryItemsFromMessages` | fn | Converts messages into `ExportedMessageRepositoryItem[]` for history export. |
445
- | `getTurnMessageContent` | fn | Extracts the text payload from an `AppendMessage`. |
446
- | `ConvertTurnsResult` | type | Result of `convertTurnsToThreadMessages` (`messages`, `foldState`, `runningTurn?`, `unstable_resume?`). |
447
- | `collectApprovalInputs` | fn | Collects decided approvals from a message into `user.tool_approval` inputs. |
448
- | `collectResponseInputs` | fn | Collects staged answers into `user.tool_response` inputs. |
449
- | `collectRequiredActionInputs` | fn | Collects both approval + response inputs once nothing is pending. |
450
- | `messageHasPendingApprovals` | fn | True if a message still has undecided tool approvals. |
451
- | `messageHasPendingResponses` | fn | True if a message still has unanswered tool responses. |
452
- | `messageHasPendingRequiredActions` | fn | True if either approvals or responses are still pending. |
453
- | `findPausedAssistantMessage` | fn | Last assistant message in `requires-action` state. |
454
- | `toTrueFoundryApprovalInputs` | fn | Applies an approval decision and returns gateway inputs. |
455
- | `SubAgentArtifact`, `SubAgentCustomMetadata` | types | Shapes attached to sub-agent tool calls / nested messages. |
456
- | `TrueFoundryMessageCustomMetadata` | type | Typed keys on `ThreadMessage.metadata.custom` written by this adapter. |
457
- | `ROOT_THREAD_ID` | const | The literal `"main"` — the gateway's root thread id. |
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`, `Session`, `Turn`, | Types | Server port + DTOs |
282
+ | `TfyAgentSpec`, `TfySession`, `isTfyToolInfo`, … | Types / guards | Gateway-concrete types from the plugin |
283
+ | `NamedAgentConfig`, `DraftAgentConfig` | Types | Agent source discriminants |
284
+
285
+ ---
458
286
 
459
287
  ## Architecture (source map)
460
288
 
461
- For contributors and agents working inside this package. Source lives in `src/`; the published entry point is `dist/index.js` (built by `tsup`).
289
+ For contributors working inside this package. Source lives in `src/`; the published entry point is `dist/index.js` (built by `tsup`).
462
290
 
463
291
  | File | Responsibility |
464
- |------|----------------|
465
- | `useTrueFoundryAgentRuntime.ts` | Public hook. Wires the external-store runtime, thread-list runtime, adapters, and extras. |
466
- | `useTrueFoundryAgentMessages.ts` | Reactive `SessionSnapshot` store: load, stream ingestion, cancel, resume; derives `messages` via pure projection; records approval/response decisions in overlay. |
467
- | `sessionSnapshot.ts` | `SessionSnapshot` shape, required-actions overlay, and immutable wrapper helpers. |
468
- | `truefoundryExtras.ts` | `createRuntimeExtras` namespace and `TrueFoundryRuntimeExtras` type. |
469
- | `hooks.ts` | Consumer hooks read selectors + action callbacks via `.get(aui)`. |
470
- | `collectPending.ts` | Derives `pendingApprovals`, `pendingToolResponses`, `pendingMcpAuth` from messages. |
471
- | `requiredActionInputs.ts` | Combined gate + `collectRequiredActionInputs` for batched resume. |
472
- | `truefoundryThreadListAdapter.ts` | `RemoteThreadListAdapter` cursor-paginated session list (`list({ after })` → `nextCursor`), create/fetch sessions. |
473
- | `convertTurnMessages.ts` | `projectSessionMessages` pure projector; `buildSnapshotFromSession` history ingest; `convertTurnsToThreadMessages` wrapper; stream-event aggregation. |
474
- | `foldPeerThreads.ts` | `PeerThreadFoldState` — folds peer/sub-agent threads under their spawning tool call. |
475
- | `messageCustomMetadata.ts` | `TrueFoundryMessageCustomMetadata` — typed `metadata.custom` keys for this adapter. |
476
- | `modelMessageContent.ts` | `model.message` events → assistant content parts (text, reasoning, tool calls). |
477
- | `streamTurn.ts` | `streamTurnContent` / `resumeTurnStream` generators over `prepareTurn`/`stream`. |
478
- | `toolApproval.ts` | Approval state, decision mapping, and `user.tool_approval` input collection. |
479
- | `toolResponse.ts` | Ask-user response state, staging, and `user.tool_response` input collection. |
480
- | `askUserQuestion.ts` | `ask_user_question` detection and argument parsing. |
481
- | `mcpAuth.ts` | MCP auth-required detection and structured authorize UI metadata. |
482
- | `turnEventHelpers.ts` | Appends approval / response / MCP-auth status onto turn updates. |
483
- | `createSubAgent.ts` | Detects the `create_sub_agent` system tool call. |
484
- | `extractTurnUserText.ts` / `lastUserMessageText.ts` | Text extraction helpers. |
485
- | `sessions.ts` | `getSession` wrapper. |
486
- | `sessionListStartTimestamp.ts` | Default `listSessions` window (1 year). |
487
- | `constants.ts` | `ROOT_THREAD_ID = "main"`. |
488
- | `types.ts` / `turnStreamUpdate.ts` | Shared option and update types. |
292
+ | ---- | -------------- |
293
+ | `server/types.ts` | `AgentChatServer` + `AgentBuilderServer`, session/turn/pagination types |
294
+ | `server/events.ts` | Concrete turn/stream event types |
295
+ | `useTrueFoundryAgentRuntime.ts` | Public hook — external-store + thread-list + extras |
296
+ | `useTrueFoundryAgentMessages.ts` | Reactive session snapshot: load, stream, cancel, resume |
297
+ | `truefoundryExtras.ts` / `hooks.ts` | Extras namespace + consumer hooks |
298
+ | `convertTurnMessages.ts` | Pure projection from snapshot thread messages |
299
+ | `foldPeerThreads.ts` | Nest peer/sub-agent threads under spawning tool calls |
300
+ | `plugins/truefoundry-agent-server-adapter/` | Gateway SDK → `AgentChatServer` |
489
301
 
490
302
  ### Invariants
491
303
 
492
304
  - One gateway **session** ⇄ one assistant-ui **thread** (`session.id` = thread `remoteId`).
493
- - The root thread id is always `"main"` (`ROOT_THREAD_ID`); sub-agent threads nest beneath their `create_sub_agent` tool call.
494
- - The runtime never holds credentials — always pass a constructed `AgentSessionClient`.
495
- - Gateway types come from `truefoundry-gateway-sdk/agents`; do not redefine event/turn shapes locally.
496
- - A paused turn's resume `input` must include **all** pending `user.tool_approval` and `user.tool_response` events across every thread in one batch.
497
- - Approval decisions are allow/deny only (`ApprovalDecision`); there is no `optionId` on the gateway wire.
498
-
499
- ## Local development
305
+ - Root thread id is always `"main"` (`ROOT_THREAD_ID`); sub-agents nest under their `create_sub_agent` tool call.
306
+ - The runtime never holds credentials — only a pre-built `AgentChatServer`.
307
+ - A paused turn's resume `input` must include **all** pending approvals + tool responses across every thread in one batch.
308
+ - Two agent modes: **named** (`agentName`) and **draft** (`agent: { mode: "draft", }`).
500
309
 
501
- From this package directory:
310
+ ### Local development
502
311
 
503
312
  ```bash
504
313
  pnpm build # tsup → dist/
@@ -506,30 +315,18 @@ pnpm test # vitest run
506
315
  pnpm typecheck # tsc --noEmit
507
316
  ```
508
317
 
509
- `dist/` is generated output and is gitignored. From the repo root, `pnpm build` builds this package before the Next.js app.
318
+ ### Unsupported assistant-ui features
510
319
 
511
- ## Unsupported assistant-ui features
320
+ | Feature | Notes |
321
+ | ------- | ----- |
322
+ | Attachment rendering | Forwarded on send; user bubbles show text only today |
323
+ | Speech / Dictation / Voice | Pass-through only |
324
+ | Feedback | Pass-through only; not persisted to the gateway |
325
+ | Thread rename / archive / delete | Thread-list adapter no-ops |
326
+ | Thread title generation | Returns an empty stream |
512
327
 
513
- 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.
328
+ ---
514
329
 
515
- | Feature | Notes |
516
- |---------|-------|
517
- | Attachment rendering | Attachments are forwarded to the gateway on send when you provide an `AttachmentAdapter`, but user message bubbles show text only. |
518
- | Built-in `AttachmentAdapter` | Ships as `trueFoundryAttachmentAdapter` (opt-in via `adapters.attachments`). Not applied by default. |
519
- | Speech synthesis (`adapters.speech`) | Pass-through only. Not shipped. |
520
- | Dictation (`adapters.dictation`) | Pass-through only. Not shipped. |
521
- | Voice (`adapters.voice`) | Pass-through only. Not shipped. |
522
- | Feedback (`adapters.feedback`) | Pass-through only. Ratings are not persisted to the gateway. |
523
- | Message edit (`onEdit`) | Not wired. |
524
- | Regenerate (`onReload`) | Not wired. |
525
- | Message delete (`onDelete`) | Not wired. |
526
- | Client-side tool results (`onAddToolResult`) | Not wired. |
527
- | Tool call resume (`onResumeToolCall`) | Not wired. |
528
- | Message queue (`queue`) | Not wired. |
529
- | Branch switching | Not wired. |
530
- | Thread rename / archive / delete | Thread-list adapter no-ops. |
531
- | Thread title generation | Returns an empty stream. |
532
- | Generative UI message parts | Not mapped from gateway events. |
533
- | Source citation parts | Not mapped from gateway events. |
534
- | Message import / external state | `onImport`, `onExportExternalState`, `onLoadExternalState` not wired. |
535
- | Composer suggestions | `suggestions` not populated. |
330
+ ## License
331
+
332
+ See [LICENSE](../../LICENSE).