Files
Cortex-Inara/cortex/tools/agent_notes.py
Scott Idem c02d2462b0 feat: agent notes, OpenRouter onboarding, usage tracking, per-role tools docs
Agent notes tool (cortex/tools/agent_notes.py):
- Private durable notepad for the orchestrator — not user-visible
- agent_notes_read/write/append/clear with 3 rolling backups
- Per-persona isolation via ContextVars; no TOOL_ROLES gating needed
- PROTOCOLS.md updated to make this a core proactive tool

OpenRouter guided onboarding:
- Setup Step 3 (/setup/model) — OpenRouter quick-connect with curated model list
- Amber banner in chat for users on server-default model
- Settings quick-link card (/settings/models OpenRouter section)
- POST /setup/model/skip for users who want to bypass Step 3
- Holly pre-configured: DeepSeek V4 Flash (OpenRouter) → Gemma Medium (local) → claude_cli

Usage tracking:
- cortex/routers/usage.py — GET /api/usage, /api/usage/summary, /api/usage/all (admin)

Documentation:
- HELP.md: Tools section rewritten — full tool table by category, per-role tool sets explained
- TOOLS.md: Agent Notes section added; count corrected to 44
- ARCH__SYSTEM.md, ARCH__BACKENDS.md, MASTER.md, CLAUDE.md, README.md updated
- TODO__Agents.md: onboarding task checked off with deviation notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 21:25:31 -04:00

156 lines
5.3 KiB
Python

"""
Agent private notes — AGENT_NOTES.md.
A persistent notepad only the orchestrator can write to. The file itself is
never exposed in the Files panel or loaded into user-facing context tiers.
Up to 3 rolling backups are kept automatically before each write so past
versions can be reviewed.
Use for: observations about the user's patterns, working hypotheses,
long-running goals, things to remember across sessions that shouldn't
be part of the distilled memory visible to the user.
"""
import asyncio
from datetime import datetime, timezone
from pathlib import Path
from google.genai import types
from persona import persona_path
_FILENAME = "AGENT_NOTES.md"
_N_BACKUPS = 3
def _notes_path() -> Path:
return persona_path() / _FILENAME
def _now_label() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
def _rotate(path: Path) -> None:
"""Rotate up to _N_BACKUPS rolling backups before a write."""
if not path.exists():
return
for i in range(_N_BACKUPS, 1, -1):
older = path.parent / f"{path.stem}.bak{i}.md"
newer = path.parent / f"{path.stem}.bak{i - 1}.md"
if newer.exists():
older.write_text(newer.read_text())
bak1 = path.parent / f"{path.stem}.bak1.md"
bak1.write_text(path.read_text())
# ── Sync implementations ────────────────────────────────────────────────────
def _agent_notes_read() -> str:
p = _notes_path()
if not p.exists() or not p.read_text().strip():
return "Agent notes are empty."
return p.read_text()
def _agent_notes_write(content: str) -> str:
p = _notes_path()
_rotate(p)
p.write_text(content.rstrip() + "\n")
return "Agent notes updated."
def _agent_notes_append(content: str, heading: str | None = None) -> str:
p = _notes_path()
_rotate(p)
existing = p.read_text() if p.exists() else ""
label = heading or _now_label()
section = f"\n## {label}\n\n{content.strip()}\n"
p.write_text(existing.rstrip() + "\n" + section)
return f"Appended to agent notes: {label}"
def _agent_notes_clear() -> str:
p = _notes_path()
_rotate(p)
p.write_text("")
return "Agent notes cleared."
# ── Async wrappers ───────────────────────────────────────────────────────────
async def agent_notes_read() -> str:
return await asyncio.to_thread(_agent_notes_read)
async def agent_notes_write(content: str) -> str:
return await asyncio.to_thread(_agent_notes_write, content)
async def agent_notes_append(content: str, heading: str | None = None) -> str:
return await asyncio.to_thread(_agent_notes_append, content, heading)
async def agent_notes_clear() -> str:
return await asyncio.to_thread(_agent_notes_clear)
# ── Gemini FunctionDeclarations ──────────────────────────────────────────────
DECLARATIONS = [
types.FunctionDeclaration(
name="agent_notes_read",
description=(
"Read your private agent notes — a persistent notepad only you can write to. "
"Use this to recall observations, working hypotheses, long-running goals, or "
"anything you want to remember across sessions without surfacing it to the user. "
"This file is never shown in the user's Files panel."
),
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
),
types.FunctionDeclaration(
name="agent_notes_write",
description=(
"Replace your private agent notes with new content. "
"A backup is saved automatically before writing. "
"Use agent_notes_append to add without replacing."
),
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"content": types.Schema(
type=types.Type.STRING,
description="The new notes content (markdown supported).",
),
},
required=["content"],
),
),
types.FunctionDeclaration(
name="agent_notes_append",
description=(
"Add a new section to your private agent notes without replacing existing content. "
"A backup is saved automatically before writing. "
"Each section gets a UTC timestamp heading unless you supply one."
),
parameters=types.Schema(
type=types.Type.OBJECT,
properties={
"content": types.Schema(
type=types.Type.STRING,
description="The content to append (markdown supported).",
),
"heading": types.Schema(
type=types.Type.STRING,
description="Optional section heading. Defaults to current UTC timestamp.",
),
},
required=["content"],
),
),
types.FunctionDeclaration(
name="agent_notes_clear",
description=(
"Erase all private agent notes. A backup is saved automatically before clearing."
),
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
),
]