JSONL-backed store for pipeline lessons.
| 348 | |
| 349 | |
| 350 | class EvolutionStore: |
| 351 | """JSONL-backed store for pipeline lessons.""" |
| 352 | |
| 353 | def __init__(self, store_dir: Path) -> None: |
| 354 | self._dir = store_dir |
| 355 | self._dir.mkdir(parents=True, exist_ok=True) |
| 356 | self._lessons_path = self._dir / "lessons.jsonl" |
| 357 | |
| 358 | @property |
| 359 | def lessons_path(self) -> Path: |
| 360 | return self._lessons_path |
| 361 | |
| 362 | def append(self, lesson: LessonEntry) -> None: |
| 363 | """Append a single lesson to the store.""" |
| 364 | with self._lessons_path.open("a", encoding="utf-8") as f: |
| 365 | f.write(json.dumps(lesson.to_dict(), ensure_ascii=False) + "\n") |
| 366 | |
| 367 | def append_many(self, lessons: list[LessonEntry]) -> None: |
| 368 | """Append multiple lessons atomically.""" |
| 369 | if not lessons: |
| 370 | return |
| 371 | with self._lessons_path.open("a", encoding="utf-8") as f: |
| 372 | for lesson in lessons: |
| 373 | f.write(json.dumps(lesson.to_dict(), ensure_ascii=False) + "\n") |
| 374 | logger.info("Appended %d lessons to evolution store", len(lessons)) |
| 375 | |
| 376 | def load_all(self) -> list[LessonEntry]: |
| 377 | """Load all lessons from disk.""" |
| 378 | if not self._lessons_path.exists(): |
| 379 | return [] |
| 380 | lessons: list[LessonEntry] = [] |
| 381 | for line in self._lessons_path.read_text(encoding="utf-8").splitlines(): |
| 382 | line = line.strip() |
| 383 | if not line: |
| 384 | continue |
| 385 | try: |
| 386 | data = json.loads(line) |
| 387 | lessons.append(LessonEntry.from_dict(data)) |
| 388 | except (json.JSONDecodeError, TypeError): |
| 389 | continue |
| 390 | return lessons |
| 391 | |
| 392 | def query_for_stage( |
| 393 | self, stage_name: str, *, max_lessons: int = 5 |
| 394 | ) -> list[LessonEntry]: |
| 395 | """Return the most relevant lessons for a stage, weighted by recency. |
| 396 | |
| 397 | Includes lessons that directly match the stage, plus high-severity |
| 398 | lessons from related stages. |
| 399 | """ |
| 400 | all_lessons = self.load_all() |
| 401 | scored: list[tuple[float, LessonEntry]] = [] |
| 402 | for lesson in all_lessons: |
| 403 | weight = _time_weight(lesson.timestamp) |
| 404 | if weight <= 0.0: |
| 405 | continue |
| 406 | # Boost direct stage matches |
| 407 | if lesson.stage_name == stage_name: |
no outgoing calls