Skip to content
AgentStat

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} />;
}