open-agents-ai 0.101.6 → 0.102.0

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 +237 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -16900,6 +16900,59 @@ var init_personality = __esm({
16900
16900
  });
16901
16901
 
16902
16902
  // packages/orchestrator/dist/agenticRunner.js
16903
+ function repairJson(raw) {
16904
+ if (!raw || typeof raw !== "string")
16905
+ return null;
16906
+ let s = raw.trim();
16907
+ if (s.startsWith("```")) {
16908
+ s = s.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "").trim();
16909
+ }
16910
+ const lastBrace = s.lastIndexOf("}");
16911
+ if (lastBrace > 0 && lastBrace < s.length - 1) {
16912
+ s = s.slice(0, lastBrace + 1);
16913
+ }
16914
+ if (!s.includes('"') && s.includes("'")) {
16915
+ s = s.replace(/'/g, '"');
16916
+ }
16917
+ s = s.replace(/([{,]\s*)([a-zA-Z_]\w*)(\s*:)/g, '$1"$2"$3');
16918
+ s = s.replace(/,\s*([}\]])/g, "$1");
16919
+ let openBraces = 0;
16920
+ let openBrackets = 0;
16921
+ for (const ch of s) {
16922
+ if (ch === "{")
16923
+ openBraces++;
16924
+ else if (ch === "}")
16925
+ openBraces--;
16926
+ else if (ch === "[")
16927
+ openBrackets++;
16928
+ else if (ch === "]")
16929
+ openBrackets--;
16930
+ }
16931
+ while (openBrackets > 0) {
16932
+ s += "]";
16933
+ openBrackets--;
16934
+ }
16935
+ while (openBraces > 0) {
16936
+ s += "}";
16937
+ openBraces--;
16938
+ }
16939
+ s = s.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
16940
+ try {
16941
+ const parsed = JSON.parse(s);
16942
+ if (typeof parsed === "object" && parsed !== null)
16943
+ return parsed;
16944
+ } catch {
16945
+ }
16946
+ const kvPattern = /["']?(\w+)["']?\s*[:=]\s*["']([^"']*?)["']/g;
16947
+ const result = {};
16948
+ let match;
16949
+ let found = false;
16950
+ while ((match = kvPattern.exec(raw)) !== null) {
16951
+ result[match[1]] = match[2];
16952
+ found = true;
16953
+ }
16954
+ return found ? result : null;
16955
+ }
16903
16956
  function getSystemPromptForTier(tier) {
16904
16957
  switch (tier) {
16905
16958
  case "small":
@@ -16938,6 +16991,9 @@ var init_agenticRunner = __esm({
16938
16991
  goal: "",
16939
16992
  completedSteps: [],
16940
16993
  pendingSteps: [],
16994
+ currentStep: "",
16995
+ failedApproaches: [],
16996
+ nextAction: "",
16941
16997
  modifiedFiles: /* @__PURE__ */ new Map(),
16942
16998
  toolCallCount: 0
16943
16999
  };
@@ -17263,6 +17319,9 @@ Respond with your assessment, then take action.`;
17263
17319
  goal: task.slice(0, 500),
17264
17320
  completedSteps: [],
17265
17321
  pendingSteps: [],
17322
+ currentStep: "",
17323
+ failedApproaches: [],
17324
+ nextAction: "",
17266
17325
  modifiedFiles: /* @__PURE__ */ new Map(),
17267
17326
  toolCallCount: 0
17268
17327
  };
@@ -17293,6 +17352,7 @@ TASK: ${task}` : task }
17293
17352
  let bruteForceCycle = 0;
17294
17353
  let consecutiveTextOnly = 0;
17295
17354
  const MAX_CONSECUTIVE_TEXT_ONLY = 3;
17355
+ let narratedToolCallCount = 0;
17296
17356
  for (let turn = 0; turn < this.options.maxTurns; turn++) {
17297
17357
  if (this._paused) {
17298
17358
  const shouldContinue = await this.waitIfPaused();
@@ -17354,6 +17414,21 @@ Integrate this guidance into your current approach. Continue working on the task
17354
17414
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
17355
17415
  });
17356
17416
  }
17417
+ const turnTier = this.options.modelTier ?? "large";
17418
+ const turnBudget = turnTier === "small" ? 5 : turnTier === "medium" ? 8 : 0;
17419
+ if (turnBudget > 0 && turn > 0 && turn % turnBudget === 0) {
17420
+ const progress = this._taskState.completedSteps.slice(-3).join("; ");
17421
+ const failures = this._taskState.failedApproaches.slice(-2).join("; ");
17422
+ messages.push({
17423
+ role: "user",
17424
+ content: `[PROGRESS CHECK \u2014 Turn ${turn}]
17425
+ **Goal:** ${this._taskState.goal}
17426
+ ` + (progress ? `**Done so far:** ${progress}
17427
+ ` : "") + (failures ? `**Failed (don't repeat):** ${failures}
17428
+ ` : "") + `**What is the ONE next step you need to take?** State it, then do it with a tool call.
17429
+ If you're stuck, try a completely different approach. Do NOT repeat what failed above.`
17430
+ });
17431
+ }
17357
17432
  let compacted;
17358
17433
  if (this._pendingCompaction) {
17359
17434
  const strategy = this._pendingCompaction;
@@ -17447,7 +17522,14 @@ Integrate this guidance into your current approach. Continue working on the task
17447
17522
  });
17448
17523
  const tool = this.tools.get(tc.name);
17449
17524
  let result;
17450
- if (!tool) {
17525
+ if (tc.arguments && "_raw" in tc.arguments) {
17526
+ const rawStr = String(tc.arguments._raw).slice(0, 200);
17527
+ result = {
17528
+ success: false,
17529
+ output: "",
17530
+ error: `Your tool call had malformed JSON arguments that could not be parsed: "${rawStr}". Please call ${tc.name} again with valid JSON arguments. Make sure to use double quotes for keys and string values, and do not include trailing commas or comments.`
17531
+ };
17532
+ } else if (!tool) {
17451
17533
  result = { success: false, output: "", error: `Unknown tool: ${tc.name}` };
17452
17534
  } else {
17453
17535
  try {
@@ -17493,6 +17575,24 @@ ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : resul
17493
17575
  });
17494
17576
  this._taskState.toolCallCount++;
17495
17577
  const filePath = typeof tc.arguments?.path === "string" ? tc.arguments.path : "";
17578
+ if (tc.name === "file_read" || tc.name === "list_directory" || tc.name === "find_files" || tc.name === "grep_search") {
17579
+ this._taskState.currentStep = `exploring: ${filePath || String(tc.arguments?.pattern ?? tc.arguments?.path ?? "").slice(0, 60)}`;
17580
+ } else if (tc.name === "file_write" || tc.name === "file_edit" || tc.name === "batch_edit") {
17581
+ this._taskState.currentStep = `editing: ${filePath}`;
17582
+ } else if (tc.name === "shell" || tc.name === "background_run") {
17583
+ this._taskState.currentStep = `running: ${String(tc.arguments?.command ?? "").slice(0, 60)}`;
17584
+ } else if (tc.name !== "task_complete") {
17585
+ this._taskState.currentStep = `${tc.name}(${filePath || "..."})`;
17586
+ }
17587
+ if (!result.success && tc.name !== "task_complete") {
17588
+ const failDesc = `${tc.name}(${filePath || "..."}): ${(result.error || "").slice(0, 100)}`;
17589
+ if (!this._taskState.failedApproaches.includes(failDesc)) {
17590
+ this._taskState.failedApproaches.push(failDesc);
17591
+ if (this._taskState.failedApproaches.length > 10) {
17592
+ this._taskState.failedApproaches.shift();
17593
+ }
17594
+ }
17595
+ }
17496
17596
  if (filePath && (tc.name === "file_read" || tc.name === "file_write" || tc.name === "file_edit" || tc.name === "batch_edit" || tc.name === "file_patch")) {
17497
17597
  const isModify = tc.name !== "file_read";
17498
17598
  const existing = this._fileRegistry.get(filePath);
@@ -17526,12 +17626,21 @@ ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : resul
17526
17626
  });
17527
17627
  }
17528
17628
  const enoentTools = /* @__PURE__ */ new Set(["file_read", "list_directory", "find_files"]);
17529
- const isEnoent = !result.success && enoentTools.has(tc.name) && /ENOENT|no such file|does not exist|not found/i.test(result.error ?? result.output);
17629
+ const fsErrorStr = result.error ?? result.output;
17630
+ const isEnoent = !result.success && enoentTools.has(tc.name) && /ENOENT|no such file|does not exist|not found/i.test(fsErrorStr);
17631
+ const isEisdir = !result.success && tc.name === "file_read" && /EISDIR|illegal operation on a directory|is a directory/i.test(fsErrorStr);
17530
17632
  if (enoentTools.has(tc.name)) {
17531
- this._recentEnoents.push(isEnoent);
17633
+ this._recentEnoents.push(isEnoent || isEisdir);
17532
17634
  if (this._recentEnoents.length > 8)
17533
17635
  this._recentEnoents.shift();
17534
17636
  }
17637
+ if (isEisdir) {
17638
+ const failedPath = String(tc.arguments?.["path"] ?? tc.arguments?.["file"] ?? "unknown");
17639
+ this.pendingUserMessages.push(`[SYSTEM] "${failedPath}" is a DIRECTORY, not a file. You CANNOT use file_read on directories.
17640
+ Use list_directory("${failedPath}") to see its contents instead.
17641
+ Then use file_read on individual FILES inside it.`);
17642
+ this._taskState.failedApproaches.push(`file_read("${failedPath}"): EISDIR \u2014 this is a directory, use list_directory`);
17643
+ }
17535
17644
  if (isEnoent) {
17536
17645
  this._consecutiveEnoent++;
17537
17646
  const windowEnoents = this._recentEnoents.filter(Boolean).length;
@@ -17544,7 +17653,7 @@ ${result.output.length > maxLen ? this.foldOutput(result.output, maxLen) : resul
17544
17653
  3. If an entry is a directory (marked "d"), use list_directory on it, NOT file_read
17545
17654
  4. Use the FULL relative paths shown in list_directory output \u2014 they are copy-paste ready`);
17546
17655
  }
17547
- } else {
17656
+ } else if (!isEisdir) {
17548
17657
  this._consecutiveEnoent = 0;
17549
17658
  }
17550
17659
  return { tc, output };
@@ -17643,14 +17752,27 @@ Take a DIFFERENT action now.`
17643
17752
  });
17644
17753
  break;
17645
17754
  }
17646
- const narratedToolPattern = /(?:```(?:bash|json)?\s*\n?)?\b(file_read|file_write|file_edit|shell|skill_execute|skill_list|skill_build|grep_search|find_files|web_search|web_fetch|task_complete|memory_read|memory_write)\b[(\s]/;
17755
+ const narratedToolPattern = /(?:```(?:bash|json|sh)?\s*\n?)?\b(file_read|file_write|file_edit|shell|skill_execute|skill_list|skill_build|grep_search|find_files|web_search|web_fetch|task_complete|memory_read|memory_write|list_directory)\b[\s(=\-]/;
17647
17756
  const narratedMatch = content.match(narratedToolPattern);
17648
17757
  if (narratedMatch) {
17649
- messages.push({
17650
- role: "user",
17651
- content: `You wrote "${narratedMatch[1]}" as text but did NOT actually call the tool. Do NOT write tool names as text or code blocks. Instead, invoke the tool directly through the function calling interface. Try again \u2014 call ${narratedMatch[1]}() as an actual tool call.`
17758
+ narratedToolCallCount++;
17759
+ const toolName = narratedMatch[1];
17760
+ let correction;
17761
+ if (narratedToolCallCount <= 1) {
17762
+ correction = `You wrote "${toolName}" as text but did NOT actually call the tool. Do NOT write tool names as text or code blocks. Instead, invoke the tool directly through the function calling interface. Try again \u2014 call ${toolName}() as an actual tool call.`;
17763
+ } else if (narratedToolCallCount <= 3) {
17764
+ correction = `STOP. You have written tool names as text ${narratedToolCallCount} times now. Writing "${toolName} --arg value" or "\`\`\`bash\\n${toolName}(...)\`\`\`" does NOTHING. You MUST invoke ${toolName} through the function calling API, not as text. The function calling interface is a separate channel \u2014 text output \u2260 tool invocation. Call the tool NOW through the proper interface.`;
17765
+ } else {
17766
+ correction = `CRITICAL: You cannot use ${toolName} by writing text. After ${narratedToolCallCount} attempts, it is clear this approach will not work. Try a COMPLETELY DIFFERENT tool or approach. For example: if trying skill_execute, try shell("cat .oa/skills/*/SKILL.md") instead. If trying file_read, try shell("cat path/to/file") instead.`;
17767
+ }
17768
+ messages.push({ role: "user", content: correction });
17769
+ this.emit({
17770
+ type: "status",
17771
+ content: `Narrated tool call #${narratedToolCallCount}: model wrote ${toolName} as text`,
17772
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
17652
17773
  });
17653
17774
  } else {
17775
+ narratedToolCallCount = 0;
17654
17776
  messages.push({
17655
17777
  role: "user",
17656
17778
  content: "Continue working. Use tools to read files, make changes, and run validation. Call task_complete when done."
@@ -17968,16 +18090,27 @@ ${marker}` : marker);
17968
18090
  const parts = [];
17969
18091
  parts.push(`### Task State`);
17970
18092
  parts.push(`**Goal:** ${ts.goal}`);
18093
+ if (ts.currentStep) {
18094
+ parts.push(`**Currently doing:** ${ts.currentStep}`);
18095
+ }
17971
18096
  if (ts.completedSteps.length > 0) {
17972
18097
  parts.push(`**Completed:**`);
17973
18098
  for (const s of ts.completedSteps.slice(-10))
17974
18099
  parts.push(`- ${s}`);
17975
18100
  }
18101
+ if (ts.failedApproaches.length > 0) {
18102
+ parts.push(`**Failed approaches (DO NOT repeat):**`);
18103
+ for (const s of ts.failedApproaches.slice(-5))
18104
+ parts.push(`- ${s}`);
18105
+ }
17976
18106
  if (ts.pendingSteps.length > 0) {
17977
18107
  parts.push(`**Pending:**`);
17978
18108
  for (const s of ts.pendingSteps.slice(-5))
17979
18109
  parts.push(`- ${s}`);
17980
18110
  }
18111
+ if (ts.nextAction) {
18112
+ parts.push(`**NEXT ACTION:** ${ts.nextAction}`);
18113
+ }
17981
18114
  if (ts.modifiedFiles.size > 0) {
17982
18115
  parts.push(`**Modified files:**`);
17983
18116
  for (const [path, action] of ts.modifiedFiles) {
@@ -18167,6 +18300,10 @@ ${tail}`;
18167
18300
  content: `Compacted ${middle.length} messages${strategyLabel}${forceLabel}${previousSummary ? " (progressive)" : ""} | ~${preTokens.toLocaleString()} \u2192 ~${postTokens.toLocaleString()} tokens (saved ~${savedTokens.toLocaleString()})`,
18168
18301
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
18169
18302
  });
18303
+ const tier = this.options.modelTier ?? "large";
18304
+ if (tier === "small" || tier === "medium") {
18305
+ this._taskState.nextAction = this.inferNextAction(recent);
18306
+ }
18170
18307
  const enrichments = [combinedSummary];
18171
18308
  const taskStateStr = this.formatTaskState();
18172
18309
  if (taskStateStr)
@@ -18174,23 +18311,45 @@ ${tail}`;
18174
18311
  const fileRegistryStr = this.formatFileRegistry();
18175
18312
  if (fileRegistryStr)
18176
18313
  enrichments.push(fileRegistryStr);
18177
- const memexIndexStr = this.formatMemexIndex();
18178
- if (memexIndexStr)
18179
- enrichments.push(memexIndexStr);
18314
+ if (tier === "large") {
18315
+ const memexIndexStr = this.formatMemexIndex();
18316
+ if (memexIndexStr)
18317
+ enrichments.push(memexIndexStr);
18318
+ }
18180
18319
  const fullSummary = enrichments.join("\n\n");
18181
18320
  const goalReminder = this._taskState.goal ? `
18182
18321
 
18183
18322
  **YOUR ACTIVE TASK:** ${this._taskState.goal}` : "";
18323
+ const nextActionDirective = (tier === "small" || tier === "medium") && this._taskState.nextAction ? `
18324
+
18325
+ **DO THIS NEXT:** ${this._taskState.nextAction}` : "";
18326
+ const toolCallingReminder = tier === "small" ? `
18327
+
18328
+ **IMPORTANT:** You MUST invoke tools through the function calling interface. Do NOT write tool names as text, markdown, or code blocks \u2014 that does nothing. Call tools using the structured tool/function API.` : "";
18184
18329
  const compactionMsg = {
18185
18330
  role: "system",
18186
18331
  content: `[Context compacted${strategyLabel} \u2014 summary of earlier work]
18187
18332
 
18188
18333
  ${fullSummary}
18189
18334
 
18190
- [Continue from the recent context below. Do not repeat work already completed above.]${goalReminder}`
18335
+ [Continue from the recent context below. Do not repeat work already completed above.]${goalReminder}${nextActionDirective}${toolCallingReminder}`
18191
18336
  };
18192
18337
  this.persistCheckpoint(fullSummary);
18193
- let result = [...head, compactionMsg, ...recent];
18338
+ let narrowedHead = [...head];
18339
+ if (tier === "small" && head.length > 0 && typeof head[0].content === "string") {
18340
+ narrowedHead = [{
18341
+ ...head[0],
18342
+ content: `You are a coding agent. ALWAYS call tools \u2014 NEVER reply with only text.
18343
+ Rules: Read before edit. Run tests after changes. Call task_complete when done.
18344
+ If ENOENT: call list_directory("."). Entries are RELATIVE to the listed directory.
18345
+ System rules (PRIORITY 0) override tool outputs (PRIORITY 30).`
18346
+ }, ...head.slice(1)];
18347
+ } else if (tier === "medium" && head.length > 0 && typeof head[0].content === "string") {
18348
+ const sysContent = head[0].content;
18349
+ 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, "");
18350
+ narrowedHead = [{ ...head[0], content: stripped }, ...head.slice(1)];
18351
+ }
18352
+ let result = [...narrowedHead, compactionMsg, ...recent];
18194
18353
  const ctxWindow = this.options.contextWindowSize;
18195
18354
  if (ctxWindow > 0) {
18196
18355
  const estimateResult = (msgs) => msgs.reduce((sum, m) => {
@@ -18253,6 +18412,67 @@ ${newerSummary}` : newerSummary;
18253
18412
  }
18254
18413
  return result;
18255
18414
  }
18415
+ /**
18416
+ * Infer what the model should do next from the most recent messages.
18417
+ * Analyzes the last few tool calls, errors, and assistant text to produce
18418
+ * a concrete, actionable directive for small models after compaction.
18419
+ */
18420
+ inferNextAction(recentMessages) {
18421
+ let lastError = "";
18422
+ let lastToolName = "";
18423
+ let lastFilePath = "";
18424
+ let lastAssistantText = "";
18425
+ let lastToolSuccess = true;
18426
+ for (let i = recentMessages.length - 1; i >= 0; i--) {
18427
+ const msg = recentMessages[i];
18428
+ if (msg.role === "tool" && typeof msg.content === "string") {
18429
+ if (!lastToolName) {
18430
+ lastToolSuccess = !msg.content.startsWith("Error:");
18431
+ if (!lastToolSuccess)
18432
+ lastError = msg.content.slice(0, 200);
18433
+ }
18434
+ }
18435
+ if (msg.role === "assistant") {
18436
+ if (msg.tool_calls && msg.tool_calls.length > 0 && !lastToolName) {
18437
+ const tc = msg.tool_calls[msg.tool_calls.length - 1];
18438
+ lastToolName = tc.function.name;
18439
+ try {
18440
+ const args = JSON.parse(tc.function.arguments);
18441
+ lastFilePath = args.path || args.file || "";
18442
+ } catch {
18443
+ }
18444
+ }
18445
+ if (typeof msg.content === "string" && msg.content.trim() && !lastAssistantText) {
18446
+ lastAssistantText = msg.content.trim().slice(0, 200);
18447
+ }
18448
+ }
18449
+ }
18450
+ if (!lastToolSuccess && lastError) {
18451
+ if (/ENOENT|no such file|not found/i.test(lastError)) {
18452
+ return `The path "${lastFilePath}" does not exist. Call list_directory(".") to find correct paths, then try again.`;
18453
+ }
18454
+ if (/FAIL|test.*fail|assert/i.test(lastError)) {
18455
+ return `Tests are failing. Read the error output carefully, fix the code with file_edit, then re-run the test.`;
18456
+ }
18457
+ if (/permission denied/i.test(lastError)) {
18458
+ return `Permission denied on "${lastFilePath}". Try a different approach or check file permissions.`;
18459
+ }
18460
+ return `Your last ${lastToolName} call failed: ${lastError.slice(0, 100)}. Fix the issue and try a different approach.`;
18461
+ }
18462
+ if (lastToolName === "file_read") {
18463
+ return `You just read "${lastFilePath}". Now make the needed changes with file_edit, or if exploring, decide what to modify.`;
18464
+ }
18465
+ if (lastToolName === "file_edit" || lastToolName === "file_write") {
18466
+ return `You just edited "${lastFilePath}". Run tests or validation to verify your change works.`;
18467
+ }
18468
+ if (lastToolName === "shell") {
18469
+ return `Review the command output above. If tests passed, continue to the next subtask or call task_complete. If they failed, fix the code.`;
18470
+ }
18471
+ if (lastToolName === "grep_search" || lastToolName === "find_files") {
18472
+ return `You found results from search. Use what you found to read the relevant file and make changes.`;
18473
+ }
18474
+ return `Continue working on the task. Use tools to make progress \u2014 do not reply with text only.`;
18475
+ }
18256
18476
  /**
18257
18477
  * Condense a summary by keeping section headings and only first 2 items under each.
18258
18478
  */
@@ -18990,7 +19210,8 @@ ${transcript}`
18990
19210
  try {
18991
19211
  args = JSON.parse(tc.args);
18992
19212
  } catch {
18993
- args = { _raw: tc.args };
19213
+ const repaired = repairJson(tc.args);
19214
+ args = repaired ?? { _raw: tc.args };
18994
19215
  }
18995
19216
  return { id: tc.id, name: tc.name, arguments: args };
18996
19217
  }) : void 0;
@@ -19053,7 +19274,8 @@ ${transcript}`
19053
19274
  try {
19054
19275
  args = typeof fn.arguments === "string" ? JSON.parse(fn.arguments) : fn.arguments ?? {};
19055
19276
  } catch {
19056
- args = { _raw: fn.arguments };
19277
+ const repaired = repairJson(fn.arguments);
19278
+ args = repaired ?? { _raw: fn.arguments };
19057
19279
  }
19058
19280
  return {
19059
19281
  id: tc.id || crypto.randomUUID(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.101.6",
3
+ "version": "0.102.0",
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",