AgentThinkingUI is complementary to LLM/agent observability platforms, not a replacement. They record, store and evaluate traces (developer-facing waterfalls, metrics, evals); AgentThinkingUI is the last-mile, user-facing view you embed in your own product to show users, operators or stakeholders what the agent did — a branded, animated, scrubbable replay.
Because the ecosystem converges on OpenTelemetry GenAI and OpenInference, there's no re-instrumentation: hand the spans your platform already has to the matching adapter.
The spans live in your backend (or the platform's API). Your server returns them to the browser as JSON; the browser converts + renders:
import { fromOpenInference, fromOTLP, fromOTLPMulti } from "agentthinkingui";
import "agentthinkingui/styles.css";
// your API returns the platform's spans for one run (OTLP/JSON, or an
// OpenInference attribute array) — nothing agentthinkingui-specific is stored
const spans = await fetch(`/api/runs/${runId}/spans`).then((r) => r.json());
const trace = fromOpenInference(spans, { asker: "you" }); // pick the adapter
<AgentThinkingUI trace={trace} /> // for your source ↓| your stack | spans are… | adapter |
|---|---|---|
| Arize Phoenix, LlamaIndex | OpenInference | fromOpenInference · fromOpenInferenceMulti |
| OpenTelemetry / OpenLLMetry (Traceloop) | OTel GenAI (OTLP) | fromOTLP · fromOTLPMulti |
| Langfuse (OTLP export) | OTel GenAI | fromOTLP |
| LangSmith / LangGraph (OTel export) | OTel GenAI | fromOTLP · fromOTLPMulti |
| agentfootprint (a saved recording) | typed agentfootprint.* events |
fromRecording |
Single agent →
from…→<AgentThinkingUI>. A team (a span tree with nestedinvoke_agentspans) →from…Multi→<MultiAgentFlow>.
Phoenix stores OpenInference spans. Query the spans for a run on your server (e.g. the Phoenix Python client / its API), return them as JSON, and render:
// server: fetch the run's OpenInference spans → JSON (Phoenix is OpenInference-native)
// client:
const spans = await fetch(`/api/phoenix/runs/${id}/spans`).then((r) => r.json());
const trace = fromOpenInference(spans, { asker: "customer" });
<AgentThinkingUI trace={trace} brand={<b>Acme Support</b>} />
// multi-agent run? the same spans, as a tree → a flow graph:
const flow = fromOpenInferenceMulti(spans, { asker: "customer" });
<MultiAgentFlow trace={flow} />Anything exporting OpenTelemetry GenAI spans (Traceloop/OpenLLMetry, the OTel
Collector, Bedrock AgentCore, OpenAI Agents SDK, Google ADK, Pydantic AI, Strands…)
feeds fromOTLP directly — pass the OTLP/JSON payload or a flat span array:
const trace = fromOTLP(otlpJson, { asker: "you" });
<AgentThinkingUI trace={trace} />These keep their own data model but can emit/export OpenTelemetry spans. Point
their OTLP export at your store, then render with fromOTLP (single) or
fromOTLPMulti (a multi-agent span tree → <MultiAgentFlow>).
An agentfootprint run does not need OTel to get here — it has its own two doors.
Live, attach the producer's agentThinkingTrace() recorder and read
getTrace(): the narration is the run's own voice. Archived, hand the
recording (or the envelope persistRecording wrote around it) to
fromRecording:
import { fromRecording } from "agentthinkingui";
// one archived run: { format: "agentfootprint.recording.v1", …, recording: {…} }
const envelope = await fetch(`/api/runs/${runId}/recording`).then((r) => r.json());
<AgentThinkingUI trace={fromRecording(envelope, { asker: "you" })} />Turn starts become prompts, LLM calls become the reasoning + cost on the ask they
drove, tool calls become ask + return (a read_skill becomes an instruction
beat), progress reported from inside a running call becomes activity on that
beat, and a skill-graph cursor hop becomes one routing line. Read back after the
fact, it reconstructs the shape of the run: derived sentences are stamped
brainSource: "framework" and facts the recording does not carry stay absent —
the views show "—", never 0.0s. Full mapping table + the honesty notes:
README → Replay a saved agentfootprint run.
OTel/OpenInference reliably capture structure (tools, timing, tokens, status)
but often not the reasoning — "the thinking" — or surrounding context; that
isn't always in the protocol. The library renders a Trace, not spans, so the
adapter is just one source. Compose the Trace from the OTel skeleton plus
whatever your app kept elsewhere, joined by spanId (the adapters stamp it):
const trace = fromOpenInference(spans); // structure (may lack the reasoning)
for (const step of trace.steps) {
const extra = yourStore.get(step.spanId); // the reasoning/context you stored
if (extra) { step.brain = extra.reasoning; step.output = { ...step.output, ...extra.context }; }
}
<AgentThinkingUI trace={trace} /> // renders the full thinking, backfilledFor content that doesn't fit the schema (a raw log blob, your own widget), render
it per step with the renderDetail slot — it appears in the inspector body:
<AgentThinkingUI trace={trace} renderDetail={(step) => <RawLog spanId={step.spanId} />} />
// <MultiAgentFlow renderDetail={…} /> forwards it to the drilled-in agent's inspectorFor a run in progress, accumulate spans as they arrive and tail the newest beat
with createMonitor + the live player:
import { createMonitor } from "agentthinkingui";
const mon = createMonitor({ format: "openinference", asker: "you" }); // or "otel"; { multi:true } → FlowGraph
stream.on("spans", (batch) => setTrace(mon.push(batch))); // batch = OTLP or array
<AgentThinkingUI live trace={trace} />The adapters stamp each step/node with its source spanId (and traceId), so you
can wire the replay back to the platform — click a beat → open that span in
Langfuse / Phoenix. The library stays platform-neutral: you supply the URL.
const url = (s) => s.spanId && `${LANGFUSE_HOST}/trace/${s.traceId}?observation=${s.spanId}`;
// render an "open ↗" affordance per step / per agent:
<AgentThinkingUI trace={trace} linkResolver={url} />
<MultiAgentFlow trace={flow} linkResolver={(node) => url(node)} onNodeOpen={(n) => /* … */} />
// or handle selection yourself (analytics, two-way highlight, custom routing):
<AgentThinkingUI trace={trace} onSelect={(step, i) => openInDashboard(step.spanId)} />Non-React host? The player also dispatches a DOM event on its root:
el.addEventListener("agentthinkingui:select", (e) => {
const { spanId, traceId, index } = e.detail; // jump to the span in your tool
});- Reply type. The data / instruction / both distinction (and the
skill/steering doc) is AgentThinkingUI's own lens — it isn't in the standards.
By default tools are
data; passopts.classify(toolName, attrs)(or set the opt-inagentthinkingui.reply_type/.skillspan attributes) to mark a tool reply as aninstruction. See the API reference. - Errors are universal: a span with status
ERROR(or anexceptionevent) renders as a red beat / red agent node — for both conventions. - Custom conventions. A vendor with different attribute keys can plug a
readerintocreateMonitor(same shape as the built-in OTel/OpenInference reader maps) — no fork needed.