Skip to content

Memory plane

An agent needs to remember things at more than one timescale. Within a single dispatch it holds the conversation it is building. Across a session it accrues what happened. Across many sessions it should draw on knowledge that earlier runs produced, and it should honor the conventions a project has already established. The memory plane splits all of this into distinct tiers — each with its own lifetime, scope, and store surface — persisted through one durable port, and readable under any coordination loop.

  • The durable, addressable memory tiers — episodic, semantic, procedural — each a named MemoryTier with its own lifetime, scope, and set of store methods.
  • Working memory — the in-flight message stream a dispatch builds — and the snapshots that make a session recoverable. Working memory is deliberately not a durable tier (see below); it is per-instance and ephemeral.
  • How each tier is scoped and bound: per-agent, per sibling group, per tenant, per project — optionally into a named memory space so recall can outlive a single session.
  • Cross-run knowledge — how a later run confers over what earlier runs contributed, through the tenant-scoped semantic tier.

What it deliberately does not govern: the prompt a single model call sees (the context plane), the durability machinery itself (sessions, events & durability), and where a secret lives (the credential plane). Memory never holds a credential.

MemoryTier is a closed enum with three variants — episodic, semantic, procedural. Working memory is intentionally absent from it: it is the ephemeral state a dispatch carries, never a durable addressable space. So there are four kinds of memory in play, but only three tiers.

MemoryLifetimeScopeStore surface
Workingper-dispatch, snapshotted at node completionper-agent, unconditional read-writeworking_memory_read / _write
Episodicsession-local, or cross-session in a named spaceper-agent or per sibling groupepisodic_read / _tail / _write
Semanticcross-sessiontenant-wide, or a named spacesemantic_search / _write
Proceduralcross-sessionper project (tenant × project_id)procedural_load / _save

Working memory is the message stream — assistant turns and tool results — that an agent accumulates during a dispatch. Every agent gets it read-write, unconditionally; there is no access mode to withhold it. It is snapshotted to the store each time a node completes, where a node is one iteration of the worker’s tool-use loop — so the snapshot cadence is per-turn, not once at end-of-task. That snapshot is also the recovery point: a restarted session rehydrates working memory from the last checkpoint.

Episodic memory is what happened during a session. It is scoped by EpisodicScope to either a single agent (PerAgent) or a sibling group — agents spawned from the same parent under the same role, sharing one episodic record. Sharing is opt-in per role and defaults to per-agent. Reads come in two shapes: a full read, and a bounded recency tail used at dispatch time.

Semantic memory is the tenant-scoped, cross-session knowledge tier. A write stores content plus metadata and an embedding; a read is a hybrid search — vector similarity blended with a lexical rank (detailed below). This is the tier that carries knowledge forward across runs.

Procedural memory is a project’s operating conventions, authored as an AGENT.md document and parsed into named sections. It is per (tenant, project_id), versioned with a monotonic revision, and — unlike the other tiers — eager-loaded: it is prepended as the first system message of a fresh agent, so the conventions frame every model call from turn zero.

Three durable tiers plus ephemeral working memory, all persisted through one StateStore port with three interchangeable adapters.

Every tier persists through one StateStore port. Three adapters implement it — in-memory, SQLite, and Postgres (fronted by a Redis hot cache for working memory and event-log append) — and the tier semantics above are identical across all three. The port lives in the substrate types crate; the adapters live beside it.

An agent’s reach into the durable tiers is a MemoryScope: an AccessMode (NoneReadReadWrite) per tier, plus a Locality. Access is granted per role, so a worker can read semantic memory without being able to write it, or be denied a tier entirely. Working memory has no entry here — it is always read-write.

By default each tier resolves to its tenant-default, unnamed space. A start request can instead bind a tier to a named memory space — a semantic_space or episodic_space on the request, and a project_id for procedural — which turns session-local recall into durable, cross-session recall under that name. A named space is created explicitly (memory_space_create, exposed over gRPC and the REST memory routes) and listed per tier. Binding happens once, at role-config resolution when the agent is spawned, and is stamped onto the agent for the life of the session.

Cross-run knowledge: confer and contribute

Section titled “Cross-run knowledge: confer and contribute”

Two verbs move knowledge across the session boundary. Confer reads relevant prior knowledge into a dispatch; contribute writes what a completed session learned back out.

Confer reads at dispatch setup; contribute consolidates at synthesize-success. Both are best-effort and never block the session.

Confer happens at dispatch setup, for the planner, worker, and synthesizer tiers only — the validator tier is deliberately excluded so a judge’s evaluation stays sealed from prior-run influence. The orchestrator runs a hybrid semantic_search over the bound space, filtered by a per-agent relevance floor, and injects the results as one system message headed “Relevant prior knowledge:”. That message lands after any procedural AGENT.md block, so the project conventions stay at index zero. Confer queries the store directly and is deliberately not billed as a tool call. The episodic tier is conferred the same way, from its recency tail.

Contribute happens at the end, when a session synthesizes a non-empty result successfully. It is not a raw append: it is a consolidation pipeline — extract candidate facts, retrieve their nearest existing neighbors, classify each as add / update / delete / no-op, then apply. That is what keeps the semantic tier from accumulating near-duplicates run after run. Contribution is gated on write access to the semantic tier and is best-effort — a consolidation failure is logged, never fatal to the session.

Both verbs are inspectable: confer emits a MemoryConferred event carrying each result’s vector, lexical, and combined scores, and contribute emits MemoryContributed. The tiers themselves are loop-agnostic substrate — every coordinator writes through the same port, so every coordination loop shares one memory model rather than each inventing its own.

No embedding provider means no semantic tier

Section titled “No embedding provider means no semantic tier”

The semantic tier needs an embedding provider, and a deployment may not have one configured. In that case every semantic call is refused with a typed error that names the config key — it does not fall back to a stand-in:

semantic memory is not configured: this deployment resolved no embedding provider, so there is nothing to embed against.

This is deliberate, and the alternative is worse in a way that is easy to miss. A hash-based stand-in returns consistent rankings, so every semantic test passes, the search returns plausible-looking results, and nothing anywhere reports a problem — while the rankings mean nothing. A refusal an operator can see beats a plausible answer they cannot distinguish from a real one.

For a local-dev stack that genuinely wants hash vectors, the relaxation is explicit, named, and logged at boot:

[substrate]
embedding_provider_policy = "allow_fake_local_dev"

Unset, absent, or unrecognised resolves to the strict posture — forgetting the flag fails safe. All three store adapters behave identically here.

Semantic search is hybrid: a SemanticQuery carries a vector weight, a lexical weight, and an optional cosine relevance floor; each result reports a vector_score, a lexical score, and their weighted combined_score. The vector half is always cosine similarity over the stored embedding. The two halves are computed differently per adapter, and the difference is worth knowing:

  • In-memory — a full linear scan over the space, cosine similarity plus a hand-rolled BM25 lexical rank. No index; correct and simple, sized for dev and test. See apomesh-substrate-state.
  • SQLite — a linear-scan cosine plus SQLite’s native FTS5 bm25() ranking over a virtual table, joined so the lexical rank never gates recall. Also in apomesh-substrate-state.
  • Postgres — pgvector HNSW over a cosine index for approximate-nearest- neighbor vector search, plus Postgres full-text ts_rank_cd for the lexical half. The result field is still named for BM25 for cross-backend uniformity, but the Postgres lexical algorithm is ts_rank_cd, not literal BM25. This is the production path — see apomesh-state-store-postgres.

The query surface is identical across all three; only the index and lexical algorithm differ. Consult the crate pages for the exact index and tuning.

Procedural memory is authored, not learned. An AGENT.md document is parsed on ## headers into named sections, keeping the raw markdown verbatim alongside the structured map, under a monotonically bumped revision. An agent changes it only through propose-then-approve: the procedural_memory.propose tool submits a diff and a reason, and the orchestrator turns that into a human-in-the-loop pause. It emits a proposal pending event, installs a HITL waiter, and awaits an operator decision — approve (write the diff), redirect (write the operator’s edited version), or reject (record the reason, refuse the write). The write re-parses the sections and bumps the revision; every outcome is a durable event, so the change history is auditable. Memory never silently rewrites a project’s operating rules.

Working memory is what the agent holds; the context plane is what a single model call sees. The two are distinct, and the seam between them is compaction. As a dispatch grows, the message stream is trimmed to a token runway before the context plane composes the actual prompt — the planner’s history under an operator-selectable CompactionStrategy, the worker’s conversation under a fixed sliding-window safety net. Compaction mutates working memory; the context plane then compiles what survives into the call. The memory plane owns the durable, recoverable stream; the context plane owns the per-call composition. Keeping them separate is why a session can be replayed: what the agent remembered and what any one call saw are recorded as distinct events.

  • Tiers are scope-isolated. Semantic memory never crosses a tenant boundary; procedural memory never crosses a project; episodic memory never crosses its agent or sibling group. Isolation is enforced at the store boundary, not by convention.
  • Working memory is recoverable. It is snapshotted at every node completion, so a restarted session rehydrates from the last checkpoint rather than losing its turn-by-turn state.
  • Cross-run recall is inspectable. Confer and contribute emit events carrying the entries and scores involved, so what a run drew on and what it wrote back is auditable on the log.
  • Procedural knowledge is never silently rewritten. Every change goes through propose → approve / redirect / reject, each a durable event.
  • Consolidation is best-effort. A failed contribution never fails the session; the work already completed still lands.
  • One model, every loop. The tiers and the store port are loop-agnostic — the same memory model serves every coordination strategy.