@jovan158/vantage
Version:
See and control what your coding agent does
92 lines (91 loc) • 3.6 kB
JavaScript
// Assisted memory harvest (⑤, closing the loop).
//
// Deliberately NOT an automatic LLM distillation at session end. Two reasons:
// an LLM call after every session would silently burn quota — the very problem
// ① exists to prevent — and a wrong auto-written entry poisons every later
// session, because memory is injected into the agent's context. The concept
// said it too: always confirmed, never silently written.
//
// So this prepares the material and the exact command; a human decides what
// becomes memory. The best no-LLM summary we have is the agent's own closing
// reply, which usually states what it did.
import { summarize } from "./replay.js";
import { summarizeActions, formatActionSummary } from "./policy.js";
import { formatCost } from "./pricing.js";
const C = {
dim: "\x1b[2m",
reset: "\x1b[0m",
bold: "\x1b[1m",
cyan: "\x1b[36m",
};
function noColor() {
return new Proxy({}, { get: () => "" });
}
export function collectHarvest(sessionId, events, changedFiles = []) {
const s = summarize(sessionId, events);
const tools = [];
let closingReply = null;
for (const e of events) {
if (e.type !== "usage" || e.background)
continue;
if (e.tools)
tools.push(...e.tools);
// Keep the last non-empty reply that reads like prose rather than the
// agent's internal JSON state blob.
if (e.text && !e.text.trimStart().startsWith("{"))
closingReply = e.text;
}
return {
sessionId,
turns: s.turns,
requests: s.requests,
costUsd: s.costUsd,
unpriced: s.unpriced,
closingReply,
actions: formatActionSummary(summarizeActions(tools)),
files: changedFiles,
};
}
// Shell-safe double-quoted string for the suggested command.
function shellQuote(s) {
return `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
}
export function renderHarvest(h, color = true) {
const c = color ? C : noColor();
const lines = [];
lines.push(`${c.bold}harvest${c.reset} ${c.dim}· session ${h.sessionId}${c.reset}`);
lines.push(`${c.dim}${h.turns} turn(s) · ${formatCost(h.costUsd, h.unpriced, h.requests)}${h.actions ? ` · ${h.actions}` : ""}${c.reset}`);
lines.push("");
if (h.closingReply) {
lines.push(` ${c.cyan}what the agent said it did${c.reset}`);
lines.push(` ${h.closingReply}`);
lines.push("");
}
if (h.files.length) {
lines.push(` ${c.cyan}files changed${c.reset}`);
for (const f of h.files.slice(0, 15))
lines.push(` ${f}`);
if (h.files.length > 15)
lines.push(` ${c.dim}… and ${h.files.length - 15} more${c.reset}`);
lines.push("");
}
if (!h.closingReply && h.files.length === 0) {
lines.push(` ${c.dim}nothing substantial recorded in this session${c.reset}`);
lines.push("");
}
lines.push(`${c.dim}Nothing is written automatically. Record what is worth keeping:${c.reset}`);
const seed = h.closingReply ? shellQuote(h.closingReply) : '"…"';
lines.push(` vantage memory add decisions ${seed}`);
lines.push(` ${c.dim}(categories: decisions · architecture · conventions · glossary)${c.reset}`);
return lines.join("\n");
}
// Did this session do enough to be worth a memory prompt at the end?
export function worthHarvesting(events, changedFiles = 0) {
if (changedFiles > 0)
return true;
for (const e of events) {
if (e.type === "usage" && e.tools?.length)
return true;
}
return false;
}