Files
EverOS/docs/prompt_slots.md
Elliot Chen 518b8eca85 chore: initialize EverOS 1.0.0
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.
2026-06-06 07:33:17 +08:00

3.5 KiB

PromptSlot

PromptSlot is the layer between the algorithm code (everalgo) and the prompts it sends to LLMs. Algorithm code receives a PromptSlot parameter; the project (EverOS) supplies defaults and lets operators override.

Status (2026-05-07): the YAML loader is implemented; the higher- level PromptSlot model + sandbox dry-run + three-layer overlay resolution arrive when the memory layer ships (see Stage 2).

Three-layer overlay

config/prompt_slots/<name>.yaml          (Layer 1: defaults shipped with the package)
       ↓
~/.everos/prompt_slots/<name>.yaml       (Layer 2: app-level override; per-deployment)
       ↓
runtime override                         (Layer 3: per-call override; e.g. "force model X")

Effective prompt = layer 3 wins → layer 2 → layer 1. Layer 1 is loaded eagerly at startup; layer 2 is loaded on first reference (lazy); layer 3 is supplied at the call site.

Loader

The category loader lives at src/everos/component/config/loader.py as YamlConfigLoader:

from pathlib import Path
from everos.component.config import YamlConfigLoader

loader = YamlConfigLoader(
    root=Path("src/everos/config"),
    categories={"prompt_slots": None},   # subdir == category name
)

# Reads <root>/prompt_slots/episode_extract.yaml → dict
slot = loader.find("prompt_slots", "episode_extract")

# Refresh after on-disk edits.
loader.refresh()                         # drop the entire cache
loader.refresh("prompt_slots")           # drop one category
loader.refresh("prompt_slots", "episode_extract")  # drop one entry

Top-level YAML is required to be a mapping; a list / scalar root raises TypeError to fail-fast (loud, not silent).

YAML format (proposed; subject to change)

# config/prompt_slots/episode_extract.yaml
template: |
  Extract a single episode from this conversation:
  {{ memcell.text }}

variables:
  memcell: input memcell

output_schema:
  type: object
  properties:
    summary: { type: string }
    participants: { type: array }

llm:
  model: gpt-4o-mini
  temperature: 0.3
  max_tokens: 2000

validation:
  test_cases:
    - input: { memcell: { text: "Hi" } }
      expected: { summary: "...", participants: [] }

When layer 2 supplies an override the loader will be re-pointed at ~/.everos/prompt_slots/; the runtime resolution logic (currently TBD) sandbox-runs the merged slot before returning it.

Why YAML (not TOML)

Two reasons:

  1. Multiline templates — TOML's basic-string grammar fights prompt content (no easy {{ jinja }} variables, awkward escaping). YAML's literal block scalar (|) preserves prompts as-is.
  2. Comment + reference ergonomics — operators frequently inherit slots, tweak a few keys, and leave inline notes. YAML is more forgiving for hand-editing.

The Pydantic Settings file (config/default.toml) stays TOML — it's machine-managed and type-validated; YAML's flexibility costs more than it pays for that case.

Why a separate loader (not Pydantic Settings)

Settings = one structured tree, validated at load time, tied to a single source of truth. PromptSlots = many separate templates discovered by name, layered per-deployment. They're different shapes; forcing one model on the other gets clunky.

See also