Conscribe
Inheritance is registration. __init__ signature (plus __wiring__) is config schema.
Conscribe is a Python library that provides automatic class registration and config typing stub generation for layered architectures. It eliminates two categories of boilerplate:
- Manual registration — Write a class, inherit a base → it's registered. No
registry["foo"] = FooClass. - Config guesswork — Your
__init__parameters become the config schema, and__wiring__adds the cross-registry fields on top. IDE autocomplete and fail-fast validation come for free.
pip install conscribe
Requires Python >= 3.9. Built on Pydantic v2.
Who Is This For?
Framework developers building config-driven systems with pluggable layers (agents, LLM providers, browser backends, etc.) who need registries, factories, protocol checks, and config schemas — without N layers × boilerplate.
Framework users who write YAML configs and want IDE autocomplete, parameter docs, and fail-fast validation at startup instead of crashing N steps in.
Quick Start
1. Define a Layer
from typing import Protocol, runtime_checkable
from conscribe import create_registrar
@runtime_checkable
class ChatModelProtocol(Protocol):
def chat(self, messages: list[dict]) -> str: ...
LLMRegistrar = create_registrar(
"llm",
ChatModelProtocol,
discriminator_field="provider",
strip_prefixes=["Chat"],
)
2. Create a Base Class
class ChatBaseModel(metaclass=LLMRegistrar.Meta):
__abstract__ = True
3. Write Implementations (auto-registered)
class ChatOpenAI(ChatBaseModel):
"""OpenAI LLM provider.
Args:
model_id: Model identifier, e.g. gpt-4o
temperature: Sampling temperature, 0-2
"""
def __init__(self, *, model_id: str, temperature: float = 0.0):
self.model_id = model_id
self.temperature = temperature
def chat(self, messages): ...
# Registered as "open_ai". No decorator. No registry call.
4. Discover & Use
from conscribe import discover
discover("my_app.llm.providers")
llm_cls = LLMRegistrar.get("open_ai") # → ChatOpenAI
llm = llm_cls(model_id="gpt-4o")
print(LLMRegistrar.keys()) # ["open_ai", "anthropic", ...]
Config Typing
Your __init__ signature is the config schema. Conscribe extracts it, builds a Pydantic discriminated union, and generates stubs for IDE autocomplete:
from conscribe import build_layer_config, generate_layer_config_source
result = build_layer_config(LLMRegistrar)
source = generate_layer_config_source(result)
See the Config Typing Guide for full details.
Config Tiers
| Tier | What You Write | What Users Get |
|---|---|---|
| 1 | Plain __init__(self, *, x: int = 5) |
Names + types + defaults |
| 1.5 | + Google/NumPy docstring with Args: |
+ descriptions |
| 2 | + Annotated[int, Field(ge=0)] |
+ constraints |
| 3 | __config_schema__ = MyModel |
Full Pydantic model |
Nested Config (Hierarchical Keys)
For layers with natural hierarchies (e.g., model_type → provider):
LLM = create_registrar(
"llm", ChatModelProtocol,
discriminator_fields=["model_type", "provider"],
key_separator=".",
)
Produces hybrid YAML configs where level 0 is flat and level 1+ is nested:
llm:
model_type: openai # flat (level 0)
temperature: 0.7 # flat (level 0 param)
provider: # nested (level 1)
name: azure
deployment: my-deploy
Cross-Registry Wiring
Declare which values from other registries a class accepts. Conscribe constrains the config field to Literal[...]:
# Given: LoopRegistrar with "react" and "codeact" registered
class BaseAgent(metaclass=AgentRegistrar.Meta):
__abstract__ = True
__wiring__ = {"loop": "loop"} # all keys from "loop" registry
class SWEAgent(BaseAgent):
__wiring__ = {"loop": ("loop", ["react"])} # narrows to subset
def __init__(self, *, max_steps: int = 10): ...
Generated config:
class SweAgentConfig(BaseModel):
name: Literal["swe_agent"] = "swe_agent"
max_steps: int = 10
loop: Literal["react"] = ... # wired from: loop
Four modes:
"loop": "agent_loop"— auto-discover all keys from registry"loop": ("agent_loop", ["react", "codeact"])— explicit subset"obs": ("observation", ["terminal"], ["filesystem"])— required + optional keys"browser": ["chromium", "firefox"]— literal list (no registry)
The 3-element tuple distinguishes required and optional keys — both appear in Literal[...], but the distinction is available as metadata for downstream negotiation logic.
Inheritance: child __wiring__ merges with parent (child keys override). Use None to exclude: {"llm": None}.
A class that declares no config knobs of its own still gets its wired fields — def __init__(self) -> None produces a config model containing the receptors, not an empty one.
Capability-relative keys
When the target registry was created with a key_separator, an explicit key list may name a bare trailing segment instead of a fully-qualified key. Conscribe expands it to every registered key that ends with that segment, so a class does not have to name the provider it will run against:
# Registered: "browser.click", "desktop.click", "browser.scroll"
class PortableAgent(BaseAgent):
__wiring__ = {"action": ("action", ["click"], ["scroll"])}
# -> Literal["browser.click", "desktop.click", "browser.scroll"]
class PinnedAgent(BaseAgent):
__wiring__ = {"action": ("action", ["browser.click"])} # escape hatch
# -> Literal["browser.click"]
A key that already contains the separator is passed through unchanged. This is not a fifth mode — it only relaxes how the key strings inside the existing modes are matched, and it is inert for registries without a key_separator.
If a short name matches several providers, all of them land in the Literal[...]. Conscribe expands; it does not arbitrate. Config generation cannot know which providers a given assembled runtime will hold, so every candidate is legitimately possible at that point; picking one belongs to the consuming framework, at the moment it knows the concrete provider set.
Composed Config (Multi-Layer Inline Wiring)
Combine multiple layers into a single config schema. Wired fields become full inline config objects instead of key selectors — enabling recursive IDE autocompletion across layers:
from conscribe import build_composed_config, generate_composed_json_schema
result = build_composed_config(
{"llm": LLMRegistrar, "agent": AgentRegistrar},
inline_wiring=True, # wired fields become target layer union types
)
schema = generate_composed_json_schema(result)
YAML config with inline wiring:
agents:
- name: browser_use
use_vision: true
llm: # full LLM config inline — IDE autocompletes all fields
provider: openai
model: gpt-4o
temperature: 0.7
- name: react
llm:
provider: anthropic
model: claude-3
Use inline_wiring=False to keep wired fields as Literal[...] key selectors.
Cross-Registry Diamond Inheritance
Register a class in multiple registries:
CombinedMeta = LLM.Meta | Agent.Meta
class LLMAgent(metaclass=CombinedMeta):
... # in both LLM and Agent registries
API Reference
Registration
| API | Purpose |
|---|---|
create_registrar(name, protocol, ...) |
Create a layer registrar (recommended entry point) |
Registrar.get(key) |
Look up a registered class |
Registrar.keys() |
List all registered keys |
Registrar.children(prefix) |
Query hierarchical key descendants |
Registrar.tree() |
Get nested dict of key hierarchy |
Registrar.bridge(external_cls) |
Create bridge for external class |
Registrar.register(key) |
Manual registration decorator |
discover(*package_paths) |
Import modules to trigger registration |
scan_registrar_definitions(root) |
Static AST scan for registrar definitions |
list_registries(root, ...) |
Runtime list of all registered content |
Config Typing
| API | Purpose |
|---|---|
extract_config_schema(cls, mro_scope, mro_depth) |
Extract Pydantic model from __init__ + __wiring__ |
build_layer_config(registrar) |
Build discriminated union (flat or nested mode) |
generate_layer_config_source(result) |
Generate Python stub source code |
generate_layer_json_schema(result) |
Generate JSON Schema |
build_composed_config(registrars, inline_wiring) |
Build multi-layer composed config |
generate_composed_json_schema(result) |
Generate composed JSON Schema |
generate_composed_config_source(result) |
Generate composed Python source |
compute_registry_fingerprint(registrar) |
Compute registry fingerprint hash |
get_registry(name) |
Look up a registry by name (for wiring) |
Design Principles
- Zero registration burden — Inherit a base class = registered
__init__+__wiring__are the single source of truth — No duplicate config definitions- Fail-fast — Duplicate keys raise immediately; invalid config rejects at startup
- Domain-agnostic — Pure infrastructure, knows nothing about agents or LLMs
- Stubs and runtime are separate — Stale stubs don't affect correctness
Documentation
Full documentation is shipped inside the package (accessible at site-packages/conscribe/) and browsable on GitHub:
| Document | Description |
|---|---|
llms.txt |
AI entry point — package summary and navigation |
docs/overview.md |
Core concepts and architecture |
docs/guide-alice.md |
Tutorial: building a framework with conscribe |
docs/guide-bob.md |
Tutorial: consuming a conscribe-based framework |
docs/api-reference.md |
Full API signatures and examples |
docs/recipes.md |
Task-oriented "how do I X?" |
docs/registration.md |
Registration subsystem internals |
docs/config-typing.md |
Config typing pipeline internals |
docs/mro-and-degradation.md |
MRO chains and type degradation |
docs/cli.md |
CLI reference (scan, list, inspect, generate-*) |
License
MIT
Release files for conscribe 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| conscribe-1.3.0.tar.gz | 80.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| conscribe-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 176.2 kB
Release files / conscribe-1.3.0.tar.gz
| Download URL | conscribe-1.3.0.tar.gz |
|---|---|
| Size | 80.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cd5de370ff3a9cb4e790bbf3214a293053a1b27a27c45217b307f3de73c1c1e4
|
|
BLAKE2b-256 checksum How to use checksums |
d3e6491f840fc3c4fb15f3b05ccd8f26fbf32b566caccf9b87095146109089fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|
Release files / conscribe-1.3.0-py3-none-any.whl
| Download URL | conscribe-1.3.0-py3-none-any.whl |
|---|---|
| Size | 95.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6eac14a0b1d7006f7baed49422aee41bb690a2f7cdf697cff54300670c68ad3a
|
|
BLAKE2b-256 checksum How to use checksums |
1fdb8f078fb00960279d2d0f6c97f8eaefe3f608e522918fcff86f9d973bcd09
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.13
|