| Field | Value |
|---|---|
| RFC | 0004 |
| Title | Memory Layer |
| Status | Accepted |
| Author(s) | David Tufts (@davidscotttufts) |
| Created | 2026-05-01 |
| Updated | 2026-05-11 (Active → Accepted: integration-seams audit closed via docs/MULTI-AGENT-INTEGRATION-GAPS.md archive; conformance scenarios pass against SQLite reference host) |
| Affects | schemas/memory-entry.schema.json, schemas/memory-list-options.schema.json, schemas/agent-manifest.schema.json, spec/v1/capabilities.md, SECURITY/threat-model-secret-leakage.md |
| Compatibility | additive |
| Supersedes | — |
| Superseded by | — |
Summary
Define a host-side MemoryAdapter interface for cross-run agent memory persistence: list, get, put, delete. Memory entries carry redaction guarantees that compose with BYOK secret handling. The protocol contract is the wire shape (MemoryEntry, MemoryListOptions) and the determinism-preserving read-path semantics; the storage backend is a host choice.
Motivation
RFC 0002 declares AgentRef.memoryRef as an opaque handle for cross-run memory but doesn't specify how the handle resolves. Three categories of host need a portable contract:
1. Multi-agent debuggers that want to read what an agent remembered between runs. 2. A2A peers that want to share long-term agent context across organizations. 3. Replay machinery that must read memory at the same logical point on every replay or replays diverge non-deterministically.
The right protocol contract is _narrow_: read-path semantics and a redaction invariant. Storage shape (Redis, Postgres, Firestore, in-memory) and write-path policy (write-through, write-back, batched) remain host choices. The protocol does not prescribe an embedding model, retrieval ranking, or vector index — those are application-layer concerns.
Proposal
§A MemoryAdapter interface
Hosts that advertise capabilities.agents.memoryBackends MUST expose a MemoryAdapter with four operations. The operations are described in spec terms; SDKs MAY name them differently.
list(memoryRef, options): MemoryEntry[]
Returns memory entries visible to the run, ordered newest-first by createdAt.
Replay determinism rule (normative). Within a single run, list MUST return the snapshot of entries visible _at run start_. Mid-run mutations to the underlying store (by other runs, by direct admin writes) MUST NOT be visible to the calling run. Hosts implement this by:
- Capturing a logical timestamp / snapshot id at run start.
- Filtering subsequent
listresults to entries whosecreatedAt ≤ snapshot.t.
This rule preserves replay: a replay re-issues list and gets the same set of entries it did on first execution.
Inputs:
memoryRef: opaque string (fromAgentRef.memoryRef). Hosts resolve to a logical memory scope (tenant + agent + namespace).options:MemoryListOptions(limit,tag).
Output: MemoryEntry[] (per memory-entry.schema.json).
get(memoryRef, id): MemoryEntry | null
Returns a single entry by id. Same snapshot rule as list. Returns null if the entry does not exist _or_ was created after the run's snapshot timestamp.
put(memoryRef, entry): MemoryEntry
Writes a new entry. Returns the persisted entry with host-assigned id and createdAt.
Write-time redaction (normative, see §D). Hosts MUST run the BYOK redaction harness over entry.content and entry.tags before persisting. If a redaction rule fires, the entry is either rewritten with secrets replaced by <REDACTED:label> markers or the write fails with secret_leakage error envelope. Host policy chooses; both options MUST be documented.
delete(memoryRef, id): boolean
Deletes an entry by id. Returns true if the entry existed and was deleted, false otherwise.
Deletes are visible to _future_ runs immediately; the calling run's snapshot is unchanged (deleting an entry mid-run does not retroactively remove it from list results within the same run).
§B Capability advertisement
Hosts advertise memory support via /.well-known/openwop:
{
"capabilities": {
"agents": {
"memoryBackends": ["scratchpad", "conversation", "longTerm"]
},
"memory": {
"supported": true,
"maxEntrySizeBytes": 65536,
"ttlSupported": true
}
}
}
Three normative backend names:
scratchpad— per-task ephemeral notes, evicted on goal completion. Host-managed.conversation— per-run dialogue scope; lives within the run's conversation channel (RFC 0005).longTerm— cross-run persistence; theMemoryAdaptercontract above applies.
Hosts MAY advertise additional backends under vendor.<host>.<name> per host-extensions.md.
§C memoryRef opaque handle
memoryRef is a host-issued opaque string. The spec does not constrain its shape; common patterns:
mem_<tenantId>_<agentId>_<scope>— namespaced and tenant-isolated.- A URN:
urn:openwop:memory:<host>:<id>.
Clients MUST treat memoryRef as opaque and MUST NOT parse it. Hosts MAY rotate the encoding without breaking clients.
memoryRef values from one host are not valid on another host; A2A composition relies on host-side resolution at each peer.
§D Redaction invariant (SR-1)
The Secret Redaction invariant SR-1 (named to align with SECURITY/invariants.yaml):
SR-1. Content reaching
MemoryEntry.contentorMemoryEntry.tagsMUST NOT contain plaintext secret values that flowed through the run's BYOK harness. Hosts MUST apply the redaction harness atputtime. Reads MUST NOT post-process to leak — what was persisted is what is returned.
Hosts that cannot guarantee SR-1 (e.g., because they do not implement a BYOK redaction harness) MUST NOT advertise capabilities.agents.memoryBackends: ["longTerm"]. They MAY still advertise scratchpad and conversation if those backends are scoped to the run lifecycle and do not survive a run terminal.
§E TTL semantics
Memory entries MAY carry expiresAt. After the host's wall clock passes expiresAt:
listandgetMUST NOT return the entry.- Hosts MAY delete the entry from the underlying store; the protocol does not require GC.
- Pending writes with
expiresAtin the past MUST be rejected withvalidation_error.
expiresAt is observable: clients that wrote an entry can determine when it will expire. Hosts MAY adjust their internal TTL for storage efficiency but MUST NOT extend the protocol-visible expiresAt.
§F Authorization
Memory operations execute under the same tenant-isolation rules as the parent run (auth.md §"Tenant isolation"). A run with tenant t1 MUST NOT read or write memory whose memoryRef resolves to tenant t2. Cross-tenant memory sharing is out of scope for v1.
§G Tag conventions
Tags are free-form, but hosts SHOULD use a source: prefix to surface the entry's provenance (e.g., source:session, source:document-upload). Future minors MAY normate a small set of openwop.* reserved tag prefixes for cross-host catalog use.
Empty-string tags are tolerated by the schema (matches reference behavior) but hosts SHOULD filter them at write time.
Compatibility
Additive.
- The
MemoryAdapterinterface is new. Hosts that don't advertisecapabilities.agents.memoryBackendsare not required to implement it. - The
MemoryEntryandMemoryListOptionsschemas already ship in v1; this RFC binds them to a contract. - The
memoryReffield onAgentRefis optional (RFC 0002 §A). - SR-1 is a new normative invariant but applies only at the boundary where memory is written; existing runs that don't write memory are unaffected.
Hosts that pre-RFC-0004 surfaced memory under vendor extensions SHOULD migrate to the MemoryAdapter contract; the vendor surface MAY remain as a synonym during a deprecation window.
Conformance
Existing scenarios that touch the surface:
conformance/src/scenarios/redaction.test.ts— verifies BYOK redaction at read paths.conformance/src/scenarios/redactionAdversarial.test.ts— verifies redaction holds against adversarial inputs.
New scenarios required for Accepted:
memory-list-snapshot.test.ts— verifylistreturns the run-start snapshot; mid-run external writes are invisible.memory-put-redaction.test.ts— verify SR-1: writing a known secret value throughputresults in a redacted entry (orsecret_leakagefailure).memory-ttl-expiry.test.ts— verify entries pastexpiresAtare not returned.memory-tenant-isolation.test.ts— verify cross-tenant reads/writes fail withforbidden.
All gated on capabilities.agents.memoryBackends: ["longTerm"] advertisement.
Alternatives considered
1. Embed memory in the event log. Rejected: event logs are append-only and per-run. Cross-run continuity needs a separate substrate. 2. Mandate a vector index. Rejected: every host has a different RAG stack. The protocol contract is the read/write semantics, not the retrieval algorithm. 3. Require strict consistency on list. Rejected: would require synchronous global ordering. The snapshot-at-run-start rule is cheaper and still replay-deterministic. 4. Treat memory as a channel. Considered for conversation/scratchpad. Adopted for conversation (RFC 0005) where memory is per-run; rejected for longTerm because cross-run memory outlives any single channel.
Unresolved questions
1. Should memoryRef shape be normated for cross-host portability (e.g., URN form)? Current default: opaque. A2A composition may force a normative shape later. 2. Should list support cursor-based pagination beyond limit? Deferred to v1.2. 3. Should hosts be allowed to attach signed provenance to memory entries (so cross-tenant federation is auditable)? Deferred until cross-tenant sharing is in scope.
Implementation notes (non-normative)
- Reference TypeScript host uses a Firestore-backed adapter; the snapshot rule is implemented via a
createdAt ≤ runStartTimestampquery predicate. - In-memory reference host uses an immutable snapshot map captured at run start; mutations rebuild the map for future runs.
- The redaction harness shares code with
debug-bundleredaction (spec/v1/debug-bundle.md§Redaction).
Acceptance criteria
- [ ] Spec text merged.
- [x]
memory-entry.schema.jsonpublished. - [x]
memory-list-options.schema.jsonpublished. - [ ]
capabilities.mdupdated withcapabilities.memory.*advertisement. - [ ] Four conformance scenarios.
- [ ] CHANGELOG entry.
- [ ] Reference host (in-memory + SQLite) implements
MemoryAdapterand passes the four scenarios.
References
schemas/memory-entry.schema.jsonschemas/memory-list-options.schema.jsonschemas/agent-manifest.schema.json(§memoryShape)SECURITY/threat-model-secret-leakage.mdSECURITY/invariants.yaml(SR-1)- RFC 0002 (Agent Identity —
AgentRef.memoryRef), RFC 0003 (Agent Packs — pack-level capability advertisement)