u
This commit is contained in:
94
src/compact.js
Normal file
94
src/compact.js
Normal file
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Transcript compaction — token-saving rewrites applied to the persisted
|
||||
* conversation history between turns.
|
||||
*
|
||||
* The `patch` tool's arguments embed entire file bodies: the `search` and
|
||||
* `replace` text of every operation. Once a patch has been applied those bodies
|
||||
* are pure dead weight — they are re-sent to the model on every later turn but
|
||||
* are never needed again. `compactPatchToolCalls` rewrites each `patch` tool
|
||||
* call in the transcript so its long text fields keep only a head/tail excerpt,
|
||||
* with a marker noting how many lines were elided.
|
||||
*/
|
||||
|
||||
// Lines kept from the top and bottom of each long text field.
|
||||
const HEAD_LINES = 5;
|
||||
const TAIL_LINES = 5;
|
||||
|
||||
// Explanatory text carried by the omission marker. The marker is inserted by
|
||||
// the memory-management system when long tool-call text is elided between turns
|
||||
// to save tokens. It is NOT written by the model, and the note says so
|
||||
// explicitly so the model does not mistake it for its own earlier output.
|
||||
const OMISSION_NOTE =
|
||||
'omitted from memory to save tokens — note to self: the Fido Agent did NOT ' +
|
||||
'write this note; the memory management system did';
|
||||
|
||||
/** Escape a string so it can be embedded literally inside a RegExp. */
|
||||
const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
||||
// Matches the omission marker line emitted below, so an already-compacted field
|
||||
// can be recognised and left alone (keeping repeated passes idempotent). Built
|
||||
// from OMISSION_NOTE so the pattern can never drift from the emitted marker.
|
||||
const OMISSION_MARKER_RE = new RegExp(`^… \\[\\d+ lines? ${escapeRegExp(OMISSION_NOTE)}\\] …$`);
|
||||
|
||||
/**
|
||||
* Keep the first `head` and last `tail` lines of `text`, inserting a marker that
|
||||
* notes how many lines were omitted. Text that is already short enough
|
||||
* (<= head + tail lines), or that already carries an omission marker, is
|
||||
* returned unchanged.
|
||||
*/
|
||||
export function excerptLines(text, head = HEAD_LINES, tail = TAIL_LINES) {
|
||||
if (typeof text !== 'string') return text;
|
||||
const lines = text.split('\n');
|
||||
if (lines.length <= head + tail) return text;
|
||||
// Already compacted — don't re-truncate (the marker itself would otherwise
|
||||
// be counted as an extra line and trimmed away on the next pass).
|
||||
if (lines.some((l) => OMISSION_MARKER_RE.test(l))) return text;
|
||||
const omitted = lines.length - head - tail;
|
||||
return [
|
||||
...lines.slice(0, head),
|
||||
`… [${omitted} line${omitted === 1 ? '' : 's'} ${OMISSION_NOTE}] …`,
|
||||
...lines.slice(-tail),
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite every `patch` tool call in `messages` in place, replacing the long
|
||||
* `search` and `replace` fields of each operation with a head/tail excerpt.
|
||||
* Malformed arguments are left untouched. Returns the number of tool calls that
|
||||
* were actually compacted.
|
||||
*/
|
||||
export function compactPatchToolCalls(messages) {
|
||||
if (!Array.isArray(messages)) return 0;
|
||||
let compacted = 0;
|
||||
for (const m of messages) {
|
||||
if (!m || m.role !== 'assistant' || !Array.isArray(m.tool_calls)) continue;
|
||||
for (const tc of m.tool_calls) {
|
||||
if (tc?.function?.name !== 'patch' || typeof tc.function.arguments !== 'string') continue;
|
||||
let args;
|
||||
try {
|
||||
args = JSON.parse(tc.function.arguments);
|
||||
} catch {
|
||||
continue; // leave malformed arguments untouched
|
||||
}
|
||||
if (!args || !Array.isArray(args.operations)) continue;
|
||||
let changed = false;
|
||||
for (const op of args.operations) {
|
||||
if (!op || typeof op !== 'object') continue;
|
||||
for (const field of ['search', 'replace']) {
|
||||
const next = excerptLines(op[field]);
|
||||
if (next !== op[field]) {
|
||||
op[field] = next;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
tc.function.arguments = JSON.stringify(args);
|
||||
compacted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return compacted;
|
||||
}
|
||||
Reference in New Issue
Block a user