errlore 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {errlore-0.1.1 → errlore-0.1.2}/CHANGELOG.md +13 -0
  2. {errlore-0.1.1 → errlore-0.1.2}/PKG-INFO +4 -2
  3. {errlore-0.1.1 → errlore-0.1.2}/README.md +3 -1
  4. errlore-0.1.2/benchmarks/bench_error_reduction.py +480 -0
  5. errlore-0.1.2/benchmarks/results/error_reduction/report.md +29 -0
  6. errlore-0.1.2/examples/claude-code/README.md +23 -0
  7. errlore-0.1.2/examples/claude-code/errlore_posttooluse.py +44 -0
  8. errlore-0.1.2/examples/claude-code/errlore_sessionstart.py +26 -0
  9. errlore-0.1.2/examples/claude-code/settings.json.example +25 -0
  10. {errlore-0.1.1 → errlore-0.1.2}/pyproject.toml +3 -1
  11. errlore-0.1.2/site/demo.gif +0 -0
  12. errlore-0.1.2/site/demo_script.py +91 -0
  13. {errlore-0.1.1 → errlore-0.1.2}/site/index.html +31 -7
  14. errlore-0.1.2/site/llms.txt +31 -0
  15. errlore-0.1.2/site/og.png +0 -0
  16. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/__init__.py +1 -1
  17. {errlore-0.1.1 → errlore-0.1.2}/.github/workflows/ci.yml +0 -0
  18. {errlore-0.1.1 → errlore-0.1.2}/.gitignore +0 -0
  19. {errlore-0.1.1 → errlore-0.1.2}/LICENSE +0 -0
  20. {errlore-0.1.1 → errlore-0.1.2}/SECURITY.md +0 -0
  21. {errlore-0.1.1 → errlore-0.1.2}/benchmarks/bench_retrieval.py +0 -0
  22. {errlore-0.1.1 → errlore-0.1.2}/examples/anthropic_agent.py +0 -0
  23. {errlore-0.1.1 → errlore-0.1.2}/examples/langchain_agent.py +0 -0
  24. {errlore-0.1.1 → errlore-0.1.2}/examples/openai_agent.py +0 -0
  25. {errlore-0.1.1 → errlore-0.1.2}/integrations/openwebui/README.md +0 -0
  26. {errlore-0.1.1 → errlore-0.1.2}/integrations/openwebui/errlore_feedback_action.py +0 -0
  27. {errlore-0.1.1 → errlore-0.1.2}/integrations/openwebui/errlore_memory_filter.py +0 -0
  28. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/errmem/__init__.py +0 -0
  29. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/errmem/classifier.py +0 -0
  30. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/errmem/injector.py +0 -0
  31. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/errmem/patterns.py +0 -0
  32. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/errmem/tracker.py +0 -0
  33. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/facade.py +0 -0
  34. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/io/__init__.py +0 -0
  35. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/io/jsonl_index.py +0 -0
  36. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/io/jsonl_writer.py +0 -0
  37. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/io/repair.py +0 -0
  38. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/lessons/__init__.py +0 -0
  39. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/lessons/models.py +0 -0
  40. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/lessons/store.py +0 -0
  41. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/py.typed +0 -0
  42. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/retrieval/__init__.py +0 -0
  43. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/retrieval/backend.py +0 -0
  44. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/retrieval/index.py +0 -0
  45. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/sanitize.py +0 -0
  46. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/trust/__init__.py +0 -0
  47. {errlore-0.1.1 → errlore-0.1.2}/src/errlore/trust/engine.py +0 -0
  48. {errlore-0.1.1 → errlore-0.1.2}/tests/conftest.py +0 -0
  49. {errlore-0.1.1 → errlore-0.1.2}/tests/test_errmem.py +0 -0
  50. {errlore-0.1.1 → errlore-0.1.2}/tests/test_facade.py +0 -0
  51. {errlore-0.1.1 → errlore-0.1.2}/tests/test_io.py +0 -0
  52. {errlore-0.1.1 → errlore-0.1.2}/tests/test_lessons.py +0 -0
  53. {errlore-0.1.1 → errlore-0.1.2}/tests/test_openwebui_integration.py +0 -0
  54. {errlore-0.1.1 → errlore-0.1.2}/tests/test_regressions.py +0 -0
  55. {errlore-0.1.1 → errlore-0.1.2}/tests/test_retrieval.py +0 -0
  56. {errlore-0.1.1 → errlore-0.1.2}/tests/test_trust.py +0 -0
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.2] - 2026-07-06
9
+
10
+ ### Added
11
+ - Error-reduction A/B benchmark with committed raw outputs: repeated
12
+ workspace-convention errors 46/48 -> 0/48 (100% reduction, McNemar
13
+ p=1.8e-09); capability-gap errors honestly unaffected.
14
+ - Claude Code hooks example (examples/claude-code/): PostToolUse failure
15
+ logging + SessionStart lessons briefing.
16
+ - 15-second terminal demo GIF (real library output).
17
+
18
+ ### Fixed
19
+ - `errlore.__version__` dunder reported 0.1.0 in the 0.1.1 wheel.
20
+
8
21
  ## [0.1.1] - 2026-07-06
9
22
 
10
23
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: errlore
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server.
5
5
  Project-URL: Homepage, https://errlore.com
6
6
  Project-URL: Repository, https://github.com/Ma4etaSS/errlore
@@ -38,8 +38,10 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  *Stop the second mistake, not just the first.*
40
40
 
41
+ ![errlore demo: monday failure becomes a lesson, tuesday's prompt gets it injected](https://errlore.com/demo.gif)
42
+
41
43
  [![CI](https://github.com/Ma4etaSS/errlore/actions/workflows/ci.yml/badge.svg)](https://github.com/Ma4etaSS/errlore/actions/workflows/ci.yml)
42
- [![Python 3.12+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
44
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
43
45
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
44
46
 
45
47
  Extracted from a 324K LOC production multi-LLM orchestration system, keeping the one part that demonstrably worked: the error-memory loop that made agents stop repeating mistakes.
@@ -4,8 +4,10 @@
4
4
 
5
5
  *Stop the second mistake, not just the first.*
6
6
 
7
+ ![errlore demo: monday failure becomes a lesson, tuesday's prompt gets it injected](https://errlore.com/demo.gif)
8
+
7
9
  [![CI](https://github.com/Ma4etaSS/errlore/actions/workflows/ci.yml/badge.svg)](https://github.com/Ma4etaSS/errlore/actions/workflows/ci.yml)
8
- [![Python 3.12+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
10
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue)](https://python.org)
9
11
  [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
10
12
 
11
13
  Extracted from a 324K LOC production multi-LLM orchestration system, keeping the one part that demonstrably worked: the error-memory loop that made agents stop repeating mistakes.
@@ -0,0 +1,480 @@
1
+ #!/usr/bin/env python3
2
+ """Error-reduction A/B benchmark: does errlore lesson injection reduce
3
+ repeated mistakes?
4
+
5
+ Task families were selected via a small difficulty probe so the target
6
+ model has a NON-ZERO baseline error rate (a precondition for measuring
7
+ error reduction; a model that never errs has nothing to reduce).
8
+
9
+ Protocol (paired, deterministic validators, no LLM judges):
10
+
11
+ Pass 1 (seed): run SEED tasks with a plain prompt. Every failure is
12
+ logged into errlore; each failing *family* gets its pre-authored
13
+ corrective lesson via resolve(). Lessons are authored below, in this
14
+ file, BEFORE any pass-2 output is seen — mirroring the real workflow
15
+ where a human fixes a failure once.
16
+ Pass 2 (test): run TEST tasks (same families, different instances) twice:
17
+ arm A -- plain prompt (control)
18
+ arm B -- prompt + errlore inject_for() block
19
+ Same model, temperature 0, same order.
20
+
21
+ Metric: per-arm failure rate on TEST tasks + exact McNemar on the paired
22
+ outcomes. Raw model outputs are dumped to JSONL for independent audit.
23
+
24
+ Run:
25
+ CEREBRAS_API_KEY=... python benchmarks/bench_error_reduction.py
26
+ (options: --model, --families, --pilot)
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import math
34
+ import os
35
+ import random
36
+ import sys
37
+ import tempfile
38
+ import time
39
+ from dataclasses import dataclass
40
+ from pathlib import Path
41
+
42
+ from openai import OpenAI
43
+
44
+ from errlore import AgentMemory
45
+
46
+ RNG_SEED = 20260706
47
+ SEED_PER_FAMILY = 6
48
+ TEST_PER_FAMILY = 12
49
+
50
+ MONTHS = ["January", "February", "March", "April", "May", "June", "July",
51
+ "August", "September", "October", "November", "December"]
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Task families. Each generates (prompt, expected) pairs with deterministic
55
+ # string validators. Lessons are the one-time human fix for that error class.
56
+ # ---------------------------------------------------------------------------
57
+
58
+
59
+ @dataclass
60
+ class Task:
61
+ family: str
62
+ prompt: str
63
+ expected: str
64
+
65
+
66
+ def fam_mult4(rng: random.Random, n: int) -> list[Task]:
67
+ tasks = []
68
+ for _ in range(n):
69
+ a, b = rng.randint(1000, 9999), rng.randint(1000, 9999)
70
+ tasks.append(Task(
71
+ "mult4",
72
+ f"Compute {a}*{b}. You may show working, but the LAST LINE of "
73
+ f"your reply must be exactly the integer result alone - no "
74
+ f"thousands separators, no punctuation.",
75
+ str(a * b),
76
+ ))
77
+ return tasks
78
+
79
+
80
+ _WORDS = ["kaleidoscope", "murmuration", "photosynthesis", "labyrinth",
81
+ "quintessential", "juxtaposition", "serendipity", "onomatopoeia",
82
+ "perpendicular", "hummingbird", "thunderstorm", "wheelbarrow",
83
+ "grasshopper", "lighthouse", "watermelon", "caterpillar"]
84
+
85
+
86
+ def fam_nth_char(rng: random.Random, n: int) -> list[Task]:
87
+ tasks = []
88
+ for _ in range(n):
89
+ s = rng.choice(_WORDS) + rng.choice("-._&") + rng.choice(_WORDS)
90
+ pos = rng.randint(9, len(s) - 3)
91
+ tasks.append(Task(
92
+ "nth_char",
93
+ f'What is character number {pos} (1-indexed, counting every '
94
+ f'character including punctuation) of the string "{s}"? '
95
+ f"You may show working, but the LAST LINE of your reply must be "
96
+ f"exactly that single character alone.",
97
+ s[pos - 1],
98
+ ))
99
+ return tasks
100
+
101
+
102
+ def fam_letter_sent(rng: random.Random, n: int) -> list[Task]:
103
+ pool = ["the", "quick", "brown", "river", "jumps", "over", "seven",
104
+ "lazy", "green", "turtles", "under", "bright", "summer",
105
+ "evening", "skies", "while", "children", "gather", "berries",
106
+ "near", "wooden", "bridges", "watching", "herons", "gliding"]
107
+ tasks = []
108
+ for _ in range(n):
109
+ words = [rng.choice(pool) for _ in range(rng.randint(11, 14))]
110
+ sent = " ".join(words)
111
+ ch = rng.choice("erns")
112
+ tasks.append(Task(
113
+ "letter_sent",
114
+ f'How many times does the letter "{ch}" appear in this sentence: '
115
+ f'"{sent}"? You may show working, but the LAST LINE of your '
116
+ f'reply must be exactly the integer alone.',
117
+ str(sent.count(ch)),
118
+ ))
119
+ return tasks
120
+
121
+
122
+ def fam_reverse(rng: random.Random, n: int) -> list[Task]:
123
+ tasks = []
124
+ for _ in range(n):
125
+ s = rng.choice(_WORDS) + rng.choice("-._") + rng.choice(_WORDS)
126
+ tasks.append(Task(
127
+ "reverse",
128
+ f'Reverse the string "{s}" exactly, character by character. '
129
+ f"You may show working, but the LAST LINE of your reply must be "
130
+ f"exactly the reversed string alone.",
131
+ s[::-1],
132
+ ))
133
+ return tasks
134
+
135
+
136
+
137
+
138
+ # --- Knowledge-gap families: the failure is a missing WORKSPACE CONVENTION,
139
+ # not a capability deficit. Without memory the model cannot know the rule;
140
+ # after one failure+fix the lesson supplies it. This mirrors the product's
141
+ # core claim (schema contracts, environment quirks, domain conventions).
142
+
143
+
144
+ def fam_log_ts(rng: random.Random, n: int) -> list[Task]:
145
+ tasks = []
146
+ for _ in range(n):
147
+ y, mo, d = rng.randint(2023, 2026), rng.randint(1, 12), rng.randint(1, 28)
148
+ h, mi = rng.randint(0, 23), rng.randint(0, 59)
149
+ iso = f"{y}-{mo:02d}-{d:02d}T{h:02d}:{mi:02d}:00Z"
150
+ tasks.append(Task(
151
+ "log_ts",
152
+ f"Convert this timestamp to our internal log format: {iso}. "
153
+ f"The LAST LINE of your reply must be the formatted timestamp alone.",
154
+ f"{d:02d}|{mo:02d}|{y}@{h:02d}:{mi:02d}",
155
+ ))
156
+ return tasks
157
+
158
+
159
+ def fam_id_norm(rng: random.Random, n: int) -> list[Task]:
160
+ first = ["Anna", "Boris", "Clara", "Dmitri", "Elena", "Felix", "Greta", "Hugo"]
161
+ last = ["Koval", "Smith", "Weber", "Rossi", "Novak", "Braun", "Lang", "Mora"]
162
+ tasks = []
163
+ for _ in range(n):
164
+ raw = f"{rng.choice(first)}-{rng.choice(last)}-{rng.randint(10, 99)}"
165
+ tasks.append(Task(
166
+ "id_norm",
167
+ f'Normalize this raw user id for our system: "{raw}". '
168
+ f"The LAST LINE of your reply must be the normalized id alone.",
169
+ "u_" + raw.lower().replace("-", ""),
170
+ ))
171
+ return tasks
172
+
173
+
174
+ def fam_round_rule(rng: random.Random, n: int) -> list[Task]:
175
+ tasks = []
176
+ for _ in range(n):
177
+ cents = rng.randint(1000, 99999)
178
+ third = rng.choice([3, 6, 7, 9])
179
+ price = cents / 100 + third / 1000 # e.g. 123.456
180
+ expected = f"{int(price * 100) / 100:.2f}" # truncate = round DOWN
181
+ tasks.append(Task(
182
+ "round_rule",
183
+ f"Apply our finance rounding rule to {price:.3f} and give the "
184
+ f"amount with 2 decimals. The LAST LINE must be the number alone.",
185
+ expected,
186
+ ))
187
+ return tasks
188
+
189
+
190
+ def fam_csv_order(rng: random.Random, n: int) -> list[Task]:
191
+ names = ["Ivy", "Max", "Zoe", "Kai", "Lea", "Tom", "Ada", "Rex"]
192
+ tasks = []
193
+ for _ in range(n):
194
+ name = rng.choice(names)
195
+ uid = rng.randint(100, 999)
196
+ email = f"{name.lower()}{uid}@example.com"
197
+ fields = {"name": name, "id": str(uid), "email": email}
198
+ keys = list(fields)
199
+ rng.shuffle(keys)
200
+ jumbled = json.dumps({k: fields[k] for k in keys})
201
+ tasks.append(Task(
202
+ "csv_order",
203
+ f"Convert this record to a CSV row in our canonical column "
204
+ f"order: {jumbled}. The LAST LINE must be the CSV row alone.",
205
+ f"{email},{uid},{name}",
206
+ ))
207
+ return tasks
208
+
209
+ # capability-gap families (model skill limits) + knowledge-gap families
210
+ # (workspace conventions) — reported separately.
211
+ FAMILIES = {
212
+ "mult4": fam_mult4,
213
+ "nth_char": fam_nth_char,
214
+ "letter_sent": fam_letter_sent,
215
+ "reverse": fam_reverse,
216
+ "log_ts": fam_log_ts,
217
+ "id_norm": fam_id_norm,
218
+ "round_rule": fam_round_rule,
219
+ "csv_order": fam_csv_order,
220
+ }
221
+
222
+ KNOWLEDGE_GAP = {"log_ts", "id_norm", "round_rule", "csv_order"}
223
+
224
+ # Pre-authored corrective lessons (the "human fix", written before pass 2).
225
+ LESSONS = {
226
+ "mult4": "Never answer multi-digit multiplication from intuition. Break it into partial products and add them carefully. Verify: the last digit of the result must equal (last digit of a * last digit of b) mod 10. Then put the final integer ALONE on the last line - no commas, no words after it.",
227
+ "nth_char": "Write out the string with numbered positions (1,2,3,...), counting EVERY character including hyphens and punctuation, then pick exactly the requested position. Put that single character ALONE on the last line.",
228
+ "letter_sent": "Go word by word and count the target letter in each word, keeping a running total across ALL words. Then put the final integer ALONE on the last line.",
229
+ "reverse": "Build the reversal by writing characters from the END one at a time. Verify lengths match and the first output char equals the last input char. Then put the reversed string ALONE on the last line.",
230
+ "log_ts": "Our internal log timestamp format is DD|MM|YYYY@HH:mm (day first, pipe separators, @ before the 24h time, no seconds, no timezone). Example: 2024-03-05T14:07:00Z -> 05|03|2024@14:07.",
231
+ "id_norm": "Our user-id normalization rule: lowercase everything, remove all dashes, then add the prefix u_. Example: Anna-Koval-42 -> u_annakoval42.",
232
+ "round_rule": "Our finance rule ALWAYS truncates (rounds toward zero) to 2 decimals - never round half up or to even. Example: 123.456 -> 123.45, and 99.999 -> 99.99.",
233
+ "csv_order": "Our canonical CSV column order is: email,id,name (regardless of the order fields appear in the source record). No spaces after commas, no quotes.",
234
+ }
235
+
236
+ SYSTEM = "You are a precise assistant. Follow the required output format exactly."
237
+
238
+
239
+ # ---------------------------------------------------------------------------
240
+ # Validators
241
+ # ---------------------------------------------------------------------------
242
+
243
+
244
+ def validate(task: Task, output: str) -> bool:
245
+ out = output.strip()
246
+ if task.expected == "__JSON5__":
247
+ try:
248
+ if out.startswith("```"):
249
+ out = out.strip("`")
250
+ out = out[out.find("{"):out.rfind("}") + 1]
251
+ obj = json.loads(out)
252
+ except (json.JSONDecodeError, ValueError):
253
+ return False
254
+ fixes = obj.get("fixes")
255
+ return (
256
+ isinstance(obj, dict) and list(obj.keys()) == ["fixes"]
257
+ and isinstance(fixes, list) and len(fixes) == 5
258
+ and all(isinstance(x, str) and x.strip() for x in fixes)
259
+ and len(set(fixes)) == 5
260
+ )
261
+ # exact-match families: the contract is "last line = answer alone"
262
+ lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
263
+ last = (lines[-1] if lines else "").rstrip(".").strip().strip('"')
264
+ return last == task.expected
265
+
266
+
267
+ # ---------------------------------------------------------------------------
268
+ # Model call
269
+ # ---------------------------------------------------------------------------
270
+
271
+
272
+ def make_client(backend: str) -> tuple[OpenAI, str]:
273
+ if backend == "cerebras":
274
+ key = os.environ.get("CEREBRAS_API_KEY")
275
+ if not key:
276
+ sys.exit("CEREBRAS_API_KEY is required")
277
+ return OpenAI(base_url="https://api.cerebras.ai/v1", api_key=key), "gemma-4-31b"
278
+ if backend == "anthropic":
279
+ # Uses the Anthropic SDK directly (no OpenAI-compatible endpoint).
280
+ key = os.environ.get("ANTHROPIC_API_KEY")
281
+ if not key:
282
+ sys.exit("ANTHROPIC_API_KEY is required")
283
+ return None, "claude-haiku-4-5" # client built lazily in ask()
284
+ if backend == "gemini":
285
+ key = os.environ.get("GEMINI_API_KEY")
286
+ if not key:
287
+ sys.exit("GEMINI_API_KEY is required")
288
+ return OpenAI(
289
+ base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
290
+ api_key=key,
291
+ ), "gemini-2.5-flash-lite"
292
+ sys.exit(f"unknown backend {backend}")
293
+
294
+
295
+ _anthropic_client = None
296
+
297
+
298
+ def _ask_anthropic(model: str, system: str, prompt: str) -> str:
299
+ global _anthropic_client
300
+ import anthropic
301
+
302
+ if _anthropic_client is None:
303
+ _anthropic_client = anthropic.Anthropic()
304
+ for attempt in range(6):
305
+ try:
306
+ resp = _anthropic_client.messages.create(
307
+ model=model,
308
+ max_tokens=500,
309
+ temperature=0,
310
+ system=system,
311
+ messages=[{"role": "user", "content": prompt}],
312
+ )
313
+ return next((b.text for b in resp.content if b.type == "text"), "")
314
+ except anthropic.RateLimitError:
315
+ if attempt == 5:
316
+ raise
317
+ time.sleep(31)
318
+ raise RuntimeError("unreachable")
319
+
320
+
321
+ def ask(client: OpenAI, model: str, prompt: str, extra_system: str = "") -> str:
322
+ from openai import RateLimitError
323
+
324
+ system = SYSTEM + (("\n\n" + extra_system) if extra_system else "")
325
+ if client is None: # anthropic backend
326
+ return _ask_anthropic(model, system, prompt)
327
+ for attempt in range(6):
328
+ try:
329
+ resp = client.chat.completions.create(
330
+ model=model,
331
+ temperature=0,
332
+ max_tokens=400,
333
+ messages=[{"role": "system", "content": system},
334
+ {"role": "user", "content": prompt}],
335
+ )
336
+ return resp.choices[0].message.content or ""
337
+ except RateLimitError:
338
+ if attempt == 5:
339
+ raise
340
+ time.sleep(31) # free-tier RPM window
341
+ raise RuntimeError("unreachable")
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Experiment
346
+ # ---------------------------------------------------------------------------
347
+
348
+
349
+ def mcnemar_exact_p(b: int, c: int) -> float:
350
+ """Two-sided exact McNemar on discordant pairs (binomial)."""
351
+ n = b + c
352
+ if n == 0:
353
+ return 1.0
354
+ k = min(b, c)
355
+ p = sum(math.comb(n, i) for i in range(0, k + 1)) / (2 ** n) * 2
356
+ return min(1.0, p)
357
+
358
+
359
+ def main() -> int:
360
+ ap = argparse.ArgumentParser()
361
+ ap.add_argument("--backend", default="gemini")
362
+ ap.add_argument("--pilot", action="store_true", help="2 families, 3 test each")
363
+ ap.add_argument("--out", default=None)
364
+ args = ap.parse_args()
365
+
366
+ client, model = make_client(args.backend)
367
+ rng = random.Random(RNG_SEED)
368
+
369
+ fams = dict(FAMILIES)
370
+ seed_n, test_n = SEED_PER_FAMILY, TEST_PER_FAMILY
371
+ if args.pilot:
372
+ fams = {k: fams[k] for k in ("mult4", "nth_char")}
373
+ seed_n, test_n = 3, 3
374
+
375
+ seed_tasks: list[Task] = []
376
+ test_tasks: list[Task] = []
377
+ for _name, gen in fams.items():
378
+ all_t = gen(rng, seed_n + test_n)
379
+ seed_tasks += all_t[:seed_n]
380
+ test_tasks += all_t[seed_n:]
381
+
382
+ workdir = Path(tempfile.mkdtemp(prefix="errlore_ab_"))
383
+ mem = AgentMemory(workdir / "memory")
384
+ raw_log = open(workdir / "raw_outputs.jsonl", "w")
385
+
386
+ def record(phase: str, arm: str, task: Task, output: str, ok: bool) -> None:
387
+ raw_log.write(json.dumps({
388
+ "phase": phase, "arm": arm, "family": task.family,
389
+ "prompt": task.prompt, "expected": task.expected,
390
+ "output": output, "ok": ok,
391
+ }, ensure_ascii=False) + "\n")
392
+
393
+ # ---- Pass 1: seed (plain prompts; failures become lessons) ----
394
+ fam_fail: dict[str, int] = {}
395
+ print(f"[pass1] {len(seed_tasks)} seed tasks, model={model}")
396
+ for t in seed_tasks:
397
+ out = ask(client, model, t.prompt)
398
+ ok = validate(t, out)
399
+ record("seed", "plain", t, out, ok)
400
+ if not ok:
401
+ fam_fail[t.family] = fam_fail.get(t.family, 0) + 1
402
+ err_id = mem.log_error(model, t.family, f"WrongAnswer: {out[:120]}")
403
+ mem.resolve(err_id, "authored corrective lesson", lesson=LESSONS[t.family])
404
+ print(f"[pass1] failures by family: {fam_fail or 'none'}")
405
+ active = set(fam_fail)
406
+ if not active:
407
+ print("[pass1] model aced the seed set — no lessons to test; "
408
+ "harden the task families before drawing conclusions.")
409
+ return 1
410
+
411
+ # ---- Pass 2: paired test ----
412
+ results = [] # (family, ok_A, ok_B)
413
+ print(f"[pass2] {len(test_tasks)} test tasks x 2 arms")
414
+ for t in test_tasks:
415
+ out_a = ask(client, model, t.prompt)
416
+ ok_a = validate(t, out_a)
417
+ record("test", "A_plain", t, out_a, ok_a)
418
+
419
+ inj = mem.inject_for(t.prompt, model=model, task_type=t.family)
420
+ out_b = ask(client, model, t.prompt, extra_system=inj.text)
421
+ ok_b = validate(t, out_b)
422
+ record("test", "B_errlore", t, out_b, ok_b)
423
+ mem.report_outcome(inj, ok_b)
424
+ results.append((t.family, ok_a, ok_b))
425
+
426
+ raw_log.close()
427
+
428
+ # ---- Report ----
429
+ n = len(results)
430
+ fail_a = sum(1 for _, a, _b in results if not a)
431
+ fail_b = sum(1 for _, _a, b in results if not b)
432
+ b_disc = sum(1 for _, a, b in results if a and not b) # B fails where A passed
433
+ c_disc = sum(1 for _, a, b in results if not a and b) # B fixes A's failure
434
+ p = mcnemar_exact_p(b_disc, c_disc)
435
+
436
+ lines = [
437
+ f"# errlore error-reduction A/B — model {model}",
438
+ "",
439
+ f"tasks (test): {n} | families active (had seed failures): {sorted(active)}",
440
+ "",
441
+ "| arm | failures | fail rate |",
442
+ "|---|---|---|",
443
+ f"| A plain | {fail_a}/{n} | {fail_a/n:.1%} |",
444
+ f"| B errlore | {fail_b}/{n} | {fail_b/n:.1%} |",
445
+ "",
446
+ f"discordant pairs: errlore fixed {c_disc}, errlore broke {b_disc}",
447
+ f"exact McNemar p = {p:.4g}",
448
+ ]
449
+ if fail_a:
450
+ lines.append(f"repeat-error reduction: {(fail_a - fail_b) / fail_a:.1%}")
451
+ for label, group in (("KNOWLEDGE-GAP (workspace conventions)",
452
+ [r for r in results if r[0] in KNOWLEDGE_GAP]),
453
+ ("CAPABILITY-GAP (model skill limits)",
454
+ [r for r in results if r[0] not in KNOWLEDGE_GAP])):
455
+ if not group:
456
+ continue
457
+ gn = len(group)
458
+ ga = sum(1 for _, a, _b in group if not a)
459
+ gb = sum(1 for _, _a, b in group if not b)
460
+ red = f" | reduction {(ga - gb) / ga:.0%}" if ga else ""
461
+ lines += ["", f"### {label}: A {ga}/{gn} -> B {gb}/{gn}{red}"]
462
+
463
+ lines += ["", "per-family (fail A -> fail B):"]
464
+ for fam in sorted(fams):
465
+ fa = sum(1 for f, a, _ in results if f == fam and not a)
466
+ fb = sum(1 for f, _, b in results if f == fam and not b)
467
+ marker = " *lesson active*" if fam in active else ""
468
+ lines.append(f"- {fam}: {fa} -> {fb}{marker}")
469
+ lines += ["", f"raw outputs: {workdir}/raw_outputs.jsonl",
470
+ f"errlore stats: {mem.stats()}"]
471
+ report = "\n".join(lines)
472
+ print("\n" + report)
473
+ out_path = Path(args.out) if args.out else workdir / "report.md"
474
+ out_path.write_text(report)
475
+ print(f"\n[saved: {out_path}]")
476
+ return 0
477
+
478
+
479
+ if __name__ == "__main__":
480
+ sys.exit(main())
@@ -0,0 +1,29 @@
1
+ # errlore error-reduction A/B — model claude-haiku-4-5
2
+
3
+ tasks (test): 96 | families active (had seed failures): ['csv_order', 'id_norm', 'letter_sent', 'log_ts', 'reverse', 'round_rule']
4
+
5
+ | arm | failures | fail rate |
6
+ |---|---|---|
7
+ | A plain | 63/96 | 65.6% |
8
+ | B errlore | 20/96 | 20.8% |
9
+
10
+ discordant pairs: errlore fixed 49, errlore broke 6
11
+ exact McNemar p = 1.823e-09
12
+ repeat-error reduction: 68.3%
13
+
14
+ ### KNOWLEDGE-GAP (workspace conventions): A 46/48 -> B 0/48 | reduction 100%
15
+
16
+ ### CAPABILITY-GAP (model skill limits): A 17/48 -> B 20/48 | reduction -18%
17
+
18
+ per-family (fail A -> fail B):
19
+ - csv_order: 12 -> 0 *lesson active*
20
+ - id_norm: 12 -> 0 *lesson active*
21
+ - letter_sent: 6 -> 8 *lesson active*
22
+ - log_ts: 12 -> 0 *lesson active*
23
+ - mult4: 2 -> 1
24
+ - nth_char: 0 -> 0
25
+ - reverse: 9 -> 11 *lesson active*
26
+ - round_rule: 10 -> 0 *lesson active*
27
+
28
+ raw outputs: /tmp/errlore_ab_4iljcym1/raw_outputs.jsonl
29
+ errlore stats: {'errors_total': 29, 'errors_resolved': 29, 'errors_unresolved': 0, 'lessons_total': 23, 'lessons_applied': 14, 'pending_injections': 0, 'trust': {'claude-haiku-4-5': 0.92}}
@@ -0,0 +1,23 @@
1
+ # errlore + Claude Code
2
+
3
+ Give your coding agent a memory of its own failures across sessions:
4
+
5
+ - **PostToolUse hook** — every failed Bash command is logged into errlore.
6
+ Resolve the ones you fixed (`mem.resolve(err_id, ..., lesson=...)`) or use
7
+ `mem.add_lesson()` to capture takeaways directly.
8
+ - **SessionStart hook** — each new session begins with a briefing block of
9
+ relevant lessons and per-tool KNOWN ISSUES, printed into the context.
10
+
11
+ ## Setup
12
+
13
+ 1. `pip install errlore`
14
+ 2. Copy both scripts somewhere stable, adjust paths in
15
+ `settings.json.example`, and merge it into your `.claude/settings.json`
16
+ (project) or `~/.claude/settings.json` (global).
17
+ 3. Optional: `export ERRLORE_DATA=...` to choose where the memory lives
18
+ (defaults to `~/.errlore/claude-code`).
19
+
20
+ Notes: hook event field names can differ between Claude Code versions —
21
+ the PostToolUse script reads them defensively and never breaks the agent
22
+ loop (exit 0 on anything unexpected). Check `claude --help` / the hooks
23
+ docs for your version if events don't arrive.
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Code PostToolUse hook: failed Bash commands become errlore errors.
3
+
4
+ Reads the hook event JSON from stdin (defensively -- field names may vary
5
+ across Claude Code versions), logs failures into a shared errlore memory.
6
+ Pair with errlore_sessionstart.py, which briefs the next session on them.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import sys
12
+
13
+ from errlore import AgentMemory
14
+
15
+ DATA_DIR = os.environ.get("ERRLORE_DATA", os.path.expanduser("~/.errlore/claude-code"))
16
+
17
+
18
+ def main() -> int:
19
+ try:
20
+ event = json.load(sys.stdin)
21
+ except json.JSONDecodeError:
22
+ return 0 # never break the agent loop
23
+
24
+ tool = event.get("tool_name", "")
25
+ if tool != "Bash":
26
+ return 0
27
+
28
+ resp = event.get("tool_response") or {}
29
+ if isinstance(resp, str):
30
+ resp = {"output": resp}
31
+ exit_code = resp.get("exit_code", resp.get("exitCode", 0))
32
+ is_error = bool(resp.get("is_error") or resp.get("isError"))
33
+ if not is_error and (exit_code in (0, None)):
34
+ return 0
35
+
36
+ command = (event.get("tool_input") or {}).get("command", "")[:160]
37
+ output = str(resp.get("output") or resp.get("stderr") or "")[:200]
38
+ mem = AgentMemory(DATA_DIR)
39
+ mem.log_error("claude-code", "bash", f"CommandFailed: {command} :: {output}")
40
+ return 0
41
+
42
+
43
+ if __name__ == "__main__":
44
+ sys.exit(main())
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python3
2
+ """Claude Code SessionStart hook: brief the new session on past pitfalls.
3
+
4
+ Prints the errlore lessons + KNOWN ISSUES block to stdout; Claude Code adds
5
+ hook stdout to the session context. Empty memory -> prints nothing.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+
11
+ from errlore import AgentMemory
12
+
13
+ DATA_DIR = os.environ.get("ERRLORE_DATA", os.path.expanduser("~/.errlore/claude-code"))
14
+
15
+
16
+ def main() -> int:
17
+ mem = AgentMemory(DATA_DIR)
18
+ inj = mem.inject_for("starting a coding session in this workspace",
19
+ model="claude-code", task_type="bash")
20
+ if inj.text:
21
+ print(inj.text)
22
+ return 0
23
+
24
+
25
+ if __name__ == "__main__":
26
+ sys.exit(main())
@@ -0,0 +1,25 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Bash",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "python3 /path/to/errlore_posttooluse.py"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionStart": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "python3 /path/to/errlore_sessionstart.py"
20
+ }
21
+ ]
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "errlore"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Memory for AI agents that learns from failures: lessons, known-issues injection, and per-model trust — embedded, file-based, no server."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -62,6 +62,8 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
62
62
 
63
63
  [tool.ruff.lint.per-file-ignores]
64
64
  "src/errlore/errmem/patterns.py" = ["RUF001", "RUF003"]
65
+ "benchmarks/*" = ["E501", "SIM115"] # long authored lesson strings; long-lived log handle
66
+ "site/*" = ["E501"] # demo-script terminal strings read better unwrapped
65
67
 
66
68
  [tool.mypy]
67
69
  python_version = "3.12" # numpy stubs need 3.12 syntax; 3.10 runtime compat is enforced by ruff(py310) + CI matrix
Binary file
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """Scripted terminal demo for the errlore GIF (recorded with asciinema).
3
+
4
+ Shows the real library doing the real cycle — outputs are produced by
5
+ actually calling errlore against a temp dir, not faked strings.
6
+ """
7
+
8
+ import shutil
9
+ import sys
10
+ import time
11
+ from pathlib import Path
12
+
13
+ DATA = Path("/tmp/errlore_demo")
14
+
15
+ G = "\033[32m" # green
16
+ Y = "\033[33m" # gold
17
+ R = "\033[31m" # red
18
+ D = "\033[2m" # dim
19
+ B = "\033[1m" # bold
20
+ X = "\033[0m" # reset
21
+
22
+
23
+ def type_out(text: str, delay: float = 0.025, end: str = "\n") -> None:
24
+ for ch in text:
25
+ sys.stdout.write(ch)
26
+ sys.stdout.flush()
27
+ time.sleep(delay)
28
+ sys.stdout.write(end)
29
+ sys.stdout.flush()
30
+
31
+
32
+ def prompt(cmd: str) -> None:
33
+ sys.stdout.write(f"{G}${X} ")
34
+ sys.stdout.flush()
35
+ time.sleep(0.4)
36
+ type_out(cmd)
37
+ time.sleep(0.3)
38
+
39
+
40
+ def out(text: str, color: str = "") -> None:
41
+ print(f"{color}{text}{X}")
42
+ sys.stdout.flush()
43
+ time.sleep(0.25)
44
+
45
+
46
+ def main() -> None:
47
+ shutil.rmtree(DATA, ignore_errors=True)
48
+ from errlore import AgentMemory
49
+ mem = AgentMemory(DATA)
50
+
51
+ print(f"{B}# your agent, monday{X}")
52
+ time.sleep(0.6)
53
+ prompt("agent run 'sum the invoice amounts'")
54
+ out(" -> 224.64 (expected 224.63)", R)
55
+ out(" FAILED: used banker's rounding, finance wants truncation", R)
56
+ time.sleep(0.8)
57
+
58
+ print()
59
+ print(f"{B}# you fix it once — errlore remembers{X}")
60
+ time.sleep(0.5)
61
+ prompt("python")
62
+ type_out(f"{Y}>>>{X} err = mem.log_error('gpt-5.5', 'finance', 'rounded half-even')", 0.012)
63
+ err = mem.log_error("gpt-5.5", "finance", "WrongRounding: rounded half-even")
64
+ time.sleep(0.4)
65
+ type_out(f"{Y}>>>{X} mem.resolve(err, 'fixed', lesson='finance ALWAYS truncates to 2 decimals')", 0.012)
66
+ mem.resolve(err, "fixed", lesson="finance ALWAYS truncates to 2 decimals - never round half up or to even")
67
+ out(" lesson saved [conf 0.80]", G)
68
+ time.sleep(0.9)
69
+
70
+ print()
71
+ print(f"{B}# tuesday — same class of task{X}")
72
+ time.sleep(0.5)
73
+ prompt("agent run 'sum the refund amounts'")
74
+ inj = mem.inject_for("sum the refund amounts", model="gpt-5.5", task_type="finance")
75
+ out(" errlore injected into the prompt:", Y)
76
+ for line in inj.text.strip().splitlines():
77
+ print(f"{D} | {line}{X}")
78
+ time.sleep(0.18)
79
+ time.sleep(0.5)
80
+ out(" -> 148.90 correct", G)
81
+ mem.report_outcome(inj, success=True)
82
+ out(" outcome reported: lesson reinforced [conf 0.80 -> 0.90]", G)
83
+ time.sleep(1.0)
84
+
85
+ print()
86
+ print(f"{B}pip install errlore{X} {D}errlore.com — MIT, offline, no telemetry{X}")
87
+ time.sleep(2.2)
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -5,6 +5,11 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>errlore — memory for AI agents that learns from failures</title>
7
7
  <meta name="description" content="Your agent keeps making the same mistakes. errlore turns failures into lessons, injects them into prompts, and tracks which model to trust. Embedded, file-based, offline.">
8
+ <meta property="og:title" content="errlore — memory for AI agents that learns from failures">
9
+ <meta property="og:description" content="Every resolved error becomes a lesson, injected into future prompts. 100% elimination of repeated convention errors in a paired A/B. Offline, file-based, MIT.">
10
+ <meta property="og:image" content="https://errlore.com/og.png">
11
+ <meta property="og:url" content="https://errlore.com">
12
+ <meta name="twitter:card" content="summary_large_image">
8
13
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2312100d'/%3E%3Ctext x='16' y='23' font-family='monospace' font-size='20' fill='%23d9a441' text-anchor='middle'%3Ee%3C/text%3E%3C/svg%3E">
9
14
  <style>
10
15
  :root{
@@ -89,17 +94,18 @@
89
94
  <div class="links">
90
95
  <a href="https://github.com/Ma4etaSS/errlore">GitHub</a>
91
96
  <a href="https://pypi.org/project/errlore/">PyPI</a>
92
- <a href="#quickstart">Docs</a>
97
+ <a href="#quickstart">Quickstart</a>
93
98
  </div>
94
99
  </nav>
95
100
 
96
101
  <header>
97
102
  <h1>Your agent keeps making <em>the same mistakes.</em><br>errlore makes that stop.</h1>
98
- <p class="sub">A memory layer for AI agents that learns from failures: every resolved
103
+ <p class="sub" style="margin-top:14px;font-style:italic;color:var(--gold)">Stop the second mistake, not just the first.</p>
104
+ <p class="sub" style="margin-top:14px">A memory layer for AI agents that learns from failures: every resolved
99
105
  error becomes a lesson, lessons are injected into future prompts, and Bayesian trust
100
106
  weights track which model to believe for which job.</p>
101
107
  <div style="display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin-top:36px">
102
- <div class="pip" style="margin-top:0"><span class="dollar">$</span> pip install errlore</div>
108
+ <div class="pip" style="margin-top:0;cursor:pointer" onclick="navigator.clipboard.writeText('pip install errlore');this.querySelector('.cp').textContent='copied!'" title="Click to copy"><span class="dollar">$</span> pip install errlore <span class="cp" style="color:var(--gold-dim);font-size:12px;margin-left:8px">copy</span></div>
103
109
  <a href="https://github.com/Ma4etaSS/errlore" style="display:inline-block;border:1px solid var(--gold-dim);border-radius:8px;padding:14px 22px;font-family:var(--mono);font-size:15px;color:var(--gold)">View on GitHub &rarr;</a>
104
110
  </div>
105
111
  <div class="cta-row">MIT &middot; Python 3.10+ &middot; 186 tests &middot; CI passing &middot; offline &middot; no telemetry &middot; JSONL, file-locked</div>
@@ -141,6 +147,11 @@
141
147
  </div>
142
148
  </section>
143
149
 
150
+ <section>
151
+ <h2>Fifteen seconds of the whole idea</h2>
152
+ <img src="/demo.gif" alt="errlore demo: a failure becomes a lesson; the next similar task gets it injected and succeeds" style="width:100%;border:1px solid var(--line);border-radius:10px" loading="lazy">
153
+ </section>
154
+
144
155
  <section id="quickstart">
145
156
  <h2>Sixty seconds to a learning agent</h2>
146
157
  <div class="term">
@@ -181,16 +192,22 @@ mem.report_outcome(inj, success=<span class="c-kw">True</span>)</pre>
181
192
  <tr><td class="mono">recall@5</td><td class="zero">0.000</td><td><b>0.675</b></td></tr>
182
193
  <tr><td class="mono">MRR</td><td class="zero">0.000</td><td><b>0.488</b></td></tr>
183
194
  </table>
184
- <p class="note">Reproduce it yourself: <span class="mono">python benchmarks/bench_retrieval.py</span>the gold set ships in the repo.</p>
195
+ <p class="note">The gold set is deliberately adversarialevery query is paraphrased to
196
+ share zero words with its lesson. On natural queries with shared vocabulary, plain
197
+ word-overlap does fine; embeddings earn their keep on paraphrased ones.
198
+ Reproduce it yourself: <span class="mono">python benchmarks/bench_retrieval.py</span> — the gold set ships in the repo.</p>
185
199
  </section>
186
200
 
187
201
  <section>
188
202
  <h2>Drops into what you already run</h2>
189
- <div class="grid2">
190
- <div class="card"><h3>Open WebUI</h3><p>Two functions a Filter that injects lessons
203
+ <div class="loops">
204
+ <div class="loop"><h3>Claude Code</h3><p>Hooks wire errlore into your coding agent:
205
+ failed commands become lessons, and each session starts with a briefing of past
206
+ pitfalls. See <span class="mono">examples/claude-code/</span>.</p></div>
207
+ <div class="loop"><h3>Open WebUI</h3><p>Two functions — a Filter that injects lessons
191
208
  into every chat and a feedback Action that closes the loop from a button under each
192
209
  message. Paste, enable, done.</p></div>
193
- <div class="card"><h3>Python SDKs</h3><p>Working examples for the OpenAI SDK,
210
+ <div class="loop"><h3>Python SDKs</h3><p>Working examples for the OpenAI SDK,
194
211
  Anthropic SDK, and LangChain in <span class="mono">examples/</span> — each runnable
195
212
  offline, each showing the full learn-inject-reinforce cycle.</p></div>
196
213
  </div>
@@ -207,6 +224,8 @@ mem.report_outcome(inj, success=<span class="c-kw">True</span>)</pre>
207
224
  <tr><td class="mono">tracks per-model weaknesses</td><td class="zero">no</td><td class="zero">no</td><td><b>KNOWN ISSUES + trust</b></td></tr>
208
225
  <tr><td class="mono">works offline, no server</td><td>yes</td><td>depends</td><td><b>yes</b></td></tr>
209
226
  </table>
227
+ <p class="note">Not a vector DB, not chat-history RAG, not observability. One job:
228
+ failures become lessons that reach the next prompt.</p>
210
229
  </section>
211
230
 
212
231
  <section>
@@ -228,6 +247,11 @@ mem.report_outcome(inj, success=<span class="c-kw">True</span>)</pre>
228
247
  </div>
229
248
  </section>
230
249
 
250
+ <section style="padding-top:0">
251
+ <p class="note" style="text-align:center">Async API, log compaction and multi-agent memory are next —
252
+ <a href="https://github.com/Ma4etaSS/errlore">watch releases on GitHub</a> to follow.</p>
253
+ </section>
254
+
231
255
  <footer>
232
256
  <div>errlore · MIT · <a href="https://github.com/Ma4etaSS/errlore">github.com/Ma4etaSS/errlore</a></div>
233
257
  <div><a href="https://pypi.org/project/errlore/">pypi.org/project/errlore</a></div>
@@ -0,0 +1,31 @@
1
+ # errlore
2
+
3
+ > Memory for AI agents that learns from failures. Every resolved error
4
+ > becomes a lesson, injected into future prompts; per-model KNOWN ISSUES
5
+ > warnings; Bayesian per-model trust. Embedded, file-based, offline,
6
+ > no telemetry. MIT. Python 3.10+.
7
+
8
+ Positioning: a failure-memory / post-error learning layer — not a generic
9
+ "memory for AI" store. Unlike mem0/Zep/Letta (which store user facts and
10
+ session context), errlore stores the agent's own resolved failures and
11
+ closes the loop: it tracks whether an injected lesson actually helped
12
+ (reinforce/decay) and which model to trust per task domain.
13
+
14
+ Proof: paired A/B (deterministic validators, raw outputs committed):
15
+ repeated workspace-convention errors 46/48 -> 0/48 (100% reduction,
16
+ exact McNemar p=1.8e-09). Capability-gap errors (e.g. letter counting)
17
+ are honestly NOT improved. See the benchmark in the repository.
18
+
19
+ ## Links
20
+ - [Website](https://errlore.com)
21
+ - [Quickstart](https://errlore.com/#quickstart)
22
+ - [GitHub](https://github.com/Ma4etaSS/errlore)
23
+ - [PyPI](https://pypi.org/project/errlore/)
24
+ - [Changelog](https://github.com/Ma4etaSS/errlore/blob/main/CHANGELOG.md)
25
+ - [Error-reduction benchmark](https://github.com/Ma4etaSS/errlore/tree/main/benchmarks)
26
+ - [Open WebUI integration](https://github.com/Ma4etaSS/errlore/tree/main/integrations/openwebui)
27
+ - [Examples: OpenAI / Anthropic / LangChain / Claude Code](https://github.com/Ma4etaSS/errlore/tree/main/examples)
28
+
29
+ ## Install
30
+ pip install errlore # core, zero heavy deps
31
+ pip install errlore[embeddings] # + local semantic lesson retrieval
Binary file
@@ -1,6 +1,6 @@
1
1
  """errlore -- memory for AI agents that learns from failures."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.1.2"
4
4
 
5
5
  from errlore.facade import AgentMemory, Injection
6
6
  from errlore.lessons.store import LessonStore
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes