import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { CtxVarsConfig } from "./config.ts"; import { VarStore } from "./store.ts"; function toolResultText(text: string) { return { content: [{ type: "text" as const, text }], details: {} as Record }; } export function registerTools(pi: ExtensionAPI, cfg: CtxVarsConfig, getStore: () => VarStore | null) { pi.registerTool({ name: "context_pin", label: "Context Pin", description: "Pin a variable (var_N) or a raw conversation entry (message/entry id) so its FULL content survives the next compaction. " + "decay=true (default): a future compaction may archive it again. decay=false: persistent — only an explicit context_unpin by you can release it. " + "Pins recorded during normal work take effect at the next compaction boundary.", promptSnippet: "Protect a variable or message so its full content survives compaction", promptGuidelines: [ "Use context_pin when information in context must survive future compactions verbatim.", "Use context_pin(var_N, decay=true) for short-term protection and decay=false for long-term requirements.", ], parameters: Type.Object({ target: Type.String({ description: "Variable ref like var_12, or a message/entry id" }), decay: Type.Optional(Type.Boolean({ description: "true = decayable pin (default), false = persistent pin" })), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); const target = String(params.target ?? ""); const decay = params.decay !== false; if (/^var_\d+$/.test(target)) { const v = store.getVarByRef(target); if (!v) return toolResultText(`Unknown variable: ${target}`); store.setPinned(v.id, decay ? 1 : 2); store.recordDecision(target, "var", "pin", decay); return toolResultText(`Pinned ${target} (decay=${decay}). Its full content will be kept verbatim at the next compaction.`); } store.recordDecision(target, "entry", "pin", decay); return toolResultText(`Pin recorded for entry ${target} (decay=${decay}). It takes effect at the next compaction.`); }, }); pi.registerTool({ name: "context_unpin", label: "Context Unpin", description: "Remove a pin from a variable (var_N) or a raw conversation entry. Only the main agent can unpin; the compaction agent never unpins. Takes effect at the next compaction boundary.", promptSnippet: "Release a pin so a variable can be archived again", parameters: Type.Object({ target: Type.String({ description: "Variable ref like var_12, or a message/entry id" }), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); const target = String(params.target ?? ""); if (/^var_\d+$/.test(target)) { const v = store.getVarByRef(target); if (!v) return toolResultText(`Unknown variable: ${target}`); store.setPinned(v.id, 0); store.recordDecision(target, "var", "unpin", true); return toolResultText(`Unpinned ${target}. It may be archived at the next compaction.`); } store.recordDecision(target, "entry", "unpin", true); return toolResultText(`Unpin recorded for entry ${target}.`); }, }); pi.registerTool({ name: "context_drop", label: "Context Drop", description: "Remove a variable's (var_N) or raw entry's representation from active context — no summary line will remain. The content stays fully recoverable in the variable store via context_query/context_read. Takes effect at the next compaction boundary.", promptSnippet: "Remove a variable's representation from context (content stays in the store)", promptGuidelines: ["Use context_drop for dead-end exploration, stale tool outputs, or content that does not deserve context tokens."], parameters: Type.Object({ target: Type.String({ description: "Variable ref like var_12, or a message/entry id" }), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); const target = String(params.target ?? ""); if (/^var_\d+$/.test(target)) { const v = store.getVarByRef(target); if (!v) return toolResultText(`Unknown variable: ${target}`); store.setDropped(v.id, 1, 0); store.recordDecision(target, "var", "drop", true); return toolResultText(`Dropped ${target} from active context. Content remains searchable in the store.`); } store.recordDecision(target, "entry", "drop", true); return toolResultText(`Drop recorded for entry ${target}. It takes effect at the next compaction.`); }, }); pi.registerTool({ name: "context_archive", label: "Context Archive", description: "Archive a variable (var_N) or raw entry: only its summary stays in context. Optionally provide the summary yourself; otherwise the compaction agent writes it. Takes effect at the next compaction boundary.", promptSnippet: "Reduce a variable's context representation to a summary", parameters: Type.Object({ target: Type.String({ description: "Variable ref like var_12, or a message/entry id" }), summary: Type.Optional(Type.String({ description: "Optional summary to use" })), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); const target = String(params.target ?? ""); const summary = typeof params.summary === "string" ? params.summary : undefined; if (/^var_\d+$/.test(target)) { const v = store.getVarByRef(target); if (!v) return toolResultText(`Unknown variable: ${target}`); if (summary) store.updateSummary(v.id, summary.slice(0, cfg.summaryMaxChars)); store.recordDecision(target, "var", "archive", true, summary); return toolResultText(`Archive recorded for ${target}.${summary ? " Summary updated." : ""}`); } store.recordDecision(target, "entry", "archive", true, summary); return toolResultText(`Archive recorded for entry ${target}. It takes effect at the next compaction.`); }, }); pi.registerTool({ name: "context_read", label: "Context Read", description: "Load the full content of a stored variable (var_N) into the current context. Reading does NOT pin: if the content must survive future compactions, call context_pin afterwards. " + "Prefer reading at the start of a subtask and keep content in context rather than reading repeatedly. " + "To find a variable first, use context_query (LIKE search over content/summary).", promptSnippet: "Load a stored variable's full content into context", promptGuidelines: ["Use context_read to recover details of archived or dropped content. Batch reads for one subtask in a single turn."], parameters: Type.Object({ var_id: Type.String({ description: "Variable ref like var_12" }), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); const v = store.getVarByRef(String(params.var_id ?? "")); if (!v) return toolResultText(`Unknown variable: ${params.var_id}. Use context_query first.`); const deps = store.depsOf(v.id); const header = `[var_${v.id}] kind=${v.kind} seq=${v.seq} size=${v.size_tokens}t pinned=${v.pinned} dropped=${v.dropped}${deps.length ? ` depends_on=${deps.map((d) => `var_${d}`).join(",")}` : ""}`; const summary = v.summary ? `\nsummary: ${v.summary}` : ""; return toolResultText(`${header}${summary}\n--- full content ---\n${v.content}`); }, }); pi.registerTool({ name: "context_query", label: "Context Query", description: "Run a read-only SQL query against the session variable store (SELECT/WITH only, single statement). " + "Tables: variables(id, entry_id, kind, content, summary, size_tokens, seq, created_at, pinned, dropped, in_context, rollup_id), " + "depends_on(var_id, dep_var_id), rollups(id, range, summary), decisions(target, target_type, action, decay, summary, applied). " + "This is the ONLY way to find stored variables: use LIKE for substring search, e.g. " + "SELECT id, kind, summary FROM variables WHERE content LIKE '%term%' OR summary LIKE '%term%' ORDER BY seq DESC LIMIT 10. " + "Results are returned as JSON, capped at 50 rows.", promptSnippet: "Query the variable store directly with SQL (also the way to search it)", promptGuidelines: [ "Use context_query to find or inspect stored variables: LIKE over content/summary for search, or precise queries by kind, seq range, or pin state.", "Query results do not load full content into context; follow up with context_read on the best match.", ], parameters: Type.Object({ sql: Type.String({ description: "SQL SELECT/WITH statement" }), }), async execute(_toolCallId, params, _signal, _onUpdate, _ctx) { const store = getStore(); if (!store) return toolResultText("Variable store unavailable."); try { const rows = store.query(String(params.sql ?? "")); const capped = rows.slice(0, cfg.queryRowCap); return toolResultText(`Query returned ${rows.length} row(s)${rows.length > cfg.queryRowCap ? ` (showing first ${cfg.queryRowCap})` : ""}:\n${JSON.stringify(capped, null, 1).slice(0, 6000)}`); } catch (err) { return toolResultText(`Query error: ${err instanceof Error ? err.message : String(err)}`); } }, }); }