Hardware messages
Eleven hardware_* request types manage provider connections and hardware
jobs. Unlike the streaming execution messages, every hardware request gets
exactly one reply — either its named response type or an error
envelope ({"type": "error", "message": "..."} with no code/phase; see
errors).
Hardware state is shared across all connections — provider connections, credentials, and the job registry live in one process-wide manager, unlike the per-connection execution state.
Registered provider names: simulator, ibm, google, ionq, nvidia,
braket, azure, quantinuum. The kernel connects simulator itself at
startup, so the local simulator is always available.
Connection and credentials
Section titled “Connection and credentials”hardware_connect
Section titled “hardware_connect”| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "hardware_connect" | yes | — | |
provider | string | no | "" | One of the registered provider names. |
credentials | object | no | {} | Provider-specific string fields. |
Reply: hardware_connected with { "provider": ..., "success": true | false }.
success: false covers both unknown provider names and failed
authentication; an error envelope ("Hardware connect failed: ...") is
sent only if the connect attempt raised.
Semantics worth knowing:
- Synchronous, on the event loop. The connect handshake (which for real providers can include network validation) blocks the kernel until it finishes — expect the connection to be quiet during a slow handshake.
- Credentials persist on success. They are saved to the OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service), so the next kernel start auto-reconnects without the user re-entering tokens. The frontend can discard its in-memory copy as soon as the ack arrives.
- Empty credentials are NOT persisted.
connect_provideronly writes the keyring when the credentials object is non-empty — connecting the simulator with{}never touches the keyring. - If no keyring backend exists (headless CI, some containers), the store degrades to in-memory: the session works, persistence doesn’t.
hardware_set_credentials
Section titled “hardware_set_credentials”Identical behavior to hardware_connect today (store + connect in one
step); it is a separate message type so a future kernel can persist without
connecting, without breaking older clients. Same fields, same
hardware_connected reply.
hardware_clear_credentials
Section titled “hardware_clear_credentials”| Field | Type | Required | Default |
|---|---|---|---|
type | "hardware_clear_credentials" | yes | — |
provider | string | no | "" |
Drops the in-memory connection and wipes the stored keyring entry.
Reply: hardware_connected with success: false — the provider is now
disconnected.
hardware_connected_providers
Section titled “hardware_connected_providers”No fields besides type. Reply: hardware_connected_providers with
{ "providers": ["simulator", ...] } — used by the frontend to reconcile
its UI after a reload. Order is not guaranteed.
This replay-tested session shows set → clear → list:
[ { "request": { "type": "hardware_set_credentials", "provider": "simulator", "credentials": { "token": "demo" } }, "responses": [ { "type": "hardware_connected", "provider": "simulator", "success": true } ] }, { "request": { "type": "hardware_clear_credentials", "provider": "simulator" }, "responses": [ { "type": "hardware_connected", "provider": "simulator", "success": false } ] }, { "request": { "type": "hardware_connected_providers" }, "responses": [ { "type": "hardware_connected_providers", "providers": [] } ] }]Backends and jobs
Section titled “Backends and jobs”hardware_list_backends
Section titled “hardware_list_backends”| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "hardware_list_backends" | yes | — | |
provider | string | no | null | Omitted/null lists backends from all connected providers. Unknown or disconnected provider → empty list, not an error. |
Reply: hardware_backends with { "backends": [BackendInfo, ...] } — see
schemas.
hardware_submit
Section titled “hardware_submit”| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
type | "hardware_submit" | yes | — | |
provider | string | no | "simulator" | |
backend | string | no | "sim_qasm" | Backend/target name from hardware_list_backends. |
shots | integer | no | 1024 | |
code | string | no | "" | Source text — not a circuit object. |
language | "python" | "qsharp" | no | omitted | Q# is also recognized by regex when omitted, for older clients. |
Reply: hardware_job_submitted with a JobHandle.
The kernel routes code by language and provider before submission
(payload preparation runs off-thread; the provider’s submit call itself
runs on the event loop):
| Source | Provider | Route |
|---|---|---|
| Q# | simulator | Raw source passes through — the simulator re-runs it through the executor pipeline. |
| Q# | azure | Compiled to QIR; the QIR profile is selected per target — see submitting Q#. |
| Q# | anything else | Rejected — those provider SDKs take Python circuit objects, not Q# source or QIR. The error message suggests Azure Quantum or a Qiskit rewrite. |
| Python | simulator | Raw source passes through. |
| Python | real hardware | exec() the code, then extract a circuit object from the namespace (Qiskit QuantumCircuit, Cirq Circuit, or CUDA-Q kernel — search order is provider-aware). No recognizable circuit → error. |
The local simulator completes synchronously: the simulation runs during
the submit call (blocking the kernel’s event loop for its duration), and the
job arrives already in status: "complete" with queue_position: null.
hardware_status
Section titled “hardware_status”| Field | Type | Required | Default |
|---|---|---|---|
type | "hardware_status" | yes | — |
job_id | string | no | "" |
Reply: hardware_job_update with the current JobHandle.
Polling semantics — read carefully:
- Status lookup never re-queries the provider for job status. For
queued/runningjobs it refreshes onlyqueue_position; thestatusfield changes only via submission,hardware_cancel, or a kernel restart. For real providers, completion is discovered by pollinghardware_results, nothardware_status. stalejobs (see below) short-circuit: the stored handle is returned without touching the provider.- An unknown
job_idis not an error — the kernel synthesizes astalehandle so clients can mark the job as no-longer-tracked instead of surfacing a raw failure:
{ "type": "hardware_status", "job_id": "job-from-before-restart"}[ { "type": "hardware_job_update", "job": { "id": "job-from-before-restart", "provider": "unknown", "backend": "unknown", "status": "stale", "queue_position": null, "shots": 0, "submitted_at": "", "error": "This job is no longer tracked by the kernel (the kernel may have restarted). Re-submit to run it again." } }]stale semantics: job metadata persists to disk
(~/.nuclei/jobs.json), and on kernel start every job that was non-terminal
at shutdown is rehydrated with status: "stale" — the kernel no longer
holds the provider SDK handle, so live polling cannot resume; re-submit to
run again. Terminal jobs (complete/failed) keep their stored status so
history survives restarts.
hardware_list_jobs
Section titled “hardware_list_jobs”No fields besides type. Reply: hardware_jobs with
{ "jobs": [JobHandle, ...] } — every tracked job, including rehydrated
ones. The frontend calls this on (re)connect to repopulate its job tracker.
hardware_results
Section titled “hardware_results”| Field | Type | Required | Default |
|---|---|---|---|
type | "hardware_results" | yes | — |
job_id | string | no | "" |
Reply: hardware_result with { "job_id": ..., "data": {...} }. data is
a free-form provider dictionary — its shape depends on the provider:
- The local simulator returns a full
SimulationResultfor successful jobs, and{ "error": KernelError }for failed ones. - Real providers return counts-style dictionaries, or
{ "error": "...", "status": "..." }while the job is still pending or after a failure. Treatdata.erroras the failure signal. - An unknown
job_idreturns a stale marker rather than an error:
{ "type": "hardware_results", "job_id": "job-from-before-restart"}[ { "type": "hardware_result", "job_id": "job-from-before-restart", "data": { "error": "Results for this job are no longer available (the kernel may have restarted since it was submitted).", "status": "stale" } }]hardware_cancel
Section titled “hardware_cancel”| Field | Type | Required | Default |
|---|---|---|---|
type | "hardware_cancel" | yes | — |
job_id | string | no | "" |
Reply: hardware_job_cancelled with { "job_id": ..., "success": true | false }.
Cancel is best-effort and has sharp edges worth knowing:
- On success the job’s status is set to
failed— there is no separatecancelledwire status in protocol v1. - Providers whose jobs complete synchronously (simulator, NVIDIA) inherit a
no-op cancel that returns
true— so cancelling an already-completesimulator job “succeeds” and flips its status tofailed. Don’t offer cancel for terminal jobs in your UI. - Unknown job ids are treated as already-gone:
success: true.
hardware_dismiss
Section titled “hardware_dismiss”| Field | Type | Required | Default |
|---|---|---|---|
type | "hardware_dismiss" | yes | — |
job_id | string | no | "" |
Reply: hardware_job_dismissed with { "job_id": ..., "success": true | false }.
Dismiss is pure bookkeeping: it removes the job’s record from the kernel’s
registry and the persistent store (~/.nuclei/jobs.json), so the job
stops appearing in hardware_list_jobs — including after a kernel restart.
- It does not cancel anything provider-side — a running job keeps
running. Use
hardware_cancelto stop a job; dismiss to drop its record from the tracker. - Unknown job ids are treated as already-gone:
success: true— the desired end state (“this record is gone”) holds either way.
The full flow as a replay-tested session fixture — submit, dismiss, then
hardware_list_jobs confirming the record is gone:
[ { "request": { "type": "hardware_connect", "provider": "simulator", "credentials": {} }, "responses": [ { "type": "hardware_connected", "provider": "simulator", "success": true } ] }, { "request": { "type": "hardware_submit", "provider": "simulator", "backend": "sim_qasm", "shots": 128, "code": "from qiskit import QuantumCircuit\n\nqc = QuantumCircuit(2, 2)\nqc.h(0)\nqc.cx(0, 1)\nqc.measure([0, 1], [0, 1])\n", "language": "python" }, "responses": [ { "type": "hardware_job_submitted", "job": { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "complete", "queue_position": null, "shots": 128, "submitted_at": "<any>", "error": null } } ] }, { "request": { "type": "hardware_dismiss", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_job_dismissed", "job_id": "<job-id>", "success": true } ] }, { "request": { "type": "hardware_list_jobs" }, "responses": [ { "type": "hardware_jobs", "jobs": [] } ] }, { "request": { "type": "hardware_dismiss", "job_id": "no-such-job" }, "responses": [ { "type": "hardware_job_dismissed", "job_id": "no-such-job", "success": true } ] }]Replay-tested simulator walkthrough
Section titled “Replay-tested simulator walkthrough”The full lifecycle against the always-available local simulator — connect,
list, submit, poll, fetch results, list jobs, cancel — as a session fixture
the test suite replays end-to-end (the "<job-id>" marker carries the
server-generated id between steps):
[ { "request": { "type": "hardware_connect", "provider": "simulator", "credentials": {} }, "responses": [ { "type": "hardware_connected", "provider": "simulator", "success": true } ] }, { "request": { "type": "hardware_connected_providers" }, "responses": [ { "type": "hardware_connected_providers", "providers": ["simulator"] } ] }, { "request": { "type": "hardware_list_backends", "provider": "simulator" }, "responses": [ { "type": "hardware_backends", "backends": [ { "name": "sim_qasm", "provider": "simulator", "qubit_count": 32, "connectivity": "<any>", "queue_length": 0, "average_error_rate": 0.0005, "gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Toffoli", "SWAP", "Measure"], "status": "online" }, { "name": "sim_statevector", "provider": "simulator", "qubit_count": 24, "connectivity": "<any>", "queue_length": 0, "average_error_rate": 0.0, "gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Toffoli", "SWAP"], "status": "online" }, { "name": "sim_noise", "provider": "simulator", "qubit_count": 20, "connectivity": "<any>", "queue_length": 0, "average_error_rate": 0.001, "gate_set": ["H", "X", "Y", "Z", "CNOT", "CZ", "RX", "RY", "RZ", "T", "S", "Measure"], "status": "online" } ] } ] }, { "request": { "type": "hardware_submit", "provider": "simulator", "backend": "sim_qasm", "shots": 256, "code": "from qiskit import QuantumCircuit\n\nqc = QuantumCircuit(2, 2)\nqc.h(0)\nqc.cx(0, 1)\nqc.measure([0, 1], [0, 1])\n", "language": "python" }, "responses": [ { "type": "hardware_job_submitted", "job": { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "complete", "queue_position": null, "shots": 256, "submitted_at": "<any>", "error": null } } ] }, { "request": { "type": "hardware_status", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_job_update", "job": { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "complete", "queue_position": null, "shots": 256, "submitted_at": "<any>", "error": null } } ] }, { "request": { "type": "hardware_results", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_result", "job_id": "<job-id>", "data": { "state_vector": "<any>", "probabilities": { "00": "<approx:0.5:0.000001>", "11": "<approx:0.5:0.000001>" }, "measurements": "<any>", "bloch_coords": "<any>", "execution_time_ms": "<any>", "shot_count": 256, "metrics": {} } } ] }, { "request": { "type": "hardware_list_jobs" }, "responses": [ { "type": "hardware_jobs", "jobs": [ { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "complete", "queue_position": null, "shots": 256, "submitted_at": "<any>", "error": null } ] } ] }, { "request": { "type": "hardware_cancel", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_job_cancelled", "job_id": "<job-id>", "success": true } ] }, { "request": { "type": "hardware_status", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_job_update", "job": { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "failed", "queue_position": null, "shots": 256, "submitted_at": "<any>", "error": null } } ] }]And the failed-submission path — note status: "failed" with
error: null on the handle, with the actual diagnostic delivered by
hardware_results:
[ { "request": { "type": "hardware_submit", "provider": "simulator", "backend": "sim_qasm", "shots": 128, "code": "x = 1\n", "language": "python" }, "responses": [ { "type": "hardware_job_submitted", "job": { "id": "<job-id>", "provider": "simulator", "backend": "sim_qasm", "status": "failed", "queue_position": null, "shots": 128, "submitted_at": "<any>", "error": null } } ] }, { "request": { "type": "hardware_results", "job_id": "<job-id>" }, "responses": [ { "type": "hardware_result", "job_id": "<job-id>", "data": { "error": { "code": "unsupported_framework", "message": "No supported quantum framework detected in code.", "traceback": null, "framework": null, "dependency": null } } } ] }]Azure Quantum examples (illustrative)
Section titled “Azure Quantum examples (illustrative)”{ "type": "hardware_connect", "provider": "azure", "credentials": { "subscription_id": "00000000-0000-0000-0000-000000000000", "resource_group": "my-quantum-rg", "workspace_name": "my-workspace", "location": "eastus" }}[ { "type": "hardware_connected", "provider": "azure", "success": true }]A Q# submission to a Quantinuum target via Azure compiles to QIR with the
Adaptive_RI profile and
comes back queued. queue_position is null at
submission; Azure does not expose queue positions, so later hardware_status
polls report -1 (“unknown”) rather than a real position:
{ "type": "hardware_submit", "provider": "azure", "backend": "quantinuum.sim.h1-1e", "shots": 500, "code": "operation Main() : Result[] {\n use qs = Qubit[2];\n H(qs[0]);\n CNOT(qs[0], qs[1]);\n return [M(qs[0]), M(qs[1])];\n}\n", "language": "qsharp"}[ { "type": "hardware_job_submitted", "job": { "id": "8f4b8f6e-1c2d-4e5f-9a0b-3c4d5e6f7a8b", "provider": "azure", "backend": "quantinuum.sim.h1-1e", "status": "queued", "queue_position": null, "shots": 500, "submitted_at": "2026-06-10T12:00:00.000000+00:00", "error": null } }]