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.
- package/dist/index.js +455 -33
- 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
|
|
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
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
11531
|
-
const output = result.success ? result.output.length >
|
|
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
|
-
|
|
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 =
|
|
11636
|
-
const tailLines =
|
|
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
|
|
11666
|
-
|
|
11667
|
-
|
|
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
|
|
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
|
|
11812
|
+
const { maxSummaryChars } = this.contextLimits();
|
|
11726
11813
|
const combined = `${olderSummary}
|
|
11727
11814
|
|
|
11728
11815
|
---
|
|
11729
11816
|
|
|
11730
11817
|
${newerSummary}`;
|
|
11731
|
-
if (combined.length <=
|
|
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 >
|
|
11741
|
-
const budget =
|
|
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
|
|
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
|
|
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:
|
|
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