vacant.runtime¶
P1 runtime — 5-state lifecycle (LOCAL / ACTIVE / HIBERNATING /
STALE / SUNK / ARCHIVED), heartbeat, shadow-self drift, and the
D1–D5 self-replication paths. Path A (human-written vacant) is
intentionally absent.
state_machine
¶
Vacant lifecycle state machine.
Six states (VacantState from P0) by seven events (Event below): 42 cells.
The transition table is an explicit dict[(state, event), state']. Cells
absent from the table are invalid — applying them raises
InvalidEventError rather than silently no-op'ing, because at this layer a
misrouted event is a programming bug (the envelope-level checks in P6 §3.2
are responsible for rejecting requests before they reach this state machine).
Transitions are derived from:
- architecture/components/P1_runtime.md §3.8 (state machine diagram)
- architecture/decisions/D001_hibernation_and_stale_revival.md (warmup is
collapsed into REVIVE_REQUESTED here; P1's full warmup ceremony is a
later component concern, see follow-up note in PR description)
- architecture/decisions/D003_p1_runtime_reconciliation.md (can_review
semantics for STALE)
Predicates exposed for downstream use:
- can_review(state) — used by P3 reputation
- can_be_called(state) / is_runnable(state) — LOCAL is fully runnable
(CLAUDE.md §LOCAL); the only difference vs ACTIVE is registry visibility
- requires_revive(state) — STALE only
Sunk-heartbeat semantics (THEORY_V5 §4.2 / dispatch §1) are encoded in
heartbeat_payload(state, ...) in runtime/heartbeat.py — the state
machine itself doesn't know about payloads, just about transitions.
Event
¶
Bases: StrEnum
Events the runtime feeds into the state machine.
HEARTBEAT
class-attribute
instance-attribute
¶
A heartbeat attestation is being emitted.
CALL_RECEIVED
class-attribute
instance-attribute
¶
An incoming A2A call is being accepted (passed §3.2 envelope checks).
REVIEW_RECEIVED
class-attribute
instance-attribute
¶
A peer/caller review landed in this vacant's logbook.
REVIVE_REQUESTED
class-attribute
instance-attribute
¶
Owner / scheduler requests transition out of HIBERNATING or STALE.
ARCHIVE_REQUESTED
class-attribute
instance-attribute
¶
Sunk vacant has aged past ARCHIVED_AFTER_SUNK_DAYS.
SPAWN_REQUESTED
class-attribute
instance-attribute
¶
A spawn (D1-D5) ceremony is being initiated by the parent.
VacantStateMachine
¶
VacantStateMachine(initial: VacantState = LOCAL)
Stateful wrapper around the transition table.
Constructed with a starting state; apply(event) mutates state in
place and returns the new state. Use peek(state, event) if you want
to evaluate a transition without mutating.
Source code in src/vacant/runtime/state_machine.py
transitions
classmethod
¶
transitions() -> dict[tuple[VacantState, Event], VacantState]
A copy of the transition table; useful for exhaustive tests.
peek
classmethod
¶
peek(state: VacantState, event: Event) -> VacantState
Pure transition lookup. Raises InvalidEventError if undefined.
Source code in src/vacant/runtime/state_machine.py
can_review
¶
can_review(state: VacantState) -> bool
True iff a vacant in state may emit new peer/caller reviews.
False for STALE, SUNK, ARCHIVED (THEORY_V5 §4.1; D003 §A).
can_be_called
¶
can_be_called(state: VacantState) -> bool
is_runnable
¶
is_runnable(state: VacantState) -> bool
True iff the runtime should serve traffic. LOCAL is runnable (CLAUDE.md §LOCAL: registry visibility=none, but everything else works).
requires_revive
¶
requires_revive(state: VacantState) -> bool
heartbeat
¶
Heartbeat scheduler.
Per-state cadence and payload (dispatch §2 / THEORY_V5 §4.2 / D003):
| State | Period | Payload |
|---|---|---|
| LOCAL | HEARTBEAT_BASE_PERIOD_S |
{liveness: true} |
| ACTIVE | HEARTBEAT_BASE_PERIOD_S |
{liveness: true} |
| HIBERNATING | HEARTBEAT_HIBERNATING_PERIOD_S |
{liveness: "dormant", last_active: ts} |
| STALE | HEARTBEAT_HIBERNATING_PERIOD_S |
{liveness: false, awaiting_revive: true} |
| SUNK | HEARTBEAT_SUNK_LIVENESS_PERIOD_S |
{liveness: false, key_in_custody: true} ← load-bearing for lineage attribution (THEORY_V5 §4.2) |
| ARCHIVED | scheduler does not run | n/a |
The SUNK payload is the load-bearing one: §4.2 explicitly notes that the
sunk heartbeat is identity custody attestation, not liveness, so
liveness=false and key_in_custody=true must both appear, and the entry
kind is "HEARTBEAT_SUNK" so consumers can distinguish at a glance.
HeartbeatScheduler
¶
HeartbeatScheduler(*, logbook: Logbook, signing_key: SigningKey, state_provider: Callable[[], VacantState], sleep: Callable[[float], Any] = sleep, clock: Callable[[], datetime] = lambda: now(UTC), last_active_provider: Callable[[], datetime | None] = lambda: None)
Async scheduler that signs and appends heartbeat entries to a logbook.
Construction is dependency-injected: state is read via state_provider
(a callable, since it can change between ticks), the signing key is
pinned at construction, and the cadence is derived from the current
state on each tick (so a transition from ACTIVE → HIBERNATING
immediately stretches the next sleep interval).
Source code in src/vacant/runtime/heartbeat.py
tick
async
¶
tick(*, extra: dict[str, Any] | None = None) -> LogEntry
Append exactly one heartbeat entry for the current state.
Raises InvalidEventError if the current state is ARCHIVED.
Source code in src/vacant/runtime/heartbeat.py
run_until_archived
async
¶
Drive tick() continuously, sleeping heartbeat_period_s(state)
between ticks, and stop when state becomes ARCHIVED (or max_ticks
is reached). Returns the number of ticks emitted.
Source code in src/vacant/runtime/heartbeat.py
heartbeat_period_s
¶
heartbeat_period_s(state: VacantState) -> int
Return the cadence (seconds) for state heartbeats.
Raises InvalidEventError for ARCHIVED — by spec, the scheduler does
not run for archived vacants.
Source code in src/vacant/runtime/heartbeat.py
heartbeat_kind
¶
heartbeat_kind(state: VacantState) -> str
Log entry kind for the heartbeat. SUNK uses a distinct kind so
downstream consumers can filter custody attestations from liveness pulses
without re-inspecting the payload.
Source code in src/vacant/runtime/heartbeat.py
heartbeat_payload
¶
heartbeat_payload(state: VacantState, *, last_active: datetime | None = None, extra: dict[str, Any] | None = None) -> HeartbeatPayload
State-specific payload for the next heartbeat entry.
Source code in src/vacant/runtime/heartbeat.py
shadow_self
¶
Shadow-self drift detection (P1 §3.4 stub).
P1 needs a behavioural fingerprint that downstream code (P3 honesty signal, P5 graduation gate) can call before the real STYLO Vec16 / PROBE embeddings land. This module provides:
compute_embedding(windows)— deterministic 16-dim float vector built from the BLAKE2b digest of N output windows. Pure stdlib, pure function.AnchorDistribution— diagonal-covariance Gaussian over the embedding space (mean + per-dim std), enough to evaluate Mahalanobis-style drift without bringing in numpy. With diagonal covariance the Mahalanobis distance reduces to standardised Euclidean — a reasonable demo-scale approximation noted inarchitecture/research/T1_behavioral_fingerprint.md.compute_drift(current, anchor)→ float Mahalanobis-style distance.is_drifting(drift, threshold=STYLO_DRIFT_THRESHOLD)→ bool.drift_log_entry(...)— convenience that writes aDRIFT_DETECTEDlog entry; no automatic state change (per dispatch §3, the policy layer decides what to do with the signal).
The real STYLO Vec16 lands with P3 (research/T1) and will replace
compute_embedding; this module's API stays put.
AnchorDistribution
dataclass
¶
Diagonal-covariance reference distribution over embedding space.
mean[i] and std[i] describe the historical distribution of feature
i. Std values are floored to min_std to avoid division-by-zero on
constant features (a known artefact of the demo-scale embedding).
from_history
classmethod
¶
from_history(history: Iterable[Sequence[float]]) -> AnchorDistribution
Build a diagonal Gaussian from an iterable of past embeddings.
Source code in src/vacant/runtime/shadow_self.py
compute_embedding
¶
Hash-projection placeholder for STYLO Vec16.
Returns an EMBEDDING_DIM-dim float vector in [0, 1]^16. Empty input
is treated as [0.0] * EMBEDDING_DIM.
Deterministic, pure, no LLM calls — suitable for unit tests and demo runs until P3 wires the real embedding.
Source code in src/vacant/runtime/shadow_self.py
compute_drift
¶
compute_drift(current: Sequence[float], anchor: AnchorDistribution) -> float
Standardised-Euclidean distance (diagonal-Mahalanobis) from anchor.
Equivalent to sqrt(sum_i ((x_i - mean_i) / max(std_i, min_std))^2).
Source code in src/vacant/runtime/shadow_self.py
is_drifting
¶
is_drifting(drift: float, threshold: float = STYLO_DRIFT_THRESHOLD) -> bool
True iff drift ≥ threshold (default = STYLO_DRIFT_THRESHOLD).
drift_log_entry
¶
drift_log_entry(*, logbook: Logbook, signing_key: SigningKey, drift: float, embedding: Sequence[float], threshold: float = STYLO_DRIFT_THRESHOLD) -> LogEntry
Append a DRIFT_DETECTED log entry. No automatic state change.
Source code in src/vacant/runtime/shadow_self.py
spawn
¶
Self-replication paths (D1-D5).
Each path:
1. generates a fresh Ed25519 keypair (no key derivation — keypairs are
independent so a parent compromise does not give the controller the
child's private key; cf. D003)
2. assembles a new BehaviorBundle / SubstrateSpec per the path
3. seeds the child's logbook with a BIRTH entry that names the parent
4. appends a SPAWN entry to the parent's logbook that names the child
5. returns the new ResidentForm with parent_id set
Path A (human-written vacant) is deprecated and intentionally absent (CLAUDE.md §Things to NOT do).
D4 lineage-merge requires an explicit ParentConsent from the secondary
parent — a detached signature over the spawn intent. Without it the call
raises ConsentError.
SpawnResult
dataclass
¶
SpawnResult(child: ResidentForm, child_signing_key: SigningKey, path: str)
Output of every spawn path.
ParentConsent
dataclass
¶
ParentConsent(parent_id: VacantId, intent: str, signature: bytes)
Detached signature attesting that a parent agrees to a spawn.
signature is over consent_payload(parent_id, intent) — see
consent(). Verified by verify_consent() inside spawn_lineage_merge.
consent
¶
consent(parent_id: VacantId, parent_signing_key: SigningKey, intent: str) -> ParentConsent
Helper for tests + D4 callers: build a signed consent token.
Source code in src/vacant/runtime/spawn.py
spawn_clone_with_mutation
¶
spawn_clone_with_mutation(parent: ResidentForm, parent_signing_key: SigningKey, *, policy_mutation: str) -> SpawnResult
D1 — clone the parent's bundle, append policy_mutation to its DSL.
Same tool_whitelist, same system_prompt. Child starts in ACTIVE.
Source code in src/vacant/runtime/spawn.py
spawn_subagent_bud
¶
spawn_subagent_bud(parent: ResidentForm, parent_signing_key: SigningKey, *, narrowed_tools: list[str]) -> SpawnResult
D2 — spawn a closed subagent (registry_visibility=none → LOCAL).
narrowed_tools must be a (possibly equal) subset of the parent's
tool whitelist; child starts in LOCAL because composite-parent
children are closed by default (P5 §5.1, CLAUDE.md §Closed children).
Source code in src/vacant/runtime/spawn.py
spawn_capability_fork
¶
spawn_capability_fork(parent: ResidentForm, parent_signing_key: SigningKey, *, new_capability_text: str, new_system_prompt: str) -> SpawnResult
D3 — fork into a different capability with a different system prompt.
Same substrate spec; child starts ACTIVE. new_capability_text is
persisted on the BIRTH entry so downstream code can later mint a fresh
CapabilityCard from it (P4 owns card publication).
Source code in src/vacant/runtime/spawn.py
spawn_lineage_merge
¶
spawn_lineage_merge(parent_a: ResidentForm, parent_a_signing_key: SigningKey, parent_b: ResidentForm, parent_b_consent: ParentConsent, *, merged_system_prompt: str) -> SpawnResult
D4 — merge two parents' bundles. Requires parent_b's signed consent.
parent_a is the primary parent (the one running the ceremony).
parent_b_consent must be a ParentConsent whose parent_id matches
parent_b.identity and whose intent is _D4_INTENT. The secondary
parent is recorded inside the BIRTH log entry payload (D003 §C); only
the primary parent appears on child.parent_id.
Source code in src/vacant/runtime/spawn.py
make_d4_consent
¶
make_d4_consent(parent_b: ResidentForm, parent_b_signing_key: SigningKey) -> ParentConsent
Convenience: build a ParentConsent for the standard D4 intent.
spawn_cross_substrate_respawn
¶
spawn_cross_substrate_respawn(parent: ResidentForm, parent_signing_key: SigningKey, *, new_substrate_spec: SubstrateSpec) -> SpawnResult
D5 — same bundle, different substrate. Identity (keypair) is fresh, but the child carries the parent's prompt + policy + tools verbatim.
Source code in src/vacant/runtime/spawn.py
loop
¶
Async lifecycle loop wiring state machine + heartbeat + store.
P1's RuntimeLoop is the minimal per-vacant runtime: it accepts
Events, drives the state machine, persists logbook deltas through a
LogbookStore, and (optionally) runs a HeartbeatScheduler in the
background. Higher-level concerns — A2A endpoint, peer review pipeline,
budget bookkeeping — are intentionally not in this loop; they belong to
P3 / P4 / P6 and consume the events/logbook this loop produces.
RuntimeLoop
¶
RuntimeLoop(*, form: ResidentForm, signing_key: SigningKey, store: LogbookStore, clock: Callable[[], datetime] = lambda: now(UTC))
Per-vacant lifecycle loop.
The loop is constructed around an existing ResidentForm; identity
creation lives in P2. The signing_key is held in memory for as long
as the loop runs (vacant.identity will later wrap this in a real
custody boundary; see THEORY_V5 §0.1).
Source code in src/vacant/runtime/loop.py
submit
async
¶
submit(event: Event) -> VacantState
Apply event, persist the resulting logbook, return the new state.
Source code in src/vacant/runtime/loop.py
append_log
async
¶
append_log(kind: str, payload: dict[str, Any]) -> LogEntry
Append a free-form entry to the logbook and persist.
Source code in src/vacant/runtime/loop.py
heartbeat_scheduler
¶
heartbeat_scheduler(*, sleep: Callable[[float], Any] = sleep) -> HeartbeatScheduler
Build a HeartbeatScheduler bound to this loop's logbook + state.
Source code in src/vacant/runtime/loop.py
store
¶
Logbook persistence interface (P1) + in-memory impl.
P1's RuntimeLoop is wired through this LogbookStore Protocol so P4 can
later swap in the SQLite-backed implementation without touching runtime
code. The in-memory impl provided here is enough for unit + integration
tests and the P7 demo dashboard.
LogbookStore
¶
Bases: Protocol
Async key-value persistence over (VacantId, Logbook).
errors
¶
Error hierarchy for vacant.runtime.
RuntimeError_
¶
Bases: CoreError
Base class for vacant.runtime errors.
Suffix _ avoids shadowing the built-in RuntimeError.
InvalidEventError
¶
Bases: RuntimeError_
An Event was applied to a VacantState that does not accept it.
Example: CALL_RECEIVED while in SUNK. The state machine does not
silently no-op these — they signal a programming bug or a request that
should have been rejected upstream (P6 envelope checks, §3.2).
SpawnError
¶
Bases: RuntimeError_
A spawn (D1-D5) operation could not satisfy its preconditions.
ConsentError
¶
Bases: SpawnError
A multi-parent spawn (D4) was missing or had an invalid parent consent.