Memory should be decided, not just extracted.
Agent memory usually fails at the decision step, not at retrieval. Extraction pipelines are generous, so memory fills up with errors:
- temporary states stored as permanent facts ("in Dubai this week" becomes "lives in Dubai")
- plans stored as facts
- other people's facts stored as the user's
- stale values and ignored corrections
- the assistant's guesses saved as things the user said
- secrets
- no record of why anything was kept
Memworthy is a small Python library that sits between whatever proposes memories and whatever stores them. For each candidate it:
- runs deterministic code checks
- asks a decision model one batch of typed questions (TypeSafe's Jev by default; OpenAI's Decisions API, Microsoft-Decision-1 on Foundry or your own provider also work)
- applies an ordered, user-editable policy to choose an action:
store,update,merge,supersede,reject,review,redactorstore_labeled
Every decision is written to a ledger you can audit, replay under a changed policy, and review.
Quickstart
pip install "memworthy[jev]" # Python 3.10+
export TYPESAFE_API_KEY=... # https://typesafe.ai
from memworthy import Candidate, DictStore, Gate
gate = Gate(policy="personal-memory", store=DictStore())
decisions = gate.ingest_messages([
{"role": "user", "content": "I live in Dubai"},
{"role": "user", "content": "Flying to Riyadh tomorrow"},
{"role": "user", "content": "I moved to Riyadh last week"},
{"role": "user", "content": "Actually I'm only considering it"},
])
for d in decisions:
print(f"{d.action:10} {d.rule:16} {d.candidate.text}")
# store rules[9] I live in Dubai
# reject not_memory Flying to Riyadh tomorrow
# update update_existing I moved to Riyadh last week
# supersede retracted Actually I'm only considering it (rolls back to Dubai)
The output above is what the bundled recorded Jev answers give; a live run may differ slightly. You can reproduce it with no API key:
from memworthy import RecordedJudge
from memworthy.judges.recorded import bundled_fixture_path
gate = Gate("personal-memory", DictStore(),
judge=RecordedJudge(bundled_fixture_path("personal-memory")))
Each decision also lands in memworthy.ledger.jsonl with every signal, its probability, the rule that fired, and the policy and model version.
Other integrations
# Wrap a Mem0 client: only approved content reaches Mem0 (pip install "memworthy[jev,mem0]")
from mem0 import Memory
from memworthy import GatedMemory
memory = GatedMemory(Memory(), policy="personal-memory") # mode="input" or "candidate"
memory.add([{"role": "user", "content": "I'm in Lisbon this week"}], user_id="alice") # rejected
# Turn Claude Code / Codex sessions into an Obsidian-compatible Markdown knowledge base
memworthy run dev-sessions ~/.claude/projects --store markdown:~/kb
memworthy run dev-sessions ~/.claude/projects --store markdown:~/kb --extractor llm # OpenRouter via EXTRACTOR_MODEL
CLI
| Command | What it does |
|---|---|
memworthy test POLICY [--judge recorded|mock|jev|openai|microsoft|PLUGIN] [--pairs FILE] |
Runs the policy's tests; exits 1 on failure (CI-friendly with recorded answers) |
memworthy run POLICY SOURCE [--store dict|markdown:PATH|mem0] [--dry-run] |
Gates a candidate file, chat JSON or session folder |
memworthy replay LEDGER POLICY [--only-changed] |
Shows which past decisions a policy change would flip (no API calls when prompts are unchanged) |
memworthy lint POLICY [--strict] |
Overlapping types, unused signals, unreachable rules, vague prompts |
memworthy review LEDGER [--export-tests FILE] |
Accept, override or skip queued decisions; overrides become tests |
memworthy record POLICY INPUT [--judge jev|openai|microsoft|PLUGIN] |
Records live judge answers as fixtures |
memworthy eval POLICY |
Contrast-pair metrics, calibration and plots into eval/results/ |
Judge providers
The judge answers the typed questions. Pick one with --judge or MEMWORTHY_JUDGE, or pass any object with a name and an async judge(state, questions) method to Gate(judge=...).
| Provider | Configuration |
|---|---|
jev (default) |
TYPESAFE_API_KEY, optional TYPESAFE_BASE_URL, MEMWORTHY_MODEL (default jev-1.13.0) |
openai |
OPENAI_API_KEY, optional OPENAI_BASE_URL, OPENAI_DECISIONS_MODEL (default gpt-6-luna); uses the Decisions API, in public beta |
microsoft |
AZURE_FOUNDRY_ENDPOINT, AZURE_FOUNDRY_API_KEY, optional AZURE_FOUNDRY_DEPLOYMENT (found automatically when unset); Microsoft-Decision-1 deployed in your Foundry resource (API-key auth) |
The bundled templates' thresholds and evaluation results come from Jev. Other providers' probabilities are on a different scale, so run memworthy eval with your judge before you trust a template's cut-offs.
To ship your own provider as a package, implement the memworthy.Judge protocol (see memworthy/judges/openai_decisions.py for a complete HTTP example) and register a zero-argument factory that reads its settings from the environment:
[project.entry-points."memworthy.judges"]
myprovider = "memworthy_myprovider:MyJudge"
If your provider speaks the System One API (the format Jev and Microsoft-Decision-1 use), you don't need to write the request and response mapping: return a configured memworthy.SystemOneJudge(api_key=..., base_url=..., model=..., auth="bearer" or "api-key") from your factory.
After pip install, --judge myprovider and MEMWORTHY_JUDGE=myprovider work. memworthy.available_judges() lists what is installed.
Templates
Both are ordinary YAML files in src/memworthy/templates/; copy one and edit it.
personal-memory (v1.1) is for consumer assistants.
- Types: fact, preference, relationship, temporary, intent, hypothetical, noise.
- Rules, in order:
- Redact secrets.
- Reject anything the user didn't say.
- Roll back retractions.
- Reject temporary states, plans, hypotheticals and noise.
- Reject future or time-bounded statements. Dates are parsed by code, not the judge.
- Review sensitive data (health, finances, religion, precise location).
- Reject low-durability facts.
- Review facts about someone else.
- Update memories the new fact replaces.
- Store the rest.
rules:
- {name: secrets, when: "secret", then: redact}
- {name: not_user, when: "not from_user", then: reject}
- {name: retracted, when: "retraction > 0.7 and conflict", then: supersede, restore: true}
- {name: not_memory, when: "type in ['temporary', 'intent', 'hypothetical', 'noise']", then: reject}
- {name: bounded_time, when: "timing == 'bounded' or timing == 'future'", then: reject}
- {name: sensitive, when: "sensitivity >= 1.5", then: review}
- {name: low_durability, when: "durable < 0.6 or explicit < 0.5", then: reject}
- {name: wrong_subject, when: "about_subject < 0.5", then: review}
- {name: update_existing, when: "conflict and conflict.p > 0.6", then: update}
- {else: store}
dev-sessions (v1.1) turns coding-agent sessions into a knowledge base.
- Pipeline:
- Sessions are split into episodes.
- Each episode is classified with one Jev call, and noise is dropped.
- Candidates are extracted.
- Each candidate is gated.
- Types: task, fix, decision, discussion, explanation, concept, convention, noise.
- What happens to each kind:
- Unverified fixes and tasks are labeled
unverified, meaning no passing tests, build, commit or user sign-off. - Explanations not grounded in files read are labeled.
- Unfinished discussions become open questions.
- Proposals the user did not endorse go to review.
- Abandoned approaches and generic textbook content are rejected.
- Later decisions supersede earlier ones, and the old entry moves to
_archive/. - Secrets are redacted before the judge sees them.
- Unverified fixes and tasks are labeled
Policies use a small, safe expression language (a hand-written parser, never eval). For anything it can't express, register Python checks and rules with @memworthy.check and @memworthy.rule.
Evaluation results
All numbers come from real jev-1.13.0 answers, regenerated with memworthy eval <policy>. Full reports, including every failure, are in eval/results/.
Contrast pairs are held out: one changed word flips the correct action, labels come from the template, and the policies were frozen before the pairs were recorded.
| Template | Cases | Action accuracy | Pair consistency | Target |
|---|---|---|---|---|
personal-memory v1.1 |
272 | 97.4% | 94.4% | ≥90% / ≥85%: met |
dev-sessions v1.1 |
236 | 91.5% | 83.9% | pair consistency misses 85% |
- First run. Three pair templates had labeling or wording errors and were corrected after the first run. That run scored 94.5% / 88.1% and 87.3% / 75.4%; it is kept in the repo, and the corrections are listed in
eval/results/README.md. - Remaining failures (genuine):
- A question that embeds a fact ("Since I live in Vienna, what…") is read as an intent.
- Failed fixes are rejected as noise instead of being labeled unverified.
- Some project-specific facts are typed as explanations and labeled ungrounded.
- Latency and cost. The median Jev round trip was about 1 s from the author's machine, which misses the 500 ms target. The median cost is about $0.00003 per candidate.
- Calibration. Raw ECE is 0.05–0.29 on small samples. See the reports for what the temperature-scaled numbers do and do not mean.
- Built-in tests are not evidence. The template tests (124 and 105) pass 100%, but they were used to tune the prompts.
- LongMemEval knowledge-update, with vs without Memworthy: pending real run. Run it with
python eval/longmemeval.py longmemeval_s_cleaned.json --limit 78; details are ineval/results/longmemeval/README.md.
Playground
playground/ has a FastAPI backend that imports this library and a React front end:
- a guided Dubai → Riyadh demo, one timeline row per message: message → verdict → effect on memory, with "Why?" showing the rule, judge answers and thresholds
- free play with example messages
- a policy panel with one-click experiments, rule switches and a YAML editor; edits replay the conversation and mark changed decisions
- the recorded coding-session demo
- the evaluation page
pip install -e ".[dev,jev]"
uvicorn playground.backend.app:app --port 8765 # open http://localhost:8765
The built front end is committed in playground/web/. To change it, edit playground/ui/ and rebuild:
cd playground/ui && npm ci
npm run dev # http://localhost:5173, proxies /api to the backend on :8765
npm run build # typechecks, then writes ../web
Replay mode uses the bundled recorded answers and needs no key. Live mode needs TYPESAFE_API_KEY. It is rate-limited per IP (40 calls per hour) and globally (600 per hour), and has a daily spend cap (PLAYGROUND_DAILY_CAP_USD, default $0.50). Every decision is labeled recorded, live or needs-live.
Development
pyenv install -s 3.12 && python -m venv .venv
.venv/bin/pip install -e ".[dev,jev,mem0]"
.venv/bin/pytest -q && .venv/bin/mypy src && .venv/bin/ruff check .
Tests never call live APIs; pytest -m live runs a few real Jev calls when TYPESAFE_API_KEY is set, and OpenAI Decisions calls when OPENAI_API_KEY is set. Design decisions are in docs/decisions.md and build status is in PROGRESS.md.
License
Apache 2.0.
Metadata
Release files for memworthy 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| memworthy-0.2.0.tar.gz | 281.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| memworthy-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 610.8 kB
Release files / memworthy-0.2.0.tar.gz
| Download URL | memworthy-0.2.0.tar.gz |
|---|---|
| Size | 281.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
35958a62183864c4b9ec4d4fc67a691226c6b533a242271585bc3420f20e7a12
|
|
BLAKE2b-256 checksum How to use checksums |
709eed6adfd2acf3602848faf51ced6a2de007a6eee8be3a09088a2d43770c94
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / memworthy-0.2.0-py3-none-any.whl
| Download URL | memworthy-0.2.0-py3-none-any.whl |
|---|---|
| Size | 329.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
082f86c176274563421e6be992ffe85fa7aa4e6ff1966c9ace94b90dd9e4fc59
|
|
BLAKE2b-256 checksum How to use checksums |
d4e66bcb00b7950b8201d4e0329cac87a72c8898409a07b13204adb2103c99c9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|