Files
Cortex-Inara/cortex/context_loader.py
Scott Idem 49123cdd5c feat: per-role tool lists and system prompt overlays
Each role in model_registry.json can now carry two optional keys:
  system_append — injected into the system prompt at position 7 (after
                  memory, closest to the turn) for the active chat_role
  tools         — explicit tool allow-list; intersected with the user's
                  access-level filter so it can only restrict, never elevate

No changes needed for existing users — missing keys fall back to current
behavior. Add keys to a role to give it a specialty focus:

  "coder": {
    "primary": "claude_cli",
    "system_append": "You are in code-specialist mode...",
    "tools": ["web_search", "file_read", "shell_exec", "scratch_write"]
  }

Changes:
- model_registry.py: get_role_config() returns system_append + tools
- context_loader.py: role_append param appended as "--- Role Context ---"
- tools/__init__.py: get_tools_for_role/get_openai_tools_for_role accept
  optional tool_list and intersect with access-level filter
- orchestrator_engine.py: tool_list threaded through run/resume/checkpoint
- openai_orchestrator.py: tool_list threaded through run/resume/checkpoint;
  _build_client now calls get_openai_tools_for_role instead of returning
  unfiltered OPENAI_TOOL_SCHEMAS
- routers/orchestrator.py: pulls role_cfg for chat_role, passes both
  role_append and tool_list to context loader and engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 20:00:38 -04:00

119 lines
4.8 KiB
Python

from pathlib import Path
from persona import persona_path
_STATIC_DIR = Path(__file__).parent / "static"
# Core identity files — always loaded regardless of tier
_CORE = ["SOUL.md", "IDENTITY.md"]
# Lines of USER.md to include at Tier 1 (identity + what he cares about)
_TIER_1_USER_LINES = 30
def load_context(
tier: int = 2,
include_long: bool = True,
include_mid: bool = True,
include_short: bool = True,
role_append: str = "",
) -> str:
"""
Build the system-prompt context block for a given tier and memory toggles.
Load order (long → mid → short) keeps the most recent memory closest
to the conversation turn, which improves LLM recall.
Tier 1 — SOUL + IDENTITY + USER summary (~1,500 tokens)
Tier 2 — + USER full + PROTOCOLS + memory (~5,000 tokens)
Tier 3 — + last 2 raw session logs (~15,000 tokens)
Tier 4 — + last 7 raw session logs (~50,000 tokens)
role_append — optional text injected last (closest to the turn),
sourced from the active role's system_append config.
"""
inara_dir = persona_path()
parts = []
# ── 1. Core identity (always) ──────────────────────────────────
for filename in _CORE:
path = inara_dir / filename
if path.exists():
parts.append(f"--- {filename} ---\n{path.read_text()}")
# ── 2. USER.md ─────────────────────────────────────────────────
user_path = inara_dir / "USER.md"
if user_path.exists():
if tier == 1:
lines = user_path.read_text().splitlines()[:_TIER_1_USER_LINES]
content = "\n".join(lines)
else:
content = user_path.read_text()
parts.append(f"--- USER.md ---\n{content}")
if tier < 2:
return "\n\n".join(parts)
# ── 3. Protocols + Help reference (tier 2+) ───────────────────
proto_path = inara_dir / "PROTOCOLS.md"
if proto_path.exists():
parts.append(f"--- PROTOCOLS.md ---\n{proto_path.read_text()}")
# Global tool reference (same for all personas)
tools_path = _STATIC_DIR / "TOOLS.md"
if tools_path.exists():
parts.append(f"--- TOOLS.md ---\n{tools_path.read_text()}")
# Persona-specific help additions (optional)
help_path = inara_dir / "HELP.md"
if help_path.exists() and help_path.stat().st_size > 10:
parts.append(f"--- HELP.md ---\n{help_path.read_text()}")
# ── 4. Pending reminders (tier 2+) ────────────────────────────
# Written by cron jobs; cleared by Inara after acting on them.
reminders_path = inara_dir / "REMINDERS.md"
if reminders_path.exists() and reminders_path.stat().st_size > 10:
content = reminders_path.read_text().strip()
if content:
parts.append(f"--- REMINDERS.md ---\n{content}")
# ── 5. Tiered memory — long → mid → short ─────────────────────
# Short is last so it sits closest to the conversation turn.
if include_long:
# Fall back to legacy MEMORY.md during/after migration
long_path = inara_dir / "MEMORY_LONG.md"
if not long_path.exists():
long_path = inara_dir / "MEMORY.md"
if long_path.exists():
parts.append(f"--- {long_path.name} ---\n{long_path.read_text()}")
if include_mid:
mid_path = inara_dir / "MEMORY_MID.md"
if mid_path.exists() and mid_path.stat().st_size > 100:
content = mid_path.read_text()
if "Not yet populated" not in content:
parts.append(f"--- MEMORY_MID.md ---\n{content}")
if include_short:
short_path = inara_dir / "MEMORY_SHORT.md"
if short_path.exists() and short_path.stat().st_size > 100:
content = short_path.read_text()
if "Not yet populated" not in content:
parts.append(f"--- MEMORY_SHORT.md ---\n{content}")
# ── 6. Raw session logs (tier 3+) ──────────────────────────────
if tier >= 3:
sessions_dir = inara_dir / "sessions"
if sessions_dir.exists():
count = 2 if tier == 3 else 7
session_files = sorted(sessions_dir.glob("*.md"), reverse=True)[:count]
for sf in session_files:
parts.append(f"--- Session: {sf.name} ---\n{sf.read_text()}")
# ── 7. Role-specific instructions (always last — closest to the turn) ──
if role_append and role_append.strip():
parts.append(f"--- Role Context ---\n{role_append.strip()}")
return "\n\n".join(parts)