Read-only viewer¶
A developer tool: run a registered agent on an input, written in a form or started from a sample, and inspect the envelope it produced. It is
not an end-user interface; end users reach the agents through the Data Director app (app/).
Three HTML pages, no build step: Chat (index.html, the page that loads by default), Inspect (inspect.html) and
Agents (agents.html), switched by
the tabs under the header. Each is markup only; the behaviour is in native ES modules under js/
and the styles in viewer.css:
| File | Holds |
|---|---|
js/common.js |
DOM and formatting helpers, the per-tab input cache, the AG-UI transport, cross-page links |
js/glossary.js |
GLOSSARY, the text behind every ? tip and the Glossary dialog |
js/help.js |
the ? tips and the Glossary dialog |
js/registry.js |
the agent manifest and samples, the agent picker and its detail card, the input-class and Start from pickers |
js/payload.js |
the RJSF payload view and the derivation badges (Inspect only) |
js/inputform.js |
the editable RJSF input form, built from an input class's schema |
js/agentcard.js |
an A2A agent card laid out to read (Agents only) |
js/inspect.js, js/chat.js, js/agents.js |
each page's own behaviour |
Paths are absolute under /viewer/, because Chat is also served at /. The pages load React and
react-jsonschema-form (RJSF) from a CDN as ES modules and render an Envelope against the
contract's generated JSON Schema (/schema/envelope.schema.json). The agent and sample pickers are
filled from GET /agents (the registry's manifest, Agents) and GET /samples; the viewer names
no agent. A new agent's payload renders as soon as its class is in the LinkML schema; its badges come from
the agent's derivations in the manifest (Agents).
Serve it with uv run workbench serve and open http://127.0.0.1:8000/viewer/. The pages need
network access to esm.sh for the libraries; everything else is local. Each library is pinned to
an exact version, React's included in RJSF's ?deps=, so the page loads the same code every time
and the browser tests can replay it (see below).
The two pages share only what the browser tab holds: an input sent from Chat is kept in
sessionStorage, so Inspect → on a reply shows that turn's input, and Back returns to the same
conversation. Addresses from before Inspect had its own page (/viewer/?invocation_id=…) are
forwarded to inspect.html.
What Inspect shows¶
The left column holds the controls: an agent picker grouped by id prefix, with a card describing
the chosen agent (description, grounding mode, accepted input classes, requirement ids;
unavailable agents are listed in their own group, with their reason), the input form, Run
(Ctrl+Enter), and the stored runs.
The input form is built by RJSF from the input class's definition in the contract's generated
/schema/invocation_request.schema.json (its $defs), so a new input class gets a form as soon
as it is in the LinkML schema. An agent that accepts more than one class gets an Input class
picker. Start from fills the form from one of the samples of that class, or leaves it blank;
the user edits it from there. Run checks the form against the schema first and shows any errors
beside their fields. The workbench checks the input again before any agent runs, so the form is
a convenience, not a gate. The class designator schema_class is set by the chosen class and is
not a field. The form uses the schema alone, in LinkML slot order. The right column shows one run:
- Summary:
outcome.status,reason_code,agent_id@agent_version,grounding_mode,completed_at,invocation_id(copyable) and the statement; Problem Details when present. - Input | Payload side by side. The input shown is exactly what was sent. It is known only for runs started from the same browser tab; the envelope stores the input's hash, not the input, so a replay shows the hash instead.
- Payload, rendered by RJSF from the payload class in the envelope schema's
$defs. The uiSchema is worked out per render (payloadUiinjs/payload.js), not shipped by the agent: fields in schema order;schema_classand the payload's owngrounded_onhidden; an item'sgrounded_onfirst, resolved through evidence to the record it names. Custom templates present it as a document rather than a disabled form: label/value pairs, and an array of objects as rows under a header. - Derivation badges. The agent's manifest declares, per field, how its value came about
(
AgentSpec.derivations):model(written by a language model),template/lexical/registry. An undeclared field isverified(set by the harness or read from a registry; the unbadged default). Where a declaration namesrecorded_in, the badge reads that sibling's live value and the sibling is hidden, so a rationale that fell back to the template is badged as template even though the slot is normally model-written. - Evidence as a table: source, retrieval time, canonicalisation, snapshot and content hash,
with a cited marker where the hash appears in the payload's
grounded_on. The marker is a visual cross-check; the grounding linter is what enforces it. - Telemetry: trace id, model, token counts, and the energy slots (null /
not_measured). - Envelope JSON and Input JSON tabs, with copy and download.
- Built-in help. A
?button beside each contract term (envelope, outcome status,grounding_mode,canonicalisation,snapshot_ref, the derivation badges, …) opens a short explanation: hover to read it, click to keep it open,Escto close. The Glossary button in the header lists every term. The text lives in oneGLOSSARYobject injs/glossary.js, written from the definitions insdk/src/dd_sdk/schema/data_director.yaml; keep the two in step. A payload field's?shows the field's own schema description, so an agent documents its payload by describing its LinkML slots.
Loads a run by ?invocation_id= (AG-UI replay from the store), from the stored-runs list, or by
running a chosen agent on the input in the form.
What Chat shows¶
An agent picker (the orchestrator by default, found by grounding mode delegation), the stored
conversations, the transcript and a composer. An agent that accepts a Message gets a text box;
any other agent gets the same input form as Inspect, started from a sample or blank. The form is
kept across turns, so the next turn starts from the last input sent. Every reply card names
agent_id@agent_version, nests the invocations it delegated, and links to its envelope in
Inspect. ?conversation_id= reopens a conversation (GET /conversations/{id}).
The envelope-level uischema.json is kept as the reference description of the envelope and is
still served at /schema/uischema.json, but the page no longer renders the envelope through RJSF:
the summary, evidence and telemetry are laid out directly. TODO(#252): decide whether to drop it.
What Agents shows¶
The registry the workbench built at start-up (GET /agents/{id}), one agent at a time: its
status, the resolved spec, and its A2A agent card laid out to read (js/agentcard.js): name,
version and provider; the interfaces it can be reached on; its capabilities; each extension,
with the Data Director agent-spec/v0 params shown field by field and each schema named by its
digest; its security schemes; and a card per skill with tags and examples. The raw JSON is kept
below, behind a disclosure, with a Copy button. ?agent_id= selects an agent.
The page shows what the card says; it does not check it against the A2A specification. To do
that, run the A2A Inspector beside the workbench
and point it at the agent's URL in agents.yaml. It validates the card and lets you send
messages and watch the raw traffic.
Not here, on purpose¶
No editing of a stored run (composing a new input is not editing one), no feedback capture, no streaming, no suspended interrupt. Those arrive with the AG-UI events that need them.
Tests¶
tests/test_viewer.py checks that every agent's derivations name each of its payload's
derivation fields as some field's recorded_in, that the base covers the envelope only, that the
glossary explains every outcome status, grounding mode and derivation value, that the pages name
no agent or sample, that both pages carry the same mode tabs, that every file a page or module
references exists, and that every esm.sh module is pinned and recorded for the browser tests.
browser_tests/ loads the pages in Chromium with Playwright (#9) against the workbench app over
in-memory agents: a run on Inspect with its outcome, derivation badges and cited evidence; an
abstention; a replay by ?invocation_id= from the run store; an unknown run; a delegated Chat
turn; the Glossary from the keyboard; and the Agents page. test_viewer_accessibility.py checks
Chat, Inspect after a run, the Glossary and the Agents page against WCAG 2.2 AA with axe-core, in
light and dark, and for reflow at 320 CSS pixels (#85); .github/workflows/accessibility.yml
runs it. A test fails on any script error,
console error or request that leaves the machine. The esm.sh modules are replayed from
browser_tests/esm.sh.har.zip, so the tests need no network; after changing a pinned version,
re-record it, which does reach esm.sh:
uv run playwright install chromium # once
uv run pytest workbench/browser_tests # replays the HAR
uv run pytest workbench/browser_tests --update-har # re-records it first
The browser tests carry no requirement marker: the viewer is a development tool, so what it shows
substantiates no requirement. They are not collected by uv run pytest, which needs no browser.