Files
2026-08-07 18:15:36 -04:00

193 lines
6.8 KiB
Python

"""Read from and write to the Metamate personal brain.
The brain is the long-term memory: `briefing.md` carries active work and pinned
reminders, `preferences.md` how Adolfo likes to be worked with, `profile.md` who
he is, and `projects/` a directory per workstream. It lives remotely and is
reached through `meta agents.memory`, so everything here shells out.
Two directions:
- **In.** The three context files go into the system prompt at startup, so the
agent knows what's active without being told. Project directory names also
become vocabulary, since "CIP-Unified-Cooldown" and "pSMSL" are exactly the
words a recogniser mangles.
- **Out.** Durable things learned in conversation are appended back, following
the discipline the brain's own README sets out: lasting preferences to
`preferences.md`, techniques and lessons to `notes.md`.
`briefing.md` is deliberately *not* written to automatically. A daily cron owns
that file, and an agent appending to it unprompted would fight the cron and
corrupt the one file everything else reads first.
"""
import json
import subprocess
from pathlib import Path
from loguru import logger
META = "meta"
CONTEXT_FILES = ("briefing.md", "preferences.md", "profile.md")
# Writes go only to files a human owns, never to cron-managed ones.
WRITABLE = {
"preference": "preferences.md",
"note": "notes.md",
}
# Learned entries get their own section so they never land inside a hand-written
# one, and so it stays obvious which lines the agent added.
LEARNED_HEADING = "## Learned in voice sessions"
_READ_TIMEOUT = 25.0
_WRITE_TIMEOUT = 25.0
# Used only when the brain can't be reached, so a flight or a VPN drop doesn't
# cost the agent all of its context.
CACHE = Path.home() / ".cache" / "voice-agent" / "brain-context.json"
def _run(args: list[str], timeout: float) -> tuple[bool, str]:
try:
result = subprocess.run(
[META, "agents.memory", *args],
capture_output=True,
text=True,
timeout=timeout,
)
except (OSError, subprocess.SubprocessError) as e:
return False, str(e)
if result.returncode != 0:
return False, (result.stderr.strip() or result.stdout.strip())[:200]
return True, result.stdout
class Brain:
"""The personal brain, or a graceful no-op when it can't be reached."""
def __init__(self):
self._context: dict[str, str] = {}
self._projects: list[str] = []
self.reachable = False
def load(self) -> bool:
"""Fetch context and project names. Falls back to cache when offline."""
ok, out = _run(
["read-batch", f"--paths={','.join(CONTEXT_FILES)}", "--output=json"],
_READ_TIMEOUT,
)
if ok:
try:
payload = json.loads(out)
except json.JSONDecodeError:
ok = False
else:
self._context = {
name: entry.get("content", "")
for name, entry in payload.items()
if isinstance(entry, dict) and entry.get("content")
}
self.reachable = bool(self._context)
if self.reachable:
self._projects = self._list_projects()
self._save_cache()
logger.info(
f"Brain: loaded {len(self._context)} context files, "
f"{len(self._projects)} projects"
)
return True
if self._load_cache():
logger.warning(f"Brain unreachable ({out[:80]}); using cached context.")
return True
logger.warning(f"Brain unavailable: {out[:120]}")
return False
def _list_projects(self) -> list[str]:
ok, out = _run(["list", "--path=projects", "-l", "200"], _READ_TIMEOUT)
if not ok:
return []
names = []
for line in out.splitlines():
parts = line.split()
# Rows look like "NAME dir -"; skip the header and rules.
if len(parts) >= 2 and parts[1] == "dir":
names.append(parts[0])
return names
def _save_cache(self):
try:
CACHE.parent.mkdir(parents=True, exist_ok=True)
CACHE.write_text(
json.dumps({"context": self._context, "projects": self._projects})
)
except OSError:
pass
def _load_cache(self) -> bool:
try:
payload = json.loads(CACHE.read_text())
except (OSError, json.JSONDecodeError):
return False
self._context = payload.get("context", {})
self._projects = payload.get("projects", [])
return bool(self._context)
@property
def projects(self) -> list[str]:
return self._projects
def prompt_block(self) -> str:
"""The context files, framed as reference rather than as instructions.
Framing matters: briefing.md is dense markdown with links and bold, and
without a clear label the agent starts answering in the same register —
which is wrong out loud.
"""
if not self._context:
return ""
sections = [
f"### {name}\n{text.strip()}"
for name, text in self._context.items()
if text.strip()
]
if not sections:
return ""
return (
"Below is your memory of Adolfo's work, from his personal brain. "
"Treat it as reference you already know, not as something to read "
"back. It is written notes — never mirror their formatting when you "
"speak.\n\n" + "\n\n".join(sections)
)
def append(self, kind: str, content: str) -> tuple[bool, str]:
"""Append a line to one of the writable brain files.
Args:
kind: A key of ``WRITABLE`` — "preference" or "note".
content: One line, already phrased as a durable statement.
"""
path = WRITABLE.get(kind)
if not path:
return False, f"nothing writable for {kind!r}; expected {sorted(WRITABLE)}"
if not content.strip():
return False, "refusing to write empty content"
# Append lands at the end of the file, which would tuck the new line
# inside whatever the last section happens to be. These are curated
# files, so learned entries get their own heading the first time.
body = content.strip()
ok, existing = _run(["read", f"--path=/{path}"], _READ_TIMEOUT)
if ok and LEARNED_HEADING not in existing:
body = f"\n{LEARNED_HEADING}\n{body}"
ok, out = _run(
["append", f"--path={path}", f"--content={body}"],
_WRITE_TIMEOUT,
)
if ok:
logger.info(f"Brain: appended a {kind} to {path}")
return True, path
return False, out