UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

171 lines 6.61 kB
/** * Per-agent FIFO ring of over-budget command outputs, addressable by an * auto-assigned `<firstWord>-<N>` id (e.g. `curl-1`, `ns-lynx-2`) via * `ns-more`. Cleared — entries and id counters — on session compaction. */ import stringArgv from "string-argv"; import * as utilities from "../utils/utilities.js"; import { moreCmd } from "./commandDefs.js"; const BUFFER_CAPACITY = 25; const SOURCE_DISPLAY_MAX = 80; function breakContentIntoPages(content, tokensPerPage) { const totalTokens = utilities.getTokenCount(content); if (totalTokens <= tokensPerPage) { return [content]; } const charactersPerToken = content.length / totalTokens; const charactersPerPage = Math.ceil(tokensPerPage * charactersPerToken); const pages = []; let startIndex = 0; while (startIndex < content.length) { const endIndex = Math.min(startIndex + charactersPerPage, content.length); pages.push(content.substring(startIndex, endIndex)); startIndex = endIndex; } return pages; } function truncateLabel(label) { return label.length > SOURCE_DISPLAY_MAX ? label.slice(0, SOURCE_DISPLAY_MAX - 3) + "..." : label; } /** Reduce the first word to a shell-safe stem so the id round-trips through * expandShellArgs unchanged (`~`, `$VAR`, quotes get neutralized at * allocation; the footer-displayed id is what the LLM should type back). */ function sanitizeStem(s) { return s .toLowerCase() .replace(/[^a-z0-9-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); } export function createPagedOutputBuffer({ globalConfig }) { // Newest at index 0 so most-recent lookup is a forward scan. const entries = []; // Per-firstWord monotonic counter. Reset on clear() — ids only make sense // within the lifetime of a single uncompacted buffer. const counters = new Map(); function headerFor(entry, pageNum) { return `Source: ${truncateLabel(entry.label)} (page ${pageNum} of ${entry.pages.length})`; } function footerFor(entry, pageNum) { const totalPages = entry.pages.length; if (pageNum >= totalPages) return "No more pages."; return `Use \`ns-more ${entry.id} --page=${pageNum + 1}\` for page ${pageNum + 1} of ${totalPages}.`; } /** * `label` should be the raw command the agent issued. The first word becomes * the id prefix. Returns page 1 already framed (or the raw content when it * fits in one page and isn't stored). */ function setContent(label, content) { const pages = breakContentIntoPages(content, globalConfig().shellCommand.outputTokenMax); if (pages.length <= 1) return content; const stem = sanitizeStem(label.trim().split(/\s+/)[0] ?? "") || "cmd"; const nextN = (counters.get(stem) ?? 0) + 1; counters.set(stem, nextN); const entry = { id: `${stem}-${nextN}`, firstWord: stem, label, pages, }; entries.unshift(entry); if (entries.length > BUFFER_CAPACITY) { entries.length = BUFFER_CAPACITY; } // Page 1 is the inline response to the command the agent just ran — no // header needed (they know what command produced this). ns-more paging // adds the header so re-fetched pages identify themselves. return `${pages[0]}\n\n${footerFor(entry, 1)}`; } /** Resolve `<stem>` (most recent) or `<stem>-<N>` (exact id). */ function findByKey(key) { const k = key.toLowerCase(); if (/^.+-\d+$/.test(k)) { return entries.find((e) => e.id === k) ?? null; } // Bare stem — return most recent (entries are newest-first). return entries.find((e) => e.firstWord === k) ?? null; } function helpText() { return [ "ns-more — page through over-budget command output.", "", "Usage:", " ns-more Show this help", " ns-more <id> --page=<N> Specific page", "", "Each paged response footer shows the entry's id and the next-page invocation.", `Buffer holds the last ${BUFFER_CAPACITY} over-budget outputs (FIFO); cleared on compaction.`, ].join("\n"); } function handleCommand(cmdArgs) { const argv = stringArgv(cmdArgs); if (argv.length === 0) return helpText(); // Reject leading flag: an id is always required so the LLM is forced to // say which buffer it's paging. const first = argv[0]; if (first.startsWith("--")) { return `Specify an id first: \`ns-more <id> ${first}\`. See \`ns-more\` for help.`; } if (argv.length > 2) { return `Too many arguments. Usage: \`ns-more <id> [--page=<N>]\`.`; } const entry = findByKey(first); if (!entry) { return `No buffered output matching "${first}".`; } // Default page 2 — the agent already saw page 1 from the original command. // Accept either the advertised `--page=N` flag or a bare value (number / // first / last) for ergonomics. let pageNum; const pageArg = argv[1]; if (pageArg === undefined) { pageNum = 2; } else { const value = /^--page=(.+)$/.exec(pageArg)?.[1] ?? pageArg; if (/^first$/i.test(value)) { pageNum = 1; } else if (/^last$/i.test(value)) { pageNum = entry.pages.length; } else if (/^-?\d+$/.test(value)) { pageNum = parseInt(value, 10); } else { return `Expected --page=<N>. Got: "${pageArg}".`; } } // Clamp silently — the footer reports the actual landing page. if (pageNum < 1) pageNum = 1; if (pageNum > entry.pages.length) pageNum = entry.pages.length; return `${headerFor(entry, pageNum)}\n\n${entry.pages[pageNum - 1]}\n\n${footerFor(entry, pageNum)}`; } function clear() { entries.length = 0; counters.clear(); } /** Test-only. */ function size() { return entries.length; } const moreCommand = { command: moreCmd, handleCommand, }; return { setContent, clear, size, moreCommand, }; } //# sourceMappingURL=pagedOutputBuffer.js.map