InlayDocs

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, exitedVia reports 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.branchStatus currently reads only each branch's entry node. It can say done while 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.

On this page