DevTools
`window.__HARNESS_DEVTOOLS__` — the browser-console surface for inspecting sessions, walking checkpoints, forcing syncs, and listing tools during development.
DevTools is one surface in the frontend integration thematic island — siblings of react, server, api-routes, and query.
In development, the runtime exposes a debugging interface on
window.__HARNESS_DEVTOOLS__. The surface is small on purpose —
just enough to inspect what the runtime sees, walk back through
checkpoints, and force-sync the outbox without leaving the
browser console.
Gate the wiring behind NODE_ENV !== "production" so the surface
doesn't ship in production bundles.
import {
useHarnessDevTools,
updateDevToolsSession,
} from "@pleach/core/react";
import type { HarnessDevToolsAPI } from "@pleach/core/react";Wiring
Call the hook once near the provider:
function App() {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHarnessDevTools();
}
return <HarnessProvider runtime={runtime}>...</HarnessProvider>;
}Once mounted, window.__HARNESS_DEVTOOLS__ is the active
runtime's debug surface. Refreshing the page rebuilds it.
For TypeScript shims:
// types/global.d.ts
import type { HarnessDevToolsAPI } from "@pleach/core/react";
declare global {
interface Window {
__HARNESS_DEVTOOLS__?: HarnessDevToolsAPI;
}
}The surface
| Property | Returns | Use |
|---|---|---|
session | SessionState | Current full state — messages, tools, channels |
checkpoints() | Checkpoint[] | List checkpoints for the current session |
rollback(cpId) | Promise<void> | Time-travel to a checkpoint |
tools() | ToolDefinition[] | Active tool registry |
syncStatus() | SyncStats | Version vectors, pending changes, last sync |
forceSync() | Promise<void> | Drain the outbox now |
events() | HarnessEvent[] | Recent event log entries |
interrupts() | PendingInterrupt[] | Outstanding HITL approvals |
ledger() | AuditableCall[] | In-memory audit rows (when using MemoryProviderDecisionLedger) |
session
A property, not a method. Always reflects the latest state.
__HARNESS_DEVTOOLS__.session.messages.length
__HARNESS_DEVTOOLS__.session.pendingToolCalls
__HARNESS_DEVTOOLS__.session.versionVectorFor React state debugging — when the UI looks stale, compare
session against the rendered transcript. If they disagree, the
hook's subscription got dropped.
checkpoints()
Returns the checkpoint list for the current session, in creation order (ULID-sorted).
const cps = __HARNESS_DEVTOOLS__.checkpoints();
cps.map((c) => `${c.id} — ${c.stageId} — ${new Date(c.createdAt)}`);Each checkpoint carries id, sessionId, stageId, createdAt,
and the channel snapshot map.
rollback(checkpointId)
Time-travel. Restores the session to the checkpoint and re-renders.
await __HARNESS_DEVTOOLS__.rollback("cp_018f...");The next executeMessage continues from the restored point.
Subsequent checkpoints are preserved by default — pass
{ prune: true } as a second argument to drop them:
await __HARNESS_DEVTOOLS__.rollback("cp_018f...", { prune: true });The pruning option is what you want when branching for an eval re-run.
tools()
The currently-registered tool definitions.
__HARNESS_DEVTOOLS__.tools().map((t) => t.name);
// → ["search_corpus", "calculator", "fetch_url"]Inspect schemas:
const tool = __HARNESS_DEVTOOLS__.tools().find((t) => t.name === "search_corpus");
console.log(tool.inputSchema, tool.description);Useful when the LLM is calling a tool name you don't recognize — verify it's actually in the registry.
syncStatus()
Coarse-grained sync state.
__HARNESS_DEVTOOLS__.syncStatus();
// → {
// local: { clientId, vector },
// remote: { vector },
// pending: 3,
// lastSyncedAt: 1717350000000,
// errors: []
// }For the rich shape, the React useSyncStatus hook returns the
full SyncStats + SyncError[]. DevTools is the quick-look
surface.
forceSync()
Drains the outbox immediately rather than waiting for the next
flushIntervalMs tick. Returns when the cycle completes.
await __HARNESS_DEVTOOLS__.forceSync();
__HARNESS_DEVTOOLS__.syncStatus().pending; // → 0 if cycle succeededUseful when you want to verify a write made it through before closing the tab.
events()
Recent event log entries. Returns the last N (default 100); pass a filter for typed slices:
__HARNESS_DEVTOOLS__.events();
__HARNESS_DEVTOOLS__.events({ types: ["tool.failed"], limit: 20 });
__HARNESS_DEVTOOLS__.events({ since: "01jc8..." });The shape is the same HarnessEvent shape the event log
documents.
interrupts()
Outstanding HITL approvals on the current session. Useful when the UI's approval modal isn't surfacing what the runtime is waiting on.
__HARNESS_DEVTOOLS__.interrupts();
// → [{ id, action_request, config, description, ... }]Resolve from the console:
const [pending] = __HARNESS_DEVTOOLS__.interrupts();
await runtime.resolveInterrupt(pending.id, { type: "accept", args: null });ledger()
The in-memory audit ledger contents. Only populated when the
runtime is configured with MemoryProviderDecisionLedger. Returns
empty when the production Supabase adapter is wired (use the
query API for that).
__HARNESS_DEVTOOLS__.ledger().filter((r) => r.callClass === "synthesize");
// → typically exactly one row per turnThe one-synthesize-per-turn invariant is the easiest property to
spot-check from DevTools — if you see two synthesize rows for a
single turnId, something has drifted.
updateDevToolsSession(state)
The manual push API. Normally the hook subscribes to runtime events and updates the DevTools surface automatically; this is the escape hatch for tests or imperative state writes.
import { updateDevToolsSession } from "@pleach/core/react";
updateDevToolsSession(synthesizedState);Use sparingly. The hook subscription is the supported path.
Production safety
useHarnessDevTools does not check NODE_ENV internally —
the caller is responsible. The hook body unconditionally writes
to window.__HARNESS_DEVTOOLS__; ship it in production and your
production bundle gains a debug surface and a tree-shake escape
for the underlying modules.
Three options to gate it:
// Option 1 — conditional hook (lint rule will complain; disable it):
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line react-hooks/rules-of-hooks
useHarnessDevTools();
}
// Option 2 — separate dev-only component, code-split by env:
const DevTools = process.env.NODE_ENV !== "production"
? require("./DevTools").default
: null;
// Option 3 — always-on but pre-stripped at build time via dead-code elimination:
if (false /* @__PURE__ */) useHarnessDevTools();Option 1 is the simplest. Option 2 is the cleanest for bundle size. Option 3 is for build pipelines that don't tree-shake conditionals well.
Where to go next
React
`useHarnessDevTools` and the typed `HarnessDevToolsAPI`.
HarnessServer
Framework-agnostic handlers behind the data this surface inspects.
API routes
The HTTP + SSE wire contract feeding the session state this surface mirrors.
Query
Server-side reads for the same audit/event data — DevTools is the in-memory peek; query is the persisted view.
Query
Server-only read API over persisted harness data — usage, transcripts, events, jobs, assets, tools, analytics. Uses a service-role Supabase client and bypasses RLS; never import into browser bundles.
CLI
The pleach binary ships three subcommands — dev boots a self-contained local playground; init scaffolds a route, page, and plugin stub for your framework; schema copies the Postgres bundle.