capa.runtime¶
Per-resource-worker runtime. Each hardware resource gets its own
thread hosting its own asyncio event loop; a per-run :class:Conductor
coordinates them through :class:ThreadBridge queues.
Narrative guides:
- Runtime topology — the conductor / pool / worker structure end-to-end.
- Threading model — which loop owns which object, and what crosses a bridge.
- Channel pipeline — how device emissions become channel samples.
- Saturation and deadlines —
the 10 s output-deadline contract enforced by :class:
SaturationMonitor. - Authorization gates — the pre-arm contract every conductor run honors.
capa.runtime ¶
Per-resource worker runtime — small production facade.
This package implements the per-resource-worker concurrency model. Each
hardware resource (one serial port, one DAQmx chassis, one camera handle)
gets its own thread hosting its own asyncio event loop; cross-thread
emission and command traffic flows over :class:ThreadBridge instances;
a per-run :class:Conductor coordinates workers via a config-lifetime
:class:WorkerPool.
The names re-exported here are the intentional public surface. Test
seams, state-machine internals (edge tables, lifecycle enums), bridge
plumbing, heartbeat/saturation helpers, and dispatcher impls live in
their concrete submodules (capa.runtime.runner, capa.runtime.bridge,
capa.runtime.lifecycle, capa.runtime.dispatch etc.) and should be
imported from there. Importing from those modules directly is fine —
this package facade is deliberately small.
RUNTIME_VERSION
module-attribute
¶
Runtime code revision marker.
Bumped when conductor / worker / pool semantics change in a way that
affects bundle interpretation. Newly-sealed bundles carry this in
capa_block.engine_version for cross-version diagnostics.
Conductor ¶
Conductor(
*,
pool: WorkerPool,
session: RunSession,
runner: ConductorRunner | None = None,
runner_factory: Callable[
[RunSession, RunContext], ConductorRunner
]
| None = None,
config: ConductorConfig | None = None,
thread_name: str = "capa-conductor",
pre_completion_callback: Any = None,
)
Per-run coordinator. See module docstring for the big picture.
Construction is cheap — no thread is spawned and no I/O happens until
:meth:start is called. The pool and session are captured by
reference; both must outlive the run.
Source code in src/capa/runtime/conductor.py
state
property
¶
Atomic read; advisory. The actual transition happens on the conductor loop, but state writes are single-int stores.
bundle_path
property
¶
Filesystem path for the run bundle once the session has opened.
databus
property
¶
The authoritative :class:DataBus. None before :meth:start
has reached the bus-construction step. Procedure subscribers and
external analyzers attach here.
result_future
property
¶
Resolves with the final :class:RunResult when the conductor
thread exits. Independent of the start-up handle future — callers
that only care about the end can ignore :meth:start's return.
completion_event
property
¶
The conductor's loop-local shutdown signal.
Set when the run completes (naturally, via stop, or saturation).
Used by :class:ProcedureRunner to wire a loop-local
external_stop event for procedures that await on it.
None before :meth:_run enters its task group.
runtime_diagnostics ¶
Snapshot the per-loop / per-bridge / per-worker metrics.
The dict shape is intended for manifest.queue_health consumption
(one entry per queue/bridge/worker keyed by tag), so the existing
manifest schema works without extension:
loop.conductor— conductor-loop lag percentiles.loop.worker:<resource_id>— per-worker loop lag (zeros for now; workers expose their own LoopLagMetric and a future step plumbs it through here).bridge.outbound:<resource_id>— per-worker outbound bridge latency + blocked time.worker:<resource_id>— tick / poll durations, loop lag, command counts, last-sample age.
Returns an empty dict before :meth:_run enters its task group
(i.e. before bridges and workers exist).
Source code in src/capa/runtime/conductor.py
start ¶
Spawn the conductor thread and begin the run.
Returns a future that resolves with :class:RunHandle once the
run reaches RUNNING state (procedure has started). On failure
before RUNNING, the handle future rejects and the result future
resolves with a CRASHED outcome.
Idempotent only in the failure direction — a second call after
:meth:start was invoked raises :class:ConductorStateError.
Source code in src/capa/runtime/conductor.py
stop ¶
Request a cooperative shutdown.
Sets the completion event on the conductor loop and returns the
same future as :attr:result_future for convenience. Idempotent:
subsequent calls observe the existing shutdown but the recorded
exit_reason is the first caller's.
Source code in src/capa/runtime/conductor.py
dispatch ¶
Run-time command dispatch.
Refused outside PREPARING / RUNNING. Routes through the pool to
the worker hosting device.
Source code in src/capa/runtime/conductor.py
snapshot ¶
One-shot snapshot via the pool's worker for device.
Same state gate as :meth:dispatch.
Source code in src/capa/runtime/conductor.py
attach_ui_bridge ¶
Wire a Conductor → UI :class:ThreadBridge.
Must be called BEFORE :meth:start so the drain task picks the
reference up on first dispatch. Headless callers omit this; the
drain stays a writer-and-bus-only fan-out.
The UI side owns attach_consumer/attach_producer: the
producer is the conductor's loop, so we register that here in a
deferred manner — the actual attach_producer call lands inside
:meth:_run once the conductor's loop is running. The UI side
calls attach_consumer from its own loop after attaching.
Source code in src/capa/runtime/conductor.py
procedure_ui_sink ¶
Return a UI-only sink procedures can publish
:class:~capa.runtime.emissions.ProcedureTick payloads through.
Wraps :meth:_publish_ui so the procedure layer stays bridge-
agnostic: a tick goes to the UI mirror with no writer write,
no data-bus publish. Safe to call before :meth:start —
no-op when no UI bridge is attached (headless run); the sink
becomes live the moment :meth:attach_ui_bridge runs.
The returned sink captures self and is intended to be
invoked from the conductor loop (where the procedure runs).
Calling from a different loop is undefined; the underlying
bridge's put_nowait is thread-safe but the cross-thread
timestamp ordering would be unreliable.
Source code in src/capa/runtime/conductor.py
join ¶
Block until the conductor thread exits.
Returns True if the thread joined within timeout, False
otherwise. Intended for tests and for the CLI driver which awaits
the result future synchronously.
Source code in src/capa/runtime/conductor.py
ConductorConfig
dataclass
¶
ConductorConfig(
saturation_deadline_s: float = DEFAULT_SATURATION_DEADLINE_S,
saturation_poll_period_s: float = DEFAULT_POLL_PERIOD_S,
shutdown_grace_s: float = DEFAULT_SHUTDOWN_GRACE_S,
loop_lag_warn_ms: float = 50.0,
)
Per-run knobs, all with sensible defaults so tests can omit them.
Splits responsibilities with
:class:~capa.experiment.config.RuntimeConfig:
RuntimeConfigcarries the user-tunable knobs an operator may reasonably want to set per experiment (shutdown_grace_s,ui_bridge_capacity,loop_lag_warn_ms).ConductorConfigcarries the internal saturation-monitor timing in addition. The saturation knobs are not user-facing today — promote them toRuntimeConfigwhen a real experiment asks for it.
:meth:from_runtime builds a :class:ConductorConfig by copying the
user-tunable knobs off a :class:RuntimeConfig while keeping the
saturation defaults.
from_runtime
classmethod
¶
from_runtime(
runtime: RuntimeConfig,
*,
saturation_deadline_s: float = DEFAULT_SATURATION_DEADLINE_S,
saturation_poll_period_s: float = DEFAULT_POLL_PERIOD_S,
) -> ConductorConfig
Build a :class:ConductorConfig from the user-facing
:class:RuntimeConfig.
Saturation parameters remain code defaults. Tests and embedding code can override them here for diagnostics; the shipped CLI does not expose them as user-facing flags today.
Source code in src/capa/runtime/conductor.py
RunOutcome ¶
Bases: StrEnum
How the run actually ended.
Distinct from :class:ConductorState (which is "where am I in the
lifecycle"). The bundle's run_status field is populated from this
at finalize.
COMPLETED
class-attribute
instance-attribute
¶
Procedure ran to natural completion.
ABORTED
class-attribute
instance-attribute
¶
Operator (or supervising code) called :meth:stop before completion.
CRASHED
class-attribute
instance-attribute
¶
Unhandled exception in procedure, drain task, or pool. Bundle is still sealed; the failure cause is recorded in events.
CRASHED_BUT_SEALED
class-attribute
instance-attribute
¶
Saturation deadline tripped. The conductor sealed the bundle anyway after disarming workers.
RunResult
dataclass
¶
RunResult(
run_id: str,
bundle_path: Path | None,
outcome: RunOutcome,
exit_reason: str | None,
final_state: ConductorState,
saturation_event: SaturationEvent | None,
started_mono_ns: int,
ended_mono_ns: int,
)
Final outcome — published on :attr:Conductor.result_future.
RunSession ¶
Bases: Protocol
Per-run resources, built by the caller, used by the conductor.
The conductor uses this as an async context manager: :meth:open
materializes the bundle / writer / clock and returns a
:class:RunContext; :meth:close finalizes the bundle. The caller
builds the session (and owns its lifecycle decisions like where the
bundle lives) so the conductor stays focused on orchestration. The
production session is :class:RealRunSession which wraps
:class:RunBundleWriter + :class:WriterThread; tests use a fake.
bundle_path
property
¶
Filesystem path of the bundle, or None if the session hasn't
materialized a bundle yet (e.g. in-memory test session).
config
property
¶
The frozen run recipe. Read by the conductor at arm time for
recording-plan resolution (config.run_options.recording_policy
and config.hardware). Valid pre- and post-:meth:open.
saturation_source
property
¶
Writer-side saturation signal source. None when the session
doesn't expose one (some tests).
open
async
¶
Materialize per-run resources. Returns the :class:RunContext
that the conductor installs into every worker via pool.arm_all.
Failure here is fatal: the conductor cannot recover and the run ends in FAILED state. The session is responsible for cleaning up any partial resources before re-raising.
Source code in src/capa/runtime/conductor.py
set_outcome ¶
Inform the session what to record at finalize. Called by the
conductor before :meth:close. Set to COMPLETED by default
until :meth:close is called.
Source code in src/capa/runtime/conductor.py
ManualClient ¶
Single async facade for UI manual cards.
Routes :meth:dispatch and :meth:snapshot to a live :class:Conductor
when a run is armed (records into the bundle, gates by conductor state),
else to the :class:WorkerPool directly. Cards take a :class:ManualClient
reference at construction and remain unaware of which side is in use.
The conductor reference is fetched through a callable rather than stored
by reference because the conductor's lifetime is per-run: there is no
conductor before the first :meth:Conductor.start, no conductor between
runs once the previous one has sealed, and a freshly-constructed one
on every new run. The provider closes over the
:class:~capa.ui.state.RunController's _conductor field and resolves
at call time.
A returned conductor is only used if its
:meth:~capa.runtime.state.ConductorState.permits_dispatch is true (i.e.
PREPARING or RUNNING). DRAINING / FINALIZING / SEALED / FAILED conductors
are treated as absent — the manual command falls through to the pool, which
is correct: the run is on the way out, the pool is still open, and a
between-runs manual command should not be refused because the previous
run's conductor is still finalizing.
Source code in src/capa/runtime/dispatch.py
dispatch
async
¶
Issue cmd against device.
Routes through the conductor when a run is armed and the
conductor is dispatchable, else through the pool. The future
returned by either side is bridged into the caller's loop via
:func:asyncio.wrap_future.
Source code in src/capa/runtime/dispatch.py
snapshot
async
¶
One-shot snapshot of device.
Same routing rules as :meth:dispatch.
Source code in src/capa/runtime/dispatch.py
camera ¶
Return the underlying :class:Camera handle for device_name.
Preview JPEGs are NOT consumed through this surface — they ride
a per-camera :class:~capa.runtime.bridge.ThreadBridge owned by
the pool. Device-probe metadata (UVC ranges, supported resolutions,
per-resolution fps caps) is NOT consumed through this surface
either — use :meth:camera_metadata for a worker-loop-safe
snapshot.
Retained for tests that want to assert the wrapper hosts the expected handle; UI code should not introduce new callers. The returned handle lives on the worker loop, so touching its methods from the qasync loop violates the worker-owns-the-handle invariant.
Returns None if the device name is unknown or refers to a
non-camera adapter.
Source code in src/capa/runtime/dispatch.py
camera_metadata
async
¶
Probe a camera's metadata across loops without touching the handle.
Submits the read to the worker that owns device_name; the
worker runs camera.snapshot_metadata() on its own loop and
signals the resulting future. :func:asyncio.wrap_future bridges
back to the caller's loop — same pattern as :meth:dispatch and
:meth:snapshot.
Returns None for adapters whose underlying camera does not
expose a metadata surface (IR cameras today, plus the safety
net for any future non-webcam adapter routed here by mistake).
Card code treats None as "fall back to static widget defaults".
Raises :class:UnknownDeviceError for names that aren't configured
— same surface as :meth:dispatch. Does not route through the
conductor: metadata is a pool-resident probe with no run-state
semantics, so the same call path applies whether or not a run is
armed.
Source code in src/capa/runtime/dispatch.py
device_readback
async
¶
Probe a device's read_state_snapshot() across loops.
Returns the adapter-specific snapshot (e.g.
:class:~capa.devices.watlow.WatlowStateSnapshot) or None for
adapters that don't implement it. Card code treats None as
"fall back to widget defaults".
Does not route through the conductor: this is a pool-resident
read used by manual-control cards to prefill their widgets, with no
run-state semantics. Submits to the worker that owns device_name
and bridges the future back to the caller's loop.
Source code in src/capa/runtime/dispatch.py
ConductorStateError ¶
Bases: CapaError
Operation attempted in an incompatible :class:~capa.runtime.state.ConductorState.
Raised when a dispatch or other run-gated call arrives outside the
PREPARING / RUNNING window — typically a procedure-issued command
landing during DRAINING which would race with adapter.stop().
Source code in src/capa/runtime/errors.py
PoolStateError ¶
PoolStateError(
message: str,
*,
from_state: object | None = None,
to_state: object | None = None,
)
Bases: CapaError
Pool is in a state that does not permit the attempted operation.
open() may not be called twice; close() may not be called
while any worker is non-IDLE; arm_all() requires
:attr:~capa.runtime.lifecycle.PoolState.OPEN.
Source code in src/capa/runtime/errors.py
ResourceConflict ¶
ResourceConflict(
message: str,
*,
conflicting_names: tuple[str, ...] = (),
resource_key: str | None = None,
)
Bases: CapaError
Two adapters claim the same hardware contention domain.
Raised synchronously from
:func:~capa.runtime.build.build_workers before any worker thread is
spawned, so a misconfigured config fails fast with no hardware side
effects.
The conflicting_names attribute is the pair (or set) of adapter names
that triggered the conflict, surfaced into the operator-facing error
toast so the fix is obvious.
Source code in src/capa/runtime/errors.py
RunnerStateError ¶
Bases: CapaError
:class:~capa.runtime.runner.WorkerRunner used in a state it doesn't permit.
The runner abstraction supports both real-thread and inline test
backends; both have lifecycles (start → submit ... → stop)
and both raise this when called out of order. Kept separate from
:class:WorkerStateError so test failures point at the harness rather
than the production state machine.
UnknownDeviceError ¶
Bases: CapaError
Caller asked the pool to dispatch to a device name not in this config.
Distinct from :class:ResourceConflict (which fires at build time) —
this fires at dispatch time when a UI card or procedure step references
a device whose configuration was removed or renamed. The
configured_names attribute lists what is available, so the operator
sees the typo immediately.
Source code in src/capa/runtime/errors.py
WorkerStateError ¶
WorkerStateError(
message: str,
*,
from_state: object | None = None,
to_state: object | None = None,
resource_id: str | None = None,
)
Bases: CapaError
Worker is in a state that does not permit the attempted operation.
Raised at two kinds of site:
- Illegal transition. A caller asked the worker to move
from_state → to_statewhere that edge is not in :data:~capa.runtime.lifecycle.LEGAL_WORKER_EDGES. Thefrom_state/to_stateattributes are populated. - Operation refused in current state.
dispatch()called while the worker is DRAINING/CLOSED,arm()while not IDLE, etc. Thefrom_stateis populated;to_stateisNone.
The exception is the canonical thing the worker-loop coroutine raises;
the sync facade (Worker.dispatch → concurrent.futures.Future)
re-raises it across the thread seam via asyncio.wrap_future so the
caller observes the original.
Source code in src/capa/runtime/errors.py
HeadlessResult
dataclass
¶
HeadlessResult(
run_id: str,
bundle_path: Path | None,
run_status: str,
bundle_status: str,
integrity_status: str,
exit_reason: str | None = None,
)
Outcome of one :func:run_headless invocation.
exit_code ¶
Map the outcome to the documented process exit code.
Returns:
| Type | Description |
|---|---|
int
|
|
int
|
on procedure / runtime crash, |
int
|
failure, |
int
|
Source code in src/capa/runtime/headless.py
WorkerPool ¶
WorkerPool(
*,
workers: Mapping[str, Worker],
device_to_resource: Mapping[str, str],
preview_bridges: Mapping[
str, ThreadBridge[PreviewFrame]
]
| None = None,
)
All :class:Worker\ s for one loaded config.
The pool is constructed against a prebuilt workers map (so tests
can inject fakes without going through TOML); the
:classmethod:from_config factory builds the workers via
:func:build_workers.
Lifetime: pool lives as long as the loaded config. Reloading the
config tears down the old pool (every worker closes) and a new pool
is constructed for the new config. Manual-control-between-runs is
the load-bearing property: one :meth:open pays the adapter
cold-open cost once; every subsequent arm/disarm reuses the same
opened hardware.
Source code in src/capa/runtime/pool.py
workers
property
¶
Immutable view of the worker map keyed by resource_id.
device_names
property
¶
All device names owned by any worker in the pool, in registration order.
from_config
classmethod
¶
from_config(
config: ExperimentConfig,
*,
runner_factory: Callable[..., WorkerRunner]
| None = None,
preview_consumer_loop: AbstractEventLoop | None = None,
preview_capacity: int = 4,
) -> WorkerPool
Build a pool from a real :class:ExperimentConfig.
Materialization and validation run synchronously here.
:class:~capa.devices.materialize.ConfigMaterializationError
surfaces adapter construction failures; :class:ResourceConflict
surfaces grouping conflicts. Both fire before any worker is
constructed, so a misconfigured config has no hardware side
effects.
preview_consumer_loop: the loop that will drain preview
bridges (the qasync UI loop in production; the test loop in
integration tests). When None (headless), no preview bridges
are constructed and the camera adapter's preview lifecycle
no-ops — zero overhead for headless runs.
Outbound bridge capacity is derived per worker from each
adapter's :attr:DeviceAdapter.expected_emission_rate_hz inside
:func:build_workers; the previous outbound_capacity kwarg
has been retired.
Source code in src/capa/runtime/pool.py
preview_bridges ¶
Per-camera preview bridges, keyed by camera spec name.
Empty when the pool was constructed without a UI consumer loop (headless runs). The mapping itself is read-only — callers that need to spawn drainers should iterate the entries.
Source code in src/capa/runtime/pool.py
attach_preview_consumers ¶
Bind every preview bridge's consumer side to the running loop.
Must be called from the consumer loop (the qasync UI loop) before
:meth:open is awaited — workers attach their producers inside
:meth:Worker._open_all_impl and a producer attach can race a
consumer-side enqueue. Calling this first builds the
:class:asyncio.Queue so the eventual producer always has a
target.
Idempotent: a second call is a no-op.
Source code in src/capa/runtime/pool.py
close_preview_bridges ¶
Close every preview bridge. Called from :meth:close after
all workers have closed, so no producer can write afterwards.
Idempotent: closing an already-closed bridge is a no-op (see
:meth:ThreadBridge.close).
Source code in src/capa/runtime/pool.py
worker_for ¶
Return the worker hosting the named adapter.
Raises :class:UnknownDeviceError if the name is not configured.
Source code in src/capa/runtime/pool.py
open
async
¶
Start every worker; on first failure, roll back in reverse order.
Workers start in parallel — bus-collision avoidance is the
resource_id grouping job, not a sequential warm-up
responsibility. At 6 workers this cuts pool-open from a ~2 s
sequential warm-up to ~500 ms.
On any worker's start failure: every already-opened worker is
closed in reverse order (LIFO of completion), then the original
exception propagates. The pool returns to :attr:PoolState.CLOSED.
Source code in src/capa/runtime/pool.py
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
shutdown_close
async
¶
Best-effort close for the :class:ShutdownCoordinator.
Unlike :meth:close, this does NOT refuse on non-IDLE workers.
The coordinator has already attempted to abort the active run by
the time it calls here, but a worker may still be in
ARMED/SAMPLING if the conductor's drain itself wedged.
This method:
- Disarms any worker still in ARMED/SAMPLING (parallel, bounded
by
grace_s). Disarm failures are captured as pool errors, never raised — the goal is to release as much hardware as we can before the coordinator's hard fuse fires. - Closes every worker in parallel, exactly like :meth:
close. - Returns the aggregate :class:
PoolCloseResultso the coordinator can record non-IDLE entry and any disarm timeouts in its :class:ShutdownResult.errors.
The strict :meth:close stays for config-reload teardown where
IDLE is a real invariant.
Source code in src/capa/runtime/pool.py
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 | |
arm_all
async
¶
Transition every worker IDLE → ARMED with the same run context.
Parallel. On any failure, in-flight arms are awaited but the
pool does not undo successful arms — that's the conductor's job
via :meth:disarm_all.
Source code in src/capa/runtime/pool.py
begin_sampling_all
async
¶
begin_sampling_all(
*, consumer_loop: AbstractEventLoop
) -> dict[str, ThreadBridge[WorkerEmission]]
Transition every worker ARMED → SAMPLING. Return outbound bridges.
Returns a dict keyed by resource_id; the conductor's drain
tasks iterate bridges.items() and spawn one drain coroutine
per bridge.
consumer_loop is the loop that will drain the bridges — the
conductor's loop in production; the test's loop in integration
tests.
Source code in src/capa/runtime/pool.py
disarm_all
async
¶
Transition every worker SAMPLING/ARMED → DRAINING → IDLE.
Per-worker grace; the slowest worker bounds the overall time but every worker's disarm runs in parallel. The result maps resource_id → DisarmResult so the conductor can identify which workers force-cancelled.
Workers that are already in IDLE (e.g. never armed) are skipped rather than erroring — disarm_all is idempotent over a not-fully-armed pool.
Source code in src/capa/runtime/pool.py
dispatch ¶
Route a command to the worker hosting device.
Synchronous facade (returns a :class:concurrent.futures.Future)
— the PoolClient async-wraps this. Used directly by tests.
State-gating happens inside the worker (per worker's own state).
Pool-level state is not checked here on purpose: a CLOSED pool
has empty _device_to_resource so :meth:worker_for would
raise :class:UnknownDeviceError first.
Source code in src/capa/runtime/pool.py
snapshot ¶
One-shot adapter.snapshot() on the worker hosting device.
camera_metadata ¶
Probe one camera's metadata on the worker that owns it.
Future resolves to :class:WebcamMetadata when device is a
webcam, None otherwise (IR cameras, non-camera adapters).
Pool-level state isn't checked here — :meth:worker_for raises
:class:UnknownDeviceError on a CLOSED pool's empty routing map
before we get this far.
Source code in src/capa/runtime/pool.py
device_readback ¶
Probe one device's read_state_snapshot() on the owning worker.
Future resolves to whatever the adapter returns from its
:meth:read_state_snapshot (e.g. :class:WatlowStateSnapshot),
or None when the adapter doesn't implement that surface.
Used by manual-control cards to prefill their widgets with the
device's current operator-facing values.
Source code in src/capa/runtime/pool.py
RealRunSession ¶
RealRunSession(
*,
config: ExperimentConfig,
runs_root: Path,
run_id: str | None = None,
plugins_lock: PluginsLock | None = None,
repo_root: Path | None = None,
lockfile_source: Path | None = None,
adapter_by_device: dict[str, DeviceAdapter]
| None = None,
adapter_by_camera: dict[str, Any] | None = None,
catalog: RunCatalog | None = None,
metrics: MetricsRegistry | None = None,
engine_version: str = "conductor",
configure_logging_for_bundle: bool = True,
config_path: Path | None = None,
)
Production :class:RunSession for the Conductor.
Built by the headless entry point (and by the GUI). The session is per-run; reuse across runs is not supported.
The :meth:writer_thread / :meth:bundle_writer / :meth:clock /
:meth:authorization properties become valid only after :meth:open
has run. Callers needing those for downstream wiring (e.g.
:class:ProcedureRunner construction) get them from the conductor's
runner_factory callback, which fires after :meth:open returns.
Source code in src/capa/runtime/session.py
bundle_path
property
¶
Bundle directory on disk. None before :meth:open succeeds.
saturation_source
property
¶
The writer thread (which satisfies :class:WriterSaturationSource).
None before :meth:open.
bundle_writer
property
¶
The open bundle writer. Valid only after :meth:open.
writer_thread
property
¶
The running writer thread. Valid only after :meth:open.
authorization
property
¶
Run-arm authorization handle. Valid only after :meth:open.
logger
property
¶
Bundle-aware structlog logger. Pre-:meth:open, returns a plain
structlog logger (no bundle log sink yet); post-open, the same logger
also tees into the bundle's run.log.
config
property
¶
The frozen run recipe. Valid pre- and post-:meth:open. The
conductor reads config.run_options.recording_policy and
config.hardware during plan resolution.
update_recording_plan ¶
Write the resolved recording plan into the bundle manifest.
Called by the conductor after :meth:Procedure.plan_capture
runs at arm time. Delegates to
:meth:RunBundleWriter.update_recording_plan so the manifest
snapshot reflects what the run will actually persist before any
adapter starts emitting.
Reads policy_mode from this session's
:attr:ExperimentConfig.run_options.recording_policy so the
bundle records both the operator-facing policy and the
materialised plan in one place.
Source code in src/capa/runtime/session.py
attach_adapters ¶
attach_adapters(
*,
adapter_by_device: dict[str, DeviceAdapter]
| None = None,
adapter_by_camera: dict[str, Any] | None = None,
) -> None
Late-bind the adapter maps used for equipment/camera identity blocks at finalize.
Callers that build the pool first (and therefore the workers, which
own the adapter instances) walk pool.workers to assemble these
maps and call :meth:attach_adapters before constructing the
conductor. Idempotent — last write wins.
Source code in src/capa/runtime/session.py
open
async
¶
Open the bundle, start the writer thread, mint authorization,
build the :class:RunContext.
Idempotent — calling :meth:open twice returns the same
:class:RunContext. The conductor calls this inside its own task
group; if the caller has already opened the session externally
(e.g. to construct a :class:ProcedureRunner against the bundle
writer first), the second call is a no-op.
Source code in src/capa/runtime/session.py
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 | |
set_outcome ¶
Inform the session of the run's outcome so :meth:close can
record the right run_status in the bundle manifest.
Source code in src/capa/runtime/session.py
set_runtime_diagnostics ¶
Stash per-loop / per-bridge / per-worker metrics produced by the Conductor for inclusion in the finalize manifest.
Idempotent — last write wins. The conductor calls this once just
before :meth:close; tests / subclassed sessions may also call
it directly.
Source code in src/capa/runtime/session.py
run_headless
async
¶
run_headless(
config: ExperimentConfig,
*,
runs_root: Path,
plugins_lock: PluginsLock | None = None,
repo_root: Path | None = None,
lockfile_source: Path | None = None,
external_stop: Event | None = None,
catalog: RunCatalog | None = None,
run_id: str | None = None,
conductor_config: ConductorConfig | None = None,
saturation_deadline_s: float | None = None,
) -> HeadlessResult
Run one experiment via the conductor / pool stack.
The caller owns runs_root (this function does not create it) and
the optional catalog (lifecycle stays with the caller's with
block).
Source code in src/capa/runtime/headless.py
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 | |
install_sigint_handler ¶
Install a SIGINT handler that sets stop_event.
Idempotent against re-entry: a second Ctrl-C terminates the process via the OS default handler. The first Ctrl-C lets the conductor unwind gracefully (drain the writer, finalize the bundle).