md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
"""OME event base class + built-in tick events.
|
|
|
|
All business events should subclass BaseEvent. OME emits three built-in
|
|
ticks for engine-driven triggers (Cron / Idle / Manual).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from datetime import datetime
|
|
from functools import cache
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
from everos.component.utils.datetime import get_utc_now
|
|
|
|
|
|
class BaseEvent(BaseModel):
|
|
"""Base for all events flowing through OME.
|
|
|
|
Subclasses must be Pydantic v2 models (immutable) so `model_dump_json` /
|
|
`model_validate_json` work for crash-recovery payload persistence.
|
|
"""
|
|
|
|
model_config = ConfigDict(frozen=True, extra="forbid")
|
|
|
|
event_id: str = Field(default_factory=lambda: uuid4().hex)
|
|
ts: datetime = Field(default_factory=get_utc_now)
|
|
|
|
@classmethod
|
|
def topic(cls) -> str:
|
|
"""Stable cross-process identifier of this event class.
|
|
|
|
Returns ``"<module>:<class>"`` (colon-separated, mirroring the
|
|
Python event-sourcing community convention). Used by OME to
|
|
persist event identity into RunRecord.event_topic and to re-import
|
|
the class during crash recovery via ``resolve_topic``.
|
|
"""
|
|
return f"{cls.__module__}:{cls.__name__}"
|
|
|
|
|
|
@cache
|
|
def resolve_topic(topic: str) -> type[BaseEvent]:
|
|
"""Inverse of ``BaseEvent.topic()``; imports and returns the class.
|
|
|
|
Cached because crash recovery may resolve the same topic many times in
|
|
a tight loop, and ``importlib.import_module`` is non-trivial.
|
|
"""
|
|
module_name, sep, cls_name = topic.partition(":")
|
|
if not sep or not cls_name:
|
|
raise ValueError(f"invalid event topic: {topic!r}")
|
|
mod: Any = importlib.import_module(module_name)
|
|
cls = getattr(mod, cls_name, None)
|
|
if not (isinstance(cls, type) and issubclass(cls, BaseEvent)):
|
|
raise TypeError(f"topic {topic!r} did not resolve to a BaseEvent subclass")
|
|
return cls
|
|
|
|
|
|
class CronTick(BaseEvent):
|
|
"""Engine-emitted event for a strategy with `trigger=Cron(...)`."""
|
|
|
|
strategy_name: str
|
|
|
|
|
|
class IdleTick(BaseEvent):
|
|
"""Engine-emitted event for a strategy with `trigger=Idle(...)`."""
|
|
|
|
strategy_name: str
|
|
bucket_key: str
|
|
idle_seconds: int
|
|
|
|
|
|
class ManualTick(BaseEvent):
|
|
"""Engine-emitted event for `engine.trigger_manual(name, event=None)`."""
|
|
|
|
strategy_name: str
|