@truefoundry/assistant-ui-runtime 0.1.0-rc.1

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 (58) hide show
  1. package/README.md +567 -0
  2. package/dist/index.d.ts +300 -0
  3. package/dist/index.js +4440 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +70 -0
  6. package/src/agentSessionModule.d.ts +25 -0
  7. package/src/agentSpec.ts +44 -0
  8. package/src/askUserQuestion.ts +37 -0
  9. package/src/attachmentAdapter.test.ts +56 -0
  10. package/src/attachmentAdapter.ts +58 -0
  11. package/src/bindDraftAgentSession.test.ts +55 -0
  12. package/src/bindDraftAgentSession.ts +66 -0
  13. package/src/buildEditedUserMessageContent.test.ts +61 -0
  14. package/src/collectPending.test.ts +69 -0
  15. package/src/collectPending.ts +165 -0
  16. package/src/constants.ts +2 -0
  17. package/src/convertTurnMessages.test.ts +1991 -0
  18. package/src/convertTurnMessages.ts +1251 -0
  19. package/src/createSubAgent.ts +8 -0
  20. package/src/draftAgentConfig.test.ts +88 -0
  21. package/src/draftSessionBridge.ts +28 -0
  22. package/src/extractTurnUserText.ts +21 -0
  23. package/src/foldPeerThreads.test.ts +386 -0
  24. package/src/foldPeerThreads.ts +587 -0
  25. package/src/hooks.ts +123 -0
  26. package/src/index.ts +68 -0
  27. package/src/lastUserMessageText.ts +19 -0
  28. package/src/loadSessionSnapshot.test.ts +57 -0
  29. package/src/loadSessionSnapshot.ts +35 -0
  30. package/src/mcpAuth.ts +44 -0
  31. package/src/messageCustomMetadata.ts +37 -0
  32. package/src/modelMessageContent.ts +135 -0
  33. package/src/modelMessageImageContent.test.ts +109 -0
  34. package/src/modelMessageImageContent.ts +193 -0
  35. package/src/requiredActionInputs.test.ts +394 -0
  36. package/src/requiredActionInputs.ts +65 -0
  37. package/src/requiredActionsFromActiveUpdate.test.ts +95 -0
  38. package/src/sessionListStartTimestamp.ts +6 -0
  39. package/src/sessionSnapshot.ts +107 -0
  40. package/src/sessions.ts +36 -0
  41. package/src/streamTurn.test.ts +243 -0
  42. package/src/streamTurn.ts +119 -0
  43. package/src/toolApproval.test.ts +137 -0
  44. package/src/toolApproval.ts +459 -0
  45. package/src/toolResponse.test.ts +136 -0
  46. package/src/toolResponse.ts +427 -0
  47. package/src/truefoundryDraftThreadListAdapter.test.ts +140 -0
  48. package/src/truefoundryDraftThreadListAdapter.ts +63 -0
  49. package/src/truefoundryExtras.ts +45 -0
  50. package/src/truefoundryThreadListAdapter.test.ts +103 -0
  51. package/src/truefoundryThreadListAdapter.ts +59 -0
  52. package/src/turnEventHelpers.ts +98 -0
  53. package/src/turnStreamUpdate.ts +11 -0
  54. package/src/types.ts +92 -0
  55. package/src/useDraftAgentSpec.ts +173 -0
  56. package/src/useTrueFoundryAgentMessages.test.tsx +723 -0
  57. package/src/useTrueFoundryAgentMessages.ts +757 -0
  58. package/src/useTrueFoundryAgentRuntime.ts +268 -0
package/README.md ADDED
@@ -0,0 +1,567 @@
1
+ # @truefoundry/assistant-ui-runtime
2
+
3
+ TrueFoundry Gateway agent runtime adapter for [assistant-ui](https://www.assistant-ui.com/).
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.
6
+
7
+ ## Requirements
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
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @assistant-ui/react @truefoundry/assistant-ui-runtime truefoundry-gateway-sdk@^0.2.0
18
+ ```
19
+
20
+ ## Quickstart
21
+
22
+ ### 1. Create an `AgentSessionClient`
23
+
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.
25
+
26
+ ```tsx
27
+ import { AgentSessionClient } from "truefoundry-gateway-sdk/agents";
28
+
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.
36
+
37
+ ### 2. Set up the client runtime
38
+
39
+ ```tsx
40
+ "use client";
41
+
42
+ import { AssistantRuntimeProvider } from "@assistant-ui/react";
43
+ import { useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
44
+ import { Thread } from "@/components/assistant-ui/thread";
45
+
46
+ const AGENT_NAME = process.env.TFY_AGENT_NAME!;
47
+ const client = new AgentSessionClient({ /* ... */ });
48
+
49
+ export function MyAssistant() {
50
+ const runtime = useTrueFoundryAgentRuntime({
51
+ client,
52
+ agentName: AGENT_NAME,
53
+ });
54
+
55
+ return (
56
+ <AssistantRuntimeProvider runtime={runtime}>
57
+ <Thread />
58
+ </AssistantRuntimeProvider>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ### 3. Use the component
64
+
65
+ ```tsx
66
+ import { MyAssistant } from "@/components/MyAssistant";
67
+
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.
80
+
81
+ ## `useTrueFoundryAgentRuntime` options
82
+
83
+ `UseTrueFoundryAgentRuntimeOptions` extends assistant-ui's `ExternalStoreSharedOptions`. The adapter-specific fields are:
84
+
85
+ | Option | Type | Required | Description |
86
+ |--------|------|----------|-------------|
87
+ | `client` | `AgentSessionClient` | Yes | Pre-built gateway client. The runtime never reads credentials itself. |
88
+ | `agent` | `{ mode: "named", agentName }` \| `{ mode: "draft", defaultAgentSpec }` | Yes* | Discriminated agent source. *Or legacy `agentName` for named mode. |
89
+ | `agentName` | `string` | Named only | Legacy shorthand for `agent: { mode: "named", agentName }`. |
90
+ | `gateway` | `TrueFoundryGateway` | Draft only | Low-level gateway client for `agents.private.draftSessions` CRUD. |
91
+ | `initialSessionId` | `string` | No | Pin an existing session once on mount (uncontrolled). |
92
+ | `threadId` | `string` | No | Controlled active session id; reactive and URL-syncable. |
93
+ | `onThreadIdChange` | `(threadId: string \| undefined) => void` | No | Fires when the active session changes. |
94
+ | `onError` | `(error: unknown) => void` | No | Invoked on stream/load/turn errors. |
95
+ | `adapters` | `{ attachments?, speech?, dictation?, voice?, feedback? }` | No | Optional assistant-ui adapters forwarded to the runtime. See [Unsupported assistant-ui features](#unsupported-assistant-ui-features). |
96
+
97
+ ### Named agent mode (saved agent)
98
+
99
+ ```tsx
100
+ const runtime = useTrueFoundryAgentRuntime({
101
+ client,
102
+ agent: { mode: "named", agentName: "support-bot" },
103
+ });
104
+ ```
105
+
106
+ Legacy `agentName` shorthand is still supported.
107
+
108
+ ### Draft agent mode (inline `AgentSpec`)
109
+
110
+ Draft mode lists **draft sessions** (`agents.private.draftSessions.*`) in the thread list. Each thread's `remoteId` is a `DraftSession.id`. The runtime syncs `AgentSpec` edits via `agents.private.draftSessions.update`. Turns and history use `/agents/sessions/{draftSessionId}/turns` after validating the draft via `agents.private.draftSessions.get` — no separate conversation session is created.
111
+
112
+ ```tsx
113
+ import { TrueFoundryGateway } from "truefoundry-gateway-sdk";
114
+ import { useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
115
+
116
+ const client = new AgentSessionClient({ apiKey, baseUrl });
117
+ const gateway = new TrueFoundryGateway({ apiKey, baseUrl });
118
+
119
+ const runtime = useTrueFoundryAgentRuntime({
120
+ client,
121
+ gateway,
122
+ agent: {
123
+ mode: "draft",
124
+ defaultAgentSpec: {
125
+ model: { name: "anthropic/claude-sonnet-4-6" },
126
+ instructions: "You are a helpful assistant.",
127
+ mcpServers: [],
128
+ skills: [],
129
+ },
130
+ },
131
+ });
132
+ ```
133
+
134
+ Read and update the active draft spec from UI:
135
+
136
+ ```tsx
137
+ import { useTrueFoundryAgentSpec } from "@truefoundry/assistant-ui-runtime";
138
+
139
+ function DraftModelPicker() {
140
+ const { agentSpec, updateAgentSpec, isSpecSyncing } = useTrueFoundryAgentSpec();
141
+ if (agentSpec == null) return null;
142
+
143
+ return (
144
+ <select
145
+ value={agentSpec.model.name}
146
+ disabled={isSpecSyncing}
147
+ onChange={(e) => updateAgentSpec({ model: { name: e.target.value } })}
148
+ />
149
+ );
150
+ }
151
+ ```
152
+
153
+ `AgentSpec` maps the runtime fields from the [Agent manifest reference](https://www.truefoundry.com/docs/agent-platform/agent-harness/sdk/agent-manifest-reference) (`model`, `instructions`, `mcpServers`, `skills`, `config`, etc.). Registry metadata (`name`, `description`, `collaborators`) is not part of `AgentSpec`.
154
+
155
+ ### Adding adapters
156
+
157
+ 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.
158
+
159
+ ```tsx
160
+ import { trueFoundryAttachmentAdapter, useTrueFoundryAgentRuntime } from "@truefoundry/assistant-ui-runtime";
161
+
162
+ const runtime = useTrueFoundryAgentRuntime({
163
+ client,
164
+ agentName,
165
+ adapters: { attachments: trueFoundryAttachmentAdapter },
166
+ });
167
+ ```
168
+
169
+ Other adapters (speech, feedback, etc.) follow the same pattern:
170
+
171
+ ```tsx
172
+ const runtime = useTrueFoundryAgentRuntime({
173
+ client,
174
+ agentName,
175
+ adapters: { attachments: trueFoundryAttachmentAdapter, speech, feedback },
176
+ });
177
+ ```
178
+
179
+ ### Resuming a session
180
+
181
+ ```tsx
182
+ const runtime = useTrueFoundryAgentRuntime({
183
+ client,
184
+ agentName,
185
+ initialSessionId: "ses_abc123",
186
+ });
187
+ ```
188
+
189
+ ### Bring your own session ID (no session list)
190
+
191
+ 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.
192
+
193
+ ```tsx
194
+ const runtime = useTrueFoundryAgentRuntime({
195
+ client,
196
+ agentName,
197
+ initialSessionId: "ses_abc123",
198
+ });
199
+
200
+ return (
201
+ <AssistantRuntimeProvider runtime={runtime}>
202
+ <Thread />
203
+ </AssistantRuntimeProvider>
204
+ );
205
+ ```
206
+
207
+ Each gateway session corresponds to one assistant-ui thread.
208
+
209
+ ## Multi-agent (nested sub-agents)
210
+
211
+ 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.
212
+
213
+ Render nested threads with `MessagePartPrimitive.Messages` inside your tool fallback (recommended — no per-tool registration):
214
+
215
+ ```tsx
216
+ import { MessagePartPrimitive, MessagePrimitive } from "@assistant-ui/react";
217
+ import { useAuiState } from "@assistant-ui/store";
218
+ import type { TrueFoundryMessageCustomMetadata } from "@truefoundry/assistant-ui-runtime";
219
+
220
+ function NestedSubAgentAssistantMessage() {
221
+ const custom = useAuiState(
222
+ (s) => s.message.metadata.custom as TrueFoundryMessageCustomMetadata,
223
+ );
224
+ const heading = custom.subAgent?.title ?? custom.subAgent?.name;
225
+
226
+ return (
227
+ <>
228
+ {heading != null && (
229
+ <div className="text-sm text-muted-foreground">{heading}</div>
230
+ )}
231
+ <MessagePrimitive.Root data-role="assistant">
232
+ <MessagePrimitive.Parts />
233
+ </MessagePrimitive.Root>
234
+ </>
235
+ );
236
+ }
237
+
238
+ <MessagePartPrimitive.Messages
239
+ components={{
240
+ AssistantMessage: NestedSubAgentAssistantMessage,
241
+ UserMessage: () => null,
242
+ }}
243
+ />
244
+ ```
245
+
246
+ 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`):
247
+
248
+ Alternative: register `defineToolkit({ create_sub_agent: ... })` — all sub-agents share that one system tool name.
249
+
250
+ See the [Multi-Agent Chat UI guide](https://www.assistant-ui.com/docs/tools/multi-agent).
251
+
252
+ ## Tool approvals
253
+
254
+ 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.
255
+
256
+ For custom approval chrome (a thread-level bar instead of per-part buttons), use the extras hooks — see [Runtime extras](#runtime-extras) below.
257
+
258
+ ## Ask-user tool responses (`tool.response_required`)
259
+
260
+ 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`.
261
+
262
+ Collect the user's answer and call `respondToToolResponse({ toolCallId, content })`. The `content` string is free-form (chosen option text, typed answer, etc.).
263
+
264
+ ## Batched resume (approvals + responses)
265
+
266
+ 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:
267
+
268
+ 1. User resolves all tool approvals (`respondToToolApproval`).
269
+ 2. User answers all ask-user prompts (`respondToToolResponse`).
270
+ 3. Runtime collects `collectRequiredActionInputs(message)` → one mixed `TurnInputItem[]` → `sendTurn({ inputs })`.
271
+
272
+ Do not send partial resumes; the backend rejects incomplete input sets.
273
+
274
+ ## MCP auth
275
+
276
+ 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`).
277
+
278
+ This package only exposes the raw pending state and a resume action — the per-server authorize links and the "Continue" button live in `packages/agent-ui-sdk`'s `McpAuthContainer`/`McpAuthPrompt` (see that package's README). Resuming is always a manual user action; there is no background polling or auto-resume.
279
+
280
+ ## Runtime extras
281
+
282
+ 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).
283
+
284
+ ### Read hooks (thread-level UI)
285
+
286
+ ```tsx
287
+ import {
288
+ useTrueFoundryApprovals,
289
+ useTrueFoundryToolResponses,
290
+ useTrueFoundryMcpAuth,
291
+ } from "@truefoundry/assistant-ui-runtime";
292
+
293
+ function ApprovalBar() {
294
+ const { pending, respond } = useTrueFoundryApprovals();
295
+ if (pending.length === 0) return null;
296
+
297
+ const item = pending[0]!;
298
+ return (
299
+ <div>
300
+ <p>Allow {item.toolName}?</p>
301
+ <button onClick={() => respond({ approvalId: item.approvalId, approved: true })}>
302
+ Allow
303
+ </button>
304
+ <button onClick={() => respond({ approvalId: item.approvalId, approved: false })}>
305
+ Deny
306
+ </button>
307
+ </div>
308
+ );
309
+ }
310
+
311
+ function AskUserBar() {
312
+ const { pending, respond } = useTrueFoundryToolResponses();
313
+ if (pending.length === 0) return null;
314
+
315
+ const item = pending[0]!;
316
+ return (
317
+ <div>
318
+ <p>{item.question ?? "Answer required"}</p>
319
+ {(item.options ?? []).map((option) => (
320
+ <button key={option} onClick={() => respond({ toolCallId: item.toolCallId, content: option })}>
321
+ {option}
322
+ </button>
323
+ ))}
324
+ </div>
325
+ );
326
+ }
327
+
328
+ function McpAuthContinue() {
329
+ const { pending, resume } = useTrueFoundryMcpAuth();
330
+ if (pending == null) return null;
331
+
332
+ return (
333
+ <div>
334
+ {pending.mcpServers.map((server) => (
335
+ <a key={server.id} href={server.authUrl} target="_blank" rel="noreferrer">
336
+ Authorize {server.name}
337
+ </a>
338
+ ))}
339
+ <button onClick={() => void resume()}>Continue</button>
340
+ </div>
341
+ );
342
+ }
343
+ ```
344
+
345
+ ### Action hooks (any render context, including nested sub-agents)
346
+
347
+ ```tsx
348
+ import { useTrueFoundryRespondToToolApproval } from "@truefoundry/assistant-ui-runtime";
349
+
350
+ function NestedToolApprovalButton({ approvalId }: { approvalId: string }) {
351
+ const respond = useTrueFoundryRespondToToolApproval();
352
+ return (
353
+ <button onClick={() => respond({ approvalId, approved: true })}>
354
+ Allow
355
+ </button>
356
+ );
357
+ }
358
+ ```
359
+
360
+ 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:
361
+
362
+ ```tsx
363
+ import {
364
+ useTrueFoundryRespondToToolApproval,
365
+ useTrueFoundryRespondToToolResponse,
366
+ useTrueFoundryResumeMcpAuth,
367
+ useTrueFoundryCancel,
368
+ } from "@truefoundry/assistant-ui-runtime";
369
+
370
+ const respondToApproval = useTrueFoundryRespondToToolApproval();
371
+ const respondToResponse = useTrueFoundryRespondToToolResponse();
372
+ const resumeMcpAuth = useTrueFoundryResumeMcpAuth();
373
+ const cancel = useTrueFoundryCancel();
374
+
375
+ respondToApproval({ approvalId, approved: true });
376
+ respondToResponse({ toolCallId, content: "Option A" });
377
+ void resumeMcpAuth();
378
+ void cancel();
379
+ ```
380
+
381
+ ### Hooks reference
382
+
383
+ | Hook | Returns | Description |
384
+ |------|---------|-------------|
385
+ | `useTrueFoundryApprovals()` | `{ pending: PendingApproval[]; respond: (r: RespondToToolApprovalOptions) => void }` | Pending tool approvals (across all threads) plus a respond action. Use for thread-level approval chrome. |
386
+ | `useTrueFoundryToolResponses()` | `{ pending: PendingToolResponse[]; respond: (r: RespondToToolResponseOptions) => void }` | Pending ask-user / `tool.response_required` prompts plus a respond action. |
387
+ | `useTrueFoundryMcpAuth()` | `{ pending: { mcpServers } \| null; resume: () => Promise<void> }` | Pending MCP OAuth pause state plus a resume action. |
388
+ | `useTrueFoundryRespondToToolApproval()` | `(r: RespondToToolApprovalOptions) => void` | Respond to a tool approval from any render context, including nested sub-agent (readonly) renderers. |
389
+ | `useTrueFoundryRespondToToolResponse()` | `(r: RespondToToolResponseOptions) => void` | Respond to an ask-user / `tool.response_required` prompt from any render context. |
390
+ | `useTrueFoundryResumeMcpAuth()` | `() => Promise<void>` | Resume the paused turn after the user completes MCP OAuth in the browser. |
391
+ | `useTrueFoundryCancel()` | `() => Promise<void>` | Cancel the active turn. Calls `session.cancel()` and drains the stream to its terminal `turn.done`. |
392
+ | `useTrueFoundryAgentSpec()` | `{ agentSpec, draftSessionId, isSpecSyncing, specError, updateAgentSpec }` | Draft mode only — current inline spec and debounced sync state. |
393
+ | `useTrueFoundryUpdateAgentSpec()` | `(update: AgentSpecUpdate) => void` | Draft mode only — update spec from any render context. |
394
+
395
+ Where:
396
+
397
+ - `PendingApproval` = `{ approvalId, threadId, toolName, args, argsText }`
398
+ - `PendingToolResponse` = `{ toolCallId, threadId, toolName, args, argsText, question?, options? }`
399
+ - `RespondToToolApprovalOptions` = `{ approvalId, approved, optionId?, reason? }`
400
+ - `RespondToToolResponseOptions` = `{ toolCallId, content }`
401
+
402
+ > **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.
403
+
404
+ ### Low-level namespace
405
+
406
+ ```tsx
407
+ import { trueFoundryExtras, type TrueFoundryRuntimeExtras } from "@truefoundry/assistant-ui-runtime";
408
+
409
+ // Throws outside useTrueFoundryAgentRuntime:
410
+ const extras = trueFoundryExtras.use();
411
+
412
+ // Safe with fallback (returns default outside runtime):
413
+ const pending = trueFoundryExtras.use((e) => e.pendingApprovals, []);
414
+ ```
415
+
416
+ `TrueFoundryRuntimeExtras` fields:
417
+
418
+ | Field | Type | Purpose |
419
+ |-------|------|---------|
420
+ | `pendingApprovals` | `PendingApproval[]` | Undecided tool approvals across all threads |
421
+ | `pendingToolResponses` | `PendingToolResponse[]` | Unanswered ask-user / client-side tool prompts |
422
+ | `pendingMcpAuth` | `{ mcpServers } \| null` | MCP OAuth pause state |
423
+ | `respondToToolApproval` | `(r: { approvalId, approved, reason? }) => void` | Stage approval; batch-send when complete |
424
+ | `respondToToolResponse` | `(r: { toolCallId, content }) => void` | Stage answer; batch-send when complete |
425
+ | `resumeMcpAuth` | `() => Promise<void>` | Resume after OAuth |
426
+ | `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) |
427
+ | `draft` | `TrueFoundryDraftRuntimeExtras \| null` | Draft mode only — inline `AgentSpec`, sync state, and `updateAgentSpec` |
428
+
429
+ Per-part `respondToApproval` from assistant-ui still works for root-thread tool UIs; extras complements that for global chrome and nested renderers.
430
+
431
+ ## Cancellation
432
+
433
+ `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"`.
434
+
435
+ (Hard aborts still happen when *switching away* — starting a new run or changing sessions abandons the previous turn.)
436
+
437
+ ## Resumable streams
438
+
439
+ 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.
440
+
441
+ > **TODO:** Track the last ingested `sequenceNumber` and pass `afterSequenceNumber` on reconnect to avoid replaying already-seen events.
442
+
443
+ Contrast with the [AI SDK resumable streams guide](https://www.assistant-ui.com/docs/guides/resumable-streams), which requires a separate encoded-byte store.
444
+
445
+ ## Public API
446
+
447
+ Everything below is exported from the package root (`@truefoundry/assistant-ui-runtime`).
448
+
449
+ | Export | Kind | Purpose |
450
+ |--------|------|---------|
451
+ | `useTrueFoundryAgentRuntime` | hook | Main entry point. Returns an assistant-ui runtime bound to gateway sessions. |
452
+ | `UseTrueFoundryAgentRuntimeOptions` | type | Options for the hook (see table above). |
453
+ | `useTrueFoundryApprovals` | hook | `{ pending, respond }` for tool approvals via extras. |
454
+ | `useTrueFoundryToolResponses` | hook | `{ pending, respond }` for ask-user / `tool.response_required` prompts. |
455
+ | `useTrueFoundryMcpAuth` | hook | `{ pending, resume }` for MCP OAuth pause/resume. |
456
+ | `useTrueFoundryRespondToToolApproval` | hook | Action callback via `trueFoundryExtras.get(aui)` — works in nested renderers. |
457
+ | `useTrueFoundryRespondToToolResponse` | hook | Same pattern for tool responses. |
458
+ | `useTrueFoundryResumeMcpAuth` | hook | Same pattern for MCP resume. |
459
+ | `useTrueFoundryCancel` | hook | Same pattern for cancel. |
460
+ | `trueFoundryExtras` | namespace | `createRuntimeExtras` channel — `.use()`, `.get(aui)`, `.provide()`. |
461
+ | `TrueFoundryRuntimeExtras` | type | Shape provided into the runtime extras slot. |
462
+ | `PendingApproval`, `PendingToolResponse` | types | Derived pending items for UI rendering. |
463
+ | `createTrueFoundryThreadListAdapter` | fn | Builds the cursor-paginated `RemoteThreadListAdapter` powering `<ThreadList>` (`list({ after })` → `nextCursor`). Used internally; exported for custom wiring. |
464
+ | `createTrueFoundryDraftThreadListAdapter` | fn | Draft-session variant of the thread-list adapter (`agents.private.draftSessions.list/create/get`). |
465
+ | `createDraftSessionBridge` | fn | Reads and syncs a draft session's `AgentSpec` (`agents.private.draftSessions.get/update`). |
466
+ | `mergeAgentSpec` | fn | Immutable merge helper for partial `AgentSpec` updates. |
467
+ | `AgentSpec`, `AgentSpecUpdate`, `DraftSession` | types | Gateway inline agent definition types (re-exported). |
468
+ | `getSession` | fn | `(client, sessionId) => Promise<AgentSession>` convenience wrapper. |
469
+ | `convertTurnsToThreadMessages` | fn | Loads a session's turns and folds them into assistant-ui `ThreadMessage[]` (`ConvertTurnsResult`). |
470
+ | `buildTurnAssistantContent` | fn | Folds a single turn's events into assistant content parts. |
471
+ | `repositoryItemsFromMessages` | fn | Converts messages into `ExportedMessageRepositoryItem[]` for history export. |
472
+ | `getTurnMessageContent` | fn | Extracts the text payload from an `AppendMessage`. |
473
+ | `ConvertTurnsResult` | type | Result of `convertTurnsToThreadMessages` (`messages`, `foldState`, `runningTurn?`, `unstable_resume?`). |
474
+ | `collectApprovalInputs` | fn | Collects decided approvals from a message into `user.tool_approval` inputs. |
475
+ | `collectResponseInputs` | fn | Collects staged answers into `user.tool_response` inputs. |
476
+ | `collectRequiredActionInputs` | fn | Collects both approval + response inputs once nothing is pending. |
477
+ | `messageHasPendingApprovals` | fn | True if a message still has undecided tool approvals. |
478
+ | `messageHasPendingResponses` | fn | True if a message still has unanswered tool responses. |
479
+ | `messageHasPendingRequiredActions` | fn | True if either approvals or responses are still pending. |
480
+ | `findPausedAssistantMessage` | fn | Last assistant message in `requires-action` state. |
481
+ | `toTrueFoundryApprovalInputs` | fn | Applies an approval decision and returns gateway inputs. |
482
+ | `SubAgentArtifact`, `SubAgentCustomMetadata` | types | Shapes attached to sub-agent tool calls / nested messages. |
483
+ | `TrueFoundryMessageCustomMetadata` | type | Typed keys on `ThreadMessage.metadata.custom` written by this adapter. |
484
+ | `ROOT_THREAD_ID` | const | The literal `"main"` — the gateway's root thread id. |
485
+
486
+ ## Architecture (source map)
487
+
488
+ For contributors and agents working inside this package. Source lives in `src/`; the published entry point is `dist/index.js` (built by `tsup`).
489
+
490
+ | File | Responsibility |
491
+ |------|----------------|
492
+ | `useTrueFoundryAgentRuntime.ts` | Public hook. Wires the external-store runtime, thread-list runtime, adapters, and extras. |
493
+ | `useTrueFoundryAgentMessages.ts` | Reactive `SessionSnapshot` store: load, stream ingestion, cancel, resume; derives `messages` via pure projection; records approval/response decisions in overlay. |
494
+ | `sessionSnapshot.ts` | `SessionSnapshot` shape, required-actions overlay, and immutable wrapper helpers. |
495
+ | `truefoundryExtras.ts` | `createRuntimeExtras` namespace and `TrueFoundryRuntimeExtras` type. |
496
+ | `hooks.ts` | Consumer hooks — read selectors + action callbacks via `.get(aui)`. |
497
+ | `collectPending.ts` | Derives `pendingApprovals`, `pendingToolResponses`, `pendingMcpAuth` from messages. |
498
+ | `requiredActionInputs.ts` | Combined gate + `collectRequiredActionInputs` for batched resume. |
499
+ | `truefoundryThreadListAdapter.ts` | `RemoteThreadListAdapter` — cursor-paginated session list (`list({ after })` → `nextCursor`), create/fetch sessions. |
500
+ | `truefoundryDraftThreadListAdapter.ts` | Draft-session `RemoteThreadListAdapter` backed by `agents.private.draftSessions.*`. |
501
+ | `draftSessionBridge.ts` | Reads and syncs a draft session's `AgentSpec` via `agents.private.draftSessions.get/update`. |
502
+ | `bindDraftAgentSession.ts` | Validates a draft session via `agents.private.draftSessions.get`, then binds turns to it at `/agents/sessions/{draftSessionId}/turns`. |
503
+ | `useDraftAgentSpec.ts` | Debounced draft spec state + `agents.private.draftSessions.update` wiring. |
504
+ | `agentSpec.ts` | `AgentSpec` helpers and `mergeAgentSpec`. |
505
+ | `convertTurnMessages.ts` | `projectSessionMessages` pure projector; `buildSnapshotFromSession` history ingest; `convertTurnsToThreadMessages` wrapper; stream-event aggregation. |
506
+ | `foldPeerThreads.ts` | `PeerThreadFoldState` — folds peer/sub-agent threads under their spawning tool call. |
507
+ | `messageCustomMetadata.ts` | `TrueFoundryMessageCustomMetadata` — typed `metadata.custom` keys for this adapter. |
508
+ | `modelMessageContent.ts` | `model.message` events → assistant content parts (text, reasoning, tool calls). |
509
+ | `streamTurn.ts` | `streamTurnContent` / `resumeTurnStream` generators over `prepareTurn`/`stream`. |
510
+ | `toolApproval.ts` | Approval state, decision mapping, and `user.tool_approval` input collection. |
511
+ | `toolResponse.ts` | Ask-user response state, staging, and `user.tool_response` input collection. |
512
+ | `askUserQuestion.ts` | `ask_user_question` detection and argument parsing. |
513
+ | `mcpAuth.ts` | MCP auth-required detection and structured authorize UI metadata. |
514
+ | `turnEventHelpers.ts` | Appends approval / response / MCP-auth status onto turn updates. |
515
+ | `createSubAgent.ts` | Detects the `create_sub_agent` system tool call. |
516
+ | `extractTurnUserText.ts` / `lastUserMessageText.ts` | Text extraction helpers. |
517
+ | `sessions.ts` | `getSession` wrapper. |
518
+ | `sessionListStartTimestamp.ts` | Default `listSessions` window (1 year). |
519
+ | `constants.ts` | `ROOT_THREAD_ID = "main"`. |
520
+ | `types.ts` / `turnStreamUpdate.ts` | Shared option and update types. |
521
+
522
+ ### Invariants
523
+
524
+ - One gateway **session** ⇄ one assistant-ui **thread** (`session.id` = thread `remoteId`).
525
+ - The root thread id is always `"main"` (`ROOT_THREAD_ID`); sub-agent threads nest beneath their `create_sub_agent` tool call.
526
+ - The runtime never holds credentials — always pass a constructed `AgentSessionClient`.
527
+ - Gateway types come from `truefoundry-gateway-sdk/agents`; do not redefine event/turn shapes locally.
528
+ - A paused turn's resume `input` must include **all** pending `user.tool_approval` and `user.tool_response` events across every thread in one batch.
529
+ - Approval decisions are allow/deny only (`ApprovalDecision`); there is no `optionId` on the gateway wire.
530
+
531
+ ## Local development
532
+
533
+ From this package directory:
534
+
535
+ ```bash
536
+ pnpm build # tsup → dist/
537
+ pnpm test # vitest run
538
+ pnpm typecheck # tsc --noEmit
539
+ ```
540
+
541
+ `dist/` is generated output and is gitignored. From the repo root, `pnpm build` builds this package before the Next.js app.
542
+
543
+ ## Unsupported assistant-ui features
544
+
545
+ 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.
546
+
547
+ | Feature | Notes |
548
+ |---------|-------|
549
+ | Attachment rendering | Attachments are forwarded to the gateway on send when you provide an `AttachmentAdapter`, but user message bubbles show text only. |
550
+ | Built-in `AttachmentAdapter` | Ships as `trueFoundryAttachmentAdapter` (opt-in via `adapters.attachments`). Not applied by default. |
551
+ | Speech synthesis (`adapters.speech`) | Pass-through only. Not shipped. |
552
+ | Dictation (`adapters.dictation`) | Pass-through only. Not shipped. |
553
+ | Voice (`adapters.voice`) | Pass-through only. Not shipped. |
554
+ | Feedback (`adapters.feedback`) | Pass-through only. Ratings are not persisted to the gateway. |
555
+ | Message edit (`onEdit`) | Not wired. |
556
+ | Regenerate (`onReload`) | Not wired. |
557
+ | Message delete (`onDelete`) | Not wired. |
558
+ | Client-side tool results (`onAddToolResult`) | Not wired. |
559
+ | Tool call resume (`onResumeToolCall`) | Not wired. |
560
+ | Message queue (`queue`) | Not wired. |
561
+ | Branch switching | Not wired. |
562
+ | Thread rename / archive / delete | Thread-list adapter no-ops. |
563
+ | Thread title generation | Returns an empty stream. |
564
+ | Generative UI message parts | Not mapped from gateway events. |
565
+ | Source citation parts | Not mapped from gateway events. |
566
+ | Message import / external state | `onImport`, `onExportExternalState`, `onLoadExternalState` not wired. |
567
+ | Composer suggestions | `suggestions` not populated. |