open-agents-ai 0.31.2 → 0.31.4

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.
Files changed (2) hide show
  1. package/dist/index.js +455 -33
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10990,6 +10990,7 @@ Rules:
10990
10990
  _pauseResolve = null;
10991
10991
  _sudoPassword = null;
10992
10992
  _sudoResolve = null;
10993
+ _pendingCompaction = null;
10993
10994
  constructor(backend, options) {
10994
10995
  this.backend = backend;
10995
10996
  this.options = {
@@ -11012,6 +11013,39 @@ Rules:
11012
11013
  setContextWindowSize(size) {
11013
11014
  this.options.contextWindowSize = size;
11014
11015
  }
11016
+ // -------------------------------------------------------------------------
11017
+ // Context-aware limits — all dynamic values derived from contextWindowSize
11018
+ // and modelTier. Call this instead of using hardcoded magic numbers.
11019
+ // -------------------------------------------------------------------------
11020
+ /**
11021
+ * Compute all context-dependent limits from the current contextWindowSize
11022
+ * and modelTier. Returns sensible defaults when contextWindowSize is 0.
11023
+ */
11024
+ contextLimits() {
11025
+ const ctx = this.options.contextWindowSize;
11026
+ const tier = this.options.modelTier ?? "large";
11027
+ const compactionThreshold = ctx > 0 ? Math.min(this.options.compactionThreshold, Math.floor(ctx * 0.75)) : this.options.compactionThreshold;
11028
+ const keepRecentDivisor = tier === "small" ? 2e3 : tier === "medium" ? 3e3 : 4e3;
11029
+ const keepRecent = ctx > 0 ? Math.max(4, Math.min(12, Math.floor(ctx / keepRecentDivisor))) : 12;
11030
+ const maxOutputTokens = ctx > 0 ? Math.min(this.options.maxTokens, Math.max(2048, Math.floor(ctx * 0.25))) : this.options.maxTokens;
11031
+ const toolOutputMaxChars = ctx > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctx * 0.5))) : 8e3;
11032
+ const foldLineThreshold = tier === "small" ? 30 : tier === "medium" ? 35 : 40;
11033
+ const foldHeadLines = tier === "small" ? 12 : tier === "medium" ? 16 : 20;
11034
+ const foldTailLines = tier === "small" ? 5 : tier === "medium" ? 8 : 10;
11035
+ const maxSummaryChars = ctx > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctx * 0.2))) : 4e3;
11036
+ const repetitionWindow = tier === "small" ? 6 : tier === "medium" ? 8 : 10;
11037
+ return {
11038
+ compactionThreshold,
11039
+ keepRecent,
11040
+ maxOutputTokens,
11041
+ toolOutputMaxChars,
11042
+ foldLineThreshold,
11043
+ foldHeadLines,
11044
+ foldTailLines,
11045
+ maxSummaryChars,
11046
+ repetitionWindow
11047
+ };
11048
+ }
11015
11049
  /** Register a tool for the agent to use */
11016
11050
  registerTool(tool) {
11017
11051
  this.tools.set(tool.name, tool);
@@ -11066,6 +11100,23 @@ Rules:
11066
11100
  get isPaused() {
11067
11101
  return this._paused;
11068
11102
  }
11103
+ /**
11104
+ * Request manual context compaction at the next turn boundary.
11105
+ * The strategy controls how aggressively context is compressed:
11106
+ * - "default" — standard progressive summarization
11107
+ * - "aggressive" — maximum compression, keep only decisions + errors
11108
+ * - "decisions" — extract action→outcome pairs, discard exploration
11109
+ * - "errors" — preserve error context, compress successes
11110
+ * - "summary" — high-level paragraph, minimal detail
11111
+ */
11112
+ requestCompaction(strategy = "default") {
11113
+ this._pendingCompaction = strategy;
11114
+ this.emit({
11115
+ type: "compaction",
11116
+ content: `Manual compaction requested (strategy: ${strategy})`,
11117
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
11118
+ });
11119
+ }
11069
11120
  /**
11070
11121
  * If paused, block until resume() or abort() is called.
11071
11122
  * Returns true if the loop should continue, false if aborted while paused.
@@ -11093,7 +11144,8 @@ Rules:
11093
11144
  detectRepetition(recentToolCalls) {
11094
11145
  if (recentToolCalls.length < 4)
11095
11146
  return 0;
11096
- const window = recentToolCalls.slice(-8);
11147
+ const { repetitionWindow } = this.contextLimits();
11148
+ const window = recentToolCalls.slice(-repetitionWindow);
11097
11149
  const uniqueKeys = new Set(window.map((tc) => `${tc.name}:${tc.argsKey}`));
11098
11150
  const ratio = 1 - uniqueKeys.size / window.length;
11099
11151
  return ratio;
@@ -11212,9 +11264,15 @@ Integrate this guidance into your current approach. Continue working on the task
11212
11264
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11213
11265
  });
11214
11266
  }
11215
- const compacted = this.compactMessages(messages);
11216
- const ctxWindow = this.options.contextWindowSize;
11217
- const effectiveMaxTokens = ctxWindow > 0 ? Math.min(this.options.maxTokens, Math.max(2048, Math.floor(ctxWindow * 0.25))) : this.options.maxTokens;
11267
+ let compacted;
11268
+ if (this._pendingCompaction) {
11269
+ const strategy = this._pendingCompaction;
11270
+ this._pendingCompaction = null;
11271
+ compacted = this.compactMessages(messages, strategy, true);
11272
+ } else {
11273
+ compacted = this.compactMessages(messages);
11274
+ }
11275
+ const { maxOutputTokens: effectiveMaxTokens } = this.contextLimits();
11218
11276
  const chatRequest = {
11219
11277
  messages: compacted,
11220
11278
  tools: toolDefs,
@@ -11313,8 +11371,7 @@ Integrate this guidance into your current approach. Continue working on the task
11313
11371
  }
11314
11372
  }
11315
11373
  }
11316
- const ctxW = this.options.contextWindowSize;
11317
- const maxLen = ctxW > 0 ? Math.max(2e3, Math.min(8e3, Math.floor(ctxW * 0.5))) : 8e3;
11374
+ const { toolOutputMaxChars: maxLen } = this.contextLimits();
11318
11375
  const output = result.success ? result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output : `Error: ${result.error || "unknown error"}
11319
11376
  ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : result.output}`;
11320
11377
  this.emit({
@@ -11467,7 +11524,14 @@ Integrate this guidance into your current approach. Continue working on the task
11467
11524
  }
11468
11525
  this.emit({ type: "user_interrupt", content: userMsg.replace(/\[IMAGE_BASE64:[^\]]+\]/, "[image]").slice(0, 200), turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
11469
11526
  }
11470
- const compactedMsgs = this.compactMessages(messages);
11527
+ let compactedMsgs;
11528
+ if (this._pendingCompaction) {
11529
+ const strategy = this._pendingCompaction;
11530
+ this._pendingCompaction = null;
11531
+ compactedMsgs = this.compactMessages(messages, strategy, true);
11532
+ } else {
11533
+ compactedMsgs = this.compactMessages(messages);
11534
+ }
11471
11535
  const chatRequest = { messages: compactedMsgs, tools: toolDefs, temperature: this.options.temperature, maxTokens: this.options.maxTokens, timeoutMs: this.options.requestTimeoutMs };
11472
11536
  let response;
11473
11537
  try {
@@ -11527,8 +11591,8 @@ Integrate this guidance into your current approach. Continue working on the task
11527
11591
  }
11528
11592
  }
11529
11593
  }
11530
- const maxLen = 8e3;
11531
- const output = result.success ? result.output.length > maxLen ? result.output.slice(0, maxLen) + `
11594
+ const { toolOutputMaxChars: maxLen2 } = this.contextLimits();
11595
+ const output = result.success ? result.output.length > maxLen2 ? result.output.slice(0, maxLen2) + `
11532
11596
  ...(truncated)` : result.output : `Error: ${result.error || "unknown error"}
11533
11597
  ${result.output}`;
11534
11598
  this.emit({ type: "tool_result", toolName: tc.name, content: output.slice(0, 200), success: result.success, turn, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
@@ -11629,11 +11693,12 @@ ${marker}` : marker);
11629
11693
  // -------------------------------------------------------------------------
11630
11694
  foldOutput(output, maxChars) {
11631
11695
  const lines = output.split("\n");
11632
- if (lines.length <= 40) {
11696
+ const { foldLineThreshold, foldHeadLines, foldTailLines } = this.contextLimits();
11697
+ if (lines.length <= foldLineThreshold) {
11633
11698
  return output.slice(0, maxChars) + "\n...(truncated)";
11634
11699
  }
11635
- const headLines = 20;
11636
- const tailLines = 10;
11700
+ const headLines = foldHeadLines;
11701
+ const tailLines = foldTailLines;
11637
11702
  const head = lines.slice(0, headLines).join("\n");
11638
11703
  const tail = lines.slice(-tailLines).join("\n");
11639
11704
  const omitted = lines.length - headLines - tailLines;
@@ -11650,7 +11715,7 @@ ${tail}`;
11650
11715
  // -------------------------------------------------------------------------
11651
11716
  // Context compaction
11652
11717
  // -------------------------------------------------------------------------
11653
- compactMessages(messages) {
11718
+ compactMessages(messages, strategy = "default", force = false) {
11654
11719
  if (messages.length < 3)
11655
11720
  return messages;
11656
11721
  const totalChars = messages.reduce((sum, m) => {
@@ -11662,13 +11727,16 @@ ${tail}`;
11662
11727
  return sum;
11663
11728
  }, 0);
11664
11729
  const estimatedTokens = totalChars / 4;
11665
- const ctxWinThreshold = this.options.contextWindowSize > 0 ? Math.floor(this.options.contextWindowSize * 0.75) : Infinity;
11666
- const effectiveThreshold = Math.min(this.options.compactionThreshold, ctxWinThreshold);
11667
- if (estimatedTokens < effectiveThreshold) {
11730
+ const limits = this.contextLimits();
11731
+ if (!force && estimatedTokens < limits.compactionThreshold) {
11732
+ return messages;
11733
+ }
11734
+ if (force && messages.length < 5)
11668
11735
  return messages;
11736
+ let keepRecent = limits.keepRecent;
11737
+ if (strategy === "aggressive" || strategy === "summary") {
11738
+ keepRecent = Math.max(2, Math.floor(keepRecent / 2));
11669
11739
  }
11670
- const ctxWin = this.options.contextWindowSize;
11671
- const keepRecent = ctxWin > 0 ? Math.max(4, Math.min(12, Math.floor(ctxWin / 4e3))) : 12;
11672
11740
  const head = messages.slice(0, 2);
11673
11741
  if (messages.length <= 2 + keepRecent)
11674
11742
  return messages;
@@ -11697,16 +11765,35 @@ ${tail}`;
11697
11765
  nonCompactionMiddle.push(msg);
11698
11766
  }
11699
11767
  }
11700
- const newSummary = this.summarizeCompactedMessages(nonCompactionMiddle);
11768
+ const maskedMiddle = this.maskOldObservations(nonCompactionMiddle);
11769
+ let newSummary;
11770
+ switch (strategy) {
11771
+ case "aggressive":
11772
+ newSummary = this.summarizeAggressive(maskedMiddle);
11773
+ break;
11774
+ case "decisions":
11775
+ newSummary = this.summarizeDecisions(maskedMiddle);
11776
+ break;
11777
+ case "errors":
11778
+ newSummary = this.summarizeErrors(maskedMiddle);
11779
+ break;
11780
+ case "summary":
11781
+ newSummary = this.summarizeHighLevel(maskedMiddle);
11782
+ break;
11783
+ default:
11784
+ newSummary = this.summarizeCompactedMessages(maskedMiddle);
11785
+ }
11701
11786
  const combinedSummary = previousSummary ? this.progressiveSummarize(previousSummary, newSummary) : newSummary;
11787
+ const strategyLabel = strategy !== "default" ? ` (${strategy})` : "";
11788
+ const forceLabel = force ? " [manual]" : "";
11702
11789
  this.emit({
11703
11790
  type: "compaction",
11704
- content: `Compacted ${middle.length} messages${previousSummary ? " (progressive)" : ""}`,
11791
+ content: `Compacted ${middle.length} messages${strategyLabel}${forceLabel}${previousSummary ? " (progressive)" : ""}`,
11705
11792
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11706
11793
  });
11707
11794
  const compactionMsg = {
11708
11795
  role: "system",
11709
- content: `[Context compacted \u2014 summary of earlier work]
11796
+ content: `[Context compacted${strategyLabel} \u2014 summary of earlier work]
11710
11797
 
11711
11798
  ${combinedSummary}
11712
11799
 
@@ -11722,13 +11809,13 @@ ${combinedSummary}
11722
11809
  * When the combined text exceeds the budget, condense the older summary.
11723
11810
  */
11724
11811
  progressiveSummarize(olderSummary, newerSummary) {
11725
- const MAX_SUMMARY_CHARS = 4e3;
11812
+ const { maxSummaryChars } = this.contextLimits();
11726
11813
  const combined = `${olderSummary}
11727
11814
 
11728
11815
  ---
11729
11816
 
11730
11817
  ${newerSummary}`;
11731
- if (combined.length <= MAX_SUMMARY_CHARS) {
11818
+ if (combined.length <= maxSummaryChars) {
11732
11819
  return combined;
11733
11820
  }
11734
11821
  const condensed = this.condenseSummary(olderSummary);
@@ -11737,8 +11824,8 @@ ${newerSummary}`;
11737
11824
  ---
11738
11825
 
11739
11826
  ${newerSummary}`;
11740
- if (result.length > MAX_SUMMARY_CHARS) {
11741
- const budget = MAX_SUMMARY_CHARS - newerSummary.length - 60;
11827
+ if (result.length > maxSummaryChars) {
11828
+ const budget = maxSummaryChars - newerSummary.length - 60;
11742
11829
  return budget > 200 ? `[Earlier work, condensed]
11743
11830
  ${olderSummary.slice(0, budget)}...
11744
11831
 
@@ -11772,6 +11859,56 @@ ${newerSummary}` : newerSummary;
11772
11859
  }
11773
11860
  return condensed.join("\n");
11774
11861
  }
11862
+ /**
11863
+ * Observation masking pre-pass: replace verbose tool outputs with compact
11864
+ * placeholders. Errors and short outputs (<500 chars) are kept verbatim.
11865
+ * Inspired by OpenHands' ObservationMaskingCondenser — zero LLM cost.
11866
+ */
11867
+ maskOldObservations(messages) {
11868
+ const toolCallNames = /* @__PURE__ */ new Map();
11869
+ for (const msg of messages) {
11870
+ if (msg.tool_calls) {
11871
+ for (const tc of msg.tool_calls) {
11872
+ toolCallNames.set(tc.id, tc.function.name);
11873
+ }
11874
+ }
11875
+ }
11876
+ return messages.map((msg) => {
11877
+ if (msg.role !== "tool" || typeof msg.content !== "string")
11878
+ return msg;
11879
+ const content = msg.content;
11880
+ if (content.startsWith("Error:") || /^(FAIL|ERR!|TypeError|SyntaxError)/i.test(content))
11881
+ return msg;
11882
+ if (content.length < 500)
11883
+ return msg;
11884
+ const toolName = msg.tool_call_id ? toolCallNames.get(msg.tool_call_id) : void 0;
11885
+ const lines = content.split("\n").length;
11886
+ switch (toolName) {
11887
+ case "file_read":
11888
+ return { ...msg, content: `[file content: ${lines} lines, ${content.length} chars \u2014 omitted for compaction]` };
11889
+ case "grep_search":
11890
+ return { ...msg, content: `[grep results: ${lines} matches, ${content.length} chars \u2014 omitted for compaction]` };
11891
+ case "find_files":
11892
+ return { ...msg, content: `[find results: ${lines} files, omitted for compaction]` };
11893
+ case "list_directory":
11894
+ return { ...msg, content: `[directory listing: ${lines} entries, omitted for compaction]` };
11895
+ case "web_fetch":
11896
+ return { ...msg, content: `[web page content: ${content.length} chars \u2014 omitted for compaction]` };
11897
+ case "web_search":
11898
+ return { ...msg, content: `[search results: ${lines} results \u2014 omitted for compaction]` };
11899
+ case "shell":
11900
+ case "background_run":
11901
+ if (/PASS|FAIL|error|warning/i.test(content))
11902
+ return msg;
11903
+ return { ...msg, content: `[command output: ${lines} lines, ${content.length} chars \u2014 omitted for compaction]` };
11904
+ default:
11905
+ if (content.length > 2e3) {
11906
+ return { ...msg, content: `[${toolName ?? "tool"} output: ${content.length} chars \u2014 omitted for compaction]` };
11907
+ }
11908
+ return msg;
11909
+ }
11910
+ });
11911
+ }
11775
11912
  /**
11776
11913
  * Extract a rich structured summary from compacted messages, preserving:
11777
11914
  * - Assistant analysis/reasoning text
@@ -11939,6 +12076,253 @@ ${newerSummary}` : newerSummary;
11939
12076
  return parts.join("\n");
11940
12077
  }
11941
12078
  // -------------------------------------------------------------------------
12079
+ // Strategy-specific summarizers for manual /compact command
12080
+ // -------------------------------------------------------------------------
12081
+ /**
12082
+ * Aggressive compaction: extract only key decisions and errors.
12083
+ * Discards exploration, file reads, and search results entirely.
12084
+ * Keeps: what files were changed, what errors occurred, what approach was chosen.
12085
+ */
12086
+ summarizeAggressive(messages) {
12087
+ const decisions = [];
12088
+ const errors = [];
12089
+ const filesChanged = /* @__PURE__ */ new Map();
12090
+ let toolCallCount = 0;
12091
+ const toolCallMap = /* @__PURE__ */ new Map();
12092
+ for (const msg of messages) {
12093
+ if (msg.tool_calls) {
12094
+ for (const tc of msg.tool_calls) {
12095
+ toolCallCount++;
12096
+ const args = (() => {
12097
+ try {
12098
+ return JSON.parse(tc.function.arguments);
12099
+ } catch {
12100
+ return {};
12101
+ }
12102
+ })();
12103
+ toolCallMap.set(tc.id, { name: tc.function.name, args });
12104
+ const name = tc.function.name;
12105
+ if (name === "file_edit" || name === "file_write" || name === "batch_edit") {
12106
+ const path = String(args.path || "");
12107
+ if (path)
12108
+ filesChanged.set(path, name === "file_write" ? "created/rewritten" : "edited");
12109
+ }
12110
+ }
12111
+ }
12112
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.trim().length > 30) {
12113
+ const text = msg.content.trim();
12114
+ const decisionPatterns = /(?:I (?:will|need to|should|decided|chose|'ll)|Let me|The (?:fix|solution|approach|issue))[^.!?\n]{10,120}[.!?]/gi;
12115
+ const matches = text.match(decisionPatterns);
12116
+ if (matches) {
12117
+ for (const m of matches.slice(0, 2))
12118
+ decisions.push(m.trim());
12119
+ }
12120
+ }
12121
+ if (msg.role === "tool" && typeof msg.content === "string") {
12122
+ const tc = msg.tool_call_id ? toolCallMap.get(msg.tool_call_id) : void 0;
12123
+ if (msg.content.startsWith("Error:") || /FAIL|ERR!/i.test(msg.content)) {
12124
+ const ctx = tc ? `${tc.name}` : "tool";
12125
+ errors.push(`${ctx}: ${msg.content.split("\n")[0]?.slice(0, 150)}`);
12126
+ }
12127
+ }
12128
+ }
12129
+ const parts = [];
12130
+ parts.push(`## Aggressive Compact (${toolCallCount} tool calls compressed)
12131
+ `);
12132
+ if (filesChanged.size > 0) {
12133
+ parts.push("### Files Modified");
12134
+ for (const [path, action] of filesChanged)
12135
+ parts.push(`- \`${path}\` (${action})`);
12136
+ parts.push("");
12137
+ }
12138
+ if (decisions.length > 0) {
12139
+ parts.push("### Key Decisions");
12140
+ for (const d of decisions.slice(0, 5))
12141
+ parts.push(`- ${d}`);
12142
+ parts.push("");
12143
+ }
12144
+ if (errors.length > 0) {
12145
+ parts.push("### Errors (avoid repeating)");
12146
+ for (const e of errors.slice(0, 5))
12147
+ parts.push(`- ${e}`);
12148
+ }
12149
+ return parts.join("\n");
12150
+ }
12151
+ /**
12152
+ * Decisions compaction: extract action→outcome pairs only.
12153
+ * Each tool call becomes a single "did X → got Y" line.
12154
+ */
12155
+ summarizeDecisions(messages) {
12156
+ const actionOutcomes = [];
12157
+ const toolCallMap = /* @__PURE__ */ new Map();
12158
+ for (const msg of messages) {
12159
+ if (msg.tool_calls) {
12160
+ for (const tc of msg.tool_calls) {
12161
+ const args = (() => {
12162
+ try {
12163
+ return JSON.parse(tc.function.arguments);
12164
+ } catch {
12165
+ return {};
12166
+ }
12167
+ })();
12168
+ toolCallMap.set(tc.id, { name: tc.function.name, args });
12169
+ }
12170
+ }
12171
+ if (msg.role === "tool" && typeof msg.content === "string" && msg.tool_call_id) {
12172
+ const tc = toolCallMap.get(msg.tool_call_id);
12173
+ if (!tc)
12174
+ continue;
12175
+ const content = msg.content;
12176
+ let action;
12177
+ let outcome;
12178
+ switch (tc.name) {
12179
+ case "file_read":
12180
+ action = `Read \`${tc.args.path}\``;
12181
+ outcome = `${content.split("\n").length} lines`;
12182
+ break;
12183
+ case "file_write":
12184
+ action = `Wrote \`${tc.args.path}\``;
12185
+ outcome = content.startsWith("Error:") ? content.slice(0, 100) : "success";
12186
+ break;
12187
+ case "file_edit":
12188
+ action = `Edited \`${tc.args.path}\``;
12189
+ outcome = content.startsWith("Error:") ? content.slice(0, 100) : "applied";
12190
+ break;
12191
+ case "shell":
12192
+ case "background_run":
12193
+ action = `Ran \`${String(tc.args.command || "").slice(0, 60)}\``;
12194
+ outcome = content.startsWith("Error:") ? "failed" : /PASS|success/i.test(content) ? "passed" : "completed";
12195
+ break;
12196
+ case "grep_search":
12197
+ action = `Searched "${tc.args.pattern}"`;
12198
+ outcome = `${(content.match(/\n/g) || []).length} matches`;
12199
+ break;
12200
+ default:
12201
+ action = tc.name;
12202
+ outcome = content.startsWith("Error:") ? "failed" : "ok";
12203
+ }
12204
+ actionOutcomes.push(`${action} \u2192 ${outcome}`);
12205
+ }
12206
+ }
12207
+ const parts = [];
12208
+ parts.push(`## Decision Log (${actionOutcomes.length} actions)
12209
+ `);
12210
+ for (const ao of actionOutcomes)
12211
+ parts.push(`- ${ao}`);
12212
+ return parts.join("\n");
12213
+ }
12214
+ /**
12215
+ * Error-preserving compaction: keep full error context for failure avoidance,
12216
+ * aggressively compress successful operations to single lines.
12217
+ */
12218
+ summarizeErrors(messages) {
12219
+ const errors = [];
12220
+ const successes = [];
12221
+ const toolCallMap = /* @__PURE__ */ new Map();
12222
+ for (const msg of messages) {
12223
+ if (msg.tool_calls) {
12224
+ for (const tc of msg.tool_calls) {
12225
+ const args = (() => {
12226
+ try {
12227
+ return JSON.parse(tc.function.arguments);
12228
+ } catch {
12229
+ return {};
12230
+ }
12231
+ })();
12232
+ toolCallMap.set(tc.id, { name: tc.function.name, args });
12233
+ }
12234
+ }
12235
+ if (msg.role === "tool" && typeof msg.content === "string" && msg.tool_call_id) {
12236
+ const tc = toolCallMap.get(msg.tool_call_id);
12237
+ if (!tc)
12238
+ continue;
12239
+ const content = msg.content;
12240
+ const isError = content.startsWith("Error:") || /FAIL|ERR!|error:|TypeError|SyntaxError/i.test(content);
12241
+ const action = tc.name === "shell" ? `\`${String(tc.args.command || "").slice(0, 80)}\`` : `${tc.name}(${String(tc.args.path || tc.args.pattern || "").slice(0, 60)})`;
12242
+ if (isError) {
12243
+ errors.push({
12244
+ context: action,
12245
+ error: content.slice(0, 300)
12246
+ });
12247
+ } else {
12248
+ successes.push(`${action} \u2192 ok`);
12249
+ }
12250
+ }
12251
+ }
12252
+ const parts = [];
12253
+ parts.push(`## Error-Focused Compact
12254
+ `);
12255
+ if (errors.length > 0) {
12256
+ parts.push("### Errors & Failures (preserve to avoid repeating)");
12257
+ for (const { context, error } of errors.slice(0, 8)) {
12258
+ parts.push(`- **${context}**:`);
12259
+ parts.push(` \`\`\`
12260
+ ${error}
12261
+ \`\`\``);
12262
+ }
12263
+ parts.push("");
12264
+ }
12265
+ if (successes.length > 0) {
12266
+ parts.push(`### Successful Operations (${successes.length} total)`);
12267
+ for (const s of successes.slice(0, 10))
12268
+ parts.push(`- ${s}`);
12269
+ if (successes.length > 10)
12270
+ parts.push(`- ... and ${successes.length - 10} more`);
12271
+ }
12272
+ return parts.join("\n");
12273
+ }
12274
+ /**
12275
+ * High-level summary: paragraph-form description of what was accomplished.
12276
+ * Minimal detail, maximum compression — good for very long sessions.
12277
+ */
12278
+ summarizeHighLevel(messages) {
12279
+ const filesModified = /* @__PURE__ */ new Set();
12280
+ const filesRead = /* @__PURE__ */ new Set();
12281
+ let shellCount = 0;
12282
+ let toolCallCount = 0;
12283
+ let lastAssistantText = "";
12284
+ for (const msg of messages) {
12285
+ if (msg.tool_calls) {
12286
+ for (const tc of msg.tool_calls) {
12287
+ toolCallCount++;
12288
+ const args = (() => {
12289
+ try {
12290
+ return JSON.parse(tc.function.arguments);
12291
+ } catch {
12292
+ return {};
12293
+ }
12294
+ })();
12295
+ const name = tc.function.name;
12296
+ if (name === "file_edit" || name === "file_write" || name === "batch_edit") {
12297
+ filesModified.add(String(args.path || ""));
12298
+ } else if (name === "file_read") {
12299
+ filesRead.add(String(args.path || ""));
12300
+ } else if (name === "shell" || name === "background_run") {
12301
+ shellCount++;
12302
+ }
12303
+ }
12304
+ }
12305
+ if (msg.role === "assistant" && typeof msg.content === "string" && msg.content.trim().length > 20) {
12306
+ lastAssistantText = msg.content.trim();
12307
+ }
12308
+ }
12309
+ const modList = Array.from(filesModified).filter(Boolean);
12310
+ const readList = Array.from(filesRead).filter(Boolean);
12311
+ const parts = [];
12312
+ parts.push(`## Session Summary (${toolCallCount} tool calls)
12313
+ `);
12314
+ parts.push(`Modified ${modList.length} file(s), read ${readList.length} file(s), ran ${shellCount} command(s).
12315
+ `);
12316
+ if (modList.length > 0) {
12317
+ parts.push(`Files changed: ${modList.slice(0, 8).map((f) => `\`${f}\``).join(", ")}${modList.length > 8 ? ` +${modList.length - 8} more` : ""}`);
12318
+ }
12319
+ if (lastAssistantText) {
12320
+ parts.push(`
12321
+ Last analysis: ${lastAssistantText.slice(0, 300)}${lastAssistantText.length > 300 ? "..." : ""}`);
12322
+ }
12323
+ return parts.join("\n");
12324
+ }
12325
+ // -------------------------------------------------------------------------
11942
12326
  // Tool definition builder
11943
12327
  // -------------------------------------------------------------------------
11944
12328
  buildToolDefinitions() {
@@ -14057,6 +14441,8 @@ function renderSlashHelp() {
14057
14441
  ["/stats", "Show session dashboard (metrics, tool usage, task history)"],
14058
14442
  ["/stop", "Stop current task and save progress (alias: /pause)"],
14059
14443
  ["/resume", "Resume a previously stopped task"],
14444
+ ["/compact", "Force context compaction now (default strategy)"],
14445
+ ["/compact <strategy>", "Compact with strategy: aggressive, decisions, errors, summary"],
14060
14446
  ["/bruteforce", "Toggle brute-force mode (auto re-engage on turn limit)"],
14061
14447
  ["/tools", "List agent-created custom tools"],
14062
14448
  ["/skills", "List available AIWG skills"],
@@ -14426,6 +14812,7 @@ var init_render = __esm({
14426
14812
  "/verbose",
14427
14813
  "/dream",
14428
14814
  "/bruteforce",
14815
+ "/compact",
14429
14816
  "/tools",
14430
14817
  "/skills",
14431
14818
  "/clear",
@@ -15599,11 +15986,12 @@ async function doSetup(config, rl) {
15599
15986
  const createModelfile = await ask(rl, ` Create optimized model "${c2.bold(customName)}" with ${ctx.label} context? (Y/n) `);
15600
15987
  if (createModelfile.toLowerCase() !== "n") {
15601
15988
  try {
15989
+ const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
15602
15990
  const modelfileContent = [
15603
15991
  `FROM ${selectedVariant.tag}`,
15604
15992
  `PARAMETER num_ctx ${ctx.numCtx}`,
15605
15993
  `PARAMETER temperature 0`,
15606
- `PARAMETER num_predict 16384`,
15994
+ `PARAMETER num_predict ${numPredict}`,
15607
15995
  `PARAMETER stop "<|endoftext|>"`
15608
15996
  ].join("\n");
15609
15997
  const modelDir2 = join24(homedir9(), ".open-agents", "models");
@@ -16005,11 +16393,12 @@ function createExpandedVariant(baseModel, specs, sizeGB) {
16005
16393
  const customName = expandedModelName(baseModel);
16006
16394
  const ctx = calculateContextWindow(specs, sizeGB);
16007
16395
  try {
16396
+ const numPredict = Math.min(16384, Math.max(2048, Math.floor(ctx.numCtx * 0.25)));
16008
16397
  const modelfileContent = [
16009
16398
  `FROM ${baseModel}`,
16010
16399
  `PARAMETER num_ctx ${ctx.numCtx}`,
16011
16400
  `PARAMETER temperature 0`,
16012
- `PARAMETER num_predict 16384`,
16401
+ `PARAMETER num_predict ${numPredict}`,
16013
16402
  `PARAMETER stop "<|endoftext|>"`
16014
16403
  ].join("\n");
16015
16404
  const modelDir2 = join24(homedir9(), ".open-agents", "models");
@@ -16545,6 +16934,26 @@ async function handleSlashCommand(input, ctx) {
16545
16934
  }
16546
16935
  return "handled";
16547
16936
  }
16937
+ case "compact":
16938
+ case "gc": {
16939
+ if (!ctx.hasActiveTask?.()) {
16940
+ renderWarning("No active task to compact. Context compaction requires a running task.");
16941
+ return "handled";
16942
+ }
16943
+ const validStrategies = ["default", "aggressive", "decisions", "errors", "summary"];
16944
+ const strategy = arg && validStrategies.includes(arg) ? arg : "default";
16945
+ if (arg && !validStrategies.includes(arg)) {
16946
+ renderWarning(`Unknown strategy "${arg}". Valid: ${validStrategies.join(", ")}`);
16947
+ return "handled";
16948
+ }
16949
+ const ok = ctx.requestCompaction?.(strategy) ?? false;
16950
+ if (ok) {
16951
+ renderInfo(`Compaction requested (strategy: ${c2.bold(strategy)}). Will apply at next turn boundary.`);
16952
+ } else {
16953
+ renderWarning("Could not request compaction.");
16954
+ }
16955
+ return "handled";
16956
+ }
16548
16957
  default: {
16549
16958
  const skills = discoverSkills(ctx.repoRoot);
16550
16959
  const skill = skills.find((s) => s.name === cmd || s.name === cmd.replace(/_/g, "-"));
@@ -20983,7 +21392,7 @@ function createTaskCompleteTool() {
20983
21392
  }
20984
21393
  };
20985
21394
  }
20986
- function buildTools(repoRoot, config) {
21395
+ function buildTools(repoRoot, config, contextWindowSize) {
20987
21396
  const executionTools = [
20988
21397
  new FileReadTool(repoRoot),
20989
21398
  new FileWriteTool(repoRoot),
@@ -21044,11 +21453,11 @@ function buildTools(repoRoot, config) {
21044
21453
  ];
21045
21454
  return [
21046
21455
  ...executionTools.map(adaptTool2),
21047
- createSubAgentTool(config, repoRoot),
21456
+ createSubAgentTool(config, repoRoot, contextWindowSize),
21048
21457
  createTaskCompleteTool()
21049
21458
  ];
21050
21459
  }
21051
- function createSubAgentTool(config, repoRoot) {
21460
+ function createSubAgentTool(config, repoRoot, ctxWindowSize) {
21052
21461
  return {
21053
21462
  name: "sub_agent",
21054
21463
  description: "Delegate a sub-task to an independent agent with its own context window. Each sub-agent creates an independent backend connection, enabling TRUE PARALLEL inference when the backend supports concurrent requests (Ollama with OLLAMA_NUM_PARALLEL > 1). BEST PRACTICE: Launch multiple sub_agent calls with background=true in ONE response to maximize parallelism. Check results via task_status/task_output.",
@@ -21069,13 +21478,18 @@ function createSubAgentTool(config, repoRoot) {
21069
21478
  return { success: false, output: "", error: "task is required" };
21070
21479
  }
21071
21480
  const backend = new OllamaAgenticBackend(config.backendUrl, config.model, config.apiKey);
21481
+ const subCtxWindow = ctxWindowSize ?? 0;
21482
+ const subTier = getModelTier(config.model);
21483
+ const subCompaction = subTier === "small" ? 12e3 : subTier === "medium" ? 24e3 : 4e4;
21072
21484
  const subRunner = new AgenticRunner(backend, {
21073
21485
  maxTurns,
21074
21486
  maxTokens: 16384,
21075
21487
  temperature: 0,
21076
21488
  requestTimeoutMs: config.timeoutMs,
21077
21489
  taskTimeoutMs: config.timeoutMs * 2,
21078
- compactionThreshold: 4e4
21490
+ compactionThreshold: subCompaction,
21491
+ contextWindowSize: subCtxWindow,
21492
+ modelTier: subTier
21079
21493
  });
21080
21494
  const subTools = [
21081
21495
  new FileReadTool(repoRoot),
@@ -21145,7 +21559,7 @@ function startTask(task, config, repoRoot, voice, stream, taskStores, bruteForce
21145
21559
  // effectively unlimited — no hard timeout, agent runs until complete or aborted
21146
21560
  contextWindowSize: contextWindowSize ?? 0
21147
21561
  });
21148
- const tools = buildTools(repoRoot, config);
21562
+ const tools = buildTools(repoRoot, config, contextWindowSize);
21149
21563
  if (contextWindowSize && contextWindowSize > 0) {
21150
21564
  for (const tool of tools) {
21151
21565
  if ("setContextWindowSize" in tool && typeof tool.setContextWindowSize === "function") {
@@ -21572,7 +21986,9 @@ async function startInteractive(config, repoPath) {
21572
21986
  "/skills",
21573
21987
  "/pause",
21574
21988
  "/stop",
21575
- "/resume"
21989
+ "/resume",
21990
+ "/compact",
21991
+ "/gc"
21576
21992
  ];
21577
21993
  const discoveredSkillNames = discoverSkills(repoRoot).map((s) => `/${s.name}`);
21578
21994
  const allCompletions = [.../* @__PURE__ */ new Set([...BUILTIN_COMMANDS, ...discoveredSkillNames])].sort();
@@ -21863,6 +22279,12 @@ async function startInteractive(config, repoPath) {
21863
22279
  statusBar.setCapabilities(caps);
21864
22280
  },
21865
22281
  hasActiveTask: () => activeTask !== null,
22282
+ requestCompaction(strategy) {
22283
+ if (!activeTask)
22284
+ return false;
22285
+ activeTask.runner.requestCompaction(strategy);
22286
+ return true;
22287
+ },
21866
22288
  abortTask() {
21867
22289
  if (!activeTask)
21868
22290
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.31.2",
3
+ "version": "0.31.4",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",