Case study · synapse
synapse: memory you can prove
A symbolic knowledge graph in Rust for agents: Markdown notes synced with RDF, OWL-RL reasoning and the provenance of every fact. It started neuro-symbolic and ended with no vectors.
The problem
When an agent “remembers” something through vector search, it retrieves what looks similar, not what is true. That is fine for small talk; not for technical decisions. If I ask which modules depend on the payments module, I need the exact list, not the five most similar snippets. And I need to know where each fact came from and when the agent learned it.
I wanted a memory that answered like a database and reasoned like an ontology, without giving up the format people read: Markdown.
Decisions
Explicit facts. Everything the agent knows is an RDF triple in Oxigraph, persisted in RocksDB. Queries are SPARQL: the same question always gets the same answer.
One entity, one name. Each entity’s id is its path: people/bob.md is people/bob, unless the frontmatter declares a uri:. So the [[people/bob]] link, the file itself and whatever an agent asserts about people/bob are the same node, not three to reconcile later.
Markdown and graph, both ways. A folder of notes with YAML frontmatter and [[...]] links becomes triples. What the engine infers goes back into the file, in a backlinks section, so people see in their own editor what the agent has deduced. Synapse only touches that section: the frontmatter belongs to the person. And it ignores it when reading, so inferences never come back in as if someone had written them.
Provenance on every fact. Each source has its own named graph: every file, every ontology and every agent call. Its PROV-O provenance is stored separately, in urn:synapse:meta. For any answer, you can ask where it came from.
Reindexing replaces, it doesn’t accumulate. Since each file is a graph, reindexing it replaces what it said: if a note stops stating something, the graph stops knowing it, and if the file is deleted, its facts go with it. Unchanged files are skipped by comparing a hash of the content, computed without trailing whitespace so a blank line doesn’t count as a change.
Fixed-point reasoning. A custom reasoner applies RDFS (subclasses and subproperties) and the OWL-RL rules for transitive, symmetric and inverse properties until nothing new appears. What is inferred is as auditable as what was written.
Scenarios. Ontologies and seed data are packaged by domain: a core with Schema.org, PROV-O, SKOS and FOAF, a research assistant, and one for kthulu-go itself. The agent starts with a vocabulary, not an empty graph.
Tools with intent, not raw SPARQL. In the latest version, the agent asks for an entity’s dossier, the impact of changing a dependency or the provenance of a fact, and the server translates that into SPARQL. Free-form queries are kept for graph administration, see every graph by default, and ids are validated so nobody can inject SPARQL through them.
The agent writes too, and can take it back. assert_facts stores facts with their source, reason and datatype. retract_facts withdraws a whole assertion or single facts and records who, when and why. It never touches what comes from a file: what a person wrote only changes by editing the note.
OKF compatible. The note format accepts okf-mcp’s: type, tags, typed [[rel:target]] links and lists in the YAML. Point the indexer at a clone of its GitHub mirror and the concepts and their links line up.
What is built
- One Rust binary that serves MCP over stdio or HTTP with SSE. On first start it loads the core scenario’s ontologies, 280 triples, and skips them on later starts because they haven’t changed.
- MCP tools to read an entity as a dossier, walk its neighbourhood, measure a dependency’s impact, check provenance, sync specifications, index a Markdown folder and assert or retract facts.
- MCP Apps views: an engine dashboard and a subgraph drawn with D3. For now they are templates; they do not yet receive live data from the host.
- A Markdown digest regenerated on every ingest, so the agent starts each session by reading the state of the graph.
- 22 tests, unit and end-to-end, and a clean deployment: no leftovers from the gRPC era or nine dependencies nobody used, and a Dockerfile that no longer installs onnxruntime or protobuf.
What I learned
Synapse was born as a neuro-symbolic memory layer, with vectors, a Python SDK and gRPC. Within two months I removed all of it: the vector store kept piling up fixes for crashes, persistence and slow startup, and what agents actually used was the graph. Taking things out was the best decision in the project.
What it did not solve is finding something when you don’t know its name. That is why okf-mcp brings semantic search back, but as an index into the graph, not as a substitute for memory. Now that they read the same format, the two complement each other.
The second lesson came from giving the agent write access: the hard part wasn’t the engine, it was the design on top. With path-based identity and one graph per source, asserting, correcting and forgetting stopped being special cases. Oxigraph is still the engine; what changed is how facts are split up.