Examples
Complete files you can adapt. Each one type-checks against the current release.
Server agents in a Next.js app
Agents run in a route handler; the browser watches them over Server-Sent Events.
// lib/agent-stream.ts
import { createAgentStream } from '@dan-build/agentstat/sse';
// One stream per server process.
export const agentStream = createAgentStream();// app/api/agents/route.ts
import { createSseHandler } from '@dan-build/agentstat/sse';
import { agentStream } from '@/lib/agent-stream';
export const GET = createSseHandler(agentStream);// app/api/summarize/route.ts
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { tapAiSdkStream } from '@dan-build/agentstat/ai-sdk';
import { agentStream } from '@/lib/agent-stream';
export async function POST(request: Request) {
const { text } = (await request.json()) as { text: string };
const result = streamText({ model: openai('gpt-4o-mini'), prompt: `Summarize:\n\n${text}` });
// Report this run to the chart while the client reads the response.
// A failed stream is reported to the chart, then rethrown: don't let it go unhandled.
tapAiSdkStream(agentStream, 'summarizer', result.fullStream).catch(() => {});
return result.toTextStreamResponse();
}// app/agents/monitor.tsx
'use client';
import { useMemo } from 'react';
import { AgentStat } from '@dan-build/agentstat';
import { connectSse } from '@dan-build/agentstat/sse';
export default function Monitor() {
const source = useMemo(() => connectSse('/api/agents'), []);
return <AgentStat source={source} metric="tokens" anomalyDetection height={360} />;
}Protect /api/agents like any other route: pass createSseHandler a function that checks the request. On serverless platforms each instance has its own stream; see From a server.
A chat with a status line
useChat drives the chart and a status badge from one stream.
'use client';
import { useEffect, useMemo } from 'react';
import { useChat } from '@ai-sdk/react';
import { AgentStat, AgentStatusBadge, useAgentStream, useAgentTelemetry } from '@dan-build/agentstat';
import { createChatTracker } from '@dan-build/agentstat/ai-sdk';
export default function Chat() {
const stream = useAgentStream();
const track = useMemo(() => createChatTracker(stream, 'assistant', { clientTools: ['askUser'] }), [stream]);
const { messages, status, sendMessage } = useChat();
useEffect(() => track(messages, status), [track, messages, status]);
const [assistant] = useAgentTelemetry(stream);
return (
<div>
{assistant && <AgentStatusBadge agent={assistant} />}
<AgentStat source={stream} metric="tokens" anomalyDetection height={220} panel="never" />
<button type="button" onClick={() => sendMessage({ text: 'Plan a three-day trip to Lisbon.' })}>
Send
</button>
</div>
);
}A LangGraph agent on the server
// app/api/research/route.ts
import { ChatOpenAI } from '@langchain/openai';
import { createReactAgent } from '@langchain/langgraph/prebuilt';
import { createLangChainCallbacks } from '@dan-build/agentstat/langchain';
import { agentStream } from '@/lib/agent-stream';
const agent = createReactAgent({ llm: new ChatOpenAI({ model: 'gpt-4o-mini' }), tools: [] });
export async function POST(request: Request) {
const { question } = (await request.json()) as { question: string };
const result = await agent.invoke(
{ messages: [{ role: 'user', content: question }] },
{ callbacks: [createLangChainCallbacks(agentStream, { agentId: 'researcher' })] },
);
return Response.json(result.messages.at(-1)?.content ?? '');
}For a supervisor or swarm, use agentId: bySubgraph('supervisor') to give each sub-agent its own line.
A dashboard without the chart
An operations view: a banner for anomalies, a roster, a timeline per agent, and a hook to page someone.
'use client';
import { useMemo } from 'react';
import { AgentRoster, AgentTimeline, AnomalyBanner, useAgentTelemetry } from '@dan-build/agentstat';
import { connectSse } from '@dan-build/agentstat/sse';
export default function Operations() {
const source = useMemo(() => connectSse('/api/agents'), []);
const agents = useAgentTelemetry(source, {
onAnomaly: (id, anomaly) => {
if (anomaly.severity === 'critical') void fetch('/api/page', { method: 'POST', body: `${id}: ${anomaly.message}` });
},
});
return (
<section>
<AnomalyBanner agents={agents} />
<AgentRoster agents={agents} caption="Production agents" />
{agents.map((agent) => (
<div key={agent.id}>
<h3>{agent.name}</h3>
<AgentTimeline agent={agent} windowSeconds={600} />
</div>
))}
</section>
);
}Status history covers 5 minutes by default; pass historySeconds to the hook for a longer timeline.
Metrics you already compute
If a service already reports rates, poll it and send samples. Report waiting during tool calls yourself.
'use client';
import { useEffect, useMemo, useRef } from 'react';
import { AgentStat, createAgent, type AgentSample, type AgentStatRef } from '@dan-build/agentstat';
type Metrics = Record<string, AgentSample>;
export default function Polled() {
const ref = useRef<AgentStatRef>(null);
const agents = useMemo(() => [createAgent('indexer', 'Indexer'), createAgent('ranker', 'Ranker')], []);
useEffect(() => {
let timer: ReturnType<typeof setTimeout>;
// One request at a time; a failed poll waits for the next one.
const poll = async () => {
try {
const metrics = (await (await fetch('/api/metrics')).json()) as Metrics;
for (const [id, sample] of Object.entries(metrics)) ref.current?.updateAgent(id, sample);
} catch {
// keep the last values; try again in a second
}
timer = setTimeout(poll, 1000);
};
poll();
return () => clearTimeout(timer);
}, []);
return <AgentStat ref={ref} agents={agents} metric="tokens" anomalyDetection windowSeconds={300} />;
}Reproducible demo
For screenshots and visual tests, seed the simulator so every run is the same:
'use client';
import { AgentStat, demoAgents } from '@dan-build/agentstat';
export default function Demo() {
return <AgentStat agents={demoAgents} simulateData={{ seed: 42 }} metric="tokens" anomalyDetection height={360} />;
}