""" Scratchpad tools for Inara. A lightweight, persistent notepad stored at inara/SCRATCH.md. Nothing here is ever distilled or archived — it is intentionally transient. Good for: working notes mid-task, half-formed ideas, things too long for a chat response but not worth saving to memory or a journal entry. Operations: scratch_read — return the full contents (or a message if empty) scratch_write — replace the entire scratchpad scratch_append — add a new timestamped section at the bottom scratch_clear — erase everything """ import asyncio from datetime import datetime, timezone from pathlib import Path from google.genai import types from persona import persona_path def _scratch_path() -> Path: return persona_path() / "SCRATCH.md" def _now_label() -> str: return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") # --------------------------------------------------------------------------- # Sync implementations # --------------------------------------------------------------------------- def _scratch_read() -> str: p = _scratch_path() if not p.exists() or not p.read_text().strip(): return "Scratchpad is empty." return p.read_text() def _scratch_write(content: str) -> str: _scratch_path().write_text(content.rstrip() + "\n") return "Scratchpad updated." def _scratch_append(content: str, heading: str | None = None) -> str: p = _scratch_path() 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 section: {label}" def _scratch_clear() -> str: p = _scratch_path() p.write_text("") return "Scratchpad cleared." # --------------------------------------------------------------------------- # Async wrappers # --------------------------------------------------------------------------- async def scratch_read() -> str: return await asyncio.to_thread(_scratch_read) async def scratch_write(content: str) -> str: return await asyncio.to_thread(_scratch_write, content) async def scratch_append(content: str, heading: str | None = None) -> str: return await asyncio.to_thread(_scratch_append, content, heading) async def scratch_clear() -> str: return await asyncio.to_thread(_scratch_clear) DECLARATIONS = [ types.FunctionDeclaration( name="scratch_read", description=( "Read the full contents of the scratchpad. " "Use this to recall working notes, mid-task context, or anything previously jotted down. " "The scratchpad is transient — nothing here is distilled or archived." ), parameters=types.Schema(type=types.Type.OBJECT, properties={}), ), types.FunctionDeclaration( name="scratch_write", description=( "Replace the entire scratchpad with new content. " "Use this to set a clean working note, replacing whatever was there before. " "For adding without replacing, use scratch_append instead." ), parameters=types.Schema( type=types.Type.OBJECT, properties={ "content": types.Schema(type=types.Type.STRING, description="The new scratchpad content (markdown supported)"), }, required=["content"], ), ), types.FunctionDeclaration( name="scratch_append", description=( "Add a new section to the bottom of the scratchpad without replacing existing content. " "Each section gets a 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="scratch_clear", description="Erase everything in the scratchpad. Use when the working notes are no longer needed.", parameters=types.Schema(type=types.Type.OBJECT, properties={}), ), ]