From latestaiagents
Stream agent output correctly — text deltas, tool-use events, thinking deltas, progress indicators — without dropping events or blocking on long tool calls. Covers backpressure, error handling, and UX patterns. Use this skill when building agent UIs (chat, CLI), ensuring agents feel responsive, or debugging dropped/truncated streams. Activate when: agent streaming, stream events, text deltas, streaming UI, SSE agent, stream tool use.
npx claudepluginhub latestaiagents/agent-skills --plugin skills-authoringThis skill uses the workspace's default tool permissions.
**Streaming is the difference between "this agent works" and "this agent feels alive". Stream text as it generates, show tool activity, and never block the UI on a long tool call.**
Bootstraps a modular AI agent using OpenRouter SDK with extensible hooks, optional Ink TUI, and Node.js/TypeScript setup for standalone or interfaced use.
Guides building AI agents using ToolLoopAgent, workflow patterns, and AI SDK UI components like useChat for chatbots, tool calling, and generative UIs in React apps.
Implements A2A SSE streaming via message/stream for real-time task status and artifact updates, including TaskStatusUpdateEvent, TaskArtifactUpdateEvent, and re-subscription in agents.
Share bugs, ideas, or general feedback.
Streaming is the difference between "this agent works" and "this agent feels alive". Stream text as it generates, show tool activity, and never block the UI on a long tool call.
The SDK emits a typed stream of events. Core types:
| Event | When | UI treatment |
|---|---|---|
assistant with text block | Model generates text | Append to current message bubble |
assistant with thinking | Extended thinking | Show "thinking..." spinner |
assistant with tool_use | Model decides to call tool | Show tool badge ("calling web_fetch...") |
user with tool_result | Tool returned | Show result or just acknowledge |
system with progress | Tool progress notification | Update progress bar |
result | Turn complete | Remove spinner, finalize |
import { query } from "@anthropic-ai/claude-agent-sdk";
const ui = new ChatUI();
for await (const msg of query({ prompt, options: { model: "claude-sonnet-4-6" } })) {
switch (msg.type) {
case "assistant": {
for (const block of msg.message.content) {
if (block.type === "text") ui.appendText(block.text);
if (block.type === "thinking") ui.showThinking(block.thinking);
if (block.type === "tool_use") ui.showToolCall(block.name, block.input);
}
break;
}
case "user": {
for (const block of msg.message.content) {
if (block.type === "tool_result") ui.showToolResult(block.tool_use_id, block.content);
}
break;
}
case "result": {
ui.finalize(msg.result);
break;
}
}
}
Some SDK versions emit full messages (with cumulative text) rather than deltas. Handle both:
let lastText = "";
// on assistant text:
const newText = block.text.slice(lastText.length);
ui.appendText(newText);
lastText = block.text;
Check your SDK version's docs; deltas reduce UI work but complete messages simplify state.
For web UIs, bridge Node stream → SSE:
app.get("/chat", async (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.flushHeaders();
for await (const msg of query({ prompt: req.query.q, options: {...} })) {
res.write(`data: ${JSON.stringify(msg)}\n\n`);
if (typeof (res as any).flush === "function") (res as any).flush();
}
res.end();
});
Gotchas:
X-Accel-Buffering: no) if behind nginx: ping\n\n) every 15s to prevent proxy idle timeoutsfunction useAgent() {
const [messages, setMessages] = useState([]);
const [streaming, setStreaming] = useState(false);
const send = async (prompt: string) => {
setStreaming(true);
const res = await fetch("/chat?q=" + encodeURIComponent(prompt));
const reader = res.body!.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const events = buf.split("\n\n");
buf = events.pop()!;
for (const evt of events) {
if (evt.startsWith("data: ")) {
const msg = JSON.parse(evt.slice(6));
setMessages((m) => reduceAgentMsg(m, msg));
}
}
}
setStreaming(false);
};
return { messages, streaming, send };
}
If the UI renders slower than events arrive:
Users change their mind. Support interrupting a running agent:
const controller = new AbortController();
for await (const msg of query({
prompt,
options: { abortController: controller },
})) { /* ... */ }
// elsewhere:
cancelButton.onclick = () => controller.abort();
Always cancel gracefully — show "Stopped by user" instead of a broken state.
try {
for await (const msg of query({ prompt, options })) { /* ... */ }
} catch (err) {
if (err.name === "AbortError") ui.showStopped();
else if (err.status === 429) ui.showRateLimit();
else ui.showError(err.message);
}
Don't silently fail — users need to know whether to retry.
Raw tool calls are noisy. Map them to user-friendly UI:
const TOOL_LABELS: Record<string, string> = {
web_fetch: "Looking up the web",
code_execution: "Running code",
Task: "Delegating research",
};
ui.showToolCall = (name, input) => {
ui.addStatusLine(TOOL_LABELS[name] ?? `Running ${name}...`);
};
Collapse details into an expandable disclosure — power users want to see the raw call; others don't.
X-Accel-Buffering: no at proxies