Revert "feat: add /speak command for vocalizing conclusions"

This reverts commit 8c24bceff3.
This commit is contained in:
2026-05-23 20:37:37 +02:00
parent bfee6e12b1
commit 7970189eb1
+48 -60
View File
@@ -1,16 +1,13 @@
/** /**
* Context Warn + Voice Extension * Context Warning Extension
* *
* 1. Emits a sound alert when the context window reaches 100k tokens. * Emits a generated sound alert when the context window reaches or exceeds
* 2. Registers a /speak command so the agent can vocalize conclusions. * 100 000 tokens. Also shows a UI notification.
* *
* Usage: * Usage:
* pi install git:git.enne2.net/enne2/context-warn@main * pi -e ./context-warn-extension/index.ts
* *
* Commands: * Or place in ~/.pi/agent/extensions/context-warn/ for auto-discovery.
* /context-warn-status — Show current context usage
* /context-warn-alert — Manually test the alert sound
* /speak <text> — Speak the text aloud (for the agent to use)
*/ */
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -23,22 +20,24 @@ import { fileURLToPath } from "node:url";
// Configuration // Configuration
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const WARNING_TOKEN_THRESHOLD = 100_000; const WARNING_TOKEN_THRESHOLD = 100_000; // alert at 100k tokens
const CHECK_INTERVAL_MS = 15_000; const CHECK_INTERVAL_MS = 15_000; // re-check every 15 s
const WARNING_COOLDOWN_MS = 60_000; const WARNING_COOLDOWN_MS = 60_000; // min seconds between alerts
// Resolve the directory where this extension lives so we can find alert.wav
const EXT_DIR = dirname(fileURLToPath(import.meta.url)); const EXT_DIR = dirname(fileURLToPath(import.meta.url));
const ALERT_WAV = join(EXT_DIR, "alert.wav"); const ALERT_WAV = join(EXT_DIR, "alert.wav");
// --------------------------------------------------------------------------- /**
// Sound: play bundled alert.wav * Play the bundled alert.wav sound via aplay.
// --------------------------------------------------------------------------- * No runtime generation needed — the WAV is committed alongside this file.
*/
function playAlert(): void { function playAlert(): void {
if (!existsSync(ALERT_WAV)) { if (!existsSync(ALERT_WAV)) {
console.error(`[context-warn] alert.wav not found at ${ALERT_WAV}`); console.error(`[context-warn] alert.wav not found at ${ALERT_WAV}`);
return; return;
} }
try { try {
execFileSync("aplay", ["-q", ALERT_WAV], { stdio: "ignore" }); execFileSync("aplay", ["-q", ALERT_WAV], { stdio: "ignore" });
} catch (err) { } catch (err) {
@@ -46,29 +45,6 @@ function playAlert(): void {
} }
} }
// ---------------------------------------------------------------------------
// 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 // Extension
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -77,19 +53,28 @@ export default function (pi: ExtensionAPI) {
let lastAlertTimestamp: number | null = null; let lastAlertTimestamp: number | null = null;
let alertedAtThreshold: boolean = false; let alertedAtThreshold: boolean = false;
const checkContext = (_event: unknown, ctx: any) => { /** Check context usage and alert if threshold reached. */
const checkContext = (_event: unknown, ctx: ReturnType<ExtensionAPI["on"]> extends (ev: string, h: (e: any, c: infer C) => any) ? C : never) => {
const usage = ctx.getContextUsage(); const usage = ctx.getContextUsage();
if (!usage || usage.tokens === null || usage.tokens === undefined) return; if (!usage || usage.tokens === null || usage.tokens === undefined) {
return;
}
const currentTokens = usage.tokens; const currentTokens = usage.tokens;
// Check if we crossed the threshold (went from below to >= threshold)
const crossedThreshold = !alertedAtThreshold && currentTokens >= WARNING_TOKEN_THRESHOLD; const crossedThreshold = !alertedAtThreshold && currentTokens >= WARNING_TOKEN_THRESHOLD;
alertedAtThreshold = alertedAtThreshold || crossedThreshold; alertedAtThreshold = alertedAtThreshold || crossedThreshold;
if (crossedThreshold) { if (crossedThreshold) {
// Check cooldown
const now = Date.now(); const now = Date.now();
if (lastAlertTimestamp && (now - lastAlertTimestamp) < WARNING_COOLDOWN_MS) return; if (lastAlertTimestamp && (now - lastAlertTimestamp) < WARNING_COOLDOWN_MS) {
return; // still in cooldown
}
lastAlertTimestamp = now; lastAlertTimestamp = now;
// Show UI notification
if (ctx.hasUI) { if (ctx.hasUI) {
ctx.ui.notify( ctx.ui.notify(
"⚠️ Context Warning", "⚠️ Context Warning",
@@ -99,14 +84,25 @@ export default function (pi: ExtensionAPI) {
ctx.ui.setStatus("context-warn", `${(currentTokens / 1000).toFixed(0)}k / ${usage.contextWindow ? `${usage.contextWindow / 1000}k` : "?"}`); ctx.ui.setStatus("context-warn", `${(currentTokens / 1000).toFixed(0)}k / ${usage.contextWindow ? `${usage.contextWindow / 1000}k` : "?"}`);
} }
// Play alert sound
playAlert(); playAlert();
console.log(`[context-warn] Context: ${currentTokens.toLocaleString()} tokens (threshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()})`); console.log(`[context-warn] Context: ${currentTokens.toLocaleString()} tokens (threshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()})`);
} }
}; };
pi.on("turn_end", (_event, ctx) => checkContext(_event, ctx)); // Listen to turn_end — fired after every LLM response, which is when
pi.on("agent_end", (_event, ctx) => checkContext(_event, ctx)); // we know the new context size.
pi.on("turn_end", (_event, ctx) => {
checkContext(_event, ctx);
});
// Also listen to agent_end in case compaction or other events happen
pi.on("agent_end", (_event, ctx) => {
checkContext(_event, ctx);
});
// Listen to session_start to reset state
pi.on("session_start", (_event, ctx) => { pi.on("session_start", (_event, ctx) => {
alertedAtThreshold = false; alertedAtThreshold = false;
lastAlertTimestamp = null; lastAlertTimestamp = null;
@@ -116,13 +112,16 @@ export default function (pi: ExtensionAPI) {
} }
}); });
// Listen to session_compact to reset alert state
pi.on("session_compact", (_event, ctx) => { pi.on("session_compact", (_event, ctx) => {
alertedAtThreshold = false; alertedAtThreshold = false;
lastAlertTimestamp = Date.now(); lastAlertTimestamp = Date.now();
if (ctx.hasUI) ctx.ui.setStatus("context-warn", "compact ✓"); if (ctx.hasUI) {
ctx.ui.setStatus("context-warn", "compact ✓");
}
}); });
// /context-warn-status // Register /context-warn-status command
pi.registerCommand("context-warn-status", { pi.registerCommand("context-warn-status", {
description: "Show current context token usage and warning status", description: "Show current context token usage and warning status",
handler: async (_args, ctx) => { handler: async (_args, ctx) => {
@@ -131,7 +130,9 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("Context", "No usage data available yet.", "info"); ctx.ui.notify("Context", "No usage data available yet.", "info");
return; return;
} }
const pct = usage.percent !== null && usage.percent !== undefined ? `${usage.percent.toFixed(1)}%` : "?"; const pct = usage.percent !== null && usage.percent !== undefined
? `${usage.percent.toFixed(1)}%`
: "?";
ctx.ui.notify( ctx.ui.notify(
"Context Status", "Context Status",
`${usage.tokens.toLocaleString()} tokens / ${usage.contextWindow?.toLocaleString()} (${pct})\nThreshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()}\nAlerted: ${alertedAtThreshold ? "yes" : "no"}`, `${usage.tokens.toLocaleString()} tokens / ${usage.contextWindow?.toLocaleString()} (${pct})\nThreshold: ${WARNING_TOKEN_THRESHOLD.toLocaleString()}\nAlerted: ${alertedAtThreshold ? "yes" : "no"}`,
@@ -140,7 +141,7 @@ export default function (pi: ExtensionAPI) {
}, },
}); });
// /context-warn-alert // Register /context-warn-alert command — manual test
pi.registerCommand("context-warn-alert", { pi.registerCommand("context-warn-alert", {
description: "Play the alert sound immediately (test)", description: "Play the alert sound immediately (test)",
handler: async (_args, ctx) => { handler: async (_args, ctx) => {
@@ -148,17 +149,4 @@ export default function (pi: ExtensionAPI) {
ctx.ui.notify("🔔 Alert", "Sound played.", "info"); ctx.ui.notify("🔔 Alert", "Sound played.", "info");
}, },
}); });
// /speak <text> — 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 <text>", "info");
return;
}
speak(text.trim());
ctx.ui.notify("🔊 Speaking", `"${text.trim().substring(0, 60)}${text.trim().length > 60 ? "..." : ""}"`, "info");
},
});
} }