@assistant-ui/mcp-docs-server 0.2.0 → 0.2.2
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/.docs/organized/code-examples/waterfall.md +12 -13
- package/.docs/organized/code-examples/with-a2a.md +16 -11
- package/.docs/organized/code-examples/with-ag-ui.md +15 -13
- package/.docs/organized/code-examples/with-ai-sdk-v7.md +14 -13
- package/.docs/organized/code-examples/with-artifacts.md +14 -13
- package/.docs/organized/code-examples/with-assistant-transport.md +20 -28
- package/.docs/organized/code-examples/with-browser-extension.md +18 -11
- package/.docs/organized/code-examples/with-chain-of-thought.md +17 -15
- package/.docs/organized/code-examples/with-cloud-standalone.md +10 -11
- package/.docs/organized/code-examples/with-cloud.md +19 -14
- package/.docs/organized/code-examples/with-custom-thread-list.md +15 -14
- package/.docs/organized/code-examples/with-elevenlabs-conversational.md +14 -14
- package/.docs/organized/code-examples/with-elevenlabs-scribe.md +19 -17
- package/.docs/organized/code-examples/with-eve.md +66 -12
- package/.docs/organized/code-examples/with-expo.md +26 -31
- package/.docs/organized/code-examples/with-external-store.md +17 -12
- package/.docs/organized/code-examples/with-ffmpeg.md +21 -15
- package/.docs/organized/code-examples/with-generative-ui.md +271 -36
- package/.docs/organized/code-examples/with-google-adk.md +17 -12
- package/.docs/organized/code-examples/with-heat-graph.md +6 -7
- package/.docs/organized/code-examples/with-image-generation.md +9 -10
- package/.docs/organized/code-examples/with-interactables.md +14 -13
- package/.docs/organized/code-examples/with-langchain.md +12 -13
- package/.docs/organized/code-examples/with-langgraph.md +19 -13
- package/.docs/organized/code-examples/with-livekit.md +13 -13
- package/.docs/organized/code-examples/with-mcp.md +14 -15
- package/.docs/organized/code-examples/with-nuxt.md +2428 -0
- package/.docs/organized/code-examples/with-opencode.md +23 -14
- package/.docs/organized/code-examples/with-openui.md +449 -0
- package/.docs/organized/code-examples/with-pi.md +59 -57
- package/.docs/organized/code-examples/with-react-hook-form.md +16 -15
- package/.docs/organized/code-examples/with-react-ink-web.md +7 -8
- package/.docs/organized/code-examples/with-react-ink.md +6 -6
- package/.docs/organized/code-examples/with-react-router.md +17 -11
- package/.docs/organized/code-examples/with-resumable-stream.md +12 -13
- package/.docs/organized/code-examples/with-store.md +27 -16
- package/.docs/organized/code-examples/with-svelte.md +415 -0
- package/.docs/organized/code-examples/with-sveltekit.md +1061 -0
- package/.docs/organized/code-examples/with-tanstack.md +19 -13
- package/.docs/organized/code-examples/with-tap-runtime.md +15 -15
- package/.docs/organized/code-examples/with-virtualized-thread.md +8 -9
- package/.docs/organized/code-examples/with-vue.md +408 -0
- package/.docs/raw/docs/(docs)/cli.mdx +7 -2
- package/.docs/raw/docs/(docs)/index.mdx +9 -76
- package/.docs/raw/docs/(docs)/installation.mdx +6 -20
- package/.docs/raw/docs/(reference)/api-reference/context-providers/assistant-runtime-provider.mdx +24 -4
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/slack.mdx +5 -1
- package/.docs/raw/docs/(reference)/api-reference/generative-ui/teams.mdx +9 -2
- package/.docs/raw/docs/(reference)/api-reference/hooks/primitives.mdx +2 -2
- package/.docs/raw/docs/(reference)/api-reference/integrations/eve.mdx +47 -1
- package/.docs/raw/docs/(reference)/api-reference/integrations/react-ai-sdk.mdx +29 -4
- package/.docs/raw/docs/(reference)/api-reference/tools/status.mdx +1 -1
- package/.docs/raw/docs/(reference)/api-reference/utilities/miscellaneous.mdx +52 -1
- package/.docs/raw/docs/(reference)/api-reference/voice/session.mdx +1 -1
- package/.docs/raw/docs/cloud/ai-sdk-assistant-ui.mdx +2 -2
- package/.docs/raw/docs/cloud/ai-sdk.mdx +4 -4
- package/.docs/raw/docs/cloud/index.mdx +1 -1
- package/.docs/raw/docs/copilots/model-context.mdx +4 -3
- package/.docs/raw/docs/copilots/motivation.mdx +4 -4
- package/.docs/raw/docs/guides/attachments.mdx +2 -2
- package/.docs/raw/docs/guides/chain-of-thought.mdx +1 -1
- package/.docs/raw/docs/guides/context-api.mdx +15 -17
- package/.docs/raw/docs/guides/dictation.mdx +1 -1
- package/.docs/raw/docs/guides/electron.mdx +1 -1
- package/.docs/raw/docs/guides/mentions.mdx +2 -0
- package/.docs/raw/docs/guides/resumable-streams.mdx +74 -3
- package/.docs/raw/docs/guides/suggestions.mdx +15 -12
- package/.docs/raw/docs/ink/hooks.mdx +9 -4
- package/.docs/raw/docs/ink/primitives.mdx +5 -4
- package/.docs/raw/docs/integrations/attachments/custom-adapter.mdx +16 -0
- package/.docs/raw/docs/integrations/auth/better-auth.mdx +1 -1
- package/.docs/raw/docs/integrations/auth/clerk.mdx +1 -1
- package/.docs/raw/docs/integrations/auth/next-auth.mdx +1 -1
- package/.docs/raw/docs/integrations/frameworks/cloudflare-agents/overview.mdx +1 -1
- package/.docs/raw/docs/integrations/frameworks/mastra/full-stack.mdx +2 -2
- package/.docs/raw/docs/integrations/frameworks/mastra/separate-server.mdx +1 -1
- package/.docs/raw/docs/integrations/gateways/index.mdx +2 -2
- package/.docs/raw/docs/integrations/observability/helicone.mdx +2 -2
- package/.docs/raw/docs/integrations/observability/langfuse.mdx +1 -1
- package/.docs/raw/docs/integrations/observability/langsmith.mdx +2 -2
- package/.docs/raw/docs/integrations/persistence/custom-adapter.mdx +146 -128
- package/.docs/raw/docs/migrations/toolkit-tools.mdx +15 -13
- package/.docs/raw/docs/migrations/v0-15.mdx +118 -4
- package/.docs/raw/docs/primitives/attachment.mdx +2 -2
- package/.docs/raw/docs/primitives/composer.mdx +2 -2
- package/.docs/raw/docs/primitives/message.mdx +33 -1
- package/.docs/raw/docs/primitives/suggestion.mdx +4 -2
- package/.docs/raw/docs/primitives/thread.mdx +1 -1
- package/.docs/raw/docs/react-native/hooks.mdx +14 -4
- package/.docs/raw/docs/react-native/index.mdx +1 -1
- package/.docs/raw/docs/react-native/primitives.mdx +2 -2
- package/.docs/raw/docs/runtimes/ag-ui/runtime-options.mdx +35 -5
- package/.docs/raw/docs/runtimes/ai-sdk/v4-legacy.mdx +1 -1
- package/.docs/raw/docs/runtimes/ai-sdk/v5-legacy.mdx +1 -1
- package/.docs/raw/docs/runtimes/ai-sdk/v6-legacy.mdx +9 -10
- package/.docs/raw/docs/runtimes/ai-sdk/v7.mdx +9 -10
- package/.docs/raw/docs/runtimes/claude-managed-agents.mdx +118 -0
- package/.docs/raw/docs/runtimes/concepts/stability.mdx +2 -1
- package/.docs/raw/docs/runtimes/concepts/threads.mdx +106 -27
- package/.docs/raw/docs/runtimes/custom/assistant-transport.mdx +19 -1
- package/.docs/raw/docs/runtimes/custom/external-store.mdx +34 -1
- package/.docs/raw/docs/runtimes/custom/local-runtime.mdx +36 -9
- package/.docs/raw/docs/runtimes/eve/overview.mdx +51 -0
- package/.docs/raw/docs/runtimes/eve/quickstart.mdx +51 -2
- package/.docs/raw/docs/runtimes/google-adk/hooks.mdx +9 -12
- package/.docs/raw/docs/runtimes/langchain.mdx +1 -1
- package/.docs/raw/docs/runtimes/langgraph/streaming.mdx +5 -1
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-2.mdx +7 -7
- package/.docs/raw/docs/runtimes/langgraph/tutorial/part-3.mdx +4 -4
- package/.docs/raw/docs/runtimes/opencode/hooks.mdx +1 -0
- package/.docs/raw/docs/runtimes/opencode/overview.mdx +10 -0
- package/.docs/raw/docs/runtimes/pick-a-runtime.mdx +8 -1
- package/.docs/raw/docs/tools/backend.mdx +2 -2
- package/.docs/raw/docs/tools/defining-tools.mdx +28 -7
- package/.docs/raw/docs/tools/dynamic-tools.mdx +6 -4
- package/.docs/raw/docs/tools/generative-ui-primitive.mdx +180 -0
- package/.docs/raw/docs/tools/generative-ui-slack.mdx +167 -0
- package/.docs/raw/docs/tools/generative-ui-teams.mdx +160 -0
- package/.docs/raw/docs/tools/generative-ui.mdx +224 -211
- package/.docs/raw/docs/tools/index.mdx +2 -1
- package/.docs/raw/docs/tools/interactables.mdx +29 -17
- package/.docs/raw/docs/tools/mcp-apps.mdx +35 -6
- package/.docs/raw/docs/tools/mcp.mdx +9 -7
- package/.docs/raw/docs/tools/openui.mdx +175 -0
- package/.docs/raw/docs/tools/tool-ui.mdx +27 -24
- package/.docs/raw/docs/tools/user-managed-mcp.mdx +17 -8
- package/.docs/raw/docs/ui/attachment.mdx +27 -0
- package/.docs/raw/docs/ui/file.mdx +7 -2
- package/.docs/raw/docs/ui/image.mdx +1 -1
- package/.docs/raw/docs/ui/mcp-config.mdx +8 -3
- package/.docs/raw/docs/ui/model-selector.mdx +8 -8
- package/.docs/raw/docs/ui/part-grouping.mdx +1 -1
- package/.docs/raw/docs/ui/thread.mdx +24 -5
- package/.docs/raw/docs/utilities/react-o11y.mdx +7 -9
- package/dist/constants.js +2 -2
- package/dist/constants.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prepare-docs/prepare.js.map +1 -1
- package/dist/tools/docs.js +4 -2
- package/dist/tools/docs.js.map +1 -1
- package/dist/tools/examples.js +2 -1
- package/dist/tools/examples.js.map +1 -1
- package/dist/tools/resources.js +2 -1
- package/dist/tools/resources.js.map +1 -1
- package/dist/tools/tests/test-setup.js +2 -1
- package/dist/tools/tests/test-setup.js.map +1 -1
- package/dist/tools/xulux-templates.js +4 -2
- package/dist/tools/xulux-templates.js.map +1 -1
- package/dist/utils/mdx.js +2 -1
- package/dist/utils/mdx.js.map +1 -1
- package/dist/xulux/catalog-client.js +1 -1
- package/dist/xulux/catalog-client.js.map +1 -1
- package/package.json +4 -4
- package/src/tools/tests/docs.test.ts +2 -2
- package/.docs/raw/docs/tools/interactables-legacy.mdx +0 -410
|
@@ -155,12 +155,22 @@ export default defineToolkit({
|
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
```tsx title="ToolProvider.tsx"
|
|
158
|
-
import {
|
|
158
|
+
import {
|
|
159
|
+
AuiConfig,
|
|
160
|
+
AuiProvider,
|
|
161
|
+
Tools,
|
|
162
|
+
useAui,
|
|
163
|
+
} from "@assistant-ui/react-native";
|
|
159
164
|
import toolkit from "./weather-toolkit";
|
|
160
165
|
|
|
161
166
|
function ToolProvider({ children }: { children: React.ReactNode }) {
|
|
162
|
-
const aui = useAui(
|
|
163
|
-
|
|
167
|
+
const aui = useAui();
|
|
168
|
+
const config = AuiConfig({ tools: Tools({ toolkit }) });
|
|
169
|
+
return (
|
|
170
|
+
<AuiProvider extends={aui} config={config}>
|
|
171
|
+
{children}
|
|
172
|
+
</AuiProvider>
|
|
173
|
+
);
|
|
164
174
|
}
|
|
165
175
|
```
|
|
166
176
|
|
|
@@ -218,7 +228,7 @@ export function useMyToolToolkit(someOuterProp: string) {
|
|
|
218
228
|
}
|
|
219
229
|
```
|
|
220
230
|
|
|
221
|
-
Import the toolkit hook,
|
|
231
|
+
Import the toolkit hook, hoist `const aui = useAui()` and `const config = AuiConfig({ tools: Tools({ toolkit }) })` in the component body, and mount it with `<AuiProvider extends={aui} config={config}>`, as shown in the [Tools](#tools) section above.
|
|
222
232
|
|
|
223
233
|
### makeAssistantDataUI
|
|
224
234
|
|
|
@@ -69,7 +69,7 @@ export const maxDuration = 30;
|
|
|
69
69
|
export async function POST(req: Request) {
|
|
70
70
|
const { messages } = await req.json();
|
|
71
71
|
const result = streamText({
|
|
72
|
-
model: openai("gpt-5.
|
|
72
|
+
model: openai("gpt-5.6-luna"),
|
|
73
73
|
messages: await convertToModelMessages(messages),
|
|
74
74
|
});
|
|
75
75
|
return result.toUIMessageStreamResponse();
|
|
@@ -437,7 +437,7 @@ Renders message content parts via a `components` prop. Tool call and data parts
|
|
|
437
437
|
| `Reasoning` | `ReasoningMessagePartComponent` | Reasoning part renderer |
|
|
438
438
|
| `Source` | `SourceMessagePartComponent` | Source part renderer |
|
|
439
439
|
| `File` | `FileMessagePartComponent` | File part renderer |
|
|
440
|
-
| `Unstable_Audio` | `
|
|
440
|
+
| `Unstable_Audio` | `Unstable_AudioMessagePartComponent` | Audio part renderer (deprecated, render `audio/*` from `File`) |
|
|
441
441
|
| `tools` | `{ by_name?, Fallback? }` or `{ Override }` | Tool call rendering config — use `by_name` to map tool names to components, `Fallback` for unregistered tools, or `Override` to handle all tool calls |
|
|
442
442
|
| `data` | `{ by_name?, Fallback? }` | Data part rendering config — use `by_name` to map data event names, `Fallback` for unmatched events |
|
|
443
443
|
| `Empty` | `EmptyMessagePartComponent` | Component shown for empty messages |
|
|
@@ -541,7 +541,7 @@ Container `View` for an attachment.
|
|
|
541
541
|
|
|
542
542
|
### AttachmentPrimitive.Thumb
|
|
543
543
|
|
|
544
|
-
`Text` component displaying the file extension (e.g. `.pdf`).
|
|
544
|
+
`Text` component displaying the file extension (e.g. `.pdf`), or the attachment type (e.g. `image`) when the filename has no extension (including leading-dot names like `.env`). If you pass `children`, they override that text.
|
|
545
545
|
|
|
546
546
|
```tsx
|
|
547
547
|
<AttachmentPrimitive.Thumb style={styles.extension} />
|
|
@@ -16,7 +16,7 @@ Reference for the runtime's API surface. Start with [quickstart](/docs/runtimes/
|
|
|
16
16
|
| `showThinking` | `boolean` | Whether to render `THINKING_*` and `REASONING_*` events as visible reasoning. Defaults to `true`. |
|
|
17
17
|
| `autoCancelPendingToolCalls` | `boolean` | Cancel unresolved client-side tool calls automatically when the user sends, edits, or reloads a message. Defaults to `true`. See [below](#auto-cancelling-pending-tool-calls). |
|
|
18
18
|
| `onError` | `(e: Error) => void` | Error callback fired on `RUN_ERROR` events and protocol errors. |
|
|
19
|
-
| `onCancel` | `() => void` | Cancellation callback fired when
|
|
19
|
+
| `onCancel` | `() => void` | Cancellation callback fired when a run is cancelled, including user cancel and runtime teardown. |
|
|
20
20
|
| `adapters` | `UseAgUiRuntimeAdapters` | Standard adapter slots (see below). |
|
|
21
21
|
|
|
22
22
|
## Adapter slots
|
|
@@ -64,8 +64,20 @@ messages are gone on the next page load.
|
|
|
64
64
|
|
|
65
65
|
`fromAgUiMessages` accepts an optional second argument: pass
|
|
66
66
|
`{ showThinking: false }` to match a runtime configured with
|
|
67
|
-
`showThinking: false`, so
|
|
68
|
-
time, the same way a live run never stores
|
|
67
|
+
`showThinking: false`, so the readable text of an imported reasoning message is
|
|
68
|
+
dropped at conversion time, the same way a live run never stores it. An
|
|
69
|
+
`encryptedValue` on that message is kept, because it is opaque state the agent
|
|
70
|
+
needs back rather than something the option hides.
|
|
71
|
+
|
|
72
|
+
Reasoning makes the round trip in the shape it arrived in. `fromAgUiMessages` imports a `reasoning` record as an assistant message holding a reasoning part, and the run input converts that part back into a standalone `reasoning` record instead of dropping it, so a reloaded thread keeps its reasoning history on the next run. A reasoning part on an assistant message that also has text or tool calls leaves as its own `reasoning` record placed ahead of that assistant record; the AG-UI message body carries no run identity, so the original position of reasoning within a run is not recoverable.
|
|
73
|
+
|
|
74
|
+
The encrypted value survives with it. AG-UI describes it as an opaque chain-of-thought blob the client stores and forwards for state continuity, not as a signature computed over the text. An imported `ReasoningMessage` carrying `encryptedValue` keeps it at `providerMetadata.agui.encryptedValue` on the part, and a live run picks the same value up from the `REASONING_ENCRYPTED_VALUE` event (`subtype: "message"`, keyed by `entityId`), so reasoning from either source is re-emitted with the value intact and an agent that needs it back can replay it. A record whose readable `content` is empty and whose payload lives entirely in `encryptedValue`, the zero-data-retention shape AG-UI describes when an agent advertises `capabilities.reasoning.encrypted`, is preserved on the import path only. It has nothing to render by construction, so it never becomes a message or a part; it rides on `metadata.custom.agui.opaqueReasoning` of the message it sat next to on the wire and is replayed into the run input adjacent to that message. `showThinking` does not discard it. That option hides reasoning from the UI, and the encrypted value is opaque state the agent needs back rather than something rendered, so a hidden record keeps it and loses only the readable text. This is an import-path guarantee: a live run with `showThinking: false` opens no reasoning block, so a `REASONING_ENCRYPTED_VALUE` arriving during it resolves no slot and is not retained. The same thread therefore carries the value after a reload but not within the live session that produced it. A consumer calling `fromAgUiMessages` directly sees the metadata rather than a part.
|
|
75
|
+
|
|
76
|
+
Three limits apply to that record. A live stream that emits no readable content produces no reasoning part, so the runtime has nothing to attach the value to; only `fromAgUiMessages` preserves it, whether you call it yourself or the runtime calls it for you while importing a `MESSAGES_SNAPSHOT`. A record that sat between an assistant message and its own tool result is replayed after that tool result rather than between the two, because the import folds the result into the assistant message and the boundary is gone by export. A record in a snapshot that contains no other message has nothing to anchor to and is dropped.
|
|
77
|
+
|
|
78
|
+
Sending reasoning back is what the protocol asks for, and the inbound side has to handle it. `ag-ui-langgraph` is worth pinning for that reason: below 0.0.36 it raises `ValueError: Unsupported message role: reasoning` on the second turn of any thread that produced reasoning, because its converter recognised only the user, assistant, system, and tool roles. 0.0.36 skips inbound `reasoning` and `developer` records instead of raising, so the turn succeeds but the reasoning is discarded. 0.0.42 re-attaches an inbound `reasoning` record as a content block on the assistant message that follows it, encrypted content included, so the replay actually reaches the model; a record that no assistant message follows is still discarded there, which is what becomes of one replayed after the last message of a thread. Keep it at 0.0.36 or newer to avoid the error, and at 0.0.42 or newer for the replay to be worth anything.
|
|
79
|
+
|
|
80
|
+
What a server does with a replayed record remains its own choice, so treat continuity as best effort rather than guaranteed. `ag-ui-langgraph` covers all three behaviours across those three versions, and another integration may pick any of them; an `encryptedValue` reaches the provider only where the server forwards it.
|
|
69
81
|
|
|
70
82
|
`fromAgUiMessages` reconstructs the text, reasoning, and tool calls of each message. Multimodal user input (`image`, `audio`, `video`, and `document` parts, as well as legacy `binary` parts) is restored as attachments on the user message, so a backend that persists multimodal messages shows them again on reload and re-sends them on the next run. Legacy `binary` parts that only reference a file id are not restored.
|
|
71
83
|
|
|
@@ -183,8 +195,25 @@ The runtime parses the AG-UI event stream and maps each event type to assistant-
|
|
|
183
195
|
| `STATE_SNAPSHOT` | Replaces the agent's external state. |
|
|
184
196
|
| `STATE_DELTA` | Applies a JSON-patch-style delta to the agent's state. |
|
|
185
197
|
| `MESSAGES_SNAPSHOT` | Replaces the full message list (used for thread restore). |
|
|
186
|
-
| `CUSTOM` |
|
|
187
|
-
| `RAW` |
|
|
198
|
+
| `CUSTOM` | Appended to the in-flight assistant message as a `data` part. |
|
|
199
|
+
| `RAW` | Parsed and ignored; unrecognized wire event types are normalized into `RAW`. |
|
|
200
|
+
|
|
201
|
+
### Custom events
|
|
202
|
+
|
|
203
|
+
`CUSTOM` events are the protocol's extension mechanism for application-defined data. Each event is appended to the in-flight assistant message as a canonical `data` part in arrival order: `CUSTOM { name: "sources", value: {...} }` becomes `{ type: "data", name: "sources", data: {...} }`. Repeated names append separate parts, the `value` is passed through verbatim, and data parts reset with each run. Tool calls that carry a `parentMessageId` are anchored under that message rather than at their wire position, so a data part can render after a tool call that arrived later. A run that delivers its assistant message only through `MESSAGES_SNAPSHOT`, with no streamed text or tool calls, drops that run's data parts when the snapshot supersedes the in-flight message.
|
|
204
|
+
|
|
205
|
+
Render them by registering a per-name renderer; parts without a registered renderer are not displayed, unless a `Data` fallback component is registered, in which case the fallback receives every custom event name, including the framework plumbing listed below.
|
|
206
|
+
|
|
207
|
+
```tsx
|
|
208
|
+
import { useAssistantDataUI } from "@assistant-ui/react";
|
|
209
|
+
|
|
210
|
+
useAssistantDataUI({
|
|
211
|
+
name: "sources",
|
|
212
|
+
render: ({ data }) => <SourceList sources={data.sources} />,
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Data parts stay in the assistant-ui message but are not sent back to the agent, since the AG-UI assistant record has no field for them. History adapters, including the assistant-cloud one, persist them as part of the message JSON. Framework integrations emit their own plumbing over this channel (`on_interrupt`, `PredictState`, `Exit`, `hook_error`, `state_update_error`, `system:*`, `MultiAgentHandoff`), and those names surface as data parts like any other, so only register renderers for names your backend owns.
|
|
188
217
|
|
|
189
218
|
## Feature support
|
|
190
219
|
|
|
@@ -195,6 +224,7 @@ The runtime parses the AG-UI event stream and maps each event type to assistant-
|
|
|
195
224
|
| Tool calls and results | Yes |
|
|
196
225
|
| Tool result handoff (client-side execution) | Yes |
|
|
197
226
|
| State snapshots and deltas | Yes |
|
|
227
|
+
| Custom events (as `data` parts) | Yes |
|
|
198
228
|
| Cancellation | Yes |
|
|
199
229
|
| Message editing | Yes |
|
|
200
230
|
| Message reload | Yes |
|
|
@@ -35,7 +35,7 @@ import { openai } from "@ai-sdk/openai";
|
|
|
35
35
|
|
|
36
36
|
export async function POST(req: Request) {
|
|
37
37
|
const { messages } = await req.json();
|
|
38
|
-
const result = streamText({ model: openai("gpt-5.
|
|
38
|
+
const result = streamText({ model: openai("gpt-5.6-luna"), messages });
|
|
39
39
|
return result.toDataStreamResponse();
|
|
40
40
|
}
|
|
41
41
|
```
|
|
@@ -40,7 +40,7 @@ export const maxDuration = 30;
|
|
|
40
40
|
export async function POST(req: Request) {
|
|
41
41
|
const { messages }: { messages: Message[] } = await req.json();
|
|
42
42
|
const result = streamText({
|
|
43
|
-
model: openai("gpt-5.
|
|
43
|
+
model: openai("gpt-5.6-luna"),
|
|
44
44
|
messages,
|
|
45
45
|
tools: {
|
|
46
46
|
get_current_weather: tool({
|
|
@@ -92,7 +92,7 @@ export async function POST(req: Request) {
|
|
|
92
92
|
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
93
93
|
|
|
94
94
|
const result = streamText({
|
|
95
|
-
model: openai("gpt-5.
|
|
95
|
+
model: openai("gpt-5.6-luna"),
|
|
96
96
|
messages: await convertToModelMessages(messages), // async in v6
|
|
97
97
|
tools: {
|
|
98
98
|
get_current_weather: tool({
|
|
@@ -245,7 +245,7 @@ export async function POST(req: Request) {
|
|
|
245
245
|
} = await req.json();
|
|
246
246
|
|
|
247
247
|
const result = streamText({
|
|
248
|
-
model: openai("gpt-5.
|
|
248
|
+
model: openai("gpt-5.6-luna"),
|
|
249
249
|
system,
|
|
250
250
|
messages: await convertToModelMessages(messages),
|
|
251
251
|
tools: {
|
|
@@ -258,7 +258,7 @@ export async function POST(req: Request) {
|
|
|
258
258
|
}
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
Frontend tools are registered through `
|
|
261
|
+
Frontend tools are registered through the provider's `config` (see the [tools guide](/docs/tools/defining-tools)) and serialized for the backend via `frontendTools`.
|
|
262
262
|
|
|
263
263
|
## Multi-step tool calls
|
|
264
264
|
|
|
@@ -276,7 +276,7 @@ export async function POST(req: Request) {
|
|
|
276
276
|
const { messages } = await req.json();
|
|
277
277
|
|
|
278
278
|
const result = streamText({
|
|
279
|
-
model: openai("gpt-5.
|
|
279
|
+
model: openai("gpt-5.6-luna"),
|
|
280
280
|
messages: await convertToModelMessages(messages),
|
|
281
281
|
tools: {
|
|
282
282
|
/* ... */
|
|
@@ -305,7 +305,7 @@ export async function POST(req: Request) {
|
|
|
305
305
|
const { messages } = await req.json();
|
|
306
306
|
|
|
307
307
|
const result = streamText({
|
|
308
|
-
model: openai("gpt-5.
|
|
308
|
+
model: openai("gpt-5.6-luna"),
|
|
309
309
|
messages: await convertToModelMessages(messages),
|
|
310
310
|
tools: {
|
|
311
311
|
deploy: tool({
|
|
@@ -356,9 +356,9 @@ Render the gate with a toolkit entry. The `approval` field carries the gate stat
|
|
|
356
356
|
import { useChat } from "@ai-sdk/react";
|
|
357
357
|
import {
|
|
358
358
|
AssistantRuntimeProvider,
|
|
359
|
+
AuiConfig,
|
|
359
360
|
defineToolkit,
|
|
360
361
|
Tools,
|
|
361
|
-
useAui,
|
|
362
362
|
} from "@assistant-ui/react";
|
|
363
363
|
import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk";
|
|
364
364
|
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
|
@@ -397,10 +397,9 @@ export default function Page() {
|
|
|
397
397
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
398
398
|
});
|
|
399
399
|
const runtime = useAISDKRuntime(chat);
|
|
400
|
-
const
|
|
401
|
-
|
|
400
|
+
const config = AuiConfig({ tools: Tools({ toolkit }) });
|
|
402
401
|
return (
|
|
403
|
-
<AssistantRuntimeProvider
|
|
402
|
+
<AssistantRuntimeProvider runtime={runtime} config={config}>
|
|
404
403
|
<Thread />
|
|
405
404
|
</AssistantRuntimeProvider>
|
|
406
405
|
);
|
|
@@ -424,7 +423,7 @@ import { openai } from "@ai-sdk/openai";
|
|
|
424
423
|
export async function POST(req: Request) {
|
|
425
424
|
const { messages } = await req.json();
|
|
426
425
|
const result = streamText({
|
|
427
|
-
model: openai("gpt-5.
|
|
426
|
+
model: openai("gpt-5.6-luna"),
|
|
428
427
|
messages: await convertToModelMessages(injectQuoteContext(messages)),
|
|
429
428
|
});
|
|
430
429
|
return result.toUIMessageStreamResponse();
|
|
@@ -90,7 +90,7 @@ export async function POST(req: Request) {
|
|
|
90
90
|
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
91
91
|
|
|
92
92
|
const result = streamText({
|
|
93
|
-
model: openai("gpt-5.
|
|
93
|
+
model: openai("gpt-5.6-luna"),
|
|
94
94
|
messages: await convertToModelMessages(messages), // async in v7
|
|
95
95
|
tools: {
|
|
96
96
|
get_current_weather: tool({
|
|
@@ -245,7 +245,7 @@ export async function POST(req: Request) {
|
|
|
245
245
|
} = await req.json();
|
|
246
246
|
|
|
247
247
|
const result = streamText({
|
|
248
|
-
model: openai("gpt-5.
|
|
248
|
+
model: openai("gpt-5.6-luna"),
|
|
249
249
|
system,
|
|
250
250
|
messages: await convertToModelMessages(messages),
|
|
251
251
|
tools: {
|
|
@@ -260,7 +260,7 @@ export async function POST(req: Request) {
|
|
|
260
260
|
}
|
|
261
261
|
```
|
|
262
262
|
|
|
263
|
-
Frontend tools are registered through `
|
|
263
|
+
Frontend tools are registered through the provider's `config` (see the [tools guide](/docs/tools/defining-tools)) and serialized for the backend via `frontendTools`.
|
|
264
264
|
|
|
265
265
|
## Multi-step tool calls
|
|
266
266
|
|
|
@@ -280,7 +280,7 @@ export async function POST(req: Request) {
|
|
|
280
280
|
const { messages } = await req.json();
|
|
281
281
|
|
|
282
282
|
const result = streamText({
|
|
283
|
-
model: openai("gpt-5.
|
|
283
|
+
model: openai("gpt-5.6-luna"),
|
|
284
284
|
messages: await convertToModelMessages(messages),
|
|
285
285
|
tools: {
|
|
286
286
|
/* ... */
|
|
@@ -311,7 +311,7 @@ export async function POST(req: Request) {
|
|
|
311
311
|
const { messages } = await req.json();
|
|
312
312
|
|
|
313
313
|
const result = streamText({
|
|
314
|
-
model: openai("gpt-5.
|
|
314
|
+
model: openai("gpt-5.6-luna"),
|
|
315
315
|
messages: await convertToModelMessages(messages),
|
|
316
316
|
tools: {
|
|
317
317
|
deploy: tool({
|
|
@@ -368,9 +368,9 @@ Render the gate with a toolkit entry. The `approval` field carries the gate stat
|
|
|
368
368
|
import { useChat } from "@ai-sdk/react";
|
|
369
369
|
import {
|
|
370
370
|
AssistantRuntimeProvider,
|
|
371
|
+
AuiConfig,
|
|
371
372
|
defineToolkit,
|
|
372
373
|
Tools,
|
|
373
|
-
useAui,
|
|
374
374
|
} from "@assistant-ui/react";
|
|
375
375
|
import { useAISDKRuntime } from "@assistant-ui/react-ai-sdk";
|
|
376
376
|
import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai";
|
|
@@ -410,10 +410,9 @@ export default function Page() {
|
|
|
410
410
|
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
|
|
411
411
|
});
|
|
412
412
|
const runtime = useAISDKRuntime(chat);
|
|
413
|
-
const
|
|
414
|
-
|
|
413
|
+
const config = AuiConfig({ tools: Tools({ toolkit }) });
|
|
415
414
|
return (
|
|
416
|
-
<AssistantRuntimeProvider
|
|
415
|
+
<AssistantRuntimeProvider runtime={runtime} config={config}>
|
|
417
416
|
<Thread />
|
|
418
417
|
</AssistantRuntimeProvider>
|
|
419
418
|
);
|
|
@@ -439,7 +438,7 @@ import { openai } from "@ai-sdk/openai";
|
|
|
439
438
|
export async function POST(req: Request) {
|
|
440
439
|
const { messages } = await req.json();
|
|
441
440
|
const result = streamText({
|
|
442
|
-
model: openai("gpt-5.
|
|
441
|
+
model: openai("gpt-5.6-luna"),
|
|
443
442
|
messages: await convertToModelMessages(injectQuoteContext(messages)),
|
|
444
443
|
});
|
|
445
444
|
return createUIMessageStreamResponse({
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Claude Managed Agents
|
|
3
|
+
description: Connect Anthropic's Managed Agents sessions to assistant-ui with the external store runtime, folding the session event log into messages, rendering approval gates, and using sessions as the thread list.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
[Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) is Anthropic's hosted agent platform: Anthropic runs the agent loop and provisions a sandboxed container per session, and your client drives the session over an event stream. A session holds the entire conversation server-side as a durable event log, streams every step (text, tool calls, approval stops, status) as typed events, and accepts user messages and tool confirmations back.
|
|
7
|
+
|
|
8
|
+
That shape maps directly onto two assistant-ui primitives, with no adapter package in between:
|
|
9
|
+
|
|
10
|
+
- [`useExternalStoreRuntime`](/docs/runtimes/custom/external-store) renders messages you derive from the session's event log. The log is the single source of truth, so replaying a stored session and tailing a live one run through the same pure function and can never disagree.
|
|
11
|
+
- [`useRemoteThreadListRuntime`](/docs/api-reference/hooks/runtimes#useremotethreadlistruntime) turns the session list into the thread sidebar. A thread is a session; there is no conversations table anywhere in the app.
|
|
12
|
+
|
|
13
|
+
Anthropic ships an official reference implementation of this integration: the [Claude Managed Agents quickstart](https://github.com/anthropics/claude-quickstarts/tree/main/managed-agents/assistant-ui) is a complete Next.js app (composer, thread, sidebar, tool cards, approval gate) built exactly this way. This page teaches the pattern; the quickstart is the runnable proof.
|
|
14
|
+
|
|
15
|
+
<Callout type="info">
|
|
16
|
+
Managed Agents is a beta API (`managed-agents-2026-04-01`; the SDK sets the header automatically). The quickstart declares `@anthropic-ai/sdk` `^0.113.0` (the session event helpers and token previews need 0.109.0 or later), and `@assistant-ui/react` 0.14.27 or later for the toolkit API and the approval gate on the external store runtime.
|
|
17
|
+
</Callout>
|
|
18
|
+
|
|
19
|
+
## The event-to-message mapping
|
|
20
|
+
|
|
21
|
+
Everything the session emits arrives as a typed event. The integration is one pure fold from the event array to assistant-ui's message model:
|
|
22
|
+
|
|
23
|
+
| Managed Agents event | assistant-ui |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `user.message` | A user message |
|
|
26
|
+
| `agent.message` | Assistant text (buffered, authoritative) |
|
|
27
|
+
| `event_start` / `event_delta` | The same text, streamed early as token previews |
|
|
28
|
+
| `agent.thinking` | A reasoning part (progress signal only; the API sends no reasoning text) |
|
|
29
|
+
| `agent.tool_use` / `agent.mcp_tool_use` / `agent.custom_tool_use` | A tool-call part; the `toolCallId` is the event id |
|
|
30
|
+
| `agent.tool_result` (and mcp / custom variants) | That part's result |
|
|
31
|
+
| `session.status_idle` with `stop_reason: requires_action` | `requires-action` message status, plus an approval on each blocked tool part |
|
|
32
|
+
| `user.tool_confirmation` | The approval, settled (allowed or denied) |
|
|
33
|
+
| `session.status_running` / `status_idle` | Whether the turn is live (`isRunning`) |
|
|
34
|
+
| `session.error` | An error status on the message, or a retry banner |
|
|
35
|
+
|
|
36
|
+
Because the fold is pure, opening an old chat replays `sessions.events.list()` through it, and a live chat feeds the SSE tail through it, and the two paths cannot render differently. Approvals, denials, and charts all come back after a reload because they are in the log, not in browser state.
|
|
37
|
+
|
|
38
|
+
The runtime wiring is a structural subset of `ThreadMessageLike`, handed to the external store as-is:
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
const runtime = useExternalStoreRuntime<ThreadMessageLike>({
|
|
42
|
+
messages: snapshot.messages,
|
|
43
|
+
convertMessage: (m) => m,
|
|
44
|
+
isRunning: isBusy(snapshot),
|
|
45
|
+
|
|
46
|
+
onNew: async (message) => {
|
|
47
|
+
const id = await ensureSession();
|
|
48
|
+
await sendMessage(id, textOf(message));
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// The Stop button becomes a real server-side interrupt.
|
|
52
|
+
onCancel: async () => controller.interrupt(),
|
|
53
|
+
|
|
54
|
+
// The Allow / Deny click on a gated tool call. approvalId is the
|
|
55
|
+
// tool_use event id from session.status_idle { requires_action }.
|
|
56
|
+
onRespondToToolApproval: async ({ approvalId, approved, reason }) => {
|
|
57
|
+
controller.respondToApproval(approvalId, approved, reason);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Sessions are the thread list
|
|
63
|
+
|
|
64
|
+
The sidebar is a `RemoteThreadListAdapter` over the Managed Agents session API. Thread id and session id are the same string, so nothing maps between the two worlds:
|
|
65
|
+
|
|
66
|
+
| Adapter method | Managed Agents call |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| `list` | `sessions.list()`, filtered to the sessions this app created (a `metadata` tag) |
|
|
69
|
+
| `initialize` | `sessions.create()`, invoked by assistant-ui on the first message of a new chat |
|
|
70
|
+
| `rename` | `sessions.update({ title })` |
|
|
71
|
+
| `archive` | `sessions.archive()` |
|
|
72
|
+
| `unarchive` | Throws. Managed Agents sessions cannot be unarchived, and switching to an archived thread auto-unarchives by default, so do not render archived sessions as switchable (the quickstart's ownership gate rejects archived ids outright) |
|
|
73
|
+
| `delete` | `sessions.delete()` |
|
|
74
|
+
| `fetch` | `sessions.retrieve()` |
|
|
75
|
+
| `generateTitle` | Reads the title back after the server retitles the session from the first message, so the sidebar row updates without a second model call |
|
|
76
|
+
|
|
77
|
+
A brand-new chat has no session until the first send: the composer works immediately, and `initialize()` creates the session lazily when the first message (or first attachment upload) needs one. Kill the server, restart, reload, and every conversation comes back, because none of it ever lived in the app.
|
|
78
|
+
|
|
79
|
+
## The approval gate
|
|
80
|
+
|
|
81
|
+
Managed Agents supports per-tool [permission policies](https://platform.claude.com/docs/en/managed-agents/permission-policies). A tool configured as `always_ask` (the quickstart gates `bash` this way) does not run when the agent reaches for it. The session emits the `agent.tool_use` event, then parks:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{ "type": "session.status_idle", "stop_reason": { "type": "requires_action", "event_ids": ["sevt_..."] } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The fold stamps an approval onto that tool part and sets the message status to `requires-action`, which is everything assistant-ui needs to render Allow and Deny on the tool card. The click flows back through `onRespondToToolApproval` as a `user.tool_confirmation` event:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{ "type": "user.tool_confirmation", "tool_use_id": "sevt_...", "result": "deny", "deny_message": "Not on this box." }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Two wire details matter. The `tool_use_id` is the tool-use event id, not an Anthropic `toolu_` id. And a denial reaches the agent as the tool's result, so it adjusts course instead of retrying the same command.
|
|
94
|
+
|
|
95
|
+
Custom client-executed tools ride the same `requires_action` stop but take a `user.custom_tool_result` instead of a confirmation; sending a confirmation for one is a 400. The quickstart's inline chart tool is the worked example: the session parks, the card renders the chart from the tool's input, and the client answers so the agent continues.
|
|
96
|
+
|
|
97
|
+
## Token streaming
|
|
98
|
+
|
|
99
|
+
By default assistant text arrives as whole `agent.message` events when a model request finishes. Opting the stream into `event_deltas: ["agent.message", "agent.thinking"]` adds token previews: an `event_start` announces the upcoming event, `event_delta` fragments stream the text, and the buffered event lands last as the authoritative record. Concatenating a preview's deltas in arrival order yields a prefix of the final text, but under load the server may shed the remaining deltas for an event, so the prefix is not necessarily the whole message. The fold therefore appends fragments for display and discards the accumulated preview when the buffered event arrives; never treat a preview as final. When a turn errors or is interrupted, the buffered event may never arrive at all, but `span.model_request_end` still does, so close any unreconciled preview when you see it.
|
|
100
|
+
|
|
101
|
+
Previews are best-effort and gated per organization. Build against the buffered events and treat deltas as an enhancement: an org without the streaming gate runs the identical code path with replies arriving whole.
|
|
102
|
+
|
|
103
|
+
## Security boundary
|
|
104
|
+
|
|
105
|
+
The Anthropic API key stays server-side; the browser talks only to your own route handlers, which relay to Managed Agents. Because a session id arrives from the browser and becomes an API path parameter, validate ownership on every route: the id must resolve, belong to your agent, and carry your app's metadata tag before any read or write. The API key can see the whole workspace; that gate is what keeps a guessed id from reading it. The quickstart's [`ownedSession()`](https://github.com/anthropics/claude-quickstarts/blob/main/managed-agents/assistant-ui/lib/owned-session.ts) is the reference shape.
|
|
106
|
+
|
|
107
|
+
## Run the reference
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/anthropics/claude-quickstarts
|
|
111
|
+
cd claude-quickstarts/managed-agents/assistant-ui
|
|
112
|
+
npm install
|
|
113
|
+
cp .env.example .env # add ANTHROPIC_API_KEY, or `ant auth login` once
|
|
114
|
+
npm run setup # one-time: creates the agent + environment, paste the IDs into .env
|
|
115
|
+
npm run dev # drop sample_data/sales.csv into the chat
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The quickstart's [README](https://github.com/anthropics/claude-quickstarts/tree/main/managed-agents/assistant-ui#readme) walks through every file: the reducer, the session controller, the thread list adapter, the tool cards, and the attachment adapter that uploads composer files into the session sandbox. For the platform itself, start with Anthropic's [Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview) and [events reference](https://platform.claude.com/docs/en/managed-agents/events-and-streaming).
|
|
@@ -33,7 +33,8 @@ A non-exhaustive list of `unstable_` exports surfaced in the runtime docs.
|
|
|
33
33
|
| `unstable_humanToolNames` | `@assistant-ui/react` | Tool names that pause the run until a result is added via `addResult`. Only available on LocalRuntime; not supported in DataStream. |
|
|
34
34
|
| `unstable_threadListAdapter` | `@assistant-ui/react-langgraph` | LangGraph thread-list adapter slot on `useLangGraphRuntime`. |
|
|
35
35
|
| `unstable_createLangGraphStream` | `@assistant-ui/react-langgraph` | End-to-end cancellation primitive. |
|
|
36
|
-
| `unstable_Provider` | Various adapters |
|
|
36
|
+
| `unstable_Provider` | Various adapters | React-component face on `RemoteThreadListAdapter`. Must render children synchronously. `useRemoteThreadListRuntime` renders it when present; the `RemoteThreadList` store entry ignores it. |
|
|
37
|
+
| `unstable_useAdapters` | Various adapters | Hook face on `RemoteThreadListAdapter`. The `RemoteThreadList` store entry calls it. `useRemoteThreadListRuntime` synthesizes a `RuntimeAdapterProvider` from it when `unstable_Provider` is omitted. |
|
|
37
38
|
| `unstable_capabilities` | `ExternalStoreRuntime` | Toggle copy and other thread capabilities. |
|
|
38
39
|
| `unstable_state`, `unstable_annotations`, `unstable_data` | Message metadata | Runtime-internal fields exposed for advanced use cases. |
|
|
39
40
|
| `unstable_assistantMessageId`, `unstable_threadId`, `unstable_parentId`, `unstable_getMessage` | `ChatModelRunOptions` | Identifiers and accessors passed to your `ChatModelAdapter.run`. |
|
|
@@ -37,10 +37,11 @@ const cloud = new AssistantCloud({
|
|
|
37
37
|
const runtime = useLocalRuntime(modelAdapter, { cloud });
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Framework adapters take `cloud` directly
|
|
40
|
+
Framework adapters take `cloud` directly. `AISDKThreads({ cloud })` is the store-entry list for `AuiConfig` hosts. It mounts only the visible thread, so a switch cancels an in-flight run and that exchange is not persisted.
|
|
41
41
|
|
|
42
42
|
```tsx
|
|
43
43
|
const runtime = useChatRuntime({ cloud });
|
|
44
|
+
const threads = AISDKThreads({ cloud });
|
|
44
45
|
const runtime = useLangGraphRuntime({ cloud /* stream, load, ... */ });
|
|
45
46
|
const runtime = useAdkRuntime({ cloud, stream });
|
|
46
47
|
```
|
|
@@ -130,44 +131,68 @@ export function MyProvider({ children }: { children: React.ReactNode }) {
|
|
|
130
131
|
}
|
|
131
132
|
```
|
|
132
133
|
|
|
133
|
-
### Persisting messages
|
|
134
|
+
### Persisting messages
|
|
134
135
|
|
|
135
|
-
`RemoteThreadListAdapter` only manages thread metadata.
|
|
136
|
+
`RemoteThreadListAdapter` only manages thread metadata. Per-thread history and attachments are a separate seam with two faces:
|
|
137
|
+
|
|
138
|
+
- `unstable_useAdapters` is a hook. The `RemoteThreadList` store entry calls it inside the client tree, so any `createAssistantClient` host gets the same adapters as a React hook host. `useRemoteThreadListRuntime` also calls it when `unstable_Provider` is omitted.
|
|
139
|
+
- `unstable_Provider` is a React component. `useRemoteThreadListRuntime` renders it when present. The store entry ignores it.
|
|
140
|
+
|
|
141
|
+
On the store entry, wrap the thread factory with `withKey` so the thread remounts on a switch. History adapters load once per mount. An unkeyed factory keeps one instance, and the next thread's messages never appear.
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
import { withKey } from "@assistant-ui/tap";
|
|
145
|
+
import { RemoteThreadList } from "@assistant-ui/react";
|
|
146
|
+
|
|
147
|
+
threads: RemoteThreadList({
|
|
148
|
+
adapter,
|
|
149
|
+
thread: (id) => withKey(id, MyThread({ threadId: id })),
|
|
150
|
+
}),
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Share one hook between both faces:
|
|
136
154
|
|
|
137
155
|
```tsx
|
|
138
156
|
import {
|
|
139
157
|
RuntimeAdapterProvider,
|
|
140
158
|
useAui,
|
|
159
|
+
type RemoteThreadListAdapter,
|
|
141
160
|
type ThreadHistoryAdapter,
|
|
142
161
|
} from "@assistant-ui/react";
|
|
143
162
|
import { useMemo } from "react";
|
|
144
163
|
|
|
164
|
+
function useThreadListAdapters() {
|
|
165
|
+
const aui = useAui();
|
|
166
|
+
const history = useMemo<ThreadHistoryAdapter>(
|
|
167
|
+
() => ({
|
|
168
|
+
async load() {
|
|
169
|
+
const { remoteId } = aui.threadListItem.getState();
|
|
170
|
+
if (!remoteId) return { messages: [] };
|
|
171
|
+
const rows = await fetch(
|
|
172
|
+
`/api/threads/${remoteId}/messages`,
|
|
173
|
+
).then((r) => r.json());
|
|
174
|
+
return { messages: rows.map(toThreadMessage) };
|
|
175
|
+
},
|
|
176
|
+
async append({ message, parentId }) {
|
|
177
|
+
const { remoteId } = await aui.threadListItem.initialize();
|
|
178
|
+
await fetch(`/api/threads/${remoteId}/messages`, {
|
|
179
|
+
method: "POST",
|
|
180
|
+
body: JSON.stringify({ message, parentId }),
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
[aui],
|
|
185
|
+
);
|
|
186
|
+
return useMemo(() => ({ history }), [history]);
|
|
187
|
+
}
|
|
188
|
+
|
|
145
189
|
const adapterWithHistory: RemoteThreadListAdapter = {
|
|
146
190
|
// ...metadata methods above...
|
|
191
|
+
unstable_useAdapters: useThreadListAdapters,
|
|
147
192
|
unstable_Provider({ children }) {
|
|
148
|
-
const
|
|
149
|
-
const history = useMemo<ThreadHistoryAdapter>(
|
|
150
|
-
() => ({
|
|
151
|
-
async load() {
|
|
152
|
-
const { remoteId } = aui.threadListItem.getState();
|
|
153
|
-
if (!remoteId) return { messages: [] };
|
|
154
|
-
const rows = await fetch(
|
|
155
|
-
`/api/threads/${remoteId}/messages`,
|
|
156
|
-
).then((r) => r.json());
|
|
157
|
-
return { messages: rows.map(toThreadMessage) };
|
|
158
|
-
},
|
|
159
|
-
async append({ message, parentId }) {
|
|
160
|
-
const { remoteId } = await aui.threadListItem.initialize();
|
|
161
|
-
await fetch(`/api/threads/${remoteId}/messages`, {
|
|
162
|
-
method: "POST",
|
|
163
|
-
body: JSON.stringify({ message, parentId }),
|
|
164
|
-
});
|
|
165
|
-
},
|
|
166
|
-
}),
|
|
167
|
-
[aui],
|
|
168
|
-
);
|
|
193
|
+
const adapters = useThreadListAdapters();
|
|
169
194
|
return (
|
|
170
|
-
<RuntimeAdapterProvider adapters={
|
|
195
|
+
<RuntimeAdapterProvider adapters={adapters}>
|
|
171
196
|
{children}
|
|
172
197
|
</RuntimeAdapterProvider>
|
|
173
198
|
);
|
|
@@ -192,6 +217,15 @@ async append({ message, parentId }) {
|
|
|
192
217
|
|
|
193
218
|
`initialize()` is safe to call multiple times. It always resolves to the same `remoteId` for the active thread.
|
|
194
219
|
|
|
220
|
+
The same rule applies to a custom external store's dispatch. The runtime does not hold `onNew` or `onEdit` until the thread record exists (that would keep the user's message off screen for the whole roundtrip), so a handler that talks to a backend keyed by the remote identity must await `initialize()` itself:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
onNew: async (message) => {
|
|
224
|
+
const { remoteId } = await aui.threadListItem.initialize();
|
|
225
|
+
await sendToBackend(remoteId, message);
|
|
226
|
+
},
|
|
227
|
+
```
|
|
228
|
+
|
|
195
229
|
### Reloading after async authentication
|
|
196
230
|
|
|
197
231
|
If your adapter depends on a user that resolves asynchronously (oidc, `next-auth`, `better-auth`), the initial `list()` may run before the user is available. Call `aui.threads.reload()` after auth completes:
|
|
@@ -209,6 +243,45 @@ function ReloadOnAuth() {
|
|
|
209
243
|
|
|
210
244
|
`reload()` discards in-flight responses from superseded calls, so it is safe to invoke on every auth transition.
|
|
211
245
|
|
|
246
|
+
### Refetching the open thread
|
|
247
|
+
|
|
248
|
+
`reload()` re-runs `list()`, which refreshes thread list metadata only. It does not touch the messages of the thread the user is looking at. When the open thread's server state changes out of band, so that nothing arrives over the stream (a human-in-the-loop interrupt raised by another process, a stalled stream, a status change picked up by polling), call `aui.threads.reloadMainThread()`:
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
function RefetchOnInterrupt({ status }: { status: string }) {
|
|
252
|
+
const aui = useAui();
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (status !== "interrupted") return;
|
|
255
|
+
aui.threads.reloadMainThread().catch(reportError);
|
|
256
|
+
}, [status]);
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
What the promise means depends on the path. On the remount path, it resolves as soon as the replacement runtime attaches, which happens before that runtime's `load()` has started, so awaiting it does not mean the thread is fresh and a failed load cannot reach the caller. On the in-place path, it settles with the refetch itself and rejects when it fails, which is why the example above still handles the rejection.
|
|
262
|
+
|
|
263
|
+
A thread that has not been sent yet is left alone because it holds no remote state.
|
|
264
|
+
|
|
265
|
+
What happens to a run in progress depends on the path. The remount path drops the runtime that was rendering the run; whether the run itself stops is up to that hook's unmount cleanup, which core cannot enforce. On the in-place path the runtime that declared the capability decides, since core does not stop the run for it. Either way this belongs on an event rather than a short timer: drive it from a state change like the one above, or skip the call while `useAuiState((s) => s.thread.isRunning)` is true.
|
|
266
|
+
|
|
267
|
+
How the refetch happens depends on the runtime, in one of three ways. When it declares the capability, the thread runtime is reused: composer drafts survive, existing messages stay rendered while the fresh state loads, and the returned promise settles with the refetch, rejecting if it fails. A remote thread list without the capability remounts the runtime hook instead, which re-runs `load()` at the cost of discarding unsent composer input, and resolves once the new runtime attaches. The single and in-memory thread lists have no hook to remount: they take the in-place path when their tap `ExternalThread` was given `onRefetchThread`, and resolve without doing anything when it was not.
|
|
268
|
+
|
|
269
|
+
`useAuiState((s) => s.thread.capabilities.refetchThread)` reports which of those you would get, in place or not. It is not a signal for whether to offer a refresh at all: it is false on the remount path, where the call still does the work, and false again where the call does nothing.
|
|
270
|
+
|
|
271
|
+
Both the LangGraph and Google ADK adapters register the in-place refetch capability when their runtime hook receives a `load` function; without one they fall back to the remount path. Other remote adapters take the remount path unless they provide the capability themselves.
|
|
272
|
+
|
|
273
|
+
For an external store runtime, declare it with `onRefetchThread`, which is unrelated to `onReload` (that one re-generates an assistant message); the tap `ExternalThread` accepts the same prop:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
useExternalStoreRuntime({
|
|
277
|
+
messages,
|
|
278
|
+
onNew,
|
|
279
|
+
onRefetchThread: async () => {
|
|
280
|
+
setMessages(await fetchMessages(threadId));
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
212
285
|
### Paginating the thread list
|
|
213
286
|
|
|
214
287
|
If your backend returns thread pages, return a `nextCursor` from `list()` and consume `aui.threads.hasMore` plus `aui.threads.loadMore()` in the UI. The runtime threads `params.after` back through `list()` on every `loadMore()`; the initial call passes no `params`, so treat a missing `after` as "first page". `reload()` resets the cursor so the next load starts from page 1 again.
|
|
@@ -326,9 +399,15 @@ A few invariants worth knowing when wiring a custom UI on top of `loadMore()`:
|
|
|
326
399
|
},
|
|
327
400
|
{
|
|
328
401
|
name: "unstable_Provider",
|
|
329
|
-
type: "
|
|
402
|
+
type: "RemoteThreadListProviderComponent",
|
|
403
|
+
description:
|
|
404
|
+
"Optional React wrapper rendered around each active thread by useRemoteThreadListRuntime when present. Inject thread-scoped adapters here. Omit it to let that host use unstable_useAdapters.",
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: "unstable_useAdapters",
|
|
408
|
+
type: "() => RuntimeAdapters | null | undefined",
|
|
330
409
|
description:
|
|
331
|
-
"Optional
|
|
410
|
+
"Optional hook called by the RemoteThreadList store entry, and by useRemoteThreadListRuntime when unstable_Provider is omitted. Per-thread history requires the thread factory to be keyed with withKey.",
|
|
332
411
|
},
|
|
333
412
|
]}
|
|
334
413
|
/>
|