@gaberrb/polypus 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -142,6 +142,7 @@ var en = {
142
142
  "run.reprompt": "\u21BB no tool call \u2014 reinforcing instructions (attempt {attempt})",
143
143
  "run.autocorrect": "\u21BB tool failed \u2014 auto-correcting with extra context",
144
144
  "run.cancelled": "\u25A0 cancelled",
145
+ "compaction.done": "context compacted: ~{before} \u2192 ~{after} tokens",
145
146
  "run.jsonNeedsTask": "--json requires a task argument (headless mode has no interactive REPL).",
146
147
  "review.approveAll": "approve all",
147
148
  "review.reject": "reject",
@@ -401,6 +402,7 @@ var ptBR = {
401
402
  "run.reprompt": "\u21BB nenhuma chamada de tool \u2014 refor\xE7ando instru\xE7\xF5es (tentativa {attempt})",
402
403
  "run.autocorrect": "\u21BB tool falhou \u2014 autocorrigindo com contexto extra",
403
404
  "run.cancelled": "\u25A0 cancelado",
405
+ "compaction.done": "contexto compactado: ~{before} \u2192 ~{after} tokens",
404
406
  "run.jsonNeedsTask": "--json exige um argumento de tarefa (o modo headless n\xE3o tem REPL interativo).",
405
407
  "review.approveAll": "aprovar tudo",
406
408
  "review.reject": "rejeitar",
@@ -2092,6 +2094,56 @@ async function loadProjectInstructions(workspace) {
2092
2094
  return void 0;
2093
2095
  }
2094
2096
 
2097
+ // src/core/agent/compaction.ts
2098
+ function estimateTokens(messages) {
2099
+ let chars = 0;
2100
+ for (const m of messages) chars += m.content.length;
2101
+ return Math.ceil(chars / 4);
2102
+ }
2103
+ var RECENT_KEEP = 8;
2104
+ var MIN_TO_COMPACT = 4;
2105
+ var MAX_SUMMARY_INPUT = 4e4;
2106
+ function findSafeCut(messages, desiredKeep = RECENT_KEEP) {
2107
+ let cut = Math.max(1, messages.length - desiredKeep);
2108
+ while (cut < messages.length && (messages[cut].role === "tool" || messages[cut - 1]?.role === "assistant" && (messages[cut - 1].toolCalls?.length ?? 0) > 0)) {
2109
+ cut++;
2110
+ }
2111
+ return cut;
2112
+ }
2113
+ function serialize(messages) {
2114
+ const text2 = messages.map((m) => {
2115
+ const tools = m.toolCalls?.length ? ` [called: ${m.toolCalls.map((c) => c.name).join(", ")}]` : "";
2116
+ return `${m.role}${tools}: ${m.content}`;
2117
+ }).join("\n\n");
2118
+ return text2.length > MAX_SUMMARY_INPUT ? text2.slice(-MAX_SUMMARY_INPUT) : text2;
2119
+ }
2120
+ async function compactHistory(messages, agent, signal) {
2121
+ if (messages.length === 0) return messages;
2122
+ const system = messages[0].role === "system" ? messages[0] : void 0;
2123
+ const startIdx = system ? 1 : 0;
2124
+ const cut = findSafeCut(messages);
2125
+ if (cut >= messages.length) return messages;
2126
+ const middle = messages.slice(startIdx, cut);
2127
+ if (middle.length < MIN_TO_COMPACT) return messages;
2128
+ const tail = messages.slice(cut);
2129
+ const summary = await agent.provider.chat({
2130
+ messages: [
2131
+ {
2132
+ role: "system",
2133
+ content: "You compress a coding agent's conversation so it can continue with less context. Summarize the messages below into a concise but information-dense brief that preserves: the original task and goal, key decisions, files created/edited and why, important command/test outputs, and any remaining TODOs or open problems. Use terse bullet points. Do not invent details."
2134
+ },
2135
+ { role: "user", content: serialize(middle) }
2136
+ ],
2137
+ signal
2138
+ });
2139
+ const summaryMessage = {
2140
+ role: "user",
2141
+ content: `[Summary of earlier conversation, compacted to save context]
2142
+ ${summary.content.trim()}`
2143
+ };
2144
+ return system ? [system, summaryMessage, ...tail] : [summaryMessage, ...tail];
2145
+ }
2146
+
2095
2147
  // src/core/agent/loop.ts
2096
2148
  function looksLikeStall(text2) {
2097
2149
  const lc = text2.toLowerCase();
@@ -2146,9 +2198,22 @@ async function runAgent(opts) {
2146
2198
  const maxToolRetries = opts.maxToolRetries ?? 3;
2147
2199
  const autoCorrect = opts.autoCorrect ?? true;
2148
2200
  const usage2 = { promptTokens: 0, completionTokens: 0 };
2201
+ const compactThreshold = opts.compactThresholdTokens ?? 0;
2202
+ let lastPromptTokens = 0;
2149
2203
  for (let step = 1; step <= maxSteps; step++) {
2150
2204
  if (opts.signal?.aborted) return { finished: false, reason: "cancelled", steps: step - 1, messages, usage: usage2 };
2151
2205
  events?.onStep?.(step);
2206
+ if (compactThreshold > 0) {
2207
+ const current = lastPromptTokens || estimateTokens(messages);
2208
+ if (current >= compactThreshold) {
2209
+ const compacted = await compactHistory(messages, agent, opts.signal);
2210
+ if (compacted.length < messages.length) {
2211
+ messages.splice(0, messages.length, ...compacted);
2212
+ lastPromptTokens = estimateTokens(messages);
2213
+ events?.onCompaction?.(current, lastPromptTokens);
2214
+ }
2215
+ }
2216
+ }
2152
2217
  let response;
2153
2218
  try {
2154
2219
  response = await agent.provider.chat({
@@ -2163,6 +2228,7 @@ async function runAgent(opts) {
2163
2228
  }
2164
2229
  usage2.promptTokens += response.usage?.promptTokens ?? 0;
2165
2230
  usage2.completionTokens += response.usage?.completionTokens ?? 0;
2231
+ lastPromptTokens = response.usage?.promptTokens ?? estimateTokens(messages);
2166
2232
  events?.onUsage?.(usage2);
2167
2233
  const { toolCalls, text: text2 } = driver.parse(response);
2168
2234
  messages.push(driver.assistantMessage(response, toolCalls));
@@ -2588,6 +2654,9 @@ function createJsonCollector() {
2588
2654
  onReprompt(attempt) {
2589
2655
  log.push({ type: "reprompt", attempt });
2590
2656
  },
2657
+ onCompaction(before, after) {
2658
+ log.push({ type: "compaction", before, after });
2659
+ },
2591
2660
  onUsage() {
2592
2661
  }
2593
2662
  };
@@ -3800,6 +3869,11 @@ var Spinner = class {
3800
3869
 
3801
3870
  // src/cli/commands/run.ts
3802
3871
  var MAX_VERIFY_FIXES = 3;
3872
+ function compactionThreshold() {
3873
+ if (process.env.POLYPUS_NO_COMPACT) return 0;
3874
+ const v = Number(process.env.POLYPUS_COMPACT_THRESHOLD);
3875
+ return Number.isFinite(v) && v > 0 ? v : 12e4;
3876
+ }
3803
3877
  async function run(task, opts) {
3804
3878
  let config = await loadConfig();
3805
3879
  const workspace = process.cwd();
@@ -3925,6 +3999,7 @@ async function executeTask(task, resolved, workspace, session, json = false, ver
3925
3999
  promptContext: { workspace, mode: session.mode, allow: session.allow },
3926
4000
  history: session.history,
3927
4001
  maxSteps: session.maxSteps,
4002
+ compactThresholdTokens: compactionThreshold(),
3928
4003
  signal: controller.signal,
3929
4004
  events
3930
4005
  });
@@ -4108,6 +4183,10 @@ function renderEvents(spinner3) {
4108
4183
  spinner3.stop();
4109
4184
  console.log(pc8.yellow(" " + t("run.reprompt", { attempt })));
4110
4185
  },
4186
+ onCompaction(before, after) {
4187
+ spinner3.stop();
4188
+ console.log(pc8.dim("\u21AF " + t("compaction.done", { before: fmtTokens(before), after: fmtTokens(after) })));
4189
+ },
4111
4190
  onCorrection() {
4112
4191
  spinner3.stop();
4113
4192
  console.log(pc8.yellow(" \u21BB " + t("run.autocorrect")));