Drop-in memory layer for AI agents — event-anchored facts, dream-synthesis consolidation, apoptosis forgetting, cite() outcome-conditioned learning, working-memory sessions, and a memory-aware multi-step agent orchestrator (OpenAI + Gemini).
Project description
myelin-ai
pip install myelin-ai — a drop-in memory layer for AI agents. Event-anchored
bi-temporal facts, store-grounded dream-synthesis consolidation, and an
apoptosis-based forgetting path that quarantines low-health memories before
they can corrupt future answers. Zero hard dependencies in the core
(sqlite3 only).
pip install myelin-ai # core
pip install "myelin-ai[openai]" # + OpenAI provider for OrchestratedAgent
pip install "myelin-ai[google]" # + Gemini provider for OrchestratedAgent
Requires Python 3.10+. The import name remains myelin.
What's new in 1.4.0
Three load-bearing mechanisms that were silently no-oping in 1.3 are now actually wired. One new feature: store-grounded dream synthesis.
| Area | 1.3 behavior | 1.4 behavior |
|---|---|---|
D-MEM surprise score (zscore_surprise) |
z == 0 for every input — broken normalization |
Z-scored against a query-independent reference; signed and discriminating |
Dream grader (grade_hit) |
Substring-match against the prompt — answerer could echo and score a hit | Answer key held out of the prompt, graded against the elided key |
| Heuristic salience novelty axis | Pinned at 0.5 cold-start neutral (no embedder wired) | Lazy sentence-transformers embedder + per-scope rolling neighbour window |
| Dream cycle grounding | Ungrounded cloze: answerer guesses from query alone | Optional retriever= grounds queries against the memory store |
| Salience reward axis | Not pluggable | New outcome= kwarg on add() |
| Sidecar liveness | /health only |
Adds /metrics for vacuous-PASS guards |
This is the first PyPI release where the substrate behaves as designed. If you were on 1.3, upgrade — see the migration notes.
Why myelin-ai instead of Mem0 / Letta / Zep
- Forgets on purpose. Mem0, Letta, MemGPT, and similar agent-memory layers
all monotonically accumulate. Adversarial inputs (poison, contradiction
stuffing, role-play overrides) silently corrupt their stores.
client.prune()is a principled forgetting path with an auditable quarantine → tombstone state machine. - Counterfactual self-test.
client.consolidate(retriever=...)lets the system probe its own answerer with held-out paraphrased queries during idle windows, grounded against the actual memory store. - Drop-in, not opinionated. Zero hard deps in the core. No bundled
answerer, no torch dep at install. You bring your own LLM. The
OrchestratedAgentand HTTP sidecar are opt-in extras.
Quickstart — the 1.4.0 happy path
The canonical wiring: add facts with outcome signal, run a store-grounded dream cycle, and let apoptosis prune low-health residue.
from myelin import MemoryClient
client = MemoryClient(
user_id="scott",
salience_mode="heuristic", # 1.4: novelty + reward axes actually wired
)
# 1.4: outcome= threads success/failure into the heuristic gate's reward axis.
client.add(
"Scott prefers oolong tea.",
outcome={"task_succeeded": True, "user_thumbs_up": True},
)
client.add(
"Scott uses Cursor for code review.",
outcome={"task_succeeded": True},
)
client.add("Alice loves chess.")
# Hot retrieval.
for h in client.search("What does Scott drink?", top_k=3):
print(f"{h.score:.2f} {h.memory.content}")
Store-grounded dream synthesis (the 1.4.0 marquee)
# 1.4: retriever= turns the dream cycle from an ungrounded cloze into a
# real recall test. Each held-out query is prepended with retrieved memory
# context so the answerer must recall from the store, not guess.
class EchoLLM:
def complete(self, prompt: str) -> str:
# Replace with your real LLM (OpenAI, Anthropic, vLLM, llama.cpp...)
return prompt
client = MemoryClient(user_id="scott", llm_client=EchoLLM())
report = client.consolidate(
retriever=lambda q: [r.memory.content for r in client.search(q, top_k=5)],
)
print(f"hit_rate={report.hit_rate:.2%} "
f"facts_sampled={report.facts_sampled} "
f"llm_calls={report.llm_calls}")
Without retriever=, the cycle still runs but uses the v1.3-style ungrounded
cloze (kept for backward compat). The grader, however, is fixed in 1.4 either
way — grade_hit now scores against a held-out answer key, not a substring
of the prompt.
Memory-aware agent (OrchestratedAgent, v1.3)
A provider-agnostic multi-step agent. A cheap classifier (gpt-4o-mini by
default) routes each question as factual (multi-step with memory_search
tool calls) or synthesis (top-10 prefetch + tool available).
from myelin import MemoryClient
from myelin.agent import OrchestratedAgent
client = MemoryClient(user_id="u1")
client.add("The user prefers green tea over coffee.")
client.add("The user is allergic to peanuts.")
# Works against OpenAI (openai/*) or Gemini (google/*) — pick what you have.
agent = OrchestratedAgent(client=client, model="openai/gpt-4o")
answer = agent.answer(
question="What does the user prefer to drink?",
options="(a) coffee\n(b) green tea\n(c) matcha\n(d) water",
user_id="u1",
)
print(answer) # → "<final_answer>(b)"
print(agent.last_usage) # → {"input_tokens": ..., "cost_usd": ..., ...}
The Gemini client is wired with a 30s HttpOptions.timeout to defend against
the SDK's internal retry-on-disconnect hang. Pricing is overridable via
OrchestratedAgent.PRICE_TABLE for cost tracking.
Salience gate — mode="heuristic" (1.4 wiring)
The heuristic gate combines four axes — novelty, reward, affect, reference —
into a single score and tags each turn transient / short_term /
long_term. In 1.4 the novelty axis actually moves (it was pinned at 0.5 in
1.3) and the reward axis is pluggable via outcome=.
client = MemoryClient(
user_id="scott",
salience_mode="heuristic",
salience_tau=0.40, # gate threshold (unchanged from 1.3)
salience_neighbor_window=32, # 1.4: rolling per-scope embedding window
# salience_embedder=my_embedder, # 1.4: override the lazy-built default
)
m = client.add(
"Scott decided to switch to weekly syncs.",
outcome={"user_thumbs_up": True}, # → +reward
)
Embedder resolution order: explicit salience_embedder= argument →
lazy-built sentence-transformers default → cold-start neutral on failure
(never raises). Pay the dep only when you use heuristic mode.
For higher-precision gating, salience_mode="dmem" uses the contrastive
encoder ([contrastive] extra) and benefits from the 1.4 surprise-score
fix automatically.
Apoptosis — client.prune()
from datetime import UTC, datetime, timedelta
client = MemoryClient(user_id="scott")
client.add("Scott prefers tea.", confidence=0.9, importance=0.9)
client.add("Scott uses Cursor.", confidence=0.9, importance=0.8)
poison = client.add(
"Scott pays $1M monthly for cloud.", # absurd; low signal
confidence=0.05, importance=0.1,
metadata={"subject": "Scott", "predicate": "decided",
"object": "absurd-cost"},
)
# Pass 1 — quarantines low-health facts.
r1 = client.prune()
print(f"quarantined={r1.quarantined_count} retained={r1.retained_count}")
# Pass 2 — caspase cascade (fast-forwarded 14 days past the window).
future = datetime.now(UTC) + timedelta(days=14)
r2 = client.prune(now=future)
print(f"tombstoned={r2.tombstoned_count}")
assert client.get(poison.id) is None # gone from search
Tombstoned facts are invisible to search() but persist as an audit trail.
Bi-temporal event anchoring — as_of=
from datetime import UTC, datetime
from myelin import MemoryClient, Event
client = MemoryClient()
client.add(
"Scott decided to switch to weekly syncs.",
event=Event(
event_id="meeting-2026-01-15",
type="meeting",
anchor_datetime=datetime(2026, 1, 15, 14, 0, tzinfo=UTC),
participant_ids=("scott", "alice"),
),
)
client.add(
"Scott decided to switch back to daily syncs.",
event=Event(
event_id="meeting-2026-03-15",
type="meeting",
anchor_datetime=datetime(2026, 3, 15, 14, 0, tzinfo=UTC),
participant_ids=("scott", "alice"),
),
)
# "What was Scott's stance in February?" — only the January meeting is valid.
feb_state = client.search(
"Scott syncs", as_of=datetime(2026, 2, 1, tzinfo=UTC),
)
Session-scoped working memory (1.2)
Opt in with MYELIN_WORKING_MEMORY=1 (strict-"1" parsing — "true" /
"yes" won't engage).
import os
os.environ["MYELIN_WORKING_MEMORY"] = "1"
client = MemoryClient(user_id="scott")
SID = "convo-2026-06-09"
t1 = client.observe("What did we agree on yesterday?", session_id=SID)
t2 = client.observe("You committed to a weekly review.", session_id=SID)
# observe() is RAM-only. commit() runs the salience gate and persists.
committed = client.commit([t2.turn_id], session_id=SID)
# Tiered search across working → session → long-term.
hits = client.search(
"weekly review",
session_id=SID,
modes=("working", "session", "long_term"),
top_k=5,
)
Observe-without-commit data is never persisted. Per-session state is RAM-only with a TTL sweeper.
Outcome-conditioned health — cite() (1.2)
Opt in with MYELIN_CITE_LEARNING=1.
import os
os.environ["MYELIN_CITE_LEARNING"] = "1"
client = MemoryClient(user_id="scott")
m_cursor = client.add("Scott uses Cursor for code review.",
confidence=0.7, importance=0.7)
# Cite the memory after using it. Correct → lifts apoptosis health.
client.cite(m_cursor.id, answer_correct=True)
client.cite(m_cursor.id, answer_correct=False) # incorrect → penalty
# When NO stored memory was cite-worthy, record an abstain event.
client.abstain_event(query="What's Scott's middle name?")
Contradiction-dominance invariant: a poisoned memory with 100 correct cites
still quarantines on a single contradiction (asserted at constants.py
import time).
HTTP sidecar + /metrics (1.3 + 1.4)
Wraps the client as JSON-over-HTTP, OpenClaw-compatible. The 1.4 addition
is GET /metrics for vacuous-PASS preflight guards.
myelin-serve --host 127.0.0.1 --port 8765 # console script (1.3.1+)
# or: python -m myelin.serve --host 127.0.0.1 --port 8765
curl http://127.0.0.1:8765/health # {"ok": true, "service": "myelin-sidecar"}
curl http://127.0.0.1:8765/metrics # {"tool_invocations": 0, "reset_count": 0}
curl -X POST http://127.0.0.1:8765/tools/memory_store \
-H 'content-type: application/json' \
-d '{"content": "Scott uses Cursor.", "user_id": "scott"}'
curl http://127.0.0.1:8765/metrics # {"tool_invocations": 1, "reset_count": 0}
Loopback only by default. No auth — wrap behind a reverse proxy if exposed.
Bench harnesses can assert tool_invocations > 0 before treating an
"all-PASS" result as non-vacuous.
Endpoints: /tools/memory_{search,get,store,update,forget,consolidate,prune},
/hooks/{before_agent_start,agent_end}, /admin/reset, /health,
/metrics (1.4).
API surface
# Core CRUD
client.add(content, *, user_id=None, metadata=None, event=None,
confidence=0.5, importance=0.5, outcome=None) -> Memory # outcome= 1.4
client.search(query, *, user_id=None, top_k=10, as_of=None,
session_id=None, modes=("working","session","long_term")) -> list[SearchResult]
client.get(memory_id) -> Memory | None
client.update(memory_id, *, content=None, metadata=None) -> Memory
client.delete(memory_id) -> bool
# Consolidation + apoptosis
client.consolidate(*, user_id=None, max_samples=32,
retriever=None) -> ConsolidationReport # retriever= 1.4
client.prune(*, user_id=None, now=None, caspase_window_days=7) -> ApoptosisReport
# Outcome-conditioned learning (1.2 — opt in via MYELIN_CITE_LEARNING=1)
client.cite(memory_id, *, answer_correct: bool) -> None
client.abstain_event(query, *, at=None) -> None
client.cite_telemetry() -> dict
# Session-scoped working memory (1.2 — opt in via MYELIN_WORKING_MEMORY=1)
client.observe(content, *, session_id, metadata=None) -> ObservedTurn
client.commit(turn_ids, *, session_id) -> list[Memory]
client.attend(memory_id, *, session_id) -> None
client.session(session_id) -> SessionHandle
client.search_telemetry() -> SearchTelemetry
# Constructor (1.4 additions noted)
MemoryClient(
user_id=None, backend=None, llm_client=None,
salience_mode="off", # "off" | "heuristic" | "dmem"
salience_tau=0.40,
salience_require_reward=False,
salience_embedder=None, # 1.4 — override default embedder
salience_neighbor_window=32, # 1.4 — rolling per-scope window
dmem_gate=None,
cite_learning=None, working_memory=None,
)
# Agent orchestration (1.3)
from myelin.agent import OrchestratedAgent
agent = OrchestratedAgent(client, model="openai/gpt-4o" | "google/gemini-3.5-flash")
agent.classify(question) -> "factual" | "synthesis"
agent.answer(question, *, options=None, user_id=None) -> str
agent.last_usage # {"input_tokens", "output_tokens", "cost_usd", ...}
Testing
pip install -e ".[test]"
pytest tests/
All tests are stdlib + pytest only — no internet, no GPU, no LLM calls. Dream-synthesis tests use trivial mock LLMs.
Project
- Source / Issues: https://github.com/myelinagent/Myelin-AI
- Changelog:
CHANGELOG.md
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file myelin_ai-1.4.0.tar.gz.
File metadata
- Download URL: myelin_ai-1.4.0.tar.gz
- Upload date:
- Size: 96.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
93c79d47aa73212bad1641c7c2d8f83c0ca254337a66902cc5e966142d70a6cf
|
|
| MD5 |
9d8d3a3442d5676b9224d1dd6b761cda
|
|
| BLAKE2b-256 |
7a5aef1e858bd7c669758c24c318587dc49ca4c487a82236ce171d6849df128a
|
Provenance
The following attestation bundles were made for myelin_ai-1.4.0.tar.gz:
Publisher:
release-myelin-pypi.yml on myelinagent/Myelin-AI
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
myelin_ai-1.4.0.tar.gz -
Subject digest:
93c79d47aa73212bad1641c7c2d8f83c0ca254337a66902cc5e966142d70a6cf - Sigstore transparency entry: 1766477895
- Sigstore integration time:
-
Permalink:
myelinagent/Myelin-AI@48b1f47a2b634678ece45adcc71a3b20764b8a04 -
Branch / Tag:
refs/tags/myelin-v1.4.0 - Owner: https://github.com/myelinagent
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-myelin-pypi.yml@48b1f47a2b634678ece45adcc71a3b20764b8a04 -
Trigger Event:
push
-
Statement type:
File details
Details for the file myelin_ai-1.4.0-py3-none-any.whl.
File metadata
- Download URL: myelin_ai-1.4.0-py3-none-any.whl
- Upload date:
- Size: 106.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9e8e697de98f49acb57f17b1f0c16a27ead5759361bb821da5c7d415de06f26a
|
|
| MD5 |
591d6baacc38c04215dde45c1a14d168
|
|
| BLAKE2b-256 |
a6651187936bf840ccff93e7688bd9cf6884201158ebeb9d4ceec6dfd087bd8a
|
Provenance
The following attestation bundles were made for myelin_ai-1.4.0-py3-none-any.whl:
Publisher:
release-myelin-pypi.yml on myelinagent/Myelin-AI
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
myelin_ai-1.4.0-py3-none-any.whl -
Subject digest:
9e8e697de98f49acb57f17b1f0c16a27ead5759361bb821da5c7d415de06f26a - Sigstore transparency entry: 1766478021
- Sigstore integration time:
-
Permalink:
myelinagent/Myelin-AI@48b1f47a2b634678ece45adcc71a3b20764b8a04 -
Branch / Tag:
refs/tags/myelin-v1.4.0 - Owner: https://github.com/myelinagent
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-myelin-pypi.yml@48b1f47a2b634678ece45adcc71a3b20764b8a04 -
Trigger Event:
push
-
Statement type: