Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Message Queue
Queueing decides what happens when the user sends a message while a reply is still streaming: wait, ignore it, or stop the current run.
useChat has a message queue for this. By default, the new message waits and sends after the current run succeeds.
Quick setup#
1. Show the queue next to the messages#
useChat returns queue apart from messages. cancelQueued(id) removes an item before it sends:
import { useState } from "react";
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function Chat() {
const [input, setInput] = useState("");
const { messages, queue, sendMessage, cancelQueued } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
queue: { whenBusy: "queue", maxSize: 5 },
});
return (
<>
{messages.map((message) => (
<div key={message.id}>
{message.parts.map((part, index) =>
part.type === "text" ? <p key={index}>{part.content}</p> : null,
)}
</div>
))}
{queue.map((item) => (
<div key={item.id} className="pending">
{typeof item.content === "string" ? item.content : "[attachment]"}
<button type="button" onClick={() => cancelQueued(item.id)}>
Cancel
</button>
</div>
))}
<form
onSubmit={(event) => {
event.preventDefault();
if (input.trim() === "") {
return;
}
sendMessage(input);
setInput("");
}}
>
<input
value={input}
onChange={(event) => setInput(event.target.value)}
/>
<button type="submit">Send</button>
</form>
</>
);
}The send button stays active during a stream. Give queue items a different style from messages, so the user can see what is still waiting.
2. Pick the rule for a busy send#
Set whenBusy in the queue option. A string is a short form of { whenBusy }, so queue: "interrupt" also works.
- "queue" (default): hold the message. It sends after the current run succeeds.
- "drop": ignore the send. The promise still resolves, but the message never shows in queue or messages. If you want the user to try again, keep the composer text.
- "interrupt": stop the current stream and send the new message now. Messages that already wait in the queue stay there. They send after the interrupting message succeeds.
3. Change the rule for one send#
Pass options as the second argument to sendMessage:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function InterruptButton() {
const { sendMessage } = useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
});
return (
<button
type="button"
onClick={() =>
sendMessage("Never mind, do this instead", {
whenBusy: "interrupt",
body: { source: "composer" },
})
}
>
Do this instead
</button>
);
}body is extra JSON for this one request. The client merges it into forwardedProps.
How it works#
The client is busy in three cases:
- a stream is active
- a send is in progress
- the queue is sending its items
A send that arrives while the client is busy follows whenBusy. The queue sends its items only after a run ends with success. If the run fails or stops, the queue empties without sending.
Advanced#
All queue options#
| Option | Values | Default | What it does |
|---|---|---|---|
| whenBusy | "queue", "drop", "interrupt" | "queue" | The rule for a send while the client is busy. |
| drain | "fifo", "batch" | "fifo" | "fifo" sends queued items one at a time, in order. "batch" merges the queue into one send. |
| maxSize | a non-negative integer | no limit | How many messages can wait. 0 means that nothing waits. |
| onOverflow | "reject", "drop-oldest" | "reject" | "reject" ignores a send when the queue is full. "drop-oldest" removes the oldest item to make room. |
How "batch" merges the items:
- Text items join with \n.
- Multimodal parts stay in order.
- The body of the last item wins.
A strategy function#
For control over each send, pass a function as queue. It gets the pending message, the reason the client is busy, and the current queue. It returns an action:
import { useChat, fetchServerSentEvents } from "@tanstack/ai-react";
export function useQueuedChat() {
return useChat({
connection: fetchServerSentEvents("/api/chat"),
threadId: "support-chat",
queue: ({ pending, busyReason, queued }) => {
// Let the user cut in with a message that starts with "!".
if (typeof pending.content === "string" && pending.content.startsWith("!")) {
return { action: "interrupt" };
}
// Keep at most 3 waiting messages during a stream.
if (busyReason === "streaming" && queued.length >= 3) {
return { action: "drop" };
}
return { action: "queue" };
},
});
}busyReason is one of "streaming", "sendInFlight", or "draining". A strategy function always sends the queue in FIFO order. A whenBusy on one sendMessage call still wins over the function.
When the queue sends and when it empties#
- Send. The queue sends its items only after a stream ends with success. This includes the end of tool continuations.
- Empty without sending. The queue empties when the active run fails or stops (stop() or a real stream error). It also empties on clear(), unsubscribe(), and reload().
- interrupt keeps the queue. Items that already wait stay in the queue. They send after the interrupting turn succeeds.