The AI-Native Dependency Graph Runtime
Build, inspect, and control your Python application’s dependency graph.
📚 Read the Documentation
Browse Examples · Full Table of Contents
Beginner → Intermediate → Advanced → Expert
SUPPORT US
If you like the work we're doing, please give us a star on GitHub.
pip install melder
Zero dependencies. Nothing else comes with it. Requires Python 3.14+.
What Melder Is
Melder™ wires your application together — services, config, connections, handlers — and hands you instances when you ask for them. That part will feel familiar if you've used a DI container.
The difference is what happens next. Dependency injection frameworks build your object graph and then forget it. Melder keeps it — as a live, inspectable, permissioned structure you can query, snapshot, restore, and change while the process is running.
For the one-picture distinction, open DI container versus Melder. For the system-level view, start with Architecture and design.
How it works
Three verbs. That's the entire mental model.
bind → register what exists (classes, functions, instances)
conjure → compile + validate the graph (once — cycles and gaps fail here)
meld → resolve instances (as often as you like)
Validation happens at conjure(), not at first use. A cycle, a missing
provider, an ambiguous binding — you find out at startup, not at 3am.
Who it's for
Melder scales down to a script and up to a distributed backend. Find yourself here:
🟢 You're writing an app and tired of passing objects around Four lines gets you working DI with real lifetimes and automatic teardown. No config files, no YAML, no decorators, no base classes, and nothing new to install. If you've been threading a database handle through five function signatures, this is the fix.
🟢 You want your wiring checked before you ship it
conjure() compiles and validates the entire graph up front. Cycles, missing
providers, ambiguous resolutions — all surface at startup with a specific error,
not at 3am on the one code path nobody exercised.
🟡 You're building a web service and want honest request scoping Per-request scopes that actually end, per-connection lifetimes, and cleanup that fires deterministically instead of whenever the GC feels like it. Works with any framework — Melder doesn't own your entry point.
🟡 Your app outgrew a single container
One registry holding everything eventually becomes its own coupling problem —
every subsystem can see every other, and one conjure() has to know the whole
world before it starts. Instead, give each subsystem its own book and
conduit: it owns its registry, exposes only what its permissions allow, and
configures and tears down independently. Link them at runtime and pull specific
spells across the boundary — SpellContract even lets a consumer be built
before its provider exists, which a single shared container can't do.
🟠 You're running multi-tenant, plugin, or sandboxed workloads Aetheric frames are hard isolation walls inside one interpreter — separate registries, separate control planes, separate singletons. One process, many worlds, no leakage between them.
🟠 You write tests and want real isolation A fresh frame per test is a fresh world. No global container to reset, no state bleeding between cases, no ordering dependencies.
🚀 You're doing genuinely concurrent work on 3.14+ Lock-disciplined throughout with a documented one-way lock order, built for free-threading rather than ported to it. Resolution never enters the transaction plane, so concurrent reads don't contend.
🔵 You're building agent infrastructure Hand an AI a permission-bounded room over a live system: it can read the graph, work on real objects, execute validated code, preview a change's blast radius, and hot-swap an implementation — with every structural change transacted and reversible. See Part II.
🔵 You're building a framework or platform on top The runtime is introspectable by design — the graph is a real structure you can query, snapshot, restore, and extend. Melder carries its own architecture docs inside the package so tooling can read the system without importing it.
✨ And whatever you build, the doors are already there This is the part worth understanding before you start. You do not have to decide today whether your project will ever involve AI. Build a plain application with plain classes — and the moment you want an agent to read your architecture, operate on live objects under a permission set, checkpoint a world, or propose a change and be shown its blast radius, those surfaces are already in the runtime you're using.
No migration. No second framework. No rewrite. Nothing to bolt on. Anything you build with Melder can be opened up to agents whenever you're ready — because the doors were built in from the beginning, sitting closed until you turn the handle.
Who it's not for
Straight answer, so you don't waste an afternoon:
- You need Python 3.13 or older. Melder requires 3.14+. That's not negotiable — the whole concurrency model assumes free-threading.
- You want autowiring by magic. Melder is explicit: you bind what exists. There's no classpath scanning that guesses your intent.
- You have three objects and a
main(). Just construct them. A DI runtime earns its keep when wiring becomes a real problem, not before.
Some of this isn't built for you
Worth saying plainly: parts of Melder are designed for machines, not humans.
The AR rooms, the codegen surface, the research and impact views — those are built for an agent that can hold a whole object graph in working memory, issue hundreds of structured queries, and act on JSON-shaped answers. A human can drive them, and the API won't stop you, but you'd be hand-operating machinery built for something with different ergonomics than yours.
That's deliberate, and it's why "AI-native" is a claim rather than a label. These surfaces weren't retrofitted for agents after the fact — an agent is the intended operator.
None of it is on your critical path. Everything in Part I is written for humans first and stays complete on its own. If you never open a Rift or record a checkpoint, Melder is simply a fast, strict DI runtime and nothing is missing.
Low floor, high ceiling
Melder is beginner-first by design. Four lines gets you a working graph, and
those four lines teach the whole runtime. Nothing in the advanced surface leaks into the simple
path — no mandatory configuration, no ceremony you must understand before your
first meld().
But the ceiling is very high. The same runtime that resolves your little service graph will checkpoint an entire distributed backend, hand an AI agent a permission-bounded room over live objects, and track every structural change under governance. You grow into it; it never asks you to grow into it first.
Fast enough to not think about
Melder holds very competitive speed against the fastest containers in
Python — it keeps up with dependency-injector and dishka, the two names
usually at the top of that list. And it does that while carrying a full graph
runtime: validation, permissions, lifecycle tracking, and introspection that
those containers don't attempt.
For any real application, DI resolution was never your bottleneck — I/O is. What you get for that parity is a system you can actually see.
The systems layer for agentic software
The current AI tooling stack lives at the orchestration layer: chains, graphs of steps, prompt routing, tool registries. Those frameworks decide what an agent should do next, and they're good at it.
None of them answer the layer below. When an agent calls a tool, what is it actually touching? Usually a loose function with no identity, no lifetime, no ownership, no memory of having been called, and no way to undo what it did.
Melder is that missing layer — the low-level runtime an agent operates inside rather than a bag of callables it operates through:
| Orchestration layer | Melder (runtime layer) |
|---|---|
| Picks the next step | Owns the objects the step runs against |
| Tools are functions | Objects have identity, lifetime, and an owner |
| State lives in a scratchpad | State lives in scoped, permissioned instances |
| Changes are untracked | Every structural change is recorded and reversible |
| Access is all-or-nothing | Access is a compiled permission set per room |
The two compose cleanly. Let your orchestrator decide the plan — let Melder be the world the plan executes in, where the graph is inspectable, authority is bounded, and every change is accounted for.
Change as a first-class operation
Here's the part that has no equivalent elsewhere.
Most systems treat "the code changed" as something that happens to them — between deploys, invisibly, with a restart to absorb it. Melder treats structural change as an operation the runtime performs and records.
Versions of bound units live in lanes with forward-only history and content-addressed identity — a unit's content hash is its identity, so the same thing re-entering is recognized as a rediscovery rather than a duplicate. Before anything changes, you can ask:
- What's the blast radius? — everything transitively affected
- What drifted? — how the running world diverges from the sealed one
- What would happen? — a read-only preview of a candidate change: the diff, the radius, the verdict. Nothing executes, binds, or records.
An agent can propose a structural change, be shown exactly what it would break, and be refused — before a single object is constructed.
That's the difference between an AI that can call your system and an AI that can safely evolve it.
How to Read This
Every section below is tagged with the level it belongs to. Read in order and stop wherever you have what you need — nothing later is required to use anything earlier.
| Level | What it means | Who it's for | Runnable examples | |
|---|---|---|---|---|
| 🟢 | Beginner | The core runtime. Bind, resolve, categorize, scope, clean up. | Everyone. Start here. | 01_beginner/ |
| 🟡 | Intermediate | Connection — module registration, dynamic linking, late binding across subsystems. | When one scope isn't enough. | 02_intermediate/ |
| 🟠 | Advanced | Frame isolation, read-only introspection rooms, clusters, deep overrides. | Multi-tenant or multi-world systems. | 03_advanced/ |
| 🔵 | Expert | Agent rooms, transactions, checkpoints, governed mutation. Mostly machine-facing. | Building on Melder, or handing it to an AI. | 04_expert/ |
This README is the tour. For system shape and design decisions, open
Architecture and design;
for the visual path, start with the
engineering drawings.
The folders above contain runnable scripts, verified together by the
pytest_examples/ harness.
Pick a route:
- Evaluating Melder: read What Melder is, then scan the engineering drawings.
- Building an application: enter Part I, then work through
the
01_beginner/examples. - Building runtime or agent infrastructure: enter Part II,
then continue into the
04_expert/examples.
If you just want dependency injection, the 🟢 sections are the entire product. You can stop at the end of them and never open Part II. Everything past that point is optional capability that stays dormant until you turn it on.
Part I — The Basics
Written for humans. Complete on its own.
🟢 Beginner → 🟡 Intermediate → 🟠 Advanced
🟢 Hello, Melder
Beginner. The whole runtime in four lines.
import melder as md
class Greeter:
"""A plain class — no base class, no metaclass, no decorator."""
def greet(self) -> str:
return "hello from a melded instance"
book = md.Spellbook()
book.bind(spell=Greeter, existence="unique")
conduit = book.conjure()
greeter = conduit.meld("Greeter")
print(greeter.greet())
assert conduit.meld("Greeter") is greeter # `unique` → same instance
Your classes stay plain Python. Everything lives on md.* — if you ever need a
deep import path, that's a bug in our public surface, not a pattern to copy.
🟢 The Rhythm
Beginner. Three verbs, one order.
book = md.Spellbook()
# 1. bind everything
book.bind(spell=Config, existence="unique")
book.bind(spell=Server, existence="many")
# 2. conjure ONCE ← compiles + validates the entire graph
conduit = book.conjure()
# 3. meld everywhere
server = conduit.meld("Server")
conjure() is where the work happens. Requirements extraction, graph
construction, cycle detection, and runtime compilation all run there. A cycle, a
missing provider, an ambiguous resolution — it raises there, not at 3am when
the code path finally executes.
One book, one conjure. A Spellbook produces exactly one conduit, ever. More scopes?
create_lesser_conduit(). Another root? Another book.
🟢 The Address Law
Beginner. The one rule behind all resolution.
One rule governs all resolution:
Every spell lives at exactly one address:
(frame_key, binding_key)—frame_keyis thespellframeif given, else the spell's name;binding_keyis thebinding_nameif given, else the default slot.
book.bind(spell=PostgresStore, existence="unique", binding_name="primary")
book.bind(spell=SqliteStore, existence="unique", binding_name="local")
primary = conduit.meld("PostgresStore", binding_name="primary")
🟢 Spellframes — Categories Inside a World
Beginner. The organizing half of every address.
A spellframe is the category half of the Address Law. It can be a plain
string when you just want grouping, or a Protocol when you want a shape:
# strings — organize by role
book.bind(spell=UsersRepo, existence="unique",
spellframe="storage", binding_name="users")
book.bind(spell=UsersApi, existence="unique",
spellframe="web", binding_name="users")
repo = conduit.meld(spellframe="storage", binding_name="users")
api = conduit.meld(spellframe="web", binding_name="users")
Same binding_name, two categories, two distinct addresses — names never
collide across spellframes. And because a Protocol works as a spellframe, one
category can mean "everything that satisfies this shape," which is exactly what
collection DI resolves:
book.bind(spell=EmailHandler, existence="unique",
spellframe=Handler, binding_name="email")
🟢 Dependency Injection
Beginner → Intermediate. Annotations, explicit targets, collections.
Annotate a constructor parameter. Melder builds the graph.
class ReportService:
def __init__(self, database: Database) -> None: # ← injected
self.database = database
book.bind(spell=Database, existence="unique")
book.bind(spell=ReportService, existence="unique")
conduit = book.conjure()
report = conduit.meld("ReportService")
When an annotation isn't specific enough — two implementations of one shape, or
a named binding — declare the target with SpellMap as the parameter default:
class Consumer:
def __init__(self, store=md.SpellMap(PrimaryStore)) -> None:
self.store = store
SpellMap takes the full address: a concrete spell, a spellframe, a
binding_name, or a combination. It obeys an exactly-one law — a target
that matches zero spells or several fails at conjure(), not at runtime.
Ask for a list and get every implementation — the plugin pattern in one annotation:
class Dispatcher:
def __init__(self, handlers: list[Handler]) -> None: # ← all of them
self.handlers = handlers
book.bind(spell=EmailHandler, existence="unique",
spellframe=Handler, binding_name="email")
book.bind(spell=SmsHandler, existence="unique",
spellframe=Handler, binding_name="sms")
🟢 Six Lifetimes, Not Three
Beginner. Pick how long things live.
| Existence | One instance per | Use it for | |
|---|---|---|---|
| 🟢 | unique |
Process (frame) | Config, pools, caches |
| 🟢 | unique_per_conduit |
Scope | Request / session state |
| 🟢 | many |
Nothing — new every meld | Stateless workers, DTOs |
| 🟡 | unique_per_conduit_lineage |
A scope and its children | Shared context down a tree |
| 🟡 | unique_per_spell_space |
A request-local space | Ephemeral request scoping |
| 🟠 | unique_per_conduit_cluster |
A named group of scopes | Cross-scope coordination |
The first three are the beginner set — they cover most systems and need nothing but a static conjure. The last three arrive with the features that give them meaning: lineage and spell spaces once you have nested scopes, clusters once conduits link into groups.
Callables are always
unique. Want a fresh object every time? Bind a class asmany.
🟢 Teardown Is Part of Registration
Beginner. Cleanup without ceremony.
Declare your teardown vocabulary — the method names that mean "clean yourself up" in this system — and Melder calls them on the way down. No base class, no protocol, no context-manager ceremony:
book = md.Spellbook()
book.bind(
spell=PooledConnection,
existence="unique",
disposal_method_names=["close", "shutdown"], # this binding's ordered methods
)
book.bind(
spell=BackgroundWorker,
existence="unique",
disposal_method_names=["shutdown"],
)
conduit = book.conjure()
...
conduit.cleanup() # calls each creation's matched methods in their declared order
Each bind's list belongs to that spell, not every later bind. Melder resolves matching class methods once at bind time and preserves their order:
- If the connection implements both names,
close()runs beforeshutdown(). - The worker uses its own
shutdown()declaration. - Missing names are omitted; duplicate names execute once.
For book-wide methods, use SpellbookConfiguration. Its matching methods form
one ordered block: last by default, or first with
with_enforce_priority_disposal_methods(True). Shared names belong to the book's
block in either mode; spell-only names retain their own order. See the
configuration guide
for both arrangements.
Cleanup retains its existing reverse key/bucket traversal within each store. A failing
method stops that object's remaining method calls; cleanup continues with other
objects and aggregates failures into an ExceptionGroup.
🟢 Scopes That Nest
Beginner. Child scopes, no configuration.
root = book.conjure()
child = root.create_lesser_conduit()
assert root.meld("Config") is child.meld("Config") # shared
assert root.meld("Session") is not child.meld("Session") # per-scope
🟡 Declarative Binding by Module
Intermediate. Register where the class lives.
Prefer to declare registration where the class lives? Tag it with
@md.scan_bind, then register the whole module in one call.
services.py — declare intent next to the code:
import melder as md
@md.scan_bind(existence="unique")
class MetricsHub:
pass
@md.scan_bind(existence="many")
class JobTicket:
pass
main.py — decide when it actually binds:
import melder as md
import services
book = md.Spellbook()
bound = book.scan(services) # ← binds every tagged class in the module
conduit = book.conjure()
hub = conduit.meld("MetricsHub")
The decorator stores intent only — nothing touches the runtime until
scan() runs. Importing services registers nothing: no hidden global
container filling up behind your back, no import-order puzzle to debug. You
still decide when binding happens, and where.
Mix freely: scan a module, then bind() a few more by hand.
🟡 Dynamic Mode & Linking Conduits
Intermediate. Everything above works without this. Turn it on when separate parts of your system need to share.
By default a world is static — bind, conjure, resolve. Turn on dynamic mode and conduits gain the ability to connect to each other at runtime.
The mode is decided once, by the first conjure, and then inherited:
# SETTLEMENT — the first conjure on a fresh world sets the mode, and it locks
owner = owner_book.conjure(dynamic=True, name="owner")
# INHERITANCE — later books just attach; a plain conjure() inherits the mode.
# You never repeat the flag.
borrower = borrower_book.conjure(name="borrower")
Now link them and share a spell across the boundary:
owner.link(borrower) # open a contract between the two
borrower.add_spell_to_contract( # ← the BORROWER asks
spell_id=spell_id,
conduit=owner, # ← from the conduit that OWNS it
permissions="create",
)
shared = borrower.meld("SharedDirectory") # resolves the owner's spell
Sharing is a pull, never a push. The conduit that wants the spell asks for it, and names the owner. An owner cannot force its objects into someone else's scope — so nothing arrives in your conduit that you didn't request.
🟡 Late Binding with SpellContract
Intermediate. Wire whole subsystems together without either side importing the other.
A SpellContract is a hole you leave on purpose. The class declares the
shape it needs; the provider arrives later, across a link:
class ReportService:
def __init__(
self,
config: ConfigContract = md.SpellContract(
spellframe=ConfigContract, binding_name="platform",
),
) -> None:
self.config = config # ← filled from another conduit entirely
Name your conduits after the resolution ideas your app already has — platform,
services, workflows — and the conduit names become your factory layer. No
abstract factories, no service locator, no imports between subsystems.
The order of operations is the law. Per dependency edge, in this exact order:
platform = platform_book.conjure(dynamic=True, name="platform") # 1. provider
services = services_book.conjure(dynamic=True, name="services") # 2. consumer
platform.link(services) # 3. link — AFTER both exist
services.add_spell_to_contract( # 4. consumer pulls
spell_id=config_id, conduit=platform, permissions="create",
)
service = services.meld(spell_id=service_id) # 5. meld completes the binding
Assemble the chain edge by edge, in dependency order. services can then feed
workflows with the identical five steps one level up — the finished product of
one category becomes the dependency of the next.
🟠 Frame Boundaries — More Than One World Per Process
Advanced. Hard isolation walls inside a single interpreter.
Everything so far has lived in one aetheric frame — the default world. A frame is the outermost boundary in Melder: it owns its own conduits, registries, control plane, and dynamic posture. Name a different one and you get a second world that shares nothing with the first:
tenant_a = md.Spellbook(aetheric_frame="tenant-a")
tenant_b = md.Spellbook(aetheric_frame="tenant-b")
Use distinct binding_name or spellframe values when binding the same class
in both worlds. Spell IDs are unique across the process: an identical binding
fingerprint produces the same ID even in another frame and is refused.
A unique spell is a singleton per frame — not per process. Two distinct
bindings, two frames, two instances, one interpreter.
That makes frames the natural seam for multi-tenancy, plugin isolation, and test isolation (a fresh frame per test is a fresh world). Conduits link within a frame; they do not link across one — the wall is real.
Don't confuse the two "frames":
spellframe=is an address key that categorizes spells inside a world.aetheric_frame=is the world itself.
Three widths of category
Once you see it, the same idea repeats at three scales — each one a wider boundary than the last:
| Scale | Categorizes | Owner |
|---|---|---|
spellframe |
Spells within one world | The book that bound them |
| Conduit | Worlds of resolution, linked by contract | Each conduit owns its category |
aetheric_frame |
Entire isolated worlds | The frame itself |
Pick the narrowest one that expresses what you mean. Most systems need only the first; large ones grow into the second; the third is for genuine isolation.
🟠 Read-Only Rooms for Endpoints
Advanced. A safe window onto a running system — no agent required.
You don't need to be building AI to want a live view of your own runtime. A
static Nexus room is exactly that: a read-only projection over a running frame
that can inspect everything and change nothing.
That makes it a natural backing for the introspection surfaces you'd otherwise hand-roll:
- a health endpoint that reports what's genuinely live, not what you hope is
- an admin panel listing resolved services and their scopes
- a debug view of the real dependency graph in production
Because the room's authority is compiled and read-only, exposing it carries no
mutation risk — the worst a caller can do is look. The capability and
codegen rooms in Part II escalate from here.
What else this README skips
Real surfaces this page doesn't walk through — all covered in the documentation:
| Surface | What it does | |
|---|---|---|
| 🟡 | Spell spaces | Request-local scopes backing unique_per_spell_space |
| 🟡 | Registration hooks | pre / activation / post callbacks on the bind lifecycle |
| 🟡 | SpellBinder |
A fluent chained alternative to bind(...) |
| 🟡 | Ownership transfer | Move a spell's stewardship between conduits at runtime, repointing borrowers and clusters |
| 🟠 | Conduit clusters | A named group of scopes sharing one instance, with an elected leader |
| 🟠 | Deep override paths | Inject a variant into the middle of a live graph at meld time — override={"transport>credentials": obj} — without rebinding anything |
Part II — The Ceiling
Mostly written for agents. Optional, and off unless you turn it on. Everything above is a very good DI container. This is why Melder exists.
A note on the audience. The surfaces below assume an operator that reads structured output fast and issues many precise calls — an agent, in other words. They're documented here so you know the ceiling exists and can reason about what you're handing an agent. You are not expected to drive them by hand, and nothing in Part I depends on any of it.
🔵 The Runtime Documents Itself
Expert / agent-facing. The package carries its own architecture.
Melder ships its own architecture as queryable objects inside the wheel. An agent reads the system before touching it — no web, no scraping, no RAG:
import melder as md
md.__architecture__ # system architecture (C4)
md.__components__ # component catalog
md.__graph_network__ # object relationship graph
md.__graph_details__ # detailed wiring
help(md) # the workflow map
Most frameworks make an agent guess from source. Melder hands it the map.
🔵 Give an Agent a Room, Not Your Process
Expert / agent-facing. Bounded authority over a live frame.
Nexus is the mediated access layer into the live object world. You open a
Rift — a workspace over a running frame — and the room decides how much
authority the occupant has.
rift = nexus.create_rift(rift_name="alpha")
conduit = rift.create_nexus_frame(frame_name="ops") # rooted, live
Three room postures, escalating:
| Room | What the occupant can do |
|---|---|
static |
Read-only. Inspect live spells, never mutate topology. |
capability |
Discover conduits, traverse contracts, activate spells, mutate topology. |
codegen |
Everything above plus write and execute Python against live objects. |
static is covered back in Part I — it needs
no agent and works fine as a plain introspection endpoint. The two below are
where authority starts escalating.
The agent's seat
Open a room over a live frame, and it comes with a workbench attached:
rift = nexus.create_rift(rift_name="alpha")
root = rift.create_nexus_frame(frame_name="ops") # a real, rooted conduit
rift.create_frame_link("ops")
space = rift.space # the room
viewer = space.frame_viewer # read the live graph
workstation = space.workstation # a bench to put objects on
command = space.command_system # the room's verbs
The workstation is where an agent keeps what it's working on — bind a live object to a name, make it the active target, then operate on it:
service = root.meld("ReportService")
workstation.bind_object("svc", service) # park a LIVE object on the bench
workstation.set_target("svc") # make it the active target
workstation.call_target() # ...and drive it
Then the agent writes Python against that world. The generated code runs in a namespace you compiled — these five names and nothing else:
code = """
# viewer / workstation / target / command / codegen — the whole namespace
frames = viewer.list_nexus_frame_names() # read the live world
current = target # the bench's active object
result = {"frames": frames, "kind": type(current).__name__}
"""
outcome = command.execute_codegen(code, frame_name="ops")
# every fragment is VALIDATED before it runs; a rejected one never executes
The agent inspects the running system, works on real objects, and writes code against them — without ever holding a reference to your process.
And the ACL is a dial, compiled per frame:
imports_enabled = False # no imports at all
allowed_import_module_roots = ("json",) # ...or exactly these
denied_builtin_names = ("eval",) # kill specific builtins
unsafe_reflection_allowed = False # no type()/introspection escapes
dunder_access_allowed = False # no __class__ walks
recursive_codegen_allowed = False # code that writes code
Every generated fragment is validated before it executes. import math in a
json-only room is rejected. type(command) in a reflection-denied room is
rejected. The agent gets a real seat at the runtime, bounded by a permission set
you wrote.
🔵 An Internal DevOps Plane, With Real Transactions
Expert. How concurrent structural change stays safe.
The obvious question about everything above: if an agent can rewire my topology while the system is running, what stops it corrupting the world?
Melder carries a full internal DevOps environment — a control plane that admits, serializes, and audits every structural change. It isn't a lock around the container. It's a transaction system with ACID semantics over the object graph.
Every structural mutation is a transaction in a named family:
bind · link · unlink · cluster_link · transfer_ownership
add_to_index · remove_from_index · notch
Each one declares the scopes it touches and the mode it needs, then a mediator admits it — or makes it wait.
Atomicity — Scope acquisition is all-or-nothing. A request takes every scope it needs or none of them, and owns exactly one root session. Failures compensate: a refused operation rolls back its claims, and a mid-operation refusal restores everything it had already moved, in original order.
Consistency — Changes propagate as invalidation, not corruption. Touching a lineage marks dependents dirty; the resolver refuses to execute a stale graph and recompiles it first. A built-in consistency audit walks every bidirectional map looking for asymmetry — any mismatch is evidence a write bypassed the plane.
Isolation — Claims are moded, not binary: x exclusive, s shared, ix
intent. Disjoint work admits in parallel; shared and intent claims coexist on the
same scope; exclusive excludes everything. Two subsystems binding into different
books never see each other. Two operations targeting the same conduit serialize.
Waiting is bounded, and a timeout names which scope blocked and who held it —
not a generic deadlock.
Durability — Committed structure is recorded to the persistence twin and survives into checkpoints (next section).
Two design choices worth calling out:
- Readers never enter the plane.
meld()doesn't take transaction locks — resolution stays fast and is protected by validity gating instead. You pay for coordination only when you change something. - Ownership is asymmetric on purpose. Sharing is a pull: the conduit requesting a spell must be the one that gets it, and an owner cannot push its objects into someone else's scope.
There's also a queryable operations catalog — live transaction activity, cluster fan-out, the blast radius of a proposed ownership transfer, a whole-frame operational rollup, and that registry consistency audit. It's caller-paid: nothing runs it for you, and it costs nothing until asked.
This is what everything else stands on
The DevOps plane isn't a side feature — it's the reason the rest of this README is safe to promise. Every capability above and below runs through it:
| Capability | What the plane guarantees |
|---|---|
| 🟡 Linking conduits | link / unlink are transactions. Two subsystems wiring themselves simultaneously serialize on the shared scopes and proceed in parallel everywhere else. |
| 🔵 Hot-swapping a live spell | notch is a transaction. The index repoint, the parking of the old version, and dependent invalidation land atomically — a meld never observes a half-swapped index. |
| 🔵 Agents mutating topology | Generated code has no privileged path. command.link_frame(...) from a codegen room enters the same plane, takes the same claims, and waits like anything else. |
| 🔵 Checkpoints | Only committed structure is recorded. A checkpoint can't capture a half-applied transfer, so a restored world is one that actually existed. |
| 🚀 Parallel everything | Moded claims mean disjoint work never contends. This is what makes concurrent structural change fast rather than merely survivable. |
Take the plane away and every one of those becomes a race condition. That's why it's here, and why an agent gets to touch a live system at all.
🔵 Checkpoint the World, Rebuild It Cold
Expert. Persistence, cold boot, and pod restarts.
Crystallizer records a digital twin of your configured system — structure,
never live instances:
checkpoint_id = crystallizer.create_checkpoint(description="pre-migration")
crystallizer.describe_profile() # what's recorded right now
crystallizer.describe_checkpoint(checkpoint_id)
Restore replays that world through public verbs only — the same bind,
conjure, link calls you would have written. Which means:
- Fresh identities always. ULIDs are minted new, never rehydrated.
- All-or-nothing. Any stage failure tears down everything in reverse order.
- An honest ledger. Anything unreplayable is a named shortfall, never a silent gap.
- Drift detection. Every load re-hashes your source against the sealed fingerprint and announces divergence before building anything.
Saving is half of it — loading is the point
A checkpoint you can't boot from is a diary. CrystallizerBootstrap is the
other half: one fluent chain that brings an entire world back on a cold
process.
report = (
CrystallizerBootstrap()
.with_external_persistence_manager( # optional: your own DB
ExternalPersistenceManagerConfiguration()
.with_upload_handler(upload)
.with_download_handler(download)
.with_list_handler(list_ids)
)
.with_profile("default")
.bootstrap()
)
report["activated"] # crystallizer live
report["remote_reload"] # what was pulled back from your store
report["restored_checkpoint_id"] # which world it booted
report["restore_report"] # per-stage build counts + shortfalls
That single chain does the whole cold start: activate → attach your storage → reload the local cache → pull from remote and re-flush it locally → verify the checkpoint chain → restore the newest world → hand you a report.
Storage is yours. Melder ships no database driver and no cloud SDK. You hand it four callables — store, fetch, list, delete — and it uses them. Your Postgres, your S3, your Redis, your rules. That's why the dependency list is empty.
A pod restarts, pulls its last checkpoint, and rebuilds the world structurally intact — no serialized objects, no pickle, no version-brittle blobs. The spell identity that comes back is the same content hash that went in.
🔵 Structural Change, Under Governance
Expert / agent-facing. Hot-swap a live implementation, with foresight and rollback.
This is where "hot reload" stops being a figure of speech. An agent can swap a live implementation for a new one on a running system — but first it has to answer the only question that matters: what does this break?
research = conduit.mutation_research # the world's research record
# 1. WHAT IS THIS THING NOW? — declared lane, live state, stored custody
research.residency_view(spell_id)
# 2. WHAT DOES IT COST TO TOUCH IT? — transitive blast radius,
# joined with which lanes and campaigns those spells live in
research.impact_view(spell_id=spell_id)
# 3. WHAT *WOULD* HAPPEN? — the read-only rehearsal
candidate = """
class ReportService:
def run(self) -> str:
return "v2 report"
"""
verdict = research.preview_candidate(candidate, against_spell_id=spell_id)
# → what it defines, what it imports, how it differs from the version it
# would replace, and the radius that replacement would carry.
# NOTHING executes, binds, or records. It is a rehearsal, not a change.
Then it actually changes the thing
Foresight is half of it. The other half is the live swap — stage → notch → meld, on a running system, with the old version still sitting there:
# 1. STAGE — bind a new version as an INACTIVE member of the existing index.
# It is parked and unresolvable: nothing melds it, nothing sees it yet.
id_v2 = conduit.bind_inactive(
spell=ReportServiceV2,
spell_index=index, # the index the current version lives in
existence="unique",
binding_name="v2",
)
# 2. NOTCH — promote v2 to ACTIVE. The outgoing version parks, the index
# pointer repoints, and dependents are marked for lazy recompile.
conduit.notch_spell(spell_index=index, spell=spell_v2)
assert index.selected_spell_id == id_v2
# 3. MELD — the very next resolution builds the NEW version.
service = conduit.meld(spell_id=id_v2) # → ReportServiceV2
And because the old member is parked, not destroyed, rollback is the same verb pointed the other way:
conduit.notch_spell(spell_index=index, spell=spell_v1) # instant revert
The parked version is still in the ecosystem. Notching does not dispose of anything — v1 remains a member of the index, holding its slot and its state. That's what makes the revert above instant, but it also means retirement is a deliberate act: the agent (or you) decides when a version leaves, and removes it explicitly. Nothing is swept up for you, and nothing disappears behind your back.
That's the whole loop: an agent reads the system, previews a change, stages it beside the live one, promotes it, and can put it back — while the process keeps running. Every world entry and promotion lands in a journal you can walk, in lanes with forward-only history and content-addressed identity (a unit's content hash is its identity, so the same code re-entering is a rediscovery, not a duplicate).
The point isn't that an agent can mutate your system. It's that it's shown the cost first, the change is staged rather than smashed in, and the previous version is one call away.
🚀 All of It, in Real Parallel
Everyone. Free-threading is the foundation, not a feature.
Every DI library in Python was designed when threads couldn't run simultaneously. "Thread-safe" meant "we put a mutex on it," because contention was theoretical.
Melder was written after that stopped being true.
- Lock-disciplined registries and scopes with a documented one-way lock order
- Concurrent resolution as a first-class path, not a footnote
- Real threads, real shared memory — no multiprocessing crutches
- Warns at import if it detects a GIL-enabled build
Two planes, on purpose
The reason Melder can be both fast and safe under real parallelism is that reads and writes don't share a road:
Readers — meld() |
Writers — structural change | |
|---|---|---|
| Path | Never enter the transaction plane | Admitted through the mediator |
| Protection | Validity gating: a stale graph is refused and recompiled | Moded scope claims (x / s / ix) |
| Contention | None with other readers | Only with work touching the same scopes |
| Cost | Resolution speed, uncompromised | Paid once, at the moment you change something |
Resolution — the thing you do thousands of times — carries no transaction overhead at all. Coordination is charged only to the thing you do rarely: changing the shape of the system.
That split is what makes the whole design hold together. Without free-threading there'd be no real parallelism to protect. Without the mediator, parallel structural change would be a race. You need both, and Melder was built assuming both from the first line.
Documentation
Use the routes below to choose source-controlled architecture, runnable examples, hosted reference, or video walkthroughs. This README stays the runnable tour; long-form guides and API reference belong in the hosted documentation site.
| Where | What's there | |
|---|---|---|
| 🗺️ | Architecture and design | High-level pictures, runtime structure, utilization stories, and tradeoffs |
| 🖼️ | Engineering drawings | DI comparison, C4/C3/C2 views, use cases, lifecycle, coordination, recovery, and advanced flows |
| 🧪 | Runnable examples | Beginner-to-expert scripts organized by the same level ladder used in this README |
| ✅ | Example verification | Pytest harness and contract probes for the runnable curriculum |
| 📘 | Getting started | The guided introduction — start here after this page |
| 📚 | melder.readthedocs.io | Four learning levels, runnable examples, API reference, and full contents |
| ▶️ | Synaptic AI on YouTube | Walkthroughs, deep dives, and live builds |
License
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later) — see LICENSE.
The AGPL's network clause applies: if you run a modified version of Melder™ as a service that users interact with over a network, you must offer those users the corresponding source of your modified version.
Trademark. Melder™ is a trademark of Mark Thomas Geleta / Synaptic AI Systems. The AGPL grants rights in the software, not in the name — forks and redistributions must not use the Melder name or marks in a way that suggests endorsement by, or origin from, the trademark holder. See NOTICE.
Contributing
Issues and design discussion welcome via GitHub issues.
Melder is under active architectural development — if you're planning something substantial, open an issue first so we can align on direction.
Built by Mark Geleta · Synaptic AI Systems
A better place for intelligence to live and work.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file melder-0.2.37.tar.gz.
File metadata
- Download URL: melder-0.2.37.tar.gz
- Upload date:
- Size: 2.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cfe05730713f96d1645e8fc56a6a6155b05d7a8f930d0e3bac64e0ee6a5da632
|
|
| MD5 |
ef1bb5278c5810363f9521990368f0ac
|
|
| BLAKE2b-256 |
d7f9de5c49520d8bf51e678e47dba285b8cfd7faaa1be713c5ba3987d29b205b
|
File details
Details for the file melder-0.2.37-py3-none-any.whl.
File metadata
- Download URL: melder-0.2.37-py3-none-any.whl
- Upload date:
- Size: 3.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7cd546c8dce90b47d7cc2dd8d7e202d25764dc7008e7ffde24bb6263c80d6fea
|
|
| MD5 |
f78696f5762855865f0365c0eb37421f
|
|
| BLAKE2b-256 |
73772f288f75832818cd21bd1117228a82fbdf4ce377455033fb536299d216f4
|