# DevTools (/docs/devtools)



DevTools is one surface in the **frontend integration**
[thematic island](/docs/concept-clusters#what-lives-outside-the-cluster-pattern) —
siblings of [react](/docs/react), [server](/docs/server),
[api-routes](/docs/api-routes), and [query](/docs/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.

```typescript
import {
  useHarnessDevTools,
  updateDevToolsSession,
} from "@pleach/core/react";
import type { HarnessDevToolsAPI } from "@pleach/core/react";
```

<SourceMeta source="{ label: &#x22;src/dev/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/dev&#x22; }" />

## Wiring [#wiring]

Call the hook once near the provider:

```tsx
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:

```typescript
// types/global.d.ts
import type { HarnessDevToolsAPI } from "@pleach/core/react";

declare global {
  interface Window {
    __HARNESS_DEVTOOLS__?: HarnessDevToolsAPI;
  }
}
```

## The surface [#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` [#session]

A property, not a method. Always reflects the latest state.

```javascript
__HARNESS_DEVTOOLS__.session.messages.length
__HARNESS_DEVTOOLS__.session.pendingToolCalls
__HARNESS_DEVTOOLS__.session.versionVector
```

For React state debugging — when the UI looks stale, compare
`session` against the rendered transcript. If they disagree, the
hook's subscription got dropped.

## `checkpoints()` [#checkpoints]

Returns the checkpoint list for the current session, in creation
order (ULID-sorted).

```javascript
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)` [#rollbackcheckpointid]

Time-travel. Restores the session to the checkpoint and re-renders.

```javascript
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:

```javascript
await __HARNESS_DEVTOOLS__.rollback("cp_018f...", { prune: true });
```

The pruning option is what you want when branching for an eval
re-run.

## `tools()` [#tools]

The currently-registered tool definitions.

```javascript
__HARNESS_DEVTOOLS__.tools().map((t) => t.name);
// → ["search_corpus", "calculator", "fetch_url"]
```

Inspect schemas:

```javascript
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()` [#syncstatus]

Coarse-grained sync state.

```javascript
__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()` [#forcesync]

Drains the outbox immediately rather than waiting for the next
`flushIntervalMs` tick. Returns when the cycle completes.

```javascript
await __HARNESS_DEVTOOLS__.forceSync();
__HARNESS_DEVTOOLS__.syncStatus().pending; // → 0 if cycle succeeded
```

Useful when you want to verify a write made it through before
closing the tab.

## `events()` [#events]

Recent event log entries. Returns the last N (default 100); pass
a filter for typed slices:

```javascript
__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](/docs/event-log)
documents.

## `interrupts()` [#interrupts]

Outstanding HITL approvals on the current session. Useful when
the UI's approval modal isn't surfacing what the runtime is
waiting on.

```javascript
__HARNESS_DEVTOOLS__.interrupts();
// → [{ id, action_request, config, description, ... }]
```

Resolve from the console:

```javascript
const [pending] = __HARNESS_DEVTOOLS__.interrupts();
await runtime.resolveInterrupt(pending.id, { type: "accept", args: null });
```

## `ledger()` [#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](/docs/query) API for that).

```javascript
__HARNESS_DEVTOOLS__.ledger().filter((r) => r.callClass === "synthesize");
// → typically exactly one row per turn
```

The 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)` [#updatedevtoolssessionstate]

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.

```typescript
import { updateDevToolsSession } from "@pleach/core/react";

updateDevToolsSession(synthesizedState);
```

Use sparingly. The hook subscription is the supported path.

## Production safety [#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:

```tsx
// 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 [#where-to-go-next]

<Cards>
  <Card title="React" href="/docs/react" description="`useHarnessDevTools` and the typed `HarnessDevToolsAPI`." />

  <Card title="HarnessServer" href="/docs/server" description="Framework-agnostic handlers behind the data this surface inspects." />

  <Card title="API routes" href="/docs/api-routes" description="The HTTP + SSE wire contract feeding the session state this surface mirrors." />

  <Card title="Query" href="/docs/query" description="Server-side reads for the same audit/event data — DevTools is the in-memory peek; query is the persisted view." />
</Cards>
