Operon v0.20: Substrate Integration
Three-Layer Context, Auditable Workflows, and the Question Every Multi-Stage System Should Answer
Release: v0.20.0
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:
- Topology layer. The wiring diagram and observation structure. Structural and static within a single organism run. Epistemic properties derive from this graph.
- Ephemeral layer. The
shared_statedictionary. Carries routing hints, counters, morphogen concentrations, and temporary stage outputs. Mutable, lifetime of one run, not historically reconstructible. - Bi-temporal layer. The
BiTemporalMemorysubstrate. Carries durable factual knowledge with dual time axes. Append-only: corrections close old records and insert new ones withsupersedespointers. 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 field | Purpose |
|---|---|
read_query | Subject string or callable → SubstrateView injected before stage |
fact_extractor | Callable → assert/correct/invalidate events after stage |
emit_output_fact | Convenience: auto-records (task, stage.name, output) |
fact_tags | Default 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:
- Research — records facts about an account
(risk level, sector, revenue) via
fact_extractor. - Strategist — reads the substrate via
read_query="acct:42", produces a recommendation, auto-records it viaemit_output_fact=True. - Evaluator — records evaluation concerns via
fact_extractor. - Adversary — corrects the original risk level
assessment from “medium” to “high” via
fact_extractorwithop="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
| Suite | Tests | Status |
|---|---|---|
| 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 |
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