/** * Context Warn + Voice Extension * * 1. Emits a sound alert when the context window reaches 100k tokens. * 2. Registers a /speak command so the agent can vocalize conclusions. * * Usage: * pi install git:git.enne2.net/enne2/context-warn@main * * Commands: * /context-warn-status — Show current context usage * /context-warn-alert — Manually test the alert sound * /speak — Speak the text aloud (for the agent to use) */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; // --------------------------------------------------------------------------- // Configuration // --------------------------------------------------------------------------- const WARNING_TOKEN_THRESHOLD = 100_000; const CHECK_INTERVAL_MS = 15_000; const WARNING_COOLDOWN_MS = 60_000; const EXT_DIR = dirname(fileURLToPath(import.meta.url)); const ALERT_WAV = join(EXT_DIR, "alert.wav"); // --------------------------------------------------------------------------- // Sound: play bundled alert.wav // --------------------------------------------------------------------------- function playAlert(): void { if (!existsSync(ALERT_WAV)) { console.error(`[context-warn] alert.wav not found at ${ALERT_WAV}`); return; } try { execFileSync("aplay", ["-q", ALERT_WAV], { stdio: "ignore" }); } catch (err) { console.error(`[context-warn] Audio play failed: ${err instanceof Error ? err.message : String(err)}`); } } // --------------------------------------------------------------------------- // Voice: speak text aloud via test_tts.py (blocking, one at a time) // --------------------------------------------------------------------------- const TTS_SCRIPT = "/home/enne2/.pi/agent/test_tts.py"; /** * Speak text aloud using the local TTS script. * This is a BLOCKING call — must wait for completion before speaking again. */ function speak(text: string): void { if (!existsSync(TTS_SCRIPT)) { console.error(`[voice] TTS script not found at ${TTS_SCRIPT}`); return; } try { // execFileSync is synchronous — waits for completion execFileSync("python3", [TTS_SCRIPT, text], { stdio: "ignore" }); } catch (err) { console.error(`[voice] TTS failed: ${err instanceof Error ? err.message : String(err)}`); } } // --------------------------------------------------------------------------- // Extension // --------------------------------------------------------------------------- export default function (pi: ExtensionAPI) { let lastAlertTimestamp: number | null = null; let alertedAtThreshold: boolean = false; const checkContext = (_event: unknown, ctx: any) => { const usage = ctx.getContextUsage(); if (!usage || usage.tokens === null || usage.tokens === undefined) return; const currentTokens = usage.tokens; const crossedThreshold = !alertedAtThreshold && currentTokens >= WARNING_TOKEN_THRESHOLD; alertedAtThreshold = alertedAtThreshold || crossedThreshold; if (crossedThreshold) { const now = Date.now(); if (lastAlertTimestamp && (now - lastAlertTimestamp) < WARNING_COOLDOWN_MS) return; lastAlertTimestamp = now; if (ctx.hasUI) { ctx.ui.notify( "⚠️ Context Warning", `Context reached ${currentTokens.toLocaleString()} tokens (>${WARNING_TOKEN_THRESHOLD.toLocaleString()}). Consider compacting.`, "warning" ); ctx.ui.setStatus("context-warn", `${(currentTokens / 1000).toFixed(0)}k / ${usage.contextWindow ? `${usage.contextWindow / 1000}k` : "?"}`); } playAlert(); console.log(`[context-warn] Context: ${currentTokens.toLocaleString()} tokens (threshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()})`); } }; pi.on("turn_end", (_event, ctx) => checkContext(_event, ctx)); pi.on("agent_end", (_event, ctx) => checkContext(_event, ctx)); pi.on("session_start", (_event, ctx) => { alertedAtThreshold = false; lastAlertTimestamp = null; if (ctx.hasUI) { ctx.ui.notify("🔊 Context monitor active", `Alerts at ≥ ${WARNING_TOKEN_THRESHOLD.toLocaleString()} tokens`, "info"); ctx.ui.setStatus("context-warn", "monitoring…"); } }); pi.on("session_compact", (_event, ctx) => { alertedAtThreshold = false; lastAlertTimestamp = Date.now(); if (ctx.hasUI) ctx.ui.setStatus("context-warn", "compact ✓"); }); // /context-warn-status pi.registerCommand("context-warn-status", { description: "Show current context token usage and warning status", handler: async (_args, ctx) => { const usage = ctx.getContextUsage(); if (!usage || usage.tokens === null || usage.tokens === undefined) { ctx.ui.notify("Context", "No usage data available yet.", "info"); return; } const pct = usage.percent !== null && usage.percent !== undefined ? `${usage.percent.toFixed(1)}%` : "?"; ctx.ui.notify( "Context Status", `${usage.tokens.toLocaleString()} tokens / ${usage.contextWindow?.toLocaleString()} (${pct})\nThreshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()}\nAlerted: ${alertedAtThreshold ? "yes" : "no"}`, "info" ); }, }); // /context-warn-alert pi.registerCommand("context-warn-alert", { description: "Play the alert sound immediately (test)", handler: async (_args, ctx) => { playAlert(); ctx.ui.notify("🔔 Alert", "Sound played.", "info"); }, }); // /speak — voice the agent's conclusion pi.registerCommand("speak", { description: "Speak the provided text aloud via TTS (for vocalizing conclusions)", handler: async ([text], ctx) => { if (!text || typeof text !== "string" || text.trim().length === 0) { ctx.ui.notify("Speak", "Usage: /speak ", "info"); return; } speak(text.trim()); ctx.ui.notify("🔊 Speaking", `"${text.trim().substring(0, 60)}${text.trim().length > 60 ? "..." : ""}"`, "info"); }, }); }