CludeMem-e4b / README.md
sebs-clude's picture
CludeMem-E4B v3: adapter, GGUF Q8_0/Q4_K_M, Modelfile, prompts, card
817dc1b verified
|
Raw History Blame Contribute Delete
22.3 kB
---
base_model: google/gemma-4-e4b-it
library_name: peft
license: apache-2.0
language: [en]
pipeline_tag: text-generation
tags: [lora, peft, gemma4, agent-memory, memory, structured-output, json, gguf, ollama, mlx, contradiction-detection]
model-index:
- name: CludeMem-E4B
results:
- task: {type: text-generation, name: "Contradiction detection (RECONCILE)"}
dataset: {type: dnli, name: "DNLI_gp (Dialogue NLI gate-passing test subset)", split: test}
metrics:
- {type: accuracy, name: "Strict 4-way accuracy", value: 74.0}
- {type: precision, name: "Contradiction precision", value: 93.5}
- task: {type: text-generation, name: "Contradiction detection (RECONCILE)"}
dataset: {type: decode, name: "DECODE_gp (DECODE gate-passing test subset)", split: test}
metrics:
- {type: accuracy, name: "Strict 4-way accuracy", value: 77.3}
- {type: precision, name: "Contradiction precision", value: 92.9}
---
# CludeMem-E4B
**Structured memory operations for AI agents, on your own hardware.**
CludeMem-E4B is a LoRA adapter for Gemma 4 E4B that gives an agent a working memory: nine operations, from
extracting and classifying memories to reconciling contradictions and answering from cited evidence. Each
operation is trained to return one JSON object with a fixed schema, so replies can be validated and stored
directly, and the merged GGUF builds run offline in Ollama, so the memories an agent keeps about its user never
have to leave the device. RECONCILE, the operation that catches contradictions, is measured on two human-labelled
dialogue benchmarks and on four runtimes; the other eight ship with reference pairs checked on every runtime.
## At a glance
- **Measured on human-labelled dialogue: 74.0% and 77.3% strict 4-way accuracy.** RECONCILE, the
contradiction-detection operation, scores **74.0%** (95% CI 70.7–77.0) on DNLI_gp and **77.3%** (74.0–80.2) on
DECODE_gp, 1,450 human-labelled pairs in all. The majority class scores 33.3% and 50.0%, and the
strongest surface-cue baseline 36.4% and 54.1%.
- **Contradiction flags you can act on: 93.5% and 92.9% precision.** On these label-balanced samples,
93.5% (DNLI_gp) and 92.9% (DECODE_gp) of its contradiction flags are correct, and it flags only 2.6% and
5.1% of the pairs that do not contradict. As a yes/no contradiction detector it is right on 89.7% and
81.0% of the pairs, against 66.7% and 50.0% for the majority class. Precision depends on how common
contradictions are in your data; the false-flag rate is the figure to carry over (*Usage notes*).
- **When it misses, it keeps both memories.** It finds 74.4% and 67.1% of the gold contradictions;
177 of the 179 it misses come back as `consistent`, so a missed contradiction leaves both memories in place
rather than dropping one.
- **Runs offline in Ollama on a 5.3 GB file, within 1.1 points of PyTorch.** On the same items, GGUF Q8_0 scores
73.7 / 77.9, GGUF Q4_K_M 72.9 / 76.7 and MLX 74.1 / 77.9, each within 1.1 points of
PyTorch (74.0 / 77.3). PyTorch and MLX gave the same verdict on 1,440 of the 1,450 pairs. The Q4_K_M build is
5.3 GB, the Q8_0 build 8.0 GB; both include the base model.
- **JSON without a grammar: 5,798 of 5,800 replies parsed.** Across those four RECONCILE runs, under plain greedy
decoding with no grammar or schema constraint, 5,798 of 5,800 replies parsed as a JSON object with all four keys,
including all 1,450 from PyTorch. Of the parsed replies, 25 carried a verdict outside the four labels; they and
the 2 that did not parse were scored as wrong and count against the accuracies above.
- **No DNLI or DECODE data was used in training.** The adapter was trained on synthetic persona timelines and
tested on human-written dialogue (*Training* has the overlap check).
## The nine operations
Each call sends the operation's system prompt as the system message and the memory data as the user message,
rendered with the Gemma 4 chat template and decoded greedily. Pass `enable_thinking=True`: it only adds the
`<|think|>` line that opened the system turn in training. The model does not write a thinking block; it replies
with one JSON object. `prompts.json` documents each input format, the output keys and values, and a reference
input/output pair.
| Operation | What it does | System prompt (exact) |
|---|---|---|
| `CLASSIFY` | Types one memory as episodic or semantic and scores its importance and emotional valence, with tags and concepts. | `Classify the memory. Output JSON: {type, importance (0-1), tags[], concepts[], emotional_valence (-1..1)}.` |
| `EXTRACT` | Turns a dialogue into atomic memories, skipping lines that carry none (logs, tables, tool-call JSON, code). | `Extract atomic memories from the text. Output JSON: {memories:[{content, summary, type}]}.` |
| `ENTITIES` | Extracts people, locations, organizations and projects, with aliases and relations such as `lives_in`. | `Extract entities and relations. Output JSON: {entities:[{name,type,aliases}], relations:[{head,type,tail}]}.` |
| `TEMPORAL` | Dates a new event and links it as before or after the known events. | `Extract the event date and its temporal links to the known events. Output JSON: {event_date, precision, links:[{type,target}]}.` |
| `CONSOLIDATE` | Consolidates a set of memories into insights, each citing its evidence ids. | `Consolidate the memories into evidence-linked insights. Output JSON: {insights:[{content, evidence[]}]}.` |
| `COMPACT` | Compacts old memories into one summary that preserves the entities and the date range. | `Compact the old memories into one summary, preserving entities and the date range. Output JSON: {summary, preserved_entities[], date_range:{start,end}}.` |
| `RECONCILE` | Decides whether two memories are consistent, contradict, duplicate, or one supersedes the other; names the weaker one. | `Decide how the two memories relate. Output JSON: {verdict, resolution, weaker_id, confidence}.` |
| `QUERY` | Expands a question into search queries, entities, time constraints and an intent. | `Understand the query. Output JSON: {expanded_queries[], temporal_constraints, type_filters[], entities[], intent}.` |
| `ANSWER` | Answers only from the supplied memories, cites them, and abstains when they do not support an answer. | `Answer ONLY from the provided memories; abstain if unsupported. Output JSON: {rationale, answer, citations[], confidence, abstain}.` |
## Quick start
All three examples send the ANSWER prompt with a test item from `prompts.json`.
### Ollama (offline)
```bash
hf download sebs-clude/CludeMem-e4b gguf/cludemem-e4b-v3.Q4_K_M.gguf gguf/Modelfile --local-dir cludemem-v3
cd cludemem-v3/gguf
ollama create cludemem-e4b-v3 -f Modelfile # the Modelfile's FROM line names cludemem-e4b-v3.Q4_K_M.gguf
curl -s http://localhost:11434/api/chat -d '{
"model": "cludemem-e4b-v3", "stream": false, "options": {"temperature": 0},
"messages": [
{"role": "system", "content": "Answer ONLY from the provided memories; abstain if unsupported. Output JSON: {rationale, answer, citations[], confidence, abstain}."},
{"role": "user", "content": "Question: Where does Maya live now?\n\nMemories:\n[f2] Maya moved to Austin on 2026-10-22.\n[f1] Maya lives in Lisbon.\n[f3] Maya currently resides in Austin."}
]
}'
```
Leave `think` unset: the prompt then matches the training prompt token for token, while `"think": false` drops
the `<|think|>` line. Recorded `message.content`, parsed (Q4_K_M, Ollama 0.34.2): `{"rationale": "Supported by f2.", "answer": "Austin", "citations": ["f2"], "confidence": 0.9, "abstain": false}`
### Transformers + PEFT
```python
import json, torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
BASE = "google/gemma-4-e4b-it" # full precision (bf16), not 4-bit; "unsloth/gemma-4-E4B-it" has the same weights
tok = AutoTokenizer.from_pretrained(BASE)
model = AutoModelForCausalLM.from_pretrained(BASE, dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(model, "sebs-clude/CludeMem-e4b").eval()
def run(system, user, max_new_tokens=512):
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
inputs = tok.apply_chat_template(messages, add_generation_prompt=True, enable_thinking=True,
return_tensors="pt", return_dict=True).to(model.device)
out = model.generate(**inputs, max_new_tokens=max_new_tokens, do_sample=False)
return json.loads(tok.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True))
SYSTEM = "Answer ONLY from the provided memories; abstain if unsupported. Output JSON: {rationale, answer, citations[], confidence, abstain}."
USER = "Question: Where does Maya live now?\n\nMemories:\n[f2] Maya moved to Austin on 2026-10-22.\n[f1] Maya lives in Lisbon.\n[f3] Maya currently resides in Austin."
print(run(SYSTEM, USER))
```
### MLX (Apple silicon; converts the PEFT adapter to mlx-lm's layout on the fly)
```python
import json
from pathlib import Path
import mlx.core as mx
from huggingface_hub import snapshot_download
from mlx_lm import generate
from mlx_lm.utils import load_adapters, load_model, load_tokenizer
base = Path(snapshot_download("google/gemma-4-e4b-it"))
peft = Path(snapshot_download("sebs-clude/CludeMem-e4b", allow_patterns=["adapter_*"]))
# convert the PEFT adapter to mlx-lm's layout: lora_a = A.T, lora_b = B.T, scale = lora_alpha / r
cfg = json.loads((peft / "adapter_config.json").read_text())
adapter = Path("cludemem-e4b-mlx")
adapter.mkdir(exist_ok=True)
weights = mx.load(str(peft / "adapter_model.safetensors"))
mx.save_safetensors(str(adapter / "adapters.safetensors"), {
k.replace("base_model.model.model.language_model.", "language_model.model.")
.replace(".lora_A.weight", ".lora_a").replace(".lora_B.weight", ".lora_b"): v.T for k, v in weights.items()})
lora = {"keys": ["mlp.gate_proj", "mlp.up_proj", "mlp.down_proj"], "rank": cfg["r"],
"scale": cfg["lora_alpha"] / cfg["r"], "dropout": 0.0}
(adapter / "adapter_config.json").write_text(json.dumps({"fine_tune_type": "lora", "num_layers": 42, "lora_parameters": lora}))
model, _ = load_model(base, strict=False) # mlx-lm does not use the base's 54 KV-shared-layer tensors
model = load_adapters(model, str(adapter)).eval()
eos = json.loads((base / "generation_config.json").read_text())["eos_token_id"]
tok = load_tokenizer(base, eos_token_ids=eos if isinstance(eos, list) else [eos])
def run(system, user, max_tokens=512):
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
prompt = tok.apply_chat_template(messages, add_generation_prompt=True, enable_thinking=True, tokenize=False)
return json.loads(generate(model, tok, prompt=prompt, max_tokens=max_tokens)) # greedy by default
SYSTEM = "Answer ONLY from the provided memories; abstain if unsupported. Output JSON: {rationale, answer, citations[], confidence, abstain}."
USER = "Question: Where does Maya live now?\n\nMemories:\n[f2] Maya moved to Austin on 2026-10-22.\n[f1] Maya lives in Lisbon.\n[f3] Maya currently resides in Austin."
print(run(SYSTEM, USER))
```
Both scripts printed `{'rationale': 'Supported by f2.', 'answer': 'Austin', 'citations': ['f2'], 'confidence': 0.9, 'abstain': False}` when run on the files in this repo.
The adapter the MLX script writes has the same 252 tensors, bit for bit, as the adapter behind the MLX
results in *Evaluation*; `strict=False` is there because mlx-lm does not use the base's 54 KV-shared-layer
tensors.
**Checked as printed.** The code above was run on the files in this repo for all 10 examples in `prompts.json`
(one per operation, plus an answerable ANSWER item) with Transformers 5.15.1, PEFT 0.20.0 and PyTorch
2.14.0 on Apple MPS, mlx-lm 0.31.3 and Ollama 0.34.2. Every reply was a JSON object with the
operation's output keys. Exact matches with the reference outputs: Transformers 10/10, MLX 10/10, Ollama Q8_0 10/10, Ollama Q4_K_M 9/10; the
Q4_K_M difference is in RECONCILE (see *Usage notes*). This checks that the code and files work as printed;
accuracy is measured in *Evaluation*.
## Evaluation
RECONCILE is scored on two public, human-labelled benchmarks; the other eight operations ship with reference
pairs checked on every runtime:
| Operation | Evidence in this release |
|---|---|
| `RECONCILE` | DNLI_gp and DECODE_gp: 1,450 human-labelled pairs, scored on four runtimes (below). They cover `consistent`, `contradicts` and `duplicate`; `supersedes` needs time-ordered pairs, which these datasets do not have. |
| `CLASSIFY`, `EXTRACT`, `ENTITIES`, `TEMPORAL`, `CONSOLIDATE`, `COMPACT`, `QUERY`, `ANSWER` | The reference input/output pairs in `prompts.json` (for ANSWER, one abstaining and one answerable item), matched exactly on all four runtimes (*Quick start*). No public benchmark in this release; check them on a sample of your own data before relying on them. |
### Contradiction detection: DNLI_gp and DECODE_gp
Dialogue NLI (Welleck et al. 2019) and DECODE (Nie et al. 2021) are human-labelled statement pairs from persona
dialogues; each pair is sent to RECONCILE as Memory A and Memory B. DNLI's entailment, neutral and contradiction
labels map to `duplicate`, `consistent` and `contradicts`. A DECODE contradiction pairs the turn that annotators
marked with the final turn; a DECODE non-contradiction pairs an earlier turn by the same speaker with a final
turn that annotators verified contradicts nothing before it, and its gold verdict is `consistent`. Only the
verdict is scored.
The `_gp` sets are gate-passing subsets of the test splits, filtered until lexical rules and a bag-of-words
classifier score near chance, so word overlap does not solve them. The baseline rows below confirm that filter:
they are a check on the test, not competitors. Because of the filter, these scores are not comparable with
published results on the full test sets.
Label-balanced samples (seed 20260821): DNLI_gp 750 of 2,622 items (250 per label), DECODE_gp 700 of 1,454 (350 per
label). They were not used in training. Strict 4-way accuracy counts a reply as right only when its verdict, one
of four, equals the gold label; replies that do not parse or whose verdict is not one of the four labels count as wrong (27 of the 5,800 across the
four runs). DNLI gold uses three verdicts and DECODE two; these pairs carry no time order, so `supersedes` always
counts as wrong. A flag is a `contradicts` or `supersedes` verdict; brackets are 95% Wilson intervals. PyTorch ran
on `unsloth/gemma-4-E4B-it` in 16-bit, whose `model.safetensors` has the same SHA-256 as `google/gemma-4-e4b-it`'s
(`cfbd3d2f1cd71bd4…`), MLX on `google/gemma-4-e4b-it`, and the GGUF rows are the files in this repo served by Ollama,
checked to be the same items by item id and prompt.
| System (same items, same harness) | DNLI_gp, n = 750 | DECODE_gp, n = 700 | Flag precision | Flag recall |
|---|---:|---:|---:|---:|
| **CludeMem-E4B**, PyTorch, 16-bit base | **74.0** [70.7, 77.0] | **77.3** [74.0, 80.2] | 93.5 / 92.9 | 74.4 / 67.1 |
| CludeMem-E4B, MLX, full-precision base | 74.1 [70.9, 77.1] | 77.9 [74.6, 80.8] | 93.5 / 94.0 | 74.8 / 66.9 |
| CludeMem-E4B, GGUF Q8_0 (Ollama) | 73.7 [70.5, 76.8] | 77.9 [74.6, 80.8] | 93.5 / 93.3 | 74.4 / 67.4 |
| CludeMem-E4B, GGUF Q4_K_M (Ollama) | 72.9 [69.6, 76.0] | 76.7 [73.4, 79.7] | 94.0 / 92.8 | 75.6 / 66.0 |
| Majority class | 33.3 | 50.0 | n/a | n/a |
| Bag-of-words logistic regression (strongest surface-cue baseline) | 36.4 | 54.1 | n/a | n/a |
Other systems appear in this table only when they were scored on these same items with this harness.
**Where the errors go (PyTorch).** The model answers `consistent` on 97.2% of the DNLI_gp and 88.3% of the
DECODE_gp pairs whose gold verdict is `consistent`, and when it misses a gold contradiction it nearly always says
`consistent` (177 of 179 misses). Its high precision and its lower recall are two sides of one conservative
operating point: on both datasets it raised fewer flags (199 and 253) than there are gold contradictions
(250 and 350), and most of the flags it raised were right. On DNLI_gp `duplicate` pairs it is right on 50.4%, and
115 of the 124 it misses come back as `consistent`. A DNLI entailment pair is two sentences annotated with the
same fact, and in 164 of the 250 sampled pairs one side is a dialogue turn that carries that fact alongside other
content, so `duplicate` is the loosest of the three label mappings. By annotator agreement: on the 602
DNLI_gp pairs whose three annotations agreed on the label, strict accuracy is 78.6%; on the 148 with a
split vote, 55.4%.
## Usage notes
Practical tips for getting the most out of the model:
- **Let code do the arithmetic.** ANSWER answers from the memories it cites. Compute sums, counts and date
differences in code from those cited memories rather than asking the model to work them out.
- **RECONCILE on Q4_K_M: treat an empty `weaker_id` as `null`, or use Q8_0.** In the 10-example check, Q4_K_M's
RECONCILE reply had the right verdict but an empty-string `weaker_id` and a different `resolution` sentence;
Q8_0 matched the reference exactly.
- **English in, JSON out.** Send English, single-turn calls; expect one JSON object per reply. Validate each
reply against the output keys in `prompts.json` and retry on a miss.
- **Plan for the contradiction rate of your own data.** The 93.5% and 92.9% precision above come from
label-balanced samples; the false-flag rate is the figure to carry over. With 2.6% and 5.1% of non-contradicting
pairs flagged, a store where contradictions are rare will see a larger share of false flags among what it
flags, so confirm `contradicts` verdicts on important memories before overwriting anything.
- **Decide on verdicts, not scores.** `confidence` and `importance` take a few fixed values in the training
targets (ANSWER's `confidence` target is 0.9 for an answer and 0.8 for an abstention). Treat them as coarse
labels and base decisions on `verdict` and `abstain`.
- **Batch large memory sets.** Training sequences, prompt plus reply, were at most 2,816 tokens. Send larger
memory sets to CONSOLIDATE, COMPACT and ANSWER in batches that fit.
- **Reference dates.** QUERY and TEMPORAL inputs begin with `Today is YYYY-MM-DD.`; nearly all of their training
inputs used `Today is 2026-06-01.`, so check date handling with your own dates.
- **Full-precision base.** Load the adapter on the bf16 base, not a 4-bit one; the base's `model.safetensors` is
16.0 GB. The GGUF builds already include the base: 8.0 GB (Q8_0) and 5.3 GB (Q4_K_M).
## Training
- **Method:** QLoRA on a 4-bit (NF4) copy of Gemma 4 E4B with bf16 compute; the adapter runs on the full-precision model.
- **Adapter:** rank 16, alpha 32 (scale 2.0), dropout 0.05, on the MLP projections (`gate_proj`,
`up_proj`, `down_proj`) of all 42 decoder layers; attention is untouched. 25,804,800 trainable parameters.
- **Optimisation:** AdamW (8-bit), learning rate 1e-4, cosine decay, weight decay 0, bf16, batch 4 with
gradient accumulation 4, sequences up to 2,816 tokens, loss on the assistant reply only.
- **Data:** synthetic persona timelines with planted facts, changes, contradictions and duplicates; the examples
for all nine operations are derived from each timeline's ground truth. A second seeded generator builds RECONCILE
pairs from hidden facts (same or different subject, attribute, value, time span) and derives each verdict from them.
- **No DNLI or DECODE data was used in training.** A text-overlap check found none of the 2,765 DNLI_gp
memory texts in any training message, and one of the 2,879 DECODE_gp memory texts, a phrase of 1-3 tokens
(these texts are model inputs, not labels).
- **Software:** PEFT 0.20.0, TRL 0.24.0, Transformers 5.5.0, Unsloth 2026.9.4, PyTorch 2.12.1.
## Files
| File | Size | Contents |
|---|---:|---|
| `adapter_model.safetensors` | 103.3 MB | LoRA weights (PEFT) |
| `adapter_config.json` | 7.6 KB | PEFT config; base `google/gemma-4-e4b-it` |
| `prompts.json` | 15.5 KB | system prompts, input formats, output keys and values, one example per operation |
| `gguf/cludemem-e4b-v3.Q8_0.gguf` | 8.03 GB | adapter merged into the base, 8-bit, text weights; preferred for RECONCILE |
| `gguf/cludemem-e4b-v3.Q4_K_M.gguf` | 5.34 GB | adapter merged into the base, 4-bit, text weights |
| `gguf/Modelfile` | 1.6 KB | Ollama template and parameters (FROM names the Q4_K_M file) |
| `LICENSE` | 11.4 KB | Apache License 2.0 |
| `NOTICE` | 0.6 KB | attribution |
SHA-256 checksums. After `hf download sebs-clude/CludeMem-e4b --local-dir <dir>`, save these lines as `SHA256SUMS` in `<dir>`
and run `shasum -a 256 -c SHA256SUMS` there:
```text
4a3e16637e7e24a15eb5e2fc6392009e595b8218776971a353647d5d920d27f8 adapter_model.safetensors
12fa78677f246fd57967fcb30fb482312113d15294fd8b77c49fec9062a9f842 adapter_config.json
53abc92c14ce9fda2f517818adcaade7202626ad88b8dcb32174faa7fa1c6070 prompts.json
9bc6dbe4adb0b49aa59ac93db7ffc14fe81b0c5b802d4083e51591127e3d26c1 gguf/cludemem-e4b-v3.Q8_0.gguf
16d63338794b58bb0a62d7ff410ceaf33754dcb419de672506f643d59439dee2 gguf/cludemem-e4b-v3.Q4_K_M.gguf
221ef8867d32d2a768c1297f0760209a30de527c9f61d25f123e9a67f1807b5a gguf/Modelfile
cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
3ec43cb8576fed652d4ec1d590eeddf452438dccb7656ac82755bcb68dc5fb16 NOTICE
```
## License and attribution
CludeMem-E4B (the adapter and the merged GGUF builds) is released under the Apache License 2.0 (`LICENSE`). It is
built on Gemma 4 E4B by Google DeepMind ([`google/gemma-4-e4b-it`](https://huggingface.co/google/gemma-4-e4b-it)), whose Hugging Face repository listed its
license as `apache-2.0` on 2026-09-22 (revision `ee0ef602…`) and links the [Gemma 4 license page](https://ai.google.dev/gemma/docs/gemma_4_license); check the
repository for the current terms. See `NOTICE`. Evaluation data: Dialogue NLI (Welleck et al. 2019) and DECODE (Nie et al. 2021).
## Citation
```bibtex
@misc{cludemem_e4b,
title = {CludeMem-E4B: structured agent-memory operations on Gemma 4 E4B},
author = {Clude},
year = {2026},
howpublished = {\url{https://huggingface.co/sebs-clude/CludeMem-e4b}}
}
```