현재 Git 변경 사항과 최근 기록을 살펴본 뒤 Conventional Commits 형식으로 범위가 분명한 커밋을 만듭니다. 관련 파일만 선택해 스테이징하고 커밋 훅 오류에 대응하도록 안내합니다.
- 변경 사항을 검토하고 커밋하기
- 차이에서 관련 파일만 선택하기
- 규칙에 맞는 커밋 메시지 작성하기
새 업무 메모리 유형을 영구 저장하도록 개발 작업을 안내합니다. 필요한 Markdown, SQLite, LanceDB 조합과 Markdown 파일 방식을 정한 뒤 해당 계층의 스키마, 저장소, 작성기를 연결합니다.
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).
실행 점수 미검증
설치 방법 보기새 업무 데이터 유형을 영구 저장할 수 있도록 필요한 저장 계층과 구현 위치를 정하는 과정을 안내합니다. 사람이 읽을 수 있는 원본인 Markdown, 구조화 데이터를 담는 SQLite, 벡터 검색 등에 쓰이는 LanceDB를 필요에 맞게 조합합니다. Markdown을 사용한다면 날짜별 항목을 덧붙이는 방식, 이름이 정해진 파일을 덮어쓰는 방식, 단일 파일을 다시 쓰는 방식 중에서 선택합니다. 이어서 스키마와 쓰기 로직을 추가하고, 필요한 경우 테이블과 저장소도 구성합니다. 프로젝트의 다른 부분에서 사용할 수 있도록 새 구성 요소를 다시 내보내는 작업도 다룹니다.
Episode나 Case처럼 새 업무 기록을 영구 저장하려는 개발자에게 유용합니다. 검색, 구조화된 상태 관리, 사람이 편집할 수 있는 원본 가운데 무엇이 필요한지 정리하고 선택한 저장 계층을 일관되게 연결할 수 있습니다.
날짜별 로그 방식은 구체적인 절차가 제공됩니다. 이름 지정 파일 방식과 단일 파일 방식은 개요만 있으며, 기본 작성기가 마련되기 전까지는 MarkdownWriter를 감싸는 얇은 래퍼가 필요합니다. SQLite와 LanceDB는 필요한 경우에만 추가합니다.
이 소개문은 공개된 SKILL.md를 바탕으로 AI(OpenAI Codex)가 작성했습니다. 정확한 사양은 아래 원문을 확인하세요.
AI가 SKILL.md의 내용에서 주의가 필요한 작업과 도입 전제를 확인했습니다. 점수는 실제로 실행한 결과(실행 검증)만으로 매깁니다.
점수에는 넣지 않았습니다. 2026년 10월 29일 기준으로 AI(OpenAI Codex)가 판정했습니다. 참고용이므로 설치 전에 원문도 확인해 주세요.
설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
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.core.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 = None
For 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 UserEpisodeDailyFrontmatter
infra/persistence/markdown/writers/<name>.py"""Episode appender."""
from __future__ import annotations
from pathlib import Path
from everos.core.persistence 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 UserEpisodeAppender
from 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.core.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 EpisodeState
infra/persistence/sqlite/repos/<name>.pyfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from everos.core.persistence.sqlite import RepoBase
from ..sqlite_manager import get_session_factory
from ..tables import EpisodeState
class _EpisodeStateRepo(RepoBase[EpisodeState]):
model = EpisodeState
def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:
return get_session_factory()
episode_state_repo = _EpisodeStateRepo()
repos/__init__.pyfrom .episode import episode_state_repo as episode_state_repo
Skip this section if the kind doesn't need vector / BM25 / hybrid retrieval.
infra/persistence/records/<name>.pyfrom typing import ClassVar
from everos.core.persistence.lancedb 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 EpisodeIndex
infra/persistence/lancedb/repos/<name>.pyfrom lancedb import AsyncTable
from everos.core.persistence.lancedb import LanceRepoBase
from everos.infra.persistence.records import EpisodeIndex
from ..lancedb_manager import get_table
class _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):
schema = EpisodeIndex
async def _table_lookup(self) -> AsyncTable:
return await get_table(self.schema.TABLE_NAME, self.schema)
episode_index_repo = _EpisodeIndexRepo()
Business code writes through the everos.infra.persistence.index facade:
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)).
repos/__init__.pyfrom .episode import episode_index_repo as episode_index_repo
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 manager / lifespan | tests/unit/test_infra/test_sqlite/ |
| LanceDB LanceRepoBase logic | tests/unit/test_core/test_persistence/test_lancedb/ |
| LanceDB manager / lifespan | 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) |
Imported RepoBase from infra.persistence.sqlite | ImportError | Lives in core.persistence.sqlite (moved earlier) |
| 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 core) 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.component.utils.datetime |
__init__.py re-export rules: ../../rules/init-py-and-reexport.md아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
현재 Git 변경 사항과 최근 기록을 살펴본 뒤 Conventional Commits 형식으로 범위가 분명한 커밋을 만듭니다. 관련 파일만 선택해 스테이징하고 커밋 훅 오류에 대응하도록 안내합니다.
업데이트된 `main`에서 용도에 맞는 Git 브랜치를 만들고, 변경 유형에 따라 기능·수정·문서·CI·유지 관리·리팩터링 접두사를 선택합니다. 뒤에는 짧은 소문자 하이픈 이름을 붙입니다.
GitHub CLI `gh`를 사용해 현재 작업 브랜치에서 `main`을 대상으로 저장소 템플릿에 맞춘 풀 리퀘스트를 엽니다. 먼저 `make ci`를 실행하고, 통과하면 브랜치를 푸시한 뒤 변경 요약, 영역, 검증 결과, 체크리스트와 리뷰어 메모를 작성합니다.
everos의 새 버전을 PyPI에 배포하도록 버전과 변경 로그를 정리하고, 태그로 시작되는 자동 작업과 승인 절차를 안내합니다. 업로드 후에는 GitHub Release 초안을 검토하고 게시하는 단계까지 진행합니다.