Operon v0.20: Substrate Integration

Three-Layer Context, Auditable Workflows, and the Question Every Multi-Stage System Should Answer

Bogdan Banu · March 2026 · github.com/coredipper/operon

Release: v0.20.0
Abstract

v0.19 introduced BiTemporalMemory — an append-only fact store with dual time axes. v0.20 connects it to the SkillOrganism runtime, so multi-stage workflows get an auditable shared substrate instead of relying solely on an unstructured dictionary. The motivating question: “what did the organism know when stage X made its decision?” This release formalizes a three-layer context model (topology, ephemeral, bi-temporal), adds per-stage read/write hooks, and preserves zero overhead when no substrate is attached.

1. The Missing Layer

v0.18 gave SkillOrganism a clean multi-stage runtime: deterministic handlers, model-tier routing, and attachable components. v0.19 added BiTemporalMemory as a standalone fact store. But between them was a gap: stages could share state through a mutable dictionary, and they could query a memory system, but the two were not connected.

In practice, that means a four-stage enterprise workflow — research, strategy, evaluation, adversarial critique — has no structural answer to the question “what facts were available when the strategist made its recommendation?” The strategist’s inputs are lost in the ephemeral state the moment the next stage overwrites them.

v0.20 closes the gap by making BiTemporalMemory a first-class substrate of the organism runtime.

2. Three Layers of Context

The integration forced a question that had been implicit: when multiple stages share and revise knowledge during a workflow, which state is ephemeral coordination glue and which is durable auditable knowledge? The answer is a three-layer model:

  1. Topology layer. The wiring diagram and observation structure. Structural and static within a single organism run. Epistemic properties derive from this graph.
  2. Ephemeral layer. The shared_state dictionary. Carries routing hints, counters, morphogen concentrations, and temporary stage outputs. Mutable, lifetime of one run, not historically reconstructible.
  3. Bi-temporal layer. The BiTemporalMemory substrate. Carries durable factual knowledge with dual time axes. Append-only: corrections close old records and insert new ones with supersedes pointers. Belief state is exactly reconstructible at any historical coordinate.

Why Three Layers

The distinction matters because each layer has different lifetime and mutability semantics. Topology constrains visibility. Ephemeral state carries execution context. Bi-temporal memory provides the audit trail. Collapsing them into one dictionary (as most agent frameworks do) makes it impossible to reconstruct past belief states — the very thing you need for compliance, debugging, and trust.

3. The Substrate API

Attaching a substrate to an organism is one parameter:

from operon_ai import BiTemporalMemory, SkillStage, skill_organism

mem = BiTemporalMemory()

organism = skill_organism(
    stages=[research, strategist, evaluator, adversary],
    fast_nucleus=fast,
    deep_nucleus=deep,
    substrate=mem,
)

When substrate is None (the default), the run loop is unchanged: no datetime operations, no metadata injection, all existing tests pass unmodified. This is the zero-overhead guarantee.

4. Read Path: SubstrateView

Before a stage executes, the runtime evaluates its read_query field and packages the result into a frozen SubstrateView:

@dataclass(frozen=True)
class SubstrateView:
    facts: tuple[BiTemporalFact, ...]
    query: BiTemporalQuery | str | Callable | None
    record_time: datetime

The record_time captures the moment of the read, establishing the record-time horizon for this stage’s knowledge. The view is injected into handler stages as an additional argument (via arity-aware dispatch — existing handlers are unaffected) or into agent stage metadata.

read_query can be a subject string (for simple filtering) or a callable that receives the full execution context and returns a BiTemporalQuery, a dict, or a pre-filtered list of facts.

# Simple: filter by subject
SkillStage(name="strategist", read_query="acct:42", ...)

# Advanced: dynamic query based on prior outputs
SkillStage(name="evaluator", read_query=lambda task, state:
    BiTemporalQuery(subject=state.get("account_id")), ...)

Why Frozen Views

A SubstrateView is a frozen dataclass, not a reference to the memory. This means a stage cannot accidentally mutate the substrate through its read path. The only write path is through fact_extractor or emit_output_fact, which run after the stage completes. This separation is deliberate: reads happen before execution, writes happen after. The stage sees a consistent snapshot.

5. Write Path: Facts from Stage Results

After a stage executes, two mechanisms can emit facts:

emit_output_fact

A convenience flag. When True, the runtime auto-records the stage output as a fact with subject=task, predicate=stage.name, value=output.

SkillStage(
    name="research",
    handler=lambda task: {"risk": "medium", "sector": "fintech"},
    emit_output_fact=True,
    fact_tags=("auto", "research"),
)

fact_extractor

A callable that converts the stage result into one or more factual events. Each event specifies an operation — assert, correct, or invalidate — and is applied to the substrate via the standard BiTemporalMemory write API.

def adversary_extractor(task, state, outputs, stage, result):
    # Look up the original research fact and correct it
    old_fact = mem.retrieve_valid_at(subject="acct:42", predicate="risk_level")
    return {
        "op": "correct",
        "old_fact_id": old_fact[0].fact_id,
        "value": "high",
        "source": "adversary",
    }

SkillStage(
    name="adversary",
    handler=adversary_handler,
    fact_extractor=adversary_extractor,
)

The _coerce_fact_events() helper normalizes extractor returns: dicts, lists of dicts, BiTemporalFact instances, tuple shorthands (subject, predicate, value), and None (no-op) are all accepted.

SkillStage fieldPurpose
read_querySubject string or callable → SubstrateView injected before stage
fact_extractorCallable → assert/correct/invalidate events after stage
emit_output_factConvenience: auto-records (task, stage.name, output)
fact_tagsDefault tags on all emitted facts from this stage

6. The Audit Question

With the substrate wired in, the motivating question has a structural answer. After a workflow completes:

# What did the organism believe when the strategist ran?
strategist_time = ...  # captured in SubstrateView.record_time

belief = mem.retrieve_belief_state(
    at_valid=strategist_time,
    at_record=strategist_time,
    subject="acct:42",
)

# What changed after the adversary corrected the research?
diff = mem.diff_between(
    strategist_time, adversary_time,
    axis="record",
)

The answer is independent of subsequent corrections or ephemeral state mutations. It comes from the append-only history, not from a mutable dictionary that has long since been overwritten.

7. Backward Compatibility

The integration is entirely opt-in. When substrate=None (the default), every code path that touches the substrate is guarded behind a None check. No datetime operations are invoked, no metadata is injected, and all four original lifecycle hooks remain unmodified.

Existing handler signatures are unaffected thanks to arity-aware dispatch: _call_arity() truncates arguments to match the handler’s parameter count. A handler that accepts (task) or (task, state, outputs, stage) continues to work. A handler that accepts a fifth parameter receives the SubstrateView.

8. The Example: Enterprise Account Review

Example 71 demonstrates a four-stage enterprise workflow:

  1. Research — records facts about an account (risk level, sector, revenue) via fact_extractor.
  2. Strategist — reads the substrate via read_query="acct:42", produces a recommendation, auto-records it via emit_output_fact=True.
  3. Evaluator — records evaluation concerns via fact_extractor.
  4. Adversary — corrects the original risk level assessment from “medium” to “high” via fact_extractor with op="correct".

After the run, the example reconstructs the strategist’s belief state (which still shows “medium” risk) and diffs it against the post-adversary state (which shows the correction to “high”). The original belief state is fully preserved in the append-only history.

9. Validation

SuiteTestsStatus
Backward compatibility (substrate=None) 5 All pass
Substrate read path 4 All pass
Substrate write path (emit + extractor) 6 All pass
Integration (corrections, separation, halt) 3 All pass
Full regression suite at release 987 All pass
Reference Implementation: operon_ai/patterns/organism.py, operon_ai/patterns/types.py, examples/71_bitemporal_skill_organism.py

10. What Comes Next

v0.20 connects the memory and the runtime. The next phases on the roadmap continue building on this foundation: causal event logging that tracks not just what facts were available but why a stage chose what it chose; persistent storage backends so the append-only log survives process restarts; and temporal reasoning primitives that let agents query their own epistemic history as part of planning.

The three-layer context model is also a foundation for richer cross-organism coordination. When two organisms share a substrate, they get a shared auditable memory — without sharing ephemeral state or coupling their topologies. That is the multi-organism story for a future release.

The deeper motivation remains the same: agents that cannot explain their past decisions are agents you cannot trust with consequential ones. v0.19 gave the memory the right structure. v0.20 wires it into the place where decisions actually happen.

Code and release: github.com/coredipper/operon, operon-ai on PyPI, skill organisms docs, bi-temporal memory docs, interactive explorer