Dirac agent runtime & harness
Dirac has two runtimes, and they are not the same system:
- Chat-Dirac — the tutor. Client-side, TypeScript, BYOK calls straight to
api.anthropic.com. This is what Dirac architecture documents. - Agent-Dirac — the autonomous coder. A trusted Rust harness in
src-tauri/src/dirac/that runs a closed loop: it asks Claude for the next tool call, executes it deterministically, feeds the evidence back, and repeats until the goal is verified by real simulation — never asserted. Every side effect (file edits, kernel runs, hardware submissions) passes through a policy, budget, and workspace layer that the model cannot talk its way around.
This page documents agent-Dirac in depth. It is the runtime you care about if you want to understand what the agent can and cannot do to your machine.
Architecture at a glance
Section titled “Architecture at a glance”The frontend starts a run through a Tauri command and then only listens — all authority is on the Rust side.
┌──────────────────────────── WebView (TypeScript) ────────────────────────────┐ │ useDiracAgent.ts goal + active file ──▶ invoke("dirac_start_run") │ │ ▲ listens on "dirac://run-event" (RunEvent stream) │ └────────┼──────────────────────────────────────────────────────────────────────┘ │ Tauri IPC (the API key never crosses this boundary) ┌────────┴──────────────────── Rust harness (src-tauri/src/dirac) ─────────────┐ │ commands.rs ── spawns background thread ──▶ runner::drive_run │ │ │ │ │ orchestrator::run_agent ◀── the closed loop│ │ │ │ │ ┌───────────────┬──────────────┬──────────────┼──────────────┐ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ModelGateway AgentKernel Workspace Policy Budget │ │ (keychain, (disposable (SHA-256, (autonomy (zero-spend │ │ HTTPS to Python worker, whole-file gate, HW off ceiling) │ │ Anthropic) process group) patch/rollback) by default) │ │ │ │ │ │ ▼ ▼ │ │ api.anthropic python -I agent_worker.py ◀── parse | simulate | transpile │ │ .com/v1/msgs (new session, SIGKILL on 25s wall) │ └───────────────────────────────────────────────────────────────────────────────┘The harness was built in stages, and the module docs still name them: R1 execution supervisor, R2 secure model gateway, R3 policy/budget/workspace, R4 the orchestrator + tools (the pure, mockable core), R5 the Tauri command + run driver, R7 the live kernel submit port.
The run lifecycle
Section titled “The run lifecycle”dirac_start_run(goal, files, active_path, model) returns a run id
immediately and does the work on a background thread, so the UI never blocks.
- Resolve the worker path up front — a missing
agent_worker.pyfails now, not mid-run. - Mint a run id:
run_{ms:x}_{n}(time-seeded hex + a monotonic counter). - Register a cancel flag in
DiracRuns(aHashMap<run_id, Arc<AtomicBool>>). - Spawn the thread, build real dependencies (
RealKernel, a freshModelGateway,KernelSubmitPort,AutonomyPolicy::safe_default()), and calldrive_run.
Progress streams back as RunEvents on the dirac://run-event window channel —
Started, State, ModelText, ToolCall, ToolResult, Patch, Error, Finished. The
frontend registers the listener before invoking so no early event is lost.
Cancellation is cooperative: dirac_cancel_run sets the run’s AtomicBool,
and the loop observes it at the top of its next iteration.
Every run walks the same state machine — it begins in Planning, spends the
tool loop in Working, and ends in exactly one terminal state:
┌─────────────┐ │ Planning │ seed message built └──────┬──────┘ ▼ ┌─────────────┐ ◀─────────────┐ model turn ───▶│ Working │ ── tool ──────┘ (multi-turn feedback) └──┬───┬───┬──┘ finish / no-tool │ │ │ cancel flag ▼ │ ▼ ┌───────────┐ │ ┌───────────┐ │ Completed │ │ │ Cancelled │ └───────────┘ │ └───────────┘ ▼ ┌───────────┐ │ Failed │ budget exhausted / model error └───────────┘The orchestrator loop
Section titled “The orchestrator loop”This is the heart of the harness — a pure, injectable function (run_agent)
with no Tauri surface, so the whole loop is unit-testable against a scripted
model and a mock kernel.
build seed message (goal + workspace file list + active file) │ ▼ ┌───────────────────────── iterate (max 12, max 120s wall) ─────────────────────┐ │ 1. cancel? ─yes─▶ Cancelled │ │ 2. wall exceeded? ─yes─▶ break │ │ 3. model.complete(SYSTEM_PROMPT, messages, tools) ── Err ─▶ journal, break │ │ 4. reply has no tool_use? ─yes─▶ finished (success iff a prior compare matched)│ │ 5. append assistant { text, tool_use… } │ │ 6. for each tool_use: execute_tool(...) ─▶ evidence │ │ · track compare_quantum_results.matches │ │ · detect finish(success, summary) │ │ · journal ToolCall + ToolResult │ │ 7. append user { tool_result… } ◀── the evidence fed back to the model │ │ 8. finish requested? ─yes─▶ Completed │ └───────────────────────────────────────────────────────────────────────────────┘The multi-turn feedback in steps 5–7 is what makes it an agent rather than a
one-shot: the model’s proposed tool_use and the deterministic tool_result
are both appended to the running message list, so the next turn sees the real
outcome — a parse error, a mismatched probability, a routing cost — and repairs
its own work.
Bounds and stop conditions :
| Bound | Value | Source |
|---|---|---|
| Max model turns | 12 iterations | AgentBudget::max_iterations |
| Wall-clock | 120 s | AgentBudget::max_wall |
| Per-turn token cap | 4096 max_tokens | DEFAULT_MAX_TOKENS |
A run ends when: finish is called · the model returns no tool calls · 12 turns
elapse · 120 s elapse · the cancel flag is set · or the model call errors.
The tool set
Section titled “The tool set”The model is given 18 tools (the count is asserted in a test). Read-only and teaching tools produce evidence with no side effect; the four mutating tools are the only ones that can touch a file, the kernel, or hardware — and each of those runs through the workspace or policy layer below.
| Tool | Kind | What it does |
|---|---|---|
inspect_project | read | List files + active path |
read_quantum_file | read | Read one file’s contents |
apply_patch | write | Replace a file’s full contents (journaled, reversible) |
rollback_patch | write | Undo a prior patch by transaction id |
parse_quantum_program | read (worker parse) | Structure/syntax check |
validate_quantum_program | read | Semantic checks (qubit range, arity, collisions) |
estimate_quantum_resources | read | Qubit/gate/depth cost |
run_simulation | read (worker simulate) | Execute locally, get real probabilities |
compare_quantum_results | read | Check probabilities against a numeric target |
check_algorithm_invariant | read | Verify Bell/GHZ/uniform vs a known-good reference |
plan_hardware_run | read (shadow) | Recommend a backend, scored — never submits |
preview_backend_transpilation | read (worker transpile) | Post-transpile depth/2q metrics |
transpile_explore | read (worker transpile_explore) | Pass-by-pass transpile: before/after + which passes added the SWAPs |
submit_hardware_job | write / policy-gated | Submit to real hardware — off by default |
poll_hardware_job | read | Poll a submitted job’s status |
cancel_hardware_job | write | Cancel a pending job |
analyze_hardware_result | read | Read back measured probabilities |
finish | terminal | End the run with success + summary |
Every tool schema pins additionalProperties: false with an explicit required
list, and dispatch never panics — an unknown tool becomes ordinary failure
evidence.
The quantum tools funnel into just four worker actions — parse,
simulate, transpile, and transpile_explore — the same disposable-worker
path described below.
Workspace: whole-file patches, SHA-256, reversible
Section titled “Workspace: whole-file patches, SHA-256, reversible”Edits are whole-file replacements, not diffs: apply_patch carries the
complete new contents plus a rationale, and each application is stored as a
PatchTransaction holding both the before and after content.
Change detection uses SHA-256 — deliberately not a security boundary, just
“has this file changed since I last looked at it.” The loop threads a per-path
baseline hash; if the file changed under the agent since it last read it,
apply_patch returns a conflict and mutates nothing, and the tool tells the
model to re-read before patching.
apply_patch(path, new_content, expected_before_hash) │ ├─ hash(current) ≠ expected_before_hash ─▶ Conflict (no write) ──▶ "re-read first" │ └─ match ─▶ write (dirty=true), store PatchTransaction ──▶ Applied
rollback_patch(txn_id) └─ succeeds only while the file still matches what the patch left behindRollback is equally strict: it refuses if the transaction is unknown, already rolled back, or the file no longer matches the content that patch produced — so a rollback can never clobber a newer edit.
The disposable Python worker
Section titled “The disposable Python worker”Model-generated quantum code never runs in the kernel server or the Rust
process. Each parse/simulate/transpile action spawns a fresh, short-lived
Python worker and tears it down.
- Isolated interpreter:
python -I agent_worker.py— no site packages, noPYTHON*env, no user config. - Its own process group:
setsid()on Unix /CREATE_NEW_PROCESS_GROUPon Windows, so a runaway (or a fork bomb) can be killed as a group, not just the parent. - Hard wall-timeout:
DEFAULT_WALL = 25 sper worker call. On expiry the supervisor sendsSIGKILLto the whole process group (killpg/taskkill /T /F). - Framed over stdin/stdout: one JSON request in, exactly one JSON response
out; output is capped at 1 MiB and non-JSON/empty/oversize replies become
synthesized error codes (
worker_timeout,worker_bad_output, …).
Inside the worker there is a second, cooperative line of defense (not the
authoritative boundary — the process-group kill is): threads pinned to 1, an
import blocker for keyring, the kernel server, and every hardware SDK
(qiskit_ibm_runtime, braket, azure.quantum, …), and OS resource limits
from kernel/agent_limits.py — RLIMIT_CPU 10 s, 64 open files, and an output
cap.
Policy: hardware is off by default
Section titled “Policy: hardware is off by default”The single most important safety property: an agent cannot submit a paid
hardware job unless a human turned autonomy on. AutonomyPolicy::safe_default()
ships with autonomous_hardware_enabled: false, allow_qpu: false, an empty
provider allowlist, and max_spend: 0.0.
evaluate_submission is deterministic and reads only caller-built facts, never
model text. For real hardware, the autonomy-disabled check comes first and
dominates — before allowlists, shot ceilings, or cost:
submit_hardware_job(backend, shots, …) │ ▼ build SubmissionFacts (from the live backend list + parsed circuit — not the model) │ ▼ evaluate_submission(facts, policy, remaining_budget) │ ├─ simulator? ─▶ Allow iff allow_simulator ├─ autonomy disabled? ─▶ NeedsApproval ◀── the default; dominates all else ├─ !allow_qpu / provider / shots / qubits / depth / cost fails ─▶ Deny └─ all checks pass ─▶ Allow │ ┌────────────────┴───────────── SAFETY GATE ─────────────────┐ │ decision ≠ Allow ─▶ return evidence {submitted:false} — │ │ the submit port is NEVER reached. │ └────────────────────────────────────────────────────────────┘ │ Allow only ▼ reserve budget ─▶ submit_port.submit(...)A NeedsApproval / Deny result is an ordinary, expected outcome — the
system prompt explicitly tells the model not to retry to force it through, but
to report it and stop. The policy is “THE safety boundary”; the submit port is
“the only channel that can reach real hardware.”
Budget: a zero-spend ceiling
Section titled “Budget: a zero-spend ceiling”Because autonomous hardware is off, the spend ledger’s ceiling is literally
zero (RUN_BUDGET_CEILING = 0.0) — no autonomous submission can reserve any
budget. This is defense in depth behind the policy gate: even if a submission
somehow reached the ledger, a positive cost against a 0.0 ceiling fails to
reserve. The BudgetLedger (reserve → commit/release, with idempotent
submission records) is fully built for the day autonomy is enabled.
The secure model gateway
Section titled “The secure model gateway”The Anthropic API key lives only in the OS keychain (macOS Keychain / Windows
Credential Manager / Linux Secret Service), under service dev.getnuclei.dirac,
account anthropic-api-key.
- The
dirac_set_api_key/dirac_has_api_key/dirac_clear_api_keycommands are the frontend’s only touchpoint — the key never crosses the Tauri IPC boundary in either direction, and is never logged or placed in the model’s context. - Only
ModelGateway::completereads the key, only to set thex-api-keyheader on a blocking HTTPSPOSTtohttps://api.anthropic.com/v1/messages(anthropic-version: 2023-06-01, 120 s timeout). Error types are proven never to carry the key, so they are safe to log or return across IPC. - The background run thread builds its own
ModelGatewayreading the same keychain entry, since the gateway isn’t shared into the thread.
The journal
Section titled “The journal”Every run produces an ordered JournalEntry list, each stamped with ts
(milliseconds since run start):
| Entry | Recorded when |
|---|---|
StateChange { from, to } | Every lifecycle transition |
ModelText { text } | The model emitted prose this turn |
ToolCall { tool, input } | One per proposed tool call |
ToolResult { evidence } | One per executed tool |
Error { message } | A model-call error ended the loop |
At the driver level the journal is wrapped so every append also mirrors out
as a RunEvent to the frontend, and a successful apply_patch additionally
emits a Patch event carrying the before/after content for the UI’s diff view.
Data types & protocol
Section titled “Data types & protocol”The Rust↔worker wire format is versioned (PROTOCOL_VERSION = 1) and mirrors
kernel/agent_protocol.py. An AgentExecuteRequest carries action
(parse/simulate/transpile), framework, language, code, and
transpile-only fields (basis_gates, coupling_map, optimization_level) that
are omitted from the wire for non-transpile actions. The response keeps
snapshot/result/error as opaque JSON and a status of "ok" or "error"
— a worker failure is a structured error response, never a crash.
Defense in depth, summarized
Section titled “Defense in depth, summarized”The agent’s blast radius is bounded by concentric limits, each independent of the model’s cooperation:
┌─ Policy gate ── autonomy off by default; hardware submit ⇒ NeedsApproval ─┐ │ ┌─ Budget ── zero-spend ceiling; no reservation possible ───────────────┐ │ │ │ ┌─ Workspace ── SHA-256 conflict check; reversible whole-file patches ┐│ │ │ │ │ ┌─ Process group ── SIGKILL on 25 s wall; fork bombs reaped ──────┐ ││ │ │ │ │ │ ┌─ Worker rlimits ── CPU 10 s, fds, output cap; import blocker ┐│ ││ │ │ │ │ │ │ model-generated Python runs here — and only here ││ ││ │ │ │ │ │ └──────────────────────────────────────────────────────────────┘│ ││ │ │ │ │ └──────────────────────────────────────────────────────────────────┘ ││ │ │ │ └────────────────────────────────────────────────────────────────────── ┘│ │ │ └────────────────────────────────────────────────────────────────────────── ┘ │ └────────────────────────────────────────────────────────────────────────────────┘Adding a tool, worker action, or policy field is covered in Extending Dirac.