ainl-memory
⚠️ Alpha — API subject to change
The unified graph substrate for AINL agents.
Execution IS memory. Memory IS the graph.
What it is
ainl-memory implements the AINL unified graph: a single typed, executable, auditable artifact that simultaneously encodes an agent's memory, persona, tools, and execution history.
Unlike systems that treat memory as a separate retrieval layer (RAG, vector stores, external graph DBs), ainl-memory makes the execution graph itself the memory substrate — no retrieval boundary, no translation step, no sync problem.
Documentation map
| Topic | Where |
|---|---|
| Ecosystem narrative (non-normative) | Graph-as-memory blog · prior-art timeline PRIOR_ART.md |
Schema, FK migration, import_graph flag |
CHANGELOG.md (0.1.4-alpha+) |
SQL-level integrity layers (FK, repair import, validate_graph scope) |
src/store.rs module docs |
GraphQuery + free helpers (recall_*, walk_from, …) |
src/query.rs module docs |
| Snapshot / validation types | src/snapshot.rs |
The five memory families
| Type | Node / category | What it stores |
|---|---|---|
| Episodic | EpisodicNode | Agent turns, tool calls, outcomes; optional tags (e.g. ArmaraOS ainl-semantic-tagger strings on tool sequences) |
| Semantic | SemanticNode | Facts, beliefs, topic clusters; optional tags (correlation + tagger hints) |
| Procedural | ProceduralNode | Compiled patterns, GraphPatch labels |
| Persona | PersonaNode | Identity, evolved axis scores, dominant traits |
| Session (runtime) | RuntimeStateNode (node_type = runtime_state) |
Per-agent persisted counters: turn_count, last_extraction_at_turn, optional persona_snapshot_json (JSON-encoded compiled persona string), updated_at (unix seconds). Upserted by ainl-runtime so daemon restarts do not reset extraction cadence or force a cold persona compile on the first post-restart turn. |
AinlNodeKind::RuntimeState matches the runtime_state SQL / JSON tag; MemoryCategory::RuntimeState is the same slice for exports and analytics.
Referential integrity & edges
SQLite enforces basic referential integrity on the graph edge table:
- Table
ainl_graph_edgescolumns:from_id,to_id,label,weight,metadata(primary key(from_id, to_id, label)). FOREIGN KEY (from_id)andFOREIGN KEY (to_id)referenceainl_graph_nodes(id)withON DELETE CASCADE.PRAGMA foreign_keys = ONis applied when opening aSqliteGraphStore(seeopen/from_connection).
Legacy databases (edges table created before FK metadata): on first open, a one-time migration rebuilds ainl_graph_edges. Only rows whose both endpoints exist in ainl_graph_nodes are copied; historical dangling rows are dropped (they cannot exist under FK rules). Details: CHANGELOG.md § 0.1.4-alpha.
Write paths
| API | Role |
|---|---|
GraphStore::write_node |
Upserts node JSON, then persists embedded AinlEdge rows (node must exist before edges; order satisfies FK). |
SqliteGraphStore::write_node_with_edges |
Single transaction; fails if any embedded edge target is missing (application check + FK). |
SqliteGraphStore::insert_graph_edge |
Inserts one row; SQLite rejects invalid endpoints when FKs are on. |
SqliteGraphStore::insert_graph_edge_checked |
Pre-checks both node rows exist, then inserts (clear errors without relying on SQLite message text). |
Repair / forensic import
import_graph(snapshot, allow_dangling_edges): usefalseeverywhere in production (strict). Usetrueonly to load snapshots that violate referential integrity: FK checks are disabled only for that import, then restored. Always follow withvalidate_graphand fix data before resuming normal writes on the same connection.
Higher-level validation (orthogonal to FK row existence)
validate_graph(agent_id)reports agent-scoped edges, dangling endpoint pairs, optionalDanglingEdgeDetail(includes edge label),cross_agent_boundary_edges, orphans, andis_valid. Use this for semantics, exports alignment, and post-repair audits.
Rust snapshot type vs SQL
SnapshotEdgeusessource_id/target_id/edge_type; the database usesfrom_id/to_id/label. Import/export maps between them.
Core API
Store
use ;
use Path;
let store = open?;
let mut node = new_episode;
node.agent_id = "my-agent".into;
store.write_node?;
GraphQuery (SqliteGraphStore::query)
Builder scoped to one agent: COALESCE(json_extract(payload, '$.agent_id'), '') = <agent_id>. Edge traversal uses SQL columns from_id, to_id, label.
| Method | Purpose |
|---|---|
episodes / semantic_nodes / procedural_nodes / persona_nodes |
All nodes of that kind for the agent |
recent_episodes(limit) |
Episodes ordered by timestamp DESC |
since(ts, node_type) |
Nodes with timestamp >= ts, type normalized to SQL node_type, ascending |
subgraph_edges |
Edges with both endpoints in this agent’s node id set (aligned with export_graph) |
neighbors(node_id, edge_type) |
Targets of outgoing edges (label = edge_type) |
lineage(node_id) |
BFS on DERIVED_FROM / CAUSED_PATCH, max depth 20, excludes start node from results |
by_tag(tag) |
json_each on episode persona_signals_emitted or semantic tags |
by_topic_cluster(cluster) |
Semantic topic_cluster LIKE |
pattern_by_name(name) |
Procedural pattern_name or label |
active_patches |
Procedural rows where retired is null / false |
successful_episodes(limit) |
Episodes with node_type.outcome == "success" in JSON |
episodes_with_tool(tool, limit) |
Episodes where tools_invoked or tool_calls contains the tool |
evolved_persona |
Latest persona with trait_name == "axis_evolution_snapshot" |
read_runtime_state |
Delegates to store; same agent_id as the query |
Legacy helpers (recall_recent, find_patterns, walk_from, …) live in the same crate and take GraphStore plus explicit agent_id where needed.
use SqliteGraphStore;
let store = open?;
let recent = store.query.recent_episodes?;
let lineage = store.query.lineage?;
let internal = store.query.subgraph_edges?;
Export / import snapshots
use SqliteGraphStore;
let store = open?;
let snapshot = store.export_graph?;
// snapshot.schema_version is typically "1.0" (see SNAPSHOT_SCHEMA_VERSION).
let mut fresh = open?;
fresh.import_graph?; // strict: FK on
Graph validation
use SqliteGraphStore;
let store = open?;
let report = store.validate_graph?;
assert!;
// `dangling_edge_details`: source_id, target_id, edge_type (label)
// `cross_agent_boundary_edges`: touches agent on one side only (informational)
Session state (read_runtime_state / write_runtime_state)
Stable one row per agent_id (deterministic UUIDv5 over the agent id) — use the helpers instead of hand-rolling nodes:
use ;
use Path;
let store = open?;
let memory = from_sqlite_store;
let state = RuntimeStateNode ;
memory.write_runtime_state?;
let _loaded = memory.read_runtime_state?;
let q = memory.sqlite_store.query;
let _same = q.read_runtime_state?;
Legacy rows may still carry JSON keys last_extraction_turn, last_persona_prompt, or RFC3339 updated_at strings; RuntimeStateNode deserializes them via serde aliases / a tolerant timestamp parser.
GraphMemory forwards
GraphMemory exposes validate_graph, export_graph, import_graph, agent_subgraph_edges, write_node_with_edges, insert_graph_edge_checked, read_runtime_state, and write_runtime_state so hosts like ainl-runtime can checkpoint or boot-gate without reaching past the high-level API.
Integration tests (pointers)
| File | What it covers |
|---|---|
tests/test_query.rs |
GraphQuery filters, lineage, neighbors, outcomes |
tests/test_snapshot.rs |
Export/import roundtrip, idempotency, agent_subgraph_edges vs export |
tests/test_validate.rs |
validate_graph, strict vs import_graph(..., true), insert_graph_edge_checked |
tests/test_integrity.rs |
write_node_with_edges |
tests/test_edge_migration.rs |
Legacy edges table → FK migration drops invalid rows |
tests/graph_integration.rs |
Broader graph memory flows |
Crate ecosystem
- Publishing — order, dry-runs, and pre-release resolver pitfalls:
scripts/publish-prep-ainl-crates.sh,docs/ainl-runtime-graph-patch.md(Pre-release versions andcargo publish). - ainl-memory — this crate (storage + query); published version is
0.1.8-alphaon the workspace (aligns with ainl-runtime / ainl-graph-extractor pins — see crates.io and siblingCargo.tomlfiles). - ainl-runtime — agent turn execution, depends on ainl-memory (+ persona, extractor, semantic-tagger)
- ainl-persona — persona evolution engine, depends on ainl-memory
- ainl-graph-extractor — periodic signal extraction, depends on ainl-memory + ainl-persona
- ainl-semantic-tagger — deterministic text tagging, no ainl-memory dependency
Why this is different
Traditional stacks bolt a vector index or key-value “memory” onto an LLM and hope embeddings stay aligned with what actually ran. AINL instead treats every tool call, fact, patch, and persona shift as first-class graph data you can traverse, validate, and export as one artifact — closer to a provenance-rich program trace than a fuzzy recall cache.
License
MIT OR Apache-2.0