Debugging agent runs
Harn provides several tools for inspecting, replaying, and evaluating agent runs. This page walks through the debugging workflow.
Source-level debugging#
For step-through debugging, start the Debug Adapter Protocol server. It speaks DAP over stdio and is normally launched by an editor, but any DAP client can drive it:
harn dap
The standalone harn-dap binary also starts the same server.
In VS Code, the Harn extension contributes a harn debug configuration
automatically. The equivalent launch.json entry is:
{
"type": "harn",
"request": "launch",
"name": "Debug Current Harn File",
"program": "${file}",
"cwd": "${workspaceFolder}"
}
This supports line breakpoints, variable inspection, stack traces, and step
in / over / out against .harn files.
Privileged host-call bridge (harnHostCall)#
The debug adapter advertises supportsHarnHostCall: true in its
Capabilities response. Trusted, provenance-stamped host bridge modules may
use the privileged host_call(capability, operation, params) wire; it is not a
general script API and cannot be imported or re-exported by ordinary modules.
When such a bridge call has no built-in handler, the adapter forwards it to the
DAP client as a reverse request named harnHostCall — mirroring the DAP
runInTerminal pattern:
{"seq": 17, "type": "request", "command": "harnHostCall",
"arguments": {"capability": "workspace", "operation": "project_root",
"params": {}}}
The client replies with a normal DAP response:
{"seq": 18, "type": "response", "request_seq": 17, "command": "harnHostCall",
"success": true, "body": {"value": "/Users/x/proj"}}
On success: true, the adapter returns the body's value field (or the
whole body when value is absent) to the script. On success: false,
the adapter throws VmError::Thrown(message) so scripts can try /
catch the failure like any other Harn exception. Clients that do not
implement harnHostCall still work — the script just sees the
standalone fallbacks (workspace.project_root, workspace.cwd, etc.).
LLM telemetry output events#
During run / step-through, the adapter forwards every harness.llm.call the
VM makes as a DAP output event with category: "telemetry" and a
JSON body:
{"category": "telemetry",
"output": "{\"call_id\":\"…\",\"model\":\"…\",\"input_tokens\":…,\"output_tokens\":…,\"cost_usd\":…,\"cache_read_tokens\":…,\"cache_write_tokens\":…,\"duration_ms\":…,\"iteration\":…}"}
IDEs can parse these to show a live LLM-call ledger alongside the
debug session. These are the same normalized accounting fields returned by
harness.llm.call; the debugger does not re-price or rename them.
Run views#
Every completed harn run invocation writes a run record under .harn-runs/.
Agent loops and workflow_execute() add their richer lifecycle records there.
Inspect records through the stable harn.run_view.v1 /
harn.session_view.v1 projections rather than depending on private record
fields.
# List recent runs
ls .harn-runs/
# Inspect a stable run view
harn runs view --json .harn-runs/<run-id>.json
The view command shows a structured summary: stages executed, tools called,
token usage, timing, final output, and redacted execution evidence. Its
evidence.trace_spans tree preserves ordered span events, so an IDE or trace
viewer can show named sub-phases without reading Harn's private run-record
format.
Record the exact code path#
Use the flight recorder when a normal span tree doesn't show which branch or instruction ran:
harn run --flight-recorder main.harn
Harn prints the artifact path to stderr and links the same artifact from the automatic run record. The recording stores source locations, function and task identities, instruction offsets, and opcode names. It never stores runtime values, arguments, results, or stack contents.
The recorder keeps the newest 250,000 events in memory. Older events increment
dropped_events; their absence never looks like a complete trace. Harn keeps
the newest 16 default-location files. Change those bounds for one run:
harn run --flight-recorder \
--flight-recorder-max-events 1000000 \
--flight-recorder-retain 32 \
main.harn
Use --flight-recorder-out recording.json when another tool owns the artifact
path. Harn won't rotate other JSON files beside a caller-selected path. The
artifact is written when the VM returns, exits, or fails. A force-killed process
can lose its in-memory recording.
Correlating delegated runs#
Build one report from the root run when several agents participated:
harn runs report .harn-runs/<root-run-id>.json > run-report.json
jq '.agents[] | {agent_id, status, usage, visible_output}' run-report.json
jq '.delegations, [.checks[] | select(.status != "passed")]' run-report.json
The report follows each typed child_runs[].run_path, checks the child's
back-pointer, and keeps the source hash beside the projected evidence. Add
--events-db <path> when the run used a SQLite event log. Canonical join
receipts make coordination.unjoined exact for terminal children and separate
the three costs a slow delegation can be paying: observed_wait_ms for
scheduler wait, observed_join_ms for terminal-to-collection lag, and
observed_result_processing_ms for the parent collapsing the result. All remain
null when event evidence is absent, malformed, or truncated, and a duplicate
receipt clears all three rather than only the lag. The report never turns
missing timing into zero.
Each timeline includes coverage.returned, coverage.available, and
coverage.truncated. Treat a missing event as evidence only when truncated
is false. A truncated run report also contains a timeline_truncated warning.
Query harn.session_timeline.query with a higher limit when you need more
nodes. available: null means Harn stopped after proving truncation, before it
could count every matching node.
Ask for a quick qualitative assessment after inspecting the deterministic checks:
harn runs review --run-record .harn-runs/<root-run-id>.json \
--events-db .harn/events.sqlite > run-review.json
jq '{verdict, confidence, findings, limitations, actions}' run-review.json
Use --report run-report.json instead when a report already exists. The two
inputs are explicit and mutually exclusive; Harn does not guess from a file's
contents. Use --rubric rubric.md to supply a project-specific rubric and
--model <model> to pin a model route. The review records both hashes and the resolved
route. It cites evidence by JSON Pointer and fails if a pointer does not resolve
inside the report. Coverage limits from the report remain explicit in the
review; the model cannot fill those gaps by reading other files. Harn projects
large arrays and strings into bounded first/last samples or previews before the
call. The review records every omission's original JSON Pointer, count, and
hash plus the source and projected byte counts, and repeats omissions as
deterministic limitations. If this auditable projection still exceeds the
48,000-token estimate, review fails before the model call.
Comparing runs#
Compare two stable views with your normal JSON diff tool to identify regressions:
harn runs view --json .harn-runs/new.json > new.view.json
harn runs view --json .harn-runs/old.json > old.view.json
diff -u old.view.json new.view.json
This highlights differences in tool calls, outputs, and token consumption.
Replay#
A run-record input reconstructs and checks the saved record. It does not run the program or its tools again:
harn replay .harn-runs/<run-id>.json
This view shows each saved stage transition and checks the embedded fixture.
Use an offline_coding_replay fixture when you need to prove the recorded
read, edit, and verification steps still reproduce in a fresh workspace. That
mode runs the Harn program with its recorded LLM tape, provider access removed,
and process network disabled, then checks the tool sequence, final diff, and
producer-written agent terminal outcome. See the
replay cookbook.
Visualizing a pipeline#
When you want a quick structural view instead of a live debug session, render a Mermaid graph from the AST:
harn viz main.harn
harn viz main.harn --output docs/main.mmd
The generated graph is useful for reviewing branch-heavy pipelines, match arms, parallel blocks, and nested retries before you start stepping through them.
Evaluation#
The harn eval command scores a run or set of runs against expected outcomes:
# Evaluate a single run
harn eval .harn-runs/<run-id>.json
# Evaluate all runs in a directory
harn eval .harn-runs/
# Evaluate using a manifest
harn eval eval-suite.json
Custom metrics#
Use eval_metric() in your pipeline to record domain-specific metrics:
eval_metric("accuracy", 0.95, {dataset: "test-v2"})
eval_metric("latency_ms", 1200)
These metrics appear in run records and are aggregated by harn eval.
Token usage tracking#
Track LLM costs during a run:
const usage = harness.obs.llm_usage()
harness.stdio.log(
"Tokens used: ${usage.input_tokens + usage.output_tokens}"
)
harness.stdio.log("LLM calls: ${usage.total_calls}")
Portal#
The Harn portal is an interactive web UI for inspecting runs:
harn portal
This opens a dashboard showing all runs in .harn-runs/, with drill-down
into individual stages, tool calls, and transcript snapshots.
Tips#
- Add
eval_metric()calls to your pipelines early — they're cheap to record and invaluable for tracking quality over time. - Use replay for debugging non-deterministic failures: record the failing run, then replay it locally to step through the logic.
- Compare baselines when refactoring prompts or changing tool definitions to catch regressions before they ship.