Skip to content

vacant.substrate

LLM / runtime backend abstraction. Substrate is a resource, not the identity — the same vacant can run on a different substrate without changing its keypair. Reputation is tracked per-(vacant, substrate); client-inherited is the load-bearing deployment for D2 (vacant served via MCP uses the calling client's LLM).

base

Abstract substrate backend interface.

Concrete implementations (Anthropic / Ollama / Mock / Deterministic) are filled in by later component PRs. P0 ships only the contract so downstream code can import SubstrateBackend for type annotations.

SubstrateRequest

Bases: BaseModel

A single inference request handed to a substrate backend.

SubstrateResponse

Bases: BaseModel

A single inference response, plus optional substrate proof material.

SubstrateBackend

Bases: ABC

Backend contract. Implementations must be safe to call from async code.

errors

Substrate-module error hierarchy.

SubstrateError

Bases: CoreError

Base class for vacant.substrate errors (backend / inference failures).

SubstrateUnavailableError

Bases: SubstrateError

The substrate backend cannot be reached (missing SDK, missing API key, no network).

SubstrateRateLimitError

Bases: SubstrateError

The substrate backend rejected with rate-limit; retries exhausted.

mock

Deterministic mock substrate for tests + bit-exact CI runs.

MockSubstrate returns canned text built from the prompt + a seeded random suffix. Every (seed, system_prompt, user_prompt) tuple produces the same response, so the integration test asserts exact byte equality across runs (P7 §"Substrate determinism contract").

MockSubstrate dataclass

MockSubstrate(seed: int = 0, model_label: str = 'mock-1')

Bases: SubstrateBackend

Bit-exact reproducible. Used by every integration test.

deterministic

Canned-response substrate for reproducible demos.

DeterministicSubstrate looks up responses keyed by a hash of the prompt. Useful when a scenario needs meaningful canned text (e.g. "this is a law firm answer") rather than the raw mock prefix. Falls back to a deterministic synthesised response when the prompt hash is not in the canned table.

DeterministicSubstrate dataclass

DeterministicSubstrate(canned: dict[str, str] = dict(), model_label: str = 'det-1')

Bases: SubstrateBackend

Responses lookup table; deterministic synthesis on miss.

anthropic

Anthropic Claude substrate.

Default model: claude-sonnet-4-6 (latest Claude 4.6 Sonnet, per CLAUDE.md tech stack).

API key is read from ANTHROPIC_API_KEY env var. If python-dotenv is installed (it is — required dep, see pyproject.toml), .env files in the cwd / parent dirs are auto-loaded the first time the substrate is asked to infer, so the README workflow `echo ANTHROPIC_API_KEY=...

.env && vacant demo --substrate=anthropicworks withoutexport(F14). Initialisation does NOT create the SDK client until the firstinfer` call (lazy import).

Rate-limit handling: the SDK raises anthropic.RateLimitError; this wrapper catches it and re-raises as SubstrateRateLimitError after sleeping for retry_after seconds (header-driven). Retries up to max_retries times before surfacing the failure.

AnthropicSubstrate dataclass

AnthropicSubstrate(model: str = DEFAULT_MODEL, api_key_env: str = 'ANTHROPIC_API_KEY', max_retries: int = 3, max_tokens: int = 1024)

Bases: SubstrateBackend

Real-LLM substrate. Used by demo scenarios with --substrate=anthropic.

Tests should NOT use this (they use MockSubstrate); CI cannot reach the network and bit-exact reproducibility is not possible.

ollama

Ollama substrate (local-LLM, "token-free future" simulation).

Talks to a local Ollama server over HTTP (http://localhost:11434 by default). Useful for the multilingual_translation scenario's local-ollama-llama3 substrate slot.

Failures (server not running, model not pulled) raise SubstrateUnavailableError -- callers can catch and degrade.

OllamaSubstrate dataclass

OllamaSubstrate(model: str = 'llama3', base_url: str = 'http://localhost:11434', timeout_s: float = 60.0)

Bases: SubstrateBackend

Local Ollama backend. Used in demo to simulate the "token-free future" leg of THEORY_V5 §2 substrate diversity.

client_inherited

ClientInheritedSubstrate — borrow the calling client's LLM. (D2)

This is the substrate that closes the "嫁接到客戶端" thesis claim. The vacant carries no API key and runs no local model. Instead, when an MCP-aware client calls the vacant, the client lends its own LLM session for the duration of the call: the vacant asks the client (via MCP sampling/createMessage) to do an inference on its behalf and treats the result as the substrate's response.

Architecture:

  • SubstrateHandle — the small dataclass that travels in the envelope metadata. It names the substrate kind (e.g. "client-inherited"), the model hint the client offers, and an opaque transport callback id.
  • SamplingCallbackasync (system_prompt, user_prompt) -> text, i.e. the function serve.py builds at the moment of receiving an MCP tools/call and hands to this substrate.
  • ClientInheritedSubstrate — the SubstrateBackend proper. Its name records the borrowed identity so reputation per-substrate works (a vacant that always runs under Claude scores its records as client-inherited:<caller>:claude-sonnet-4-6).

Security model: see ADR D017. The vacant trusts the caller's LLM output, but signs its own logbook entry. The substrate identity is recorded as client-inherited:<caller_vacant_id>:<model_hint> so a reviewer can attribute behaviour to the borrowed brain.

SubstrateHandle dataclass

SubstrateHandle(substrate_kind: str = 'client-inherited', model_hint: str = 'unknown', transport_callback_id: str = '')

Caller-supplied substrate identifier, carried in envelope metadata.

The values are advisory — the actual inference happens through the SamplingCallback the serve layer constructs from the MCP session. transport_callback_id is opaque to the vacant; it lets the serve layer route the sampling request back to the right MCP session.

borrowed_from

borrowed_from(caller_vacant_id_hex: str) -> str

Reputation key: client-inherited:<caller>:<model_hint>.

Used by the logbook attestation so that "this vacant ran on a borrowed Claude session" is auditable post-hoc.

Source code in src/vacant/substrate/client_inherited.py
def borrowed_from(self, caller_vacant_id_hex: str) -> str:
    """Reputation key: `client-inherited:<caller>:<model_hint>`.

    Used by the logbook attestation so that "this vacant ran on a
    borrowed Claude session" is auditable post-hoc.
    """
    return f"{self.substrate_kind}:{caller_vacant_id_hex}:{self.model_hint}"

ClientInheritedSubstrate dataclass

ClientInheritedSubstrate(callback: SamplingCallback, handle: SubstrateHandle = SubstrateHandle(), caller_vacant_id_hex: str = '')

Bases: SubstrateBackend

Substrate that delegates inference to a caller-supplied callback.

Constructed by serve.py (or mcp_adapter.py) at the moment an incoming call carries a SubstrateHandle. The instance lives only for the duration of that one call — the vacant has no LLM state of its own.

to_logbook_entry

to_logbook_entry() -> dict[str, Any]

Logbook entry payload describing the borrowed substrate.

Use this when appending a SUBSTRATE_BORROWED log entry so the chain records "this inference was outsourced to the caller's LLM at <model_hint> for <caller>".

Source code in src/vacant/substrate/client_inherited.py
def to_logbook_entry(self) -> dict[str, Any]:
    """Logbook entry payload describing the borrowed substrate.

    Use this when appending a `SUBSTRATE_BORROWED` log entry so the
    chain records "this inference was outsourced to the caller's
    LLM at `<model_hint>` for `<caller>`".
    """
    return {
        "substrate_kind": self.handle.substrate_kind,
        "model_hint": self.handle.model_hint,
        "borrowed_from": self.caller_vacant_id_hex,
        "transport_callback_id": self.handle.transport_callback_id,
    }