Progress and topology
Which node is doing what, at every depth.
A non-trivial agent is a graph: nodes run, branch, fan out, and nest into subagents. This is the half of the stream that answers where is it up to — as opposed to what did it say.
It is a separate subscription from the transcript. A run panel driven by these hooks re-renders on node and edge lifecycle — tens of times over a run — not on every token.
The whole run
function RunPanel() {
const run = useInlayChat(conn).useRun();
return run.nodes.map((node) => (
<li key={node.path.join("/")} data-status={node.status}>
{node.path.at(-1)}
</li>
));
}useRun() gives you every node activation at every depth, each with its
absolute path, status, and the edges that fired. It is the raw material for
a graph view or a progress list.
A node's terminal status, finishedAt, error, and reported usage are
its own lifecycle facts, independent of outgoing edges or downstream work.
A completed node may not yet have a known exit; it does not mean downstream
nodes are done.
One node
function NodeCard({ path }: { path: readonly string[] }) {
const node = useInlayChat(conn).useNode(path);
return (
<article data-active={node.active}>
<h4>{node.status}</h4>
<p>{node.content}</p>
{node.exitedVia && <footer>exited via {node.exitedVia}</footer>}
</article>
);
}useNode(path) is the per-node view, and it is reactive on both channels:
status and exitedVia from the run projection, content and reasoning from
the rows. So a card showing a node's live output updates as it streams, and
still tells you when the node finished.
exitedVia is a single port, such as a router's branch label or error: the
last matching fired edge's port, or null before any exit is known. It is not
a completion signal or the full exit set.
Known limitation. For a node visited more than once in a loop,
exitedViareports the most recent exit, which is not always the one that led to the current state.
Branch views (selectBranches / useBranches, or a generated node's
branches) instead use the latest firing's observed port set. Multiple
Collect router ports can all be taken, as can multiple wires sharing a taken
port. These are taken / untaken / pending edge flags, not downstream
progress.
Known limitation.
FanOutRow.branchStatuscurrently reads only each branch's entry node. It can saydonewhile later nodes in that branch still run; it is not a whole-branch completion signal or the edge flags above.
After a reopen
After a refresh, useRun(), useNode(), and useSubagents() rebuild from
the lifecycle facts in the fetched persisted window (node-start,
node-end, and edge rows). Live and replay use the same fold, checked by
replay_parity.rs; that does not mean every live frame has been persisted
or fetched.
Progress, visit counts, and usage totals can be partial. hasEarlier tells
you earlier persisted rows exist; loadEarlier() brings them into the
projection. See Reopening during execution
for completion persistence and its limits.
One honesty boundary: a conversation written before lifecycle rows existed
reopens with an empty projection (transcript only) rather than an approximate
one. And a subgraph's content on this page is its descendants' text rolled
up in order — each inner node's own activation is separate, so the container
shows the whole subtree's output, not one activation of it.
Subagents
A subgraph node is a whole agent nested inside this one. useSubagent(path)
gives you its status, its exit port, a rollup of how many of its inner nodes are
running or done, and its own declared outputs.
Two instances of the same reusable subgraph stay distinct, because everything is keyed by absolute path rather than node id. A research subagent used twice in one graph will not collide with itself.
Black-boxed subagents report status and exit only: they emit no inner events, so there is no inner progress to report, and a generated client omits those fields rather than handing you counts that are always zero.
Token usage
For nodes that made LLM calls, usage carries { promptTokens, completionTokens, reasoningTokens }, cumulative across their reported
completions. run.totalUsage is the per-node sum. On reopen, both cover only
completion rows in the fetched persisted window, not necessarily the whole
conversation.
The public run stream carries token counts, not authoritative billed cost. You can combine them with your own rate card for a UI estimate, but label it as an estimate: server-side metering and billing can include model-specific rates, tool usage, allowances, and credits that this projection does not carry.
Use billing's server-reported balance and usage for wallets and quota-related UI. Use Observability for operator call details; neither surface is a client-side price calculation.
Paths
Every one of these hooks takes an absolute path: the enclosing subgraphPath
plus the node's own id, outermost first. A top-level node is a one-element path.
Hand-wiring those from useRun() works. Getting them by autocomplete is nicer —
a generated client exposes agent.nodes.<name>() and
agent.subagents.<name>(), so you never type a uuid.
Typed clients.
Next
Client tools — letting the agent call into your app.