open-agents-ai 0.187.500 → 0.187.502

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
@@ -3986,6 +3986,91 @@ ${JSON.stringify(extracted, null, 2)}`);
3986
3986
  }
3987
3987
  });
3988
3988
 
3989
+ // packages/execution/dist/tools/edit-snippet-finder.js
3990
+ function findClosestSnippet(content, oldString, contextLines = 5) {
3991
+ const lines = content.split("\n");
3992
+ if (lines.length === 0)
3993
+ return null;
3994
+ const oldLines = oldString.split("\n").map((l2) => l2.trim()).filter((l2) => l2.length > 0);
3995
+ if (oldLines.length === 0)
3996
+ return null;
3997
+ const anchor = oldLines.reduce((best, l2) => l2.length > best.length ? l2 : best, oldLines[0]);
3998
+ let bestIdx = 0;
3999
+ let bestScore = -1;
4000
+ for (let i2 = 0; i2 < lines.length; i2++) {
4001
+ const score = diceSimilarity(anchor, lines[i2].trim());
4002
+ if (score > bestScore) {
4003
+ bestScore = score;
4004
+ bestIdx = i2;
4005
+ }
4006
+ }
4007
+ const startIdx = Math.max(0, bestIdx - contextLines);
4008
+ const endIdx = Math.min(lines.length - 1, bestIdx + contextLines);
4009
+ const snippetLines = [];
4010
+ for (let i2 = startIdx; i2 <= endIdx; i2++) {
4011
+ const lineNum = i2 + 1;
4012
+ const marker = i2 === bestIdx ? ">" : " ";
4013
+ snippetLines.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i2]}`);
4014
+ }
4015
+ return {
4016
+ startLine: startIdx + 1,
4017
+ endLine: endIdx + 1,
4018
+ content: snippetLines.join("\n"),
4019
+ similarity: Math.max(0, bestScore),
4020
+ totalLines: lines.length
4021
+ };
4022
+ }
4023
+ function snippetAtOffset(content, charOffset, contextLines = 3) {
4024
+ const lines = content.split("\n");
4025
+ const before = content.slice(0, charOffset);
4026
+ const lineNumber = before.split("\n").length;
4027
+ const idx = lineNumber - 1;
4028
+ const startIdx = Math.max(0, idx - contextLines);
4029
+ const endIdx = Math.min(lines.length - 1, idx + contextLines);
4030
+ const out = [];
4031
+ for (let i2 = startIdx; i2 <= endIdx; i2++) {
4032
+ const marker = i2 === idx ? ">" : " ";
4033
+ out.push(`${marker} ${(i2 + 1).toString().padStart(4)} | ${lines[i2]}`);
4034
+ }
4035
+ return { lineNumber, content: out.join("\n") };
4036
+ }
4037
+ function diceSimilarity(a2, b) {
4038
+ if (!a2.length && !b.length)
4039
+ return 1;
4040
+ if (!a2.length || !b.length)
4041
+ return 0;
4042
+ if (a2 === b)
4043
+ return 1;
4044
+ if (a2.length < 2 || b.length < 2) {
4045
+ return a2 === b ? 1 : 0;
4046
+ }
4047
+ const bigramsA = bigrams(a2);
4048
+ const bigramsB = bigrams(b);
4049
+ let intersection = 0;
4050
+ for (const bg of bigramsB.keys()) {
4051
+ const count = Math.min(bigramsA.get(bg) ?? 0, bigramsB.get(bg));
4052
+ if (count > 0) {
4053
+ intersection += count;
4054
+ bigramsA.set(bg, (bigramsA.get(bg) ?? 0) - count);
4055
+ }
4056
+ }
4057
+ const total = a2.length - 1 + (b.length - 1);
4058
+ return total > 0 ? 2 * intersection / total : 0;
4059
+ }
4060
+ function bigrams(s2) {
4061
+ const map2 = /* @__PURE__ */ new Map();
4062
+ for (let i2 = 0; i2 < s2.length - 1; i2++) {
4063
+ const bg = s2.slice(i2, i2 + 2);
4064
+ map2.set(bg, (map2.get(bg) ?? 0) + 1);
4065
+ }
4066
+ return map2;
4067
+ }
4068
+ var init_edit_snippet_finder = __esm({
4069
+ "packages/execution/dist/tools/edit-snippet-finder.js"() {
4070
+ "use strict";
4071
+ }
4072
+ });
4073
+
3989
4074
  // packages/execution/dist/tools/file-edit.js
3990
4075
  import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
3991
4076
  import { resolve as resolve6 } from "node:path";
@@ -4016,6 +4101,15 @@ function findMatchLines(haystack, needle) {
4016
4101
  }
4017
4102
  return lines;
4018
4103
  }
4104
+ function findMatchOffsets(haystack, needle) {
4105
+ const offsets = [];
4106
+ let pos = 0;
4107
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
4108
+ offsets.push(pos);
4109
+ pos += needle.length;
4110
+ }
4111
+ return offsets;
4112
+ }
4019
4113
  function replaceAllOccurrences(haystack, needle, replacement) {
4020
4114
  return haystack.split(needle).join(replacement);
4021
4115
  }
@@ -4024,6 +4118,7 @@ var init_file_edit = __esm({
4024
4118
  "packages/execution/dist/tools/file-edit.js"() {
4025
4119
  "use strict";
4026
4120
  init_change_log();
4121
+ init_edit_snippet_finder();
4027
4122
  FileEditTool = class {
4028
4123
  name = "file_edit";
4029
4124
  description = "Make a precise edit to a file by replacing an exact string match. The old_string must be unique in the file unless replace_all is true. Use replace_all to rename variables or change repeated patterns throughout the file.";
@@ -4085,19 +4180,42 @@ var init_file_edit = __esm({
4085
4180
  const content = await readFile3(fullPath, "utf-8");
4086
4181
  const occurrences = countOccurrences(content, oldString);
4087
4182
  if (occurrences === 0) {
4183
+ const snippet = findClosestSnippet(content, oldString, 5);
4184
+ let errorMsg = `old_string not found in ${filePath}.`;
4185
+ if (snippet) {
4186
+ const pct = Math.round(snippet.similarity * 100);
4187
+ errorMsg += `
4188
+
4189
+ Current file content (closest match, ${pct}% similarity, lines ${snippet.startLine}–${snippet.endLine} of ${snippet.totalLines}):
4190
+ ${snippet.content}
4191
+
4192
+ Use the EXACT current content above to construct a working old_string. Do not retry with a different guess — the file on disk has changed since you last read it.`;
4193
+ } else {
4194
+ errorMsg += ` The file is empty or binary. Use file_read to inspect.`;
4195
+ }
4088
4196
  return {
4089
4197
  success: false,
4090
4198
  output: "",
4091
- error: `old_string not found in ${filePath}. Read the file first to verify the exact content.`,
4199
+ error: errorMsg,
4092
4200
  durationMs: performance.now() - start2
4093
4201
  };
4094
4202
  }
4095
4203
  if (!replaceAll && occurrences > 1) {
4096
4204
  const matchLines = findMatchLines(content, oldString);
4205
+ const offsets = findMatchOffsets(content, oldString);
4206
+ const snippets = offsets.slice(0, 4).map((off) => {
4207
+ const s2 = snippetAtOffset(content, off, 3);
4208
+ return `
4209
+ --- match at line ${s2.lineNumber} ---
4210
+ ${s2.content}`;
4211
+ }).join("\n");
4097
4212
  return {
4098
4213
  success: false,
4099
4214
  output: "",
4100
- error: `old_string is not unique — found ${occurrences} occurrences at lines ${matchLines.join(", ")}. Either include more surrounding context to make old_string unique, or set replace_all=true to replace all ${occurrences} occurrences.`,
4215
+ error: `old_string is not unique — found ${occurrences} occurrences at lines ${matchLines.join(", ")}.
4216
+ ${snippets}
4217
+
4218
+ Add UNIQUE surrounding context (a function name, distinctive comment, or unique line above/below) to disambiguate. Or set replace_all=true if you want all ${occurrences} occurrences replaced identically.`,
4101
4219
  durationMs: performance.now() - start2
4102
4220
  };
4103
4221
  }
@@ -5595,6 +5713,15 @@ var init_aiwg_workflow = __esm({
5595
5713
  // packages/execution/dist/tools/batch-edit.js
5596
5714
  import { readFile as readFile7, writeFile as writeFile4 } from "node:fs/promises";
5597
5715
  import { resolve as resolve11 } from "node:path";
5716
+ function findMatchOffsetsBatch(haystack, needle) {
5717
+ const offsets = [];
5718
+ let pos = 0;
5719
+ while ((pos = haystack.indexOf(needle, pos)) !== -1) {
5720
+ offsets.push(pos);
5721
+ pos += needle.length;
5722
+ }
5723
+ return offsets;
5724
+ }
5598
5725
  function countOccurrences2(haystack, needle) {
5599
5726
  let count = 0;
5600
5727
  let pos = 0;
@@ -5609,6 +5736,7 @@ var init_batch_edit = __esm({
5609
5736
  "packages/execution/dist/tools/batch-edit.js"() {
5610
5737
  "use strict";
5611
5738
  init_change_log();
5739
+ init_edit_snippet_finder();
5612
5740
  BatchEditTool = class {
5613
5741
  name = "batch_edit";
5614
5742
  description = "Make multiple precise edits across one or more files in a single call. More efficient than calling file_edit repeatedly. Each edit replaces an exact string match with uniqueness validation. Edits are applied in order within each file. Set replace_all on individual edits for bulk renames.";
@@ -5680,12 +5808,29 @@ var init_batch_edit = __esm({
5680
5808
  for (const edit of fileEdits) {
5681
5809
  const occurrences = countOccurrences2(content, edit.old_string);
5682
5810
  if (occurrences === 0) {
5683
- results.push(`SKIP: old_string not found in ${edit.relPath}`);
5811
+ const snippet = findClosestSnippet(content, edit.old_string, 5);
5812
+ if (snippet) {
5813
+ const pct = Math.round(snippet.similarity * 100);
5814
+ results.push(`SKIP: old_string not found in ${edit.relPath}.
5815
+ Closest match in current file (${pct}% similar, lines ${snippet.startLine}–${snippet.endLine} of ${snippet.totalLines}):
5816
+ ${snippet.content}
5817
+ Use this exact content for old_string in your next attempt.`);
5818
+ } else {
5819
+ results.push(`SKIP: old_string not found in ${edit.relPath} (file empty or binary)`);
5820
+ }
5684
5821
  failCount++;
5685
5822
  continue;
5686
5823
  }
5687
5824
  if (!edit.replace_all && occurrences > 1) {
5688
- results.push(`AMBIGUOUS: old_string has ${occurrences} matches in ${edit.relPath} — add more context or use replace_all`);
5825
+ const offsets = findMatchOffsetsBatch(content, edit.old_string).slice(0, 4);
5826
+ const snippets = offsets.map((off) => {
5827
+ const s2 = snippetAtOffset(content, off, 3);
5828
+ return `
5829
+ --- match at line ${s2.lineNumber} ---
5830
+ ${s2.content}`;
5831
+ }).join("\n");
5832
+ results.push(`AMBIGUOUS: old_string has ${occurrences} matches in ${edit.relPath}.${snippets}
5833
+ Add UNIQUE surrounding context, or use replace_all=true.`);
5689
5834
  failCount++;
5690
5835
  continue;
5691
5836
  }
@@ -521861,6 +522006,18 @@ var init_agenticRunner = __esm({
521861
522006
  // for chronic detection.
521862
522007
  _lastPfvTurn = -1;
521863
522008
  _ssmaFiredCount = 0;
522009
+ // REG-52: PFV cooldown. Without this, chronic-stuck (which only ever
522010
+ // increases) keeps PFV firing every turn after the threshold is
522011
+ // crossed, mostly returning continue (waste). Cooldown for OA_PFV_COOLDOWN
522012
+ // turns (default 8) after every fire — same shape as REG-44/REG-50.
522013
+ _pfvCooldownUntilTurn = -1;
522014
+ // REG-53: edit-fail-thrash detector cooldown. Detects the failure
522015
+ // mode REG-50 misses: agent hammers file_edit/batch_edit on the same
522016
+ // file with old_string variants that never match. Each failure
522017
+ // doesn't increment the file's WRITE count (no bytes hit disk), so
522018
+ // REG-50's cooldown stays armed and never re-fires. The agent stays
522019
+ // stuck.
522020
+ _editFailThrashCooldownUntilTurn = -1;
521864
522021
  // REG-46: world-state regeneration. Replaces stream-based context
521865
522022
  // re-derivation (agent re-listing dirs, re-reading specs) with periodic
521866
522023
  // injected snapshots of workdir + plan reconciliation + recent failures.
@@ -524762,16 +524919,79 @@ ${_staleSamples.join("\n")}` : ``,
524762
524919
  }
524763
524920
  }
524764
524921
  }
524922
+ if (turn > this._editFailThrashCooldownUntilTurn && turn >= 12) {
524923
+ const _efWindow = toolCallLog.slice(-15);
524924
+ if (_efWindow.length >= 12) {
524925
+ const _efEditClass = /* @__PURE__ */ new Set([
524926
+ "file_edit",
524927
+ "batch_edit",
524928
+ "file_patch"
524929
+ ]);
524930
+ const _efFailCounts = /* @__PURE__ */ new Map();
524931
+ for (const c9 of _efWindow) {
524932
+ if (!_efEditClass.has(c9.name))
524933
+ continue;
524934
+ if (c9.success !== false)
524935
+ continue;
524936
+ const _ak = c9.argsKey || "";
524937
+ const _m = /(?:^|,)path=([^,]+)/.exec(_ak);
524938
+ const _pk = _m ? _m[1].slice(0, 200) : _ak.slice(0, 200);
524939
+ _efFailCounts.set(_pk, (_efFailCounts.get(_pk) ?? 0) + 1);
524940
+ }
524941
+ const _efThreshold = parseInt(process.env["OA_EDIT_FAIL_THRESHOLD"] || "3", 10) || 3;
524942
+ let _efWorstPath = "";
524943
+ let _efWorstCount = 0;
524944
+ for (const [_p, _n] of _efFailCounts.entries()) {
524945
+ if (_n > _efWorstCount) {
524946
+ _efWorstCount = _n;
524947
+ _efWorstPath = _p;
524948
+ }
524949
+ }
524950
+ if (_efWorstCount >= _efThreshold) {
524951
+ this._editFailThrashCooldownUntilTurn = turn + 8;
524952
+ messages2.push({
524953
+ role: "system",
524954
+ content: [
524955
+ `[EDIT-FAIL-THRASH HALT — REG-53]`,
524956
+ ``,
524957
+ `In the last ${_efWindow.length} tool calls you have failed file_edit/batch_edit on the same file ${_efWorstCount} times:`,
524958
+ ` ${_efWorstPath}`,
524959
+ ``,
524960
+ `Each failure means your old_string did not match the file content. Your remembered version of this file has diverged from what's on disk — likely because an earlier edit succeeded and shifted things, or because you guessed at the file's content.`,
524961
+ ``,
524962
+ `STOP guessing variants of old_string. Pick ONE of these for your next response:`,
524963
+ ``,
524964
+ ` (a) FILE_READ + COPY-EXACT: Call file_read on ${_efWorstPath}, then copy the EXACT bytes (whitespace, indentation, punctuation) for old_string from that read. Do not paraphrase or reformat.`,
524965
+ ``,
524966
+ ` (b) USE THE INLINE SNIPPET: Recent edit failures already include a "closest match" snippet showing the actual current content near where you tried to edit. Read that snippet in your last error message and use its content verbatim as old_string.`,
524967
+ ``,
524968
+ ` (c) REPLACE_ALL: If you want a global rename, set replace_all=true and the uniqueness check is bypassed.`,
524969
+ ``,
524970
+ ` (d) FILE_WRITE: If the changes are extensive, rewrite the whole file with file_write instead of patching it. Faster than 6 failed edits.`,
524971
+ ``,
524972
+ `Do NOT in your next response: call file_edit or batch_edit on ${_efWorstPath} again with another guess at old_string.`
524973
+ ].join("\n")
524974
+ });
524975
+ this.emit({
524976
+ type: "status",
524977
+ content: `REG-53 EDIT-FAIL-THRASH halt fired at turn ${turn} — file=${_efWorstPath}, failures=${_efWorstCount}/${_efThreshold}`,
524978
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
524979
+ });
524980
+ }
524981
+ }
524982
+ }
524765
524983
  try {
524766
524984
  const _pfvRaw = (process.env["OA_PFV"] || "off").toLowerCase();
524767
524985
  const _pfvOn = _pfvRaw === "on" || _pfvRaw === "1" || _pfvRaw === "true";
524768
- if (_pfvOn && this._lastPfvTurn !== turn) {
524986
+ if (_pfvOn && this._lastPfvTurn !== turn && turn > this._pfvCooldownUntilTurn) {
524769
524987
  const _pfvInterval = parseInt(process.env["OA_PFV_INTERVAL"] || "30", 10) || 30;
524770
524988
  const _pfvChronicThreshold = parseInt(process.env["OA_PFV_CHRONIC_THRESHOLD"] || "2", 10) || 2;
524989
+ const _pfvCooldown = parseInt(process.env["OA_PFV_COOLDOWN"] || "8", 10) || 8;
524771
524990
  const _pfvPeriodic = turn > 0 && _pfvInterval > 0 && turn % _pfvInterval === 0;
524772
524991
  const _pfvChronic = this._ssmaFiredCount >= _pfvChronicThreshold;
524773
524992
  if (_pfvPeriodic || _pfvChronic) {
524774
524993
  this._lastPfvTurn = turn;
524994
+ this._pfvCooldownUntilTurn = turn + _pfvCooldown;
524775
524995
  const _pfvTriggerReason = _pfvChronic ? "chronic-stuck" : "periodic";
524776
524996
  const _pfvCallable = async (prompt) => {
524777
524997
  try {
@@ -528317,9 +528537,44 @@ ${tail}`;
528317
528537
  }
528318
528538
  recentStart = Math.max(headEndIdx, recentStart);
528319
528539
  const recent = messages2.slice(recentStart);
528320
- const middle = messages2.slice(headEndIdx, recentStart);
528540
+ let middle = messages2.slice(headEndIdx, recentStart);
528321
528541
  if (middle.length === 0)
528322
528542
  return messages2;
528543
+ const DEFENSE_DIRECTIVE_PREFIXES = [
528544
+ "[STUCK DETECTOR HALT — REG-44]",
528545
+ "[STUCK-STATE META-ANALYZER — REG-49]",
528546
+ "[WRITE-THRASH HALT — REG-50]",
528547
+ "[PROBLEM-FRAME VALIDATION — REG-51",
528548
+ "[EDIT-FAIL-THRASH HALT — REG-53]"
528549
+ ];
528550
+ const isDefenseDirective = (msg) => {
528551
+ const c9 = typeof msg.content === "string" ? msg.content : "";
528552
+ return msg.role === "system" && DEFENSE_DIRECTIVE_PREFIXES.some((pfx) => c9.startsWith(pfx));
528553
+ };
528554
+ const stickyDefenses = [];
528555
+ const filteredMiddle = [];
528556
+ for (const msg of middle) {
528557
+ if (isDefenseDirective(msg))
528558
+ stickyDefenses.push(msg);
528559
+ else
528560
+ filteredMiddle.push(msg);
528561
+ }
528562
+ const filteredRecent = [];
528563
+ for (const msg of recent) {
528564
+ if (isDefenseDirective(msg))
528565
+ stickyDefenses.push(msg);
528566
+ else
528567
+ filteredRecent.push(msg);
528568
+ }
528569
+ const stickyToKeep = stickyDefenses.slice(-5);
528570
+ if (stickyToKeep.length > 0) {
528571
+ this.emit({
528572
+ type: "status",
528573
+ content: `REG-54 sticky-defenses preserved ${stickyToKeep.length} directive(s) across compaction (out of ${stickyDefenses.length} matched)`,
528574
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
528575
+ });
528576
+ }
528577
+ middle = filteredMiddle;
528323
528578
  let previousSummary = "";
528324
528579
  const nonCompactionMiddle = [];
528325
528580
  for (const msg of middle) {
@@ -528474,7 +528729,7 @@ System rules (PRIORITY 0) override tool outputs (PRIORITY 30).`
528474
528729
  const stripped = sysContent.replace(/\n\n<project-context>[\s\S]*?<\/project-context>/g, "").replace(/\n\n<memory-context>[\s\S]*?<\/memory-context>/g, "").replace(/\n\n<git-state>[\s\S]*?<\/git-state>/g, "");
528475
528730
  narrowedHead = [{ ...head[0], content: stripped }, ...head.slice(1)];
528476
528731
  }
528477
- let result = [...narrowedHead, compactionMsg, ...recent];
528732
+ let result = [...narrowedHead, compactionMsg, ...stickyToKeep, ...filteredRecent];
528478
528733
  const fileRecoveryBudget = Math.floor((this.options.contextWindowSize || 32768) * 0.15);
528479
528734
  const maxRecoverFiles = tier === "small" ? 3 : tier === "medium" ? 4 : 5;
528480
528735
  const recoveredFiles = [];
@@ -528543,18 +528798,18 @@ ${content.slice(0, 8e3)}
528543
528798
  return sum + chars;
528544
528799
  }, 0) / 4;
528545
528800
  const safetyTarget = Math.floor(ctxWindow * 0.65);
528546
- let trimmedRecent = [...recent];
528801
+ let trimmedRecent = [...filteredRecent];
528547
528802
  while (estimateResult(result) > safetyTarget && trimmedRecent.length > 2) {
528548
528803
  trimmedRecent = trimmedRecent.slice(1);
528549
528804
  while (trimmedRecent.length > 1 && trimmedRecent[0]?.role === "tool") {
528550
528805
  trimmedRecent = trimmedRecent.slice(1);
528551
528806
  }
528552
- result = [...head, compactionMsg, ...trimmedRecent];
528807
+ result = [...head, compactionMsg, ...stickyToKeep, ...trimmedRecent];
528553
528808
  }
528554
- if (trimmedRecent.length < recent.length) {
528809
+ if (trimmedRecent.length < filteredRecent.length) {
528555
528810
  this.emit({
528556
528811
  type: "status",
528557
- content: `Post-compaction trim: reduced recent from ${recent.length} to ${trimmedRecent.length} messages to fit ${ctxWindow.toLocaleString()}-token context window`,
528812
+ content: `Post-compaction trim: reduced recent from ${filteredRecent.length} to ${trimmedRecent.length} messages to fit ${ctxWindow.toLocaleString()}-token context window`,
528558
528813
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
528559
528814
  });
528560
528815
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.500",
3
+ "version": "0.187.502",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "open-agents-ai",
9
- "version": "0.187.500",
9
+ "version": "0.187.502",
10
10
  "hasInstallScript": true,
11
11
  "license": "CC-BY-NC-4.0",
12
12
  "dependencies": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.500",
3
+ "version": "0.187.502",
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",