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>
This commit is contained in:
155
cortex/tools/agent_notes.py
Normal file
155
cortex/tools/agent_notes.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""
|
||||
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={}),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user