Constant-cost semantic memory for multi-agent systems Semvec, a semantic memory system from Versino PsiOmega, achieves near-mem0 quality on the LOCOMO benchmark with zero generative-LLM calls at ingest, reducing context tokens by 87% and ingest time by ~8x, enabling constant-cost memory for multi-agent systems. The system, built on Semvec 0.7.0 and Neo4j, is demonstrated in a healthcare assistant scenario and is available in the public repository MichaelNeuberger/neo4j-agent-integrations. Constant-cost semantic memory for multi-agent systems CEO, Versino PsiOmega 24 min read Semvec, Semvec Cortex, and Neo4j A clinical assistant that remembers a patient’s full history across a 12-hour shift handover and knows which colleague already investigated the same case three hours earlier behaves nothing like one that treats every conversation as a fresh start. LLMs do not have native memory; they fake it by re-reading the entire conversation on each turn. This means token costs grow linearly. Context windows snap. And the moment a second agent enters the picture, even that illusion breaks down. This post shows how Semvec gives a single agent persistent semantic memory in roughly three lines of Python, and how Semvec Cortex scales the same engine across teams of agents, with Neo4j as the single source of truth for both the domain and the conversational state. We build a healthcare assistant that starts as a single Python process and grows into a multi-department network with shared memory, drift consensus, cross-shift handover, deliberate consensus voting, and a behavioural-consistency probe that catches algorithmic drift after import. All code comes from the public reference repository MichaelNeuberger/neo4j-agent-integrations, runs on Semvec 0.7.0 from PyPI, and is exercised by a 274-test pytest suite. About SemvecClient Every code snippet in this post uses a class called SemvecClient. SemvecClient is not part of pip install semvec. It is the in-process facade defined in this repository at src/semvec neo4j/core/semvec client.py. It composes SessionManager, ClusterManager, RegionalManager, GlobalObserver, and NetworkManager from semvec.api. into one ergonomic surface that returns plain Python dictionaries, well suited for direct mirroring into Neo4j. The PyPI package semvec exports the underlying primitives: SemvecState, SemvecConfig, MultiResolutionMemory, PhaseDetector, LiteralCache, ResonanceTrigger, the cortex package SemvecAgentNetwork, ConsensusEngine, AttentionAggregation, … , and the full exception hierarchy. To reproduce the snippets below, clone the integrations repository; pip install semvec alone does not include SemvecClient, the persistence stores, or the demo scenarios. The numbers, up front On the LOCOMO long-term conversational-memory benchmark, the same suite mem0, Zep, Letta, and the GPT-4-turbo full-context baseline report against, Semvec lands at mem0-near quality with zero generative-LLM calls at ingest. The headline numbers below are from the public Semvec benchmark page semvec-docs.pages.dev/benchmarks https://semvec-docs.pages.dev/benchmarks , measured against mem0 0.1.x in a 1:1 evaluation setup gpt-4o-mini reader + LLM-as-Judge, T=0 : Benchmark LOCOMO 10 conversations, 1986 QAs; gpt-4o-mini reader + judge, T=0 Result J 0.605 LLM-as-Judge, Cat 1–4, n=1540 at zero ingest LLM calls; F1 0.424 Porter-stemmed token-F1, n=1986 ; ~2,000 context tokens per reader call ~8× fewer than full-context replay, 87 % reduction ; ~3 min vs. ~24.5 min ingest on conv-44 / 675 turns ~8× faster Reference baseline mem0 J 0.669 paper A note on the speed figures: the ~8× above is single-conversation ingest wall-clock conv-44, 675 turns . The Semvec docs separately report a 17× end-to-end wall-clock vs. mem0 for the full 1986-QA suite, measured on a GPU reference platform with hybrid BM25 enabled — a different measurement, documented at semvec-docs.pages.dev/benchmarks https://semvec-docs.pages.dev/benchmarks . Those savings are what make multi-agent scenarios economically practical. A ward of ten specialists making 50 queries each per shift is no longer prohibitive. The per-turn update is sub-millisecond at dimension 384 on a recent x86 64 CPU; the Rust core runs outside the GIL, so the math does not contend with Python. Architecture in one picture Domain knowledge and conversational state coexist in the same Neo4j graph. Patients, medications, providers, and diagnoses live alongside agent sessions, drift events, and cluster memberships. A single Cypher query can join “what does the agent know” with “what was asked, when, and by whom.” That bridge is the INVESTIGATED relationship, and it is the central piece of the architecture. Semvec does the math; Neo4j stores the result. Embedding generation, drift scoring, cluster aggregation, observer sampling, fact extraction, and consensus voting all happen behind a small in-process API. The Neo4j integration is intentionally thin: stores that persist sessions, states, drift events, cluster relationships, and verbatim facts as the engine produces them. There is no duplicated logic. Part one. Single-agent memory with Semvec The smallest useful agent now fits on a screen. Install the package and wire it up: pip install semvec Then, inside the integrations repo:from semvec neo4j.core.embedder import SentenceTransformerEmbedderfrom semvec neo4j.core.semvec client import SemvecClient semvec = SemvecClient embedder=SentenceTransformerEmbedder default: all-mpnet-base-v2 768-d session id = Noneprev response = Nonefor user msg in conversation: result = semvec.run message=user msg, session id=session id, response=prev response session id = result "session id" captured on first turn answer = call llm result "context" , user msg your LLM, your prompt prev response = answer stored alongside the next .run Two details matter. First, the session id is allocated by Semvec on the first call and reused thereafter. That is how the same agent picks up where it left off across process restarts. Second, the previous LLM response is buffered and sent with the next run call. Store-and-retrieve happen together, so there is no race window where an answer is given but not yet remembered. PydanticAI integration The Semvec context becomes part of the agent’s system prompt via the @agent.system prompt decorator. Each turn, only the memories the engine deems relevant to the current user message are injected: php @agent.system promptasync def inject semvec context ctx: RunContext Deps - str: if ctx.deps.semvec context: return f"Memory context from previous conversations:\n{ctx.deps.semvec context}" return "" What you get for free Each Semvec response carries more than compressed text. The result dict includes: • drift phase: stable / shifting / drifted; a jump signals a topic switch distinct from the six conversational state phases — initialization through instability • drift score: 0.0–1.0 numeric distance from the accumulated context • top similarity: cosine similarity to the rolling top-K of recent embeddings • short circuit: true when a paraphrase of an earlier question is recognised and the LLM call can be skipped The session is also immediately queryable in Neo4j as a chain of SemanticState nodes, with a CURRENT STATE pointer to the latest one. Live token-savings panel The compression numbers in the table at the top of this post are not a once-and-for-all benchmark. Every turn carries its own measurement. SemvecChatProxy wraps any OpenAI-shaped llm call callable and returns the PSS-compressed prompt token count next to what the same conversation would have cost if you replayed the full history. The wrapper exposes it via SemvecClient.create chat proxy: proxy = semvec.create chat proxy llm call=my llm, any callable; expose .last usage for exact counts system prompt="You are a clinical assistant.", for question in clinic questions: turn = proxy.turn question print turn "pss input tokens" , "vs", turn "baseline input tokens" , "phase", turn "phase" print proxy.summary totals + per-turn breakdown Scenario 1 of the demo prints both per-turn and cumulative token counts as you type. The compression ratio rises with conversation depth, exactly as the headline benchmark predicts, and it does so against your own questions and your own LLM, not a curated dataset. Anchors, triggers, and synthetic memory injection When an agent has a fixed domain, pin the session with an anchor and inject synthetic memories from the Neo4j domain graph. For an oncology pharmacist, every contraindication in the graph becomes a long-term memory before the session even begins: session id = semvec.create session "session id" for c in contraindications from neo4j: fetched via Cypher text = f"CRITICAL: {c 'drug a' } and {c 'drug b' } are contraindicated." semvec.inject memory session id=session id, embedding=embedder.get embedding text .astype float .tolist , text=text, tier="long term", importance=1.0, semvec.add trigger session id, keyword="contraindication" semvec.add anchor session id, embedder.get embedding "oncology chemotherapy treatment" .astype float .tolist semvec.set isolation session id, level="QUARANTINE", similarity threshold=0.55 Verbatim facts: dates, dosages, identifiers Semantic memory works well for meaning. It is unsafe for exact values. Compress “the next infusion is on 2026–05–15” into an embedding and a clinician later asking “when is the next infusion?” may receive “next month” or, worse, the wrong date. The compliance-extractor path pulls regex-recognised facts ISO/DE/US dates, EUR/USD/kg/% numerics, UUID/IBAN/DE-VAT identifiers out of free text before the input is folded into the EMA vector. Each fact lands in the literal cache where it survives compression byte-for-byte: text = "Carlos starts therapy on 2026-05-15. Reimbursement IBAN DE89 3704 0044 0532 0130 00." facts = semvec.extract facts text {"kind": "date", "raw": "2026-05-15", "value": "2026-05-15T00:00:00+00:00", ...}, {"kind": "identifier", "raw": "DE89 3704 0044 0532 0130 00", "value": "DE89370400440532013000", "id type": "iban", ...} semvec.store facts as entities session id, text {"stored": 2} entries land in the session's literal cache Two practical caveats. The upstream unit whitelist is small built for finance and operations , so medical units like mg/m² are not auto-detected. For those, prefer inject memory with the full sentence. And the literal cache stores facts under the kind constant because the upstream EntityKind enum is closed; the original kind numeric / date / identifier is preserved in the entity context so downstream queries can still tell them apart. Part two. Multi-agent memory with Semvec Cortex Cortex methods sit on the same SemvecClient. There is no additional service to deploy. Cluster, region, observer, network, consensus engine, and chat proxy are all bundled in the semvec package and reachable through one facade. Layer 2. Clusters: shared memory across agents A cluster is a shared semantic state that multiple agent sessions read from and write to. Three specialists examining David Park essential hypertension + COPD : cluster = semvec.create cluster name="ward round david park", aggregation mode="weighted average" cid = cluster "cluster id" semvec.add cluster member cid, chen session for question, answer in chens baseline qa pairs: semvec.cluster store cid, message=question, response=answer semvec.add cluster member cid, volkov session result = semvec.cluster run cluster id=cid, message="What's the patient's current oxygen saturation trend?", short circuit threshold=0.52, calibrated for all-mpnet-base-v2 768-d The Neo4j side stores cluster membership and INVESTIGATED edges from each agent to the entities they touched. A single Cypher query then answers “who has looked at David Park, and what did each one find?” Layer 3. Regions: consensus drift across clusters A region groups multiple clusters and watches for correlated drift events. If both cardiology at Memorial General and emergency at Riverside Medical drift toward the same off-domain topic within a configurable time window, the region emits a consensus drift event: region = semvec.create region name="hospital network north", consensus threshold=0.5, vote window seconds=60.0 rid = region "region id" semvec.add region cluster rid, cardiology cluster id semvec.add region cluster rid, emergency cluster id events = semvec.get region events rid, limit=20 What this catches in practice: a hospital-network IT outage shows up as multiple clusters simultaneously drifting from clinical to “I cannot access the chart” queries, long before any single ticket reaches the help desk. Layer 4. Global Observer: cross-region anomaly detection The observer sits one level above regions and detects anomalies no individual region would flag: a cluster silent for too long, a region whose consensus events are accelerating, or an unusual coupling between two regions. semvec.create observer sample interval seconds=30.0, region ids= north region id, south region id semvec.observer sample for a in semvec.get anomalies limit=20 : print a "severity" , a "anomaly type" , a "affected cluster ids" , a "description" Layer 5. Shift handover via export / import The night-shift physician’s session has built up deep context about a difficult patient. At 6 a.m. she exports it with SHA-256 checksum , and the day-shift physician imports it. His first query immediately surfaces the relevant memories, with top similarity = 0.578 instead of 0.000 starting cold: exported = semvec.export session volkov session id {"state dict": ..., "checksum": "