Harness UI and Studio: streaming run events, AG-UI adapter, React studio
Traceability
- Spec ID:
2026-08-15-harness-ui-studio - Status: Superseded
This historical implementation was superseded by Native Harness run streams in Studio. The AG-UI adapter and its package are no longer current product surfaces.
Intent
The Harness DSL currently runs only in batch mode: compileHarness → resolveHarness → executor.execute() returns one HarnessRunResult after the run ends, and the compare CLI writes evidence JSON to disk. Neither path gives a user any live view of a running harness, and the compare/inspector evidence has no interactive UI.
This spec adds three bounded layers without changing DSL semantics:
- Streaming run-event seam in
@qoder-ai/harness: executors emit host-neutralHarnessRunEventvalues while a run is in flight. @qoder-ai/harness-ui: a protocol adapter that translates neutral run events into AG-UI protocol events and serves them over SSE, so any AG-UI-compatible frontend (CopilotKit, custom clients) can drive a harness run.@qoder-ai/harness-studio: a React application (bundled with esbuild, served by a small Node CLI) with a live Run view fed by the AG-UI stream and a Compare view fed byharness-compare.v1evidence directories.
The DSL core stays UI-free: no grammar, IR, or resolver change. The existing static-HTML harness inspector remains the zero-dependency offline report; the studio is an additive, richer surface, not a replacement.
Acceptance scenarios
- AC-1 (neutral event seam):
QoderSdkExecutoraccepts anonRunEventlistener, requests the SDK's partial-message stream, and emits text deltas as they arrive rather than waiting for a completed assistant turn. The lifecycle-ordered sequence contains onerun-startedfirst, framedmessage-started/text-delta/message-finishedtext, pairedtool-call-started/tool-call-finishedevents,run-erroronly on failure, and exactly one final neutralrun-finishedwith the exit code and metrics. When Qoder exposes a retainedtool_result, the seam correlates it back to the originating tool-call id without leaking credential-shaped fields (the existing trace redaction applies before mapping). - AC-2 (event mapping is deterministic and tested): the lifecycle emitter and SDK-message mapping (
applyQoderSdkMessage) are exported and covered by unit tests using scripted partial and completed SDK messages, with no live SDK required. Completed assistant payloads do not duplicate text already emitted from partial messages, and completed-only messages remain a fallback for older or injected SDK implementations. - AC-3 (AG-UI translation):
@qoder-ai/harness-uitranslates a neutral event sequence into a valid AG-UI event sequence:RUN_STARTED, pairedTEXT_MESSAGE_START/TEXT_MESSAGE_CONTENT/TEXT_MESSAGE_END,TOOL_CALL_START/TOOL_CALL_ARGS/TOOL_CALL_END, optional correlatedTOOL_CALL_RESULT, and exactly one terminal event:RUN_FINISHEDon success orRUN_ERRORon failure. Open text messages are always closed before a tool call, an error, or run completion. Message and tool-call ids are namespaced byrunId, so repeated runs in one thread cannot overwrite earlier AG-UI entities. - AC-4 (SSE endpoint): the harness-ui server accepts an AG-UI
RunAgentInputPOST (threadId, runId, messages) and responds with an SSE stream (data: <json>\n\nframes) of the translated events, echoing the caller's threadId/runId. The run layer owns the outer lifecycle: failures while constructing or invoking an injected executor still produceRUN_STARTEDfollowed by oneRUN_ERROR. Browser requests are same-origin by default; explicitly allowlisted origins receive an exact CORS response, while untrusted origins, non-loopbackHost/matching-Originpairs, and non-JSON POST bodies are rejected before an executor is created. A request larger than 1 MiB receives a structured HTTP 413 response rather than a reset socket. Verified by in-process and raw-HTTP tests with injected fake executors; no network or live SDK in tests. - AC-5 (interactive studio run trace): the studio's AG-UI event reducer folds an event stream into UI state (messages, tool calls, results, run status, error). Run renders workbench-style, keyboard-expandable tool cards with a compact argument preview, formatted arguments, retained result, call id, and preparing/running/completed/failed/result-unavailable/interrupted status. Tool-result error metadata survives the neutral and AG-UI seams; a successful run without a retained result is not labelled as a completed result. Retained results are capped at 64 KiB with explicit truncation metadata. Pure model tests cover streamed arguments, result correlation, terminal settlement, failure/truncation metadata, and malformed/plain-text payload formatting; browser verification expands a real rendered tool card.
- AC-6 (studio compare view): the studio server exposes the evidence directory's
verdict.jsonvia/api/evidence, and the compare summary model derives per-variant rows (pass rate, mean score, cost) from a core-validatedHarnessCompareVerdictvalue. Malformed aggregates or trial rows produce a readable evidence error rather than crashing React. Both are covered by tests against fixture and malformed data. - AC-7 (pipeline parity): both new packages follow the existing
packages/harnesspipeline rules: npm workspace membership, root<pkg>:build/<pkg>:testscripts, CI build+test steps,prepackbuild and test,publishConfig.access: public, MIT license, repoenginesrange. - AC-8 (CLI help contract):
harness-ui --helpandharness-studio --helpprint usage and exit 0 without reading the workspace, compiling a harness, or opening a port. - AC-9 (responsive studio shell): Run controls and Compare tables remain usable at desktop and 390px-wide browser viewports. Wide evidence tables scroll inside labelled containers without increasing the document width; browser verification records console/page errors and screenshots.
- AC-10 (automated interaction regression): a Playwright test drives the built Studio through its real HTTP server, starts a scripted run, expands and collapses a failed/truncated Tool Call with the keyboard, verifies its retained details and status, checks 390px document containment, and fails on browser console/page errors. CI runs this gate once on Ubuntu/Node 22 after installing Chromium; package unit tests remain cross-platform and do not require a browser download.
- AC-11 (GitHub-owned publication):
.github/workflows/release.ymlremains the only publication entrypoint. Its manual dispatch selects exactly one of the root package,@qoder-ai/harness,@qoder-ai/harness-ui, or@qoder-ai/harness-studio, runs the complete repository check first, and publishes with the protectednpmenvironment and repository secret. Local development commands build, test, pack, and dry-run only; this change never executes a localnpm publish.
Non-goals
- No change to DSL grammar, IR version, resolver semantics, or the
advisorymaterialization cap. - No dependency on
@ag-ui/core(0.0.x, pre-stable, pulls rxjs/zod). The adapter implements the AG-UI wire format with local types; conformance is asserted by tests on the emitted JSON shapes. Revisit when AG-UI reaches a stable major. - No replacement of the static-HTML harness inspector or the compare
verdict.html; the studio reads the same evidence, it does not own it. - No Pi live-run view in the studio (the seam is executor-generic; the Qoder executor is the first emitter, and the Pi executor emits text deltas only).
- No local publication or registry mutation. Package versions and the selected release target must be reviewed in git, then publication is dispatched from the repository's protected GitHub Actions
Publish npmworkflow. - No authentication or remote deployment story for the SSE server; it binds to
127.0.0.1by default and is a local development surface. This does not waive browser-origin protection: cross-origin access requires an explicit exact-origin allowlist.
Design
.harness ── @qoder-ai/harness ── HarnessRunEvent (neutral, streaming)
│
@qoder-ai/harness-ui: AG-UI translator + SSE server
│
any AG-UI client ◄── SSE ──┤
│
@qoder-ai/harness-studio: React Run view + Compare view
(verdict.json / evidence)packages/harness/src/exec/events.ts:HarnessRunEventunion plusHarnessRunEmitter, the lifecycle guard that enforces the framing invariants (single started/finished, message frames, paired tool calls).QoderSdkExecutorandPiSdkExecutorgainonRunEvent; the mapping from redacted SDK messages lives inapplyQoderSdkMessage(). Qoder enables SDK partial messages and keeps per-parent mapping state so the later completed assistant message is a fallback, not duplicate output.packages/harness-ui:protocol.ts(AG-UI wire types),translate.ts(stateful neutral→AG-UI translator),sse.ts(frame encoding),run.ts(compile+resolve+execute with an injected executor factory),server.ts(POST /agui,GET /healthz, origin/content-type policy),cli.ts(harness-ui serve, repeatable--allow-origin).packages/harness-studio: React app undersrc/app/(pure state modulesagui-store.ts,compare-model.tskept separate from components), Node server undersrc/server/serving the esbuild bundle,/api/evidence, and an embedded harness-ui/aguiroute when a.harnessfile is provided.
Plan and tasks
- Tighten the neutral Qoder mapper around the SDK's
stream_eventcontract, retain completed-message fallback behavior, and test de-duplication. - Make the AG-UI run layer own start/terminal framing and namespace protocol entity ids with the caller's run id.
- Add a shared local-browser request policy to the standalone and embedded
/aguihandlers, plus an explicit origin allowlist for external local UIs. - Move verdict validation to the core compare owner and make Studio consume that validated contract.
- Add workbench-style interactive tool cards, correlated result rendering, responsive table containers, and verify Run/Compare in a real browser at desktop and narrow viewports.
- Keep the truncated-declaration compiler diagnostic fix as a supporting robustness change: malformed harness input must reach the AG-UI error path as diagnostics rather than an uncaught compiler exception.
- Harden the browser boundary against client-controlled Host trust and make oversized request handling return a real 413 response without destroying the socket before the response is written.
- Carry tool-result error/truncation metadata through a namespaced AG-UI custom event, distinguish failed and result-unavailable cards, memoize payload formatting, and add one built-app Playwright regression to CI.
- Extend the existing GitHub Actions publication workflow with a constrained package selector so each Harness workspace is publishable without adding a local release path.
Risks
- Duplicate streaming text: Qoder emits both partial events and a completed assistant message. Mapping state must suppress only the matching completed text and reset at the assistant boundary.
- Browser-triggered runs: a loopback listener is reachable from arbitrary webpages. Reject untrusted
Originvalues, DNS-rebinding-shaped non-loopbackHostvalues, and simple non-JSON POSTs before reading or executing the harness; never derive trust from an arbitrary client-supplied Host or emit wildcard CORS. - Protocol identity collisions: neutral emitter counters are run-local. The AG-UI adapter must namespace every correlated message/tool id consistently without changing the neutral contract.
- Evidence drift: Studio consumes persisted JSON that can be stale, truncated, or hand-edited. Validate the core verdict contract before deriving render rows and return bounded diagnostics.
- Layout regression: compare tables are intentionally wide. Constrain overflow to the table region and verify both desktop and narrow viewports.
- False tool completion:
TOOL_CALL_ENDends the streamed argument declaration, not necessarily execution. Keep the card in a running state untilTOOL_CALL_RESULTor the run terminal settles it; retain failed and result-unavailable as distinct terminal states. - Large retained results: tool output can be megabytes and otherwise amplify SSE buffering, React state, JSON parsing, and DOM work. Bound retained output at the neutral event owner, carry the original byte length, and memoize Tool Call rendering so unrelated text deltas do not reformat settled cards.
Test evidence
packages/harness/test/events.test.ts— AC-1, AC-2, including partial/full de-duplication and completed-message fallbackpackages/harness-ui/test/translate.test.ts— AC-3, including run-id namespacing and mutually exclusive terminal eventspackages/harness-ui/test/server.test.ts— AC-4, AC-8, including factory failure, same-origin/allowlisted CORS, and rejected hostile/simple requestspackages/harness-studio/test/agui-store.test.ts,packages/harness-studio/test/tool-call-model.test.ts— AC-5, including result correlation, interrupted calls, and payload formattingpackages/harness-studio/test/compare-model.test.ts,packages/harness-studio/test/server.test.ts— AC-6, AC-8, including malformed verdict rejection- CI:
harness-ui:build/harness-ui:test/harness-studio:build/harness-studio:teststeps in.github/workflows/ci.yml— AC-7 - Release: manual
Publish npmworkflow package choices plus its protected environment, full-check, and workspace-scoped publish steps — AC-11 - Browser: built Studio Run and Compare views at desktop and 390px width, console/page error inspection, and saved screenshots — AC-5, AC-6, AC-9
Validation record
npm run checkpassed on 2026-08-15: root 94 files / 1324 tests, harness 9 files / 93 tests, harness-ui 2 files / 20 tests, harness-studio 4 files / 25 tests, plus generated-source and package verification gates.npx vitest run test/skills-docs/doc-link-graph.test.mjspassed after regeneratingdocs/better-harness-doc-links.mmd.- Browser fixture verification exercised an expanded
Readtool card with formatted arguments and correlated result. At an exact 390px content viewport, the document remained 390px wide; tool cards stayed within their container and Compare tables scrolled inside their labelled regions. Browser console/page logs were empty. - Automated Playwright verification exercised a failed 64 KiB-truncated Bash result through the built Studio server at 390px, expanded and collapsed the card by keyboard, asserted document containment, saved a screenshot, and observed no console/page errors.
- Raw HTTP tests reject a matching attacker Host/Origin pair and return JSON 413 responses for both declared-length and chunked bodies over 1 MiB. The GitHub
Publish npmworkflow parsed successfully with constrained package choices; no local publication command was executed. - Workspace
npm pack --dry-rungates passed without registry mutation: Harness UI contained 17 entries (12.2 kB) including the browser-safe protocol entrypoint; Harness Studio contained 28 entries (296.1 kB) including the built React application.