Add a new business memory kind end-to-end. Pick the storage combination (Markdown / SQLite / LanceDB), pick the markdown strategy (daily-log / skill-named / single-file), then wire up the schema(s), repo(s), and writer(s).
84%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
Adding a new persisted business entity (Episode, Case, Skill, AtomicFact, Foresight, Profile, or something custom). Multiple storage layers may be involved; this skill walks the decision then the wiring.
A memory kind does not have to use all three layers. Pick by what the kind actually needs:
| Need | Markdown | SQLite | LanceDB |
|---|---|---|---|
| Human-readable / agent-editable source-of-truth text | ✅ | ||
| Structured state, ACID transactions, joins, predicates | ✅ | ||
| Vector / BM25 / hybrid retrieval | ✅ |
Common combinations seen in EverOS:
| Combo | Example | Rationale |
|---|---|---|
| md only | scratch notes / dump bins | text-of-truth, no index needed |
| md + lancedb | episode / memcell / case | text-of-truth + semantic retrieval |
| md + sqlite | profile / playbook / soul.md state | text-of-truth + structured state to query |
| md + sqlite + lancedb | full-blown business records | when you need both transactional state AND retrieval |
| sqlite only | audit log / task queue / LSN watermark | system state, never user-facing |
| lancedb only | rare; usually you still want md | derived embeddings without text-of-truth |
Rule of thumb: markdown is the truth; sqlite and lancedb are derived indexes that can be rebuilt from md. Drop md only when the kind has no human-readable form (pure system state).
Three strategies — declared in the EverOS Markdown First spec:
| Strategy | Filename | Mutation | Examples |
|---|---|---|---|
| Daily-log append | <prefix>-YYYY-MM-DD.md | append entries | memcell / episode / case / atomic_fact / foresight |
| Skill-named in-place | skill_<name>.md | overwrite the file | skills (procedural memory) |
| Single-file rewrite | user.md / agent.md / soul.md / behaviors.md / tools.md | overwrite the file | profiles / playbooks |
This skill currently has a complete recipe for daily-log append.
Skill-named and single-file recipes are sketched at the bottom — their
base writers (BaseSkillWriter / BaseProfileWriter) land later in the
project; until then build a thin wrapper over MarkdownWriter
directly.
infra/persistence/markdown/_mds/<name>.py"""Episode daily-log frontmatter."""
from __future__ import annotations
import datetime as _dt
from typing import ClassVar, Literal
from everos.infra.persistence.markdown import UserScopedFrontmatter
class UserEpisodeDailyFrontmatter(UserScopedFrontmatter):
"""``users/<u>/episodes/episode-<YYYY-MM-DD>.md``."""
ENTRY_ID_PREFIX: ClassVar[str] = "ep"
DIR_NAME: ClassVar[str] = "episodes"
FILE_PREFIX: ClassVar[str] = "episode"
type: Literal["user_episode_daily"] = "user_episode_daily"
date: _dt.date
entry_count: int = 0
last_appended_at: _dt.datetime | None = NoneFor agent-track kinds subclass AgentScopedFrontmatter instead. If
user-track and agent-track share a kind name (e.g. memcell), give
each a distinct ENTRY_ID_PREFIX (e.g. umc vs amc) so reverse
lookup is unambiguous.
mds/__init__.pyfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatterinfra/persistence/markdown/_writers/<name>.py"""Episode appender."""
from __future__ import annotations
from pathlib import Path
from everos.infra.persistence.markdown import MarkdownReader
from ..mds import UserEpisodeDailyFrontmatter
from .base import BaseDailyWriter
class UserEpisodeAppender(BaseDailyWriter):
schema = UserEpisodeDailyFrontmatter
# OPTIONAL: override the count strategy. Default is len(entries);
# override to trust the frontmatter field instead.
def _current_count(self, path: Path) -> int:
if not path.exists():
return 0
return MarkdownReader.read(path).frontmatter.get("entry_count", 0)writers/__init__.pyfrom .episode import UserEpisodeAppender as UserEpisodeAppenderfrom everos.infra.persistence.markdown._writers import UserEpisodeAppender
appender = UserEpisodeAppender(memory_root)
eid = appender.append("u_jason", "I went to the doctor today.")
# → users/u_jason/episodes/episode-<today>.md
# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)Skip this section if the kind doesn't need structured state beyond markdown.
infra/persistence/sqlite/_tables/<name>.pyfrom everos.infra.persistence.sqlite import BaseTable, Field
class EpisodeState(BaseTable, table=True):
__tablename__ = "episode_state" # type: ignore[assignment]
id: int | None = Field(default=None, primary_key=True)
entry_id: str = Field(index=True, unique=True)
cluster_id: str | None = Field(default=None, index=True)
status: str = Field(default="active")BaseTable already provides created_at / updated_at (auto-bumped).
tables/__init__.pyfrom .episode import EpisodeState as EpisodeStateinfra/persistence/sqlite/_repos/<name>.pyfrom everos.infra.persistence.sqlite._base.repository import RepoBase
from ..tables import EpisodeState
class EpisodeStateRepo(RepoBase[EpisodeState]):
"""Rows of ``episode_state``."""
model = EpisodeStateThe session factory comes in through RepoBase.__init__. Do not create
a module-level instance: make check-singletons rejects one, and the
repository would be bound to no database.
repos/__init__.py, repositories.py# repos/__init__.py
from .episode import EpisodeStateRepo as EpisodeStateRepoAdd a field to SqliteRepos (sqlite/repositories.py) and build it in
SqliteRepos.over(database); the bootstrap hands that bundle to whatever
needs the table.
Skip this section if the kind doesn't need vector / BM25 / hybrid retrieval.
infra/persistence/records/<name>.pyfrom typing import ClassVar
from everos.infra.persistence.records import BaseLanceTable, Vector
class EpisodeIndex(BaseLanceTable):
TABLE_NAME: ClassVar[str] = "episode_index"
PRIMARY_KEY: ClassVar[tuple[str, ...]] = ("app_id", "project_id", "id")
id: str
app_id: str = "default"
project_id: str = "default"
entry_id: str
text: str
tags: list[str]
vector: Vector(1024) | None = None # type: ignore[valid-type]TABLE_NAME / BM25_FIELDS / PRIMARY_KEY are the backend-neutral
declarations every index backend reads (IndexRecordMeta). PRIMARY_KEY
has no default: check_primary_keys refuses to start, and LanceDB
upsert refuses to write, a table that does not declare one. id is unique
only within (app_id, project_id), so every business table declares the
three-column key — LanceDB merges on it, Milvus keys the collection on a
composed __pk and rejects key components containing /. Vector(N) must
match your embedding dimension.
records/__init__.pyfrom .episode import EpisodeIndex as EpisodeIndexinfra/persistence/lancedb/_repos/<name>.pyfrom everos.infra.persistence.lancedb._base.repository import LanceRepoBase
from ...records.episode import EpisodeIndex
class _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):
schema = EpisodeIndexThe table comes from the LanceDbConnection passed as source=; no
module-level instance (make check-singletons rejects one).
upsert(records) merges on the declared key and is the only write; look a
row up with find_one_where(primary_key(app_id, project_id, row_id)).
index/repositories.py + each backendAdd the record to BUSINESS_SCHEMAS and a field to IndexRepositories
(infra/persistence/index/repositories.py), then build the repository in
LanceIndexBackend.__init__ (_EpisodeIndexRepo(source=connection)) and in
MilvusIndexBackend.__init__. Business code reaches it as
index.repositories.<field>, through the DerivedIndex the bootstrap
builds.
When the new memory kind needs:
BaseSkillWriter, or use MarkdownWriter.write_markdown directly
with a thin wrapper.user.md) — wait for
BaseProfileWriter, same fallback.These strategies do not use entry markers; their frontmatter schema
does not need ENTRY_ID_PREFIX (only id / type / schema_version plus
the scope mixin fields).
make lint — ruff + import-linter cleanmake test — existing manager / lifespan / writer tests still passTests by layer:
| Tests for | Location |
|---|---|
| Markdown frontmatter schema | tests/unit/test_infra/test_markdown/test_mds/ |
| Markdown business appender | tests/unit/test_infra/test_markdown/test_writers/ |
| SQLite RepoBase logic | tests/unit/test_core/test_persistence/test_sqlite/ |
| SQLite database object | tests/unit/test_infra/test_sqlite/ |
| LanceDB LanceRepoBase logic | tests/unit/test_core/test_persistence/test_lancedb/ |
| LanceDB connection object | tests/unit/test_infra/test_lancedb/ |
| Mistake | Symptom | Fix |
|---|---|---|
Forgot ENTRY_ID_PREFIX / DIR_NAME / FILE_PREFIX on a daily-log schema | BaseDailyWriter.__init__ raises TypeError | Add all three ClassVars |
Same ENTRY_ID_PREFIX on user + agent variants | MemoryLayout.locate_for_entry collision error | Use distinct prefixes (e.g. umc vs amc) |
| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each __init__.py |
Vector(N) mismatched with embedding dim | LanceDB raises on insert | Make N exactly match the model output |
Imported MemoryLayout from a writer (infra) | import-linter fails (infra → memory reverse dep) | Use MemoryRoot (in infra.persistence) and let the schema's ClassVars drive paths |
Hand-rolling datetime.now() instead of today_with_timezone() | Day-boundary drift across timezones | Always go through everos.core.utils.datetime |
__init__.py re-export rules: ../../rules/init-py-and-reexport.mdEverMind-AI/EverOS
489ac7e
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.