Event streams
You forward what your agent emits — a request started, tokens arrived, a tool ran, the run finished — and AgentStat derives the token rate, time to first token, inter-token latency and status. You don't compute rates.
Create a stream
'use client';
import { AgentStat, useAgentStream } from '@dan-build/agentstat';
export default function Monitor() {
const stream = useAgentStream(); // the same stream for the component's lifetime
return <AgentStat source={stream} metric="tokens" anomalyDetection />;
}useAgentStream() gives a component its own stream. To push from anywhere in your app, create one at module scope with createAgentStream() instead. You can push before any chart mounts.
Pass the same stream object on every render. A different source (or none) starts the chart's stream data over and replays the new source's history.
Push events
stream.push('coder', { type: 'start' }); // → thinking
stream.push('coder', { type: 'tokens', count: 7 }); // → active
stream.push('coder', { type: 'tool-start', name: 'bash' }); // → waiting
stream.push('coder', { type: 'tool-end', name: 'bash' }); // → thinking
stream.push('coder', { type: 'finish' }); // → complete| Event | Fields | Effect |
|---|---|---|
start | — | A model request was sent. Status thinking; starts the time-to-first-token clock; clears open tool calls. |
tokens | count or total, optional since | Output arrived. Status active. |
tool-start | name? | A tool call began. Status waiting while any call is open. |
tool-end | name?, error? | A call returned. Status thinking once none are open. error: true counts an error. |
error | — | Something failed. Counts an error. |
finish | — | The run ended. Status complete; open tool calls are closed. |
Every event also takes an optional time.
Send finish when a run ends. An agent left active with no output looks like a stall.
Token counts
countis the number of tokens in this chunk.totalis a running count for the agent. The first one only sets the starting point; a smaller one than before starts a new count.sinceis for output that arrives as one block after the fact, such as a call reported when it ended. The tokens are plotted at their average rate over[since, time], and the time fromstarttosinceis the time to first token.
The rate adapts to how often tokens arrive, so per-chunk and batched delivery read as the same steady rate. It drops to zero as soon as the model hands off to a tool or finishes.
Tool calls and waiting
waiting means the agent is blocked on something outside the model: a tool call, subagents, a person. Quiet output is expected while waiting, so the anomaly detector only flags a wait longer than waitingStallMs (5 minutes by default).
Tool calls can nest or run in parallel. The agent stays waiting while any call is open. tool-end closes the most recent open call with the same name; without a name, the most recent unnamed call. If nothing matches, it closes the most recent call. A start clears any calls still open.
Time
time is in the performance.now() clock of the page or process that pushes. When you omit it, the event is stamped when you push it.
A time in the future is treated as now. A time earlier than something already recorded for the agent is moved up to it.
Don't send time across machines: another machine's performance.now() means nothing here. Push without it, or use the SSE adapter, which converts times for you.
Names, colors and removal
stream.describe('coder', { name: 'Coder', color: 'var(--chart-2)' });
stream.describe('coder', { config: { expectedTokensPerSec: [20, 80] } });
stream.remove('coder');Without describe, an agent is named after its id and takes the next color of the theme's palette.
You can combine a stream with the agents prop. Agents in the prop come first, in their order, and keep their names and colors. Stream agents follow in the order their first events arrived.
Late mounts and several charts
A stream keeps the last 5 minutes of messages (up to 20,000). A chart that mounts later replays them at their original times, then follows live events.
One stream can feed several charts, and the components from Without the chart as well.
What AgentStat derived
Read it from the chart's ref:
'use client';
import { useRef } from 'react';
import { AgentStat, useAgentStream, type AgentStatRef } from '@dan-build/agentstat';
export default function Monitor() {
const ref = useRef<AgentStatRef>(null);
const stream = useAgentStream();
function logStats() {
console.log(ref.current?.getStats('coder'));
// { totalTokens, ttftMs, interTokenMs, tool, statusMs }
}
return (
<>
<AgentStat ref={ref} source={stream} metric="tokens" />
<button onClick={logStats}>Log stats</button>
</>
);
}| Field | Meaning |
|---|---|
totalTokens | Tokens counted from tokens events since the agent was added. |
ttftMs | Time from the last start (or tool-end) to the first token after it. null until measured. |
interTokenMs | Typical gap between token events while streaming. null until measured. |
tool | Name of the innermost open tool call ('' if unnamed), or null. |
statusMs | How long the agent has had its current status. |
Without a chart, useAgentTelemetry returns the same numbers for every agent.
Samples instead of events
If you already have rates, report them with the ref's updateAgent. It takes a partial sample, so send only what changed:
ref.current?.updateAgent('planner', { tokensRate: 18.4 });
ref.current?.updateAgent('planner', { status: 'thinking' });
ref.current?.updateAgent('planner', { latencyMs: 820, progress: 40 });
ref.current?.updateAgent('planner', { status: 'waiting' }); // tool call, subagents, a person- Use events or
updateAgent'stokensRatefor an agent, not both. - With samples, report
waitingyourself during tool calls; otherwise a long tool call reads as a stall. - A
stuckorhallucinatingstatus set withupdateAgentholds until you change it there; events don't override it. - Invalid input (
NaN,Infinity, unknown statuses or ids) is ignored with a one-time console warning. - The positional form
updateAgent(id, tokensRate, progress, status)still works.
Progress
Progress is optional. Report it with updateAgent(id, { progress }) (0–100).
metric | Plots |
|---|---|
'auto' (default) | Progress once every visible agent reports it, otherwise token rate. |
'tokens' | Token rate. |
'progress' | Progress. An agent that hasn't reported progress has no line. |
'both' | Progress on the left axis, token rate on the right. |
Progress an agent never reported is unknown, not 0%: it reads "–" when inspected, keyboard inspection skips it, and getLiveMetrics(id).progressKnown is false.
Your own source
source accepts anything that implements AgentSource:
interface AgentSource {
subscribe(listener: (message: AgentStreamMessage) => void, after?: number): () => void;
}subscribe delivers the messages it kept with a seq greater than after, in seq order, then new ones, until the returned function is called. seq increases by one per message. A chart that subscribes again to the same source (React StrictMode does this) passes the last seq it applied, so nothing is applied twice.
In most cases it's simpler to push into a createAgentStream() from your transport. See Custom sources.