open-agents-ai 0.187.460 → 0.187.462

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
@@ -254253,6 +254253,25 @@ var init_todo_write = __esm({
254253
254253
  }
254254
254254
  const sessionId = getTodoSessionId();
254255
254255
  const oldTodos = readTodos(sessionId);
254256
+ const canonicalize = (todos) => JSON.stringify(todos.map((t2) => ({
254257
+ content: t2.content,
254258
+ status: t2.status,
254259
+ parentId: t2.parentId ?? null,
254260
+ blocker: t2.blocker ?? null
254261
+ })));
254262
+ const oldKey = canonicalize(oldTodos);
254263
+ const newKey = canonicalize(incoming);
254264
+ if (oldKey === newKey && oldTodos.length > 0) {
254265
+ return {
254266
+ success: true,
254267
+ output: JSON.stringify({
254268
+ reminder: "[NO-OP] You called todo_write with the same plan you already have. The list is unchanged. Use todo_write ONLY to add new tasks, mark a task in_progress / completed, or record a blocker — not as a way to re-read your plan (use todo_read for that, but the current plan is also surfaced automatically in your context). Proceed with the current in_progress task.",
254269
+ todos: oldTodos,
254270
+ noop: true
254271
+ }),
254272
+ durationMs: performance.now() - start2
254273
+ };
254274
+ }
254256
254275
  const result = writeTodos(sessionId, incoming);
254257
254276
  const closedCount = result.newTodos.filter((t2) => t2.status === "completed").length;
254258
254277
  const oldClosedCount = oldTodos.filter((t2) => t2.status === "completed").length;
@@ -518395,6 +518414,115 @@ ${body}`;
518395
518414
  * Returns null when the disable knob is set or the backend is missing the
518396
518415
  * chatCompletion method.
518397
518416
  */
518417
+ /**
518418
+ * REG-3: Render the current todo list as a compact transient block so the
518419
+ * agent can read its own plan without calling todo_read or re-emitting
518420
+ * todo_write as a scratchpad. Returns null when there are no todos yet.
518421
+ */
518422
+ _renderTodoStateBlock(turn) {
518423
+ try {
518424
+ const { readTodos: readTodos2 } = __require("@open-agents/execution");
518425
+ const { getTodoSessionId: getTodoSessionId2 } = __require("@open-agents/execution");
518426
+ const sessionId = getTodoSessionId2();
518427
+ const todos = readTodos2(sessionId);
518428
+ if (!todos || todos.length === 0)
518429
+ return null;
518430
+ const lines = ["[CURRENT TODO PLAN — already in your state, no need to call todo_write to see it]"];
518431
+ const inProg = todos.filter((t2) => t2.status === "in_progress");
518432
+ const pending = todos.filter((t2) => t2.status === "pending");
518433
+ const done = todos.filter((t2) => t2.status === "completed");
518434
+ const blocked = todos.filter((t2) => t2.status === "blocked");
518435
+ if (inProg.length > 0) {
518436
+ lines.push(`▶ in_progress (${inProg.length}):`);
518437
+ for (const t2 of inProg)
518438
+ lines.push(` • ${t2.content}`);
518439
+ }
518440
+ if (pending.length > 0) {
518441
+ lines.push(`◯ pending (${pending.length}):`);
518442
+ for (const t2 of pending.slice(0, 8))
518443
+ lines.push(` • ${t2.content}`);
518444
+ if (pending.length > 8)
518445
+ lines.push(` … +${pending.length - 8} more`);
518446
+ }
518447
+ if (blocked.length > 0) {
518448
+ lines.push(`✕ blocked (${blocked.length}):`);
518449
+ for (const t2 of blocked)
518450
+ lines.push(` • ${t2.content}${t2.blocker ? ` — ${t2.blocker}` : ""}`);
518451
+ }
518452
+ if (done.length > 0) {
518453
+ lines.push(`✓ completed (${done.length}): ${done.map((t2) => t2.content.slice(0, 40)).join(" | ")}`);
518454
+ }
518455
+ lines.push(`(turn ${turn} — call todo_write ONLY to update state, never to re-read this plan)`);
518456
+ return lines.join("\n");
518457
+ } catch {
518458
+ return null;
518459
+ }
518460
+ }
518461
+ /**
518462
+ * REG-2: Render a compact "files known to me" block from `_worldFacts`.
518463
+ * Goal: tell the model what it has already touched this run so it stops
518464
+ * re-running `find src -type f` and `list_directory("...")` to confirm
518465
+ * its own outputs.
518466
+ *
518467
+ * Returns null when there is nothing useful to surface yet.
518468
+ */
518469
+ _renderFilesystemStateBlock(turn) {
518470
+ const wf = this._worldFacts;
518471
+ if (!wf)
518472
+ return null;
518473
+ const files = [...wf.files.entries()];
518474
+ const lists = [...wf.lastLists.entries()];
518475
+ if (files.length === 0 && lists.length === 0 && !wf.lastTest?.summary)
518476
+ return null;
518477
+ const writes = files.filter(([, v]) => (v.writeCount ?? 0) > 0);
518478
+ const reads = files.filter(([, v]) => (v.writeCount ?? 0) === 0);
518479
+ const groupByDir = (entries) => {
518480
+ const m2 = /* @__PURE__ */ new Map();
518481
+ for (const [p2] of entries) {
518482
+ const slash = p2.lastIndexOf("/");
518483
+ const dir = slash > 0 ? p2.slice(0, slash) : ".";
518484
+ const arr = m2.get(dir) ?? [];
518485
+ arr.push(p2);
518486
+ m2.set(dir, arr);
518487
+ }
518488
+ return m2;
518489
+ };
518490
+ const renderGroup = (m2, cap) => {
518491
+ const lines = [];
518492
+ const sorted = [...m2.entries()].sort((a2, b) => b[1].length - a2[1].length);
518493
+ let total = 0;
518494
+ for (const [dir, paths] of sorted) {
518495
+ if (total >= cap) {
518496
+ lines.push(` ... and ${sorted.slice(lines.length).reduce((s2, [, p2]) => s2 + p2.length, 0)} more files in other dirs`);
518497
+ break;
518498
+ }
518499
+ const show = paths.slice(0, 6);
518500
+ lines.push(` ${dir}/ → ${show.map((p2) => p2.slice(dir.length + 1)).join(", ")}${paths.length > show.length ? ` (+${paths.length - show.length} more)` : ""}`);
518501
+ total += paths.length;
518502
+ }
518503
+ return lines.join("\n");
518504
+ };
518505
+ const sections = [];
518506
+ sections.push("[FILES KNOWN — already touched this run; do not re-discover via find/list_directory unless filesystem may have changed externally]");
518507
+ if (writes.length > 0) {
518508
+ sections.push(`Files YOU WROTE this run (${writes.length}):`);
518509
+ sections.push(renderGroup(groupByDir(writes), 40));
518510
+ }
518511
+ if (reads.length > 0) {
518512
+ sections.push(`Files YOU READ this run (${reads.length}):`);
518513
+ sections.push(renderGroup(groupByDir(reads), 20));
518514
+ }
518515
+ if (lists.length > 0) {
518516
+ const recentLists = lists.sort((a2, b) => b[1].lastSeenTurn - a2[1].lastSeenTurn).slice(0, 8);
518517
+ sections.push(`Directories you've listed (${lists.length}):`);
518518
+ sections.push(recentLists.map(([dir, info]) => ` ${dir} (${info.entriesCount} entries, turn ${info.lastSeenTurn})`).join("\n"));
518519
+ }
518520
+ if (wf.lastTest?.summary) {
518521
+ sections.push(`Last test outcome: ${wf.lastTest.passed ? "PASSED" : "FAILED"} (turn ${wf.lastTest.turn ?? "?"})`);
518522
+ }
518523
+ sections.push(`(turn ${turn} — this block is regenerated every turn from your tool history)`);
518524
+ return sections.join("\n");
518525
+ }
518398
518526
  makePhaseSummarizer() {
518399
518527
  if (process.env["OA_DISABLE_PHASE_SUMMARIZER"] === "1")
518400
518528
  return null;
@@ -519095,7 +519223,7 @@ TASK: ${task}` : task;
519095
519223
  const recentToolResults = /* @__PURE__ */ new Map();
519096
519224
  const toolCallBudget = /* @__PURE__ */ new Map();
519097
519225
  const loopTier = this.options.modelTier ?? "large";
519098
- const toolBudgets = loopTier === "small" ? { web_search: 6, web_fetch: 4, list_directory: 8, find_files: 6, grep_search: 8 } : loopTier === "medium" ? { web_search: 10, web_fetch: 8, list_directory: 12, find_files: 10, grep_search: 12 } : { web_search: 20, web_fetch: 15, list_directory: 20, find_files: 15, grep_search: 20 };
519226
+ const toolBudgets = loopTier === "small" ? { web_search: 6, web_fetch: 4, list_directory: 12, find_files: 10, grep_search: 12 } : loopTier === "medium" ? { web_search: 10, web_fetch: 8, list_directory: 18, find_files: 14, grep_search: 18 } : { web_search: 20, web_fetch: 15, list_directory: 30, find_files: 20, grep_search: 30 };
519099
519227
  for (const [tool, budget] of Object.entries(toolBudgets)) {
519100
519228
  toolCallBudget.set(tool, budget);
519101
519229
  }
@@ -519462,6 +519590,22 @@ ${memoryLines.join("\n")}`
519462
519590
  }
519463
519591
  });
519464
519592
  }
519593
+ const _injections = [];
519594
+ const fsBlock = this._renderFilesystemStateBlock(turn);
519595
+ if (fsBlock)
519596
+ _injections.push(fsBlock);
519597
+ const todoBlock = this._renderTodoStateBlock(turn);
519598
+ if (todoBlock)
519599
+ _injections.push(todoBlock);
519600
+ if (_injections.length > 0) {
519601
+ const reqMsgs = chatRequest.messages;
519602
+ if (Array.isArray(reqMsgs)) {
519603
+ const insertAt = Math.max(0, reqMsgs.length - 1);
519604
+ for (const blk of _injections) {
519605
+ reqMsgs.splice(insertAt, 0, { role: "system", content: blk });
519606
+ }
519607
+ }
519608
+ }
519465
519609
  let response;
519466
519610
  try {
519467
519611
  response = this.options.streamEnabled && this.hasStreamingSupport() ? await this.streamingRequest(chatRequest, turn) : await this.backend.chatCompletion(chatRequest);
@@ -519741,14 +519885,56 @@ ${memoryLines.join("\n")}`
519741
519885
  content: `Phase contraction: ${contracted.join(", ")} → contracted (${summarizer ? "LLM-summarized" : "stub-summarized"}, anchors retained)`,
519742
519886
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
519743
519887
  });
519888
+ try {
519889
+ const archDir = __require("node:path").join(process.cwd(), ".oa", "phases");
519890
+ __require("node:fs").mkdirSync(archDir, { recursive: true });
519891
+ for (const phaseName of contracted) {
519892
+ const node = this._contextTree.getSnapshot().phases[phaseName];
519893
+ if (!node)
519894
+ continue;
519895
+ const stamp = `${phaseName}-${Date.now().toString(36)}-turn${turn}`;
519896
+ const archPath = __require("node:path").join(archDir, `${stamp}.jsonl`);
519897
+ const archived = {
519898
+ phase: phaseName,
519899
+ contractedAtTurn: turn,
519900
+ contractedAtIso: (/* @__PURE__ */ new Date()).toISOString(),
519901
+ anchors: node.anchors,
519902
+ summary: node.summary ?? null,
519903
+ messages: node.messages ?? []
519904
+ };
519905
+ __require("node:fs").writeFileSync(archPath, JSON.stringify(archived) + "\n", "utf-8");
519906
+ this._contextTree.archive(phaseName, archPath);
519907
+ }
519908
+ this.emit({
519909
+ type: "status",
519910
+ content: `Phase archive: ${contracted.length} phase(s) written to .oa/phases/`,
519911
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519912
+ });
519913
+ } catch (archErr) {
519914
+ this.emit({
519915
+ type: "status",
519916
+ content: `Phase archive failed (non-fatal): ${archErr instanceof Error ? archErr.message : String(archErr)}`,
519917
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519918
+ });
519919
+ }
519744
519920
  }
519745
519921
  this._phaseMessageStartIdx = messages2.length;
519746
519922
  this._taskState.phase = transition.to;
519747
519923
  this._taskState.phaseSince = turn;
519924
+ for (const [tool2, budget] of Object.entries(toolBudgets)) {
519925
+ toolCallBudget.set(tool2, budget);
519926
+ }
519927
+ this.emit({
519928
+ type: "status",
519929
+ content: `Tool budgets reset for new phase (${Object.keys(toolBudgets).length} tools)`,
519930
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
519931
+ });
519748
519932
  }
519749
519933
  }
519934
+ const _argsKeyForBudget = `${tc.name}:${argsKey}`;
519935
+ const _isCachedHit = recentToolResults.has(_argsKeyForBudget);
519750
519936
  const budgetRemaining = toolCallBudget.get(tc.name);
519751
- if (budgetRemaining !== void 0) {
519937
+ if (budgetRemaining !== void 0 && !_isCachedHit) {
519752
519938
  if (budgetRemaining <= 0) {
519753
519939
  this.emit({
519754
519940
  type: "tool_call",
@@ -519757,7 +519943,7 @@ ${memoryLines.join("\n")}`
519757
519943
  turn,
519758
519944
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
519759
519945
  });
519760
- const budgetMsg = `[BUDGET EXHAUSTED] You have used all ${toolBudgets[tc.name]} allowed ${tc.name} calls for this task. You ALREADY have enough information from previous calls. DO NOT try to call ${tc.name} again — it will be blocked. Summarize what you found and call task_complete with your answer NOW.`;
519946
+ const budgetMsg = `[BUDGET EXHAUSTED] You have used all ${toolBudgets[tc.name]} allowed ${tc.name} calls for the current phase. You ALREADY have enough information from previous calls. DO NOT try to call ${tc.name} again — it will be blocked. If your todo list shows more phases pending: mark the current phase completed via todo_write so a new budget allowance kicks in. If all phases are done: call task_complete with your final summary.`;
519761
519947
  this.emit({
519762
519948
  type: "tool_result",
519763
519949
  toolName: tc.name,
@@ -519939,10 +520125,33 @@ ${cachedEntry2.result.slice(0, 500)}` : `[BLOCKED — the observer confirmed thi
519939
520125
  updated.lastOutcomeTurn = turn;
519940
520126
  this._argCohorts.set(cohortKey, updated);
519941
520127
  try {
520128
+ if (tc.name === "file_write" || tc.name === "file_edit" || tc.name === "edit_block") {
520129
+ const p2 = String(tc.arguments?.["path"] ?? tc.arguments?.["file_path"] ?? tc.arguments?.["file"] ?? "");
520130
+ if (p2 && result.success) {
520131
+ const prev = this._worldFacts.files.get(p2);
520132
+ this._worldFacts.files.set(p2, {
520133
+ exists: true,
520134
+ size: prev?.size,
520135
+ hashSample: prev?.hashSample,
520136
+ lastSeenTurn: turn,
520137
+ lastWriteTurn: turn,
520138
+ writeCount: (prev?.writeCount ?? 0) + 1
520139
+ });
520140
+ }
520141
+ }
519942
520142
  if (tc.name === "file_read") {
519943
520143
  const p2 = String(tc.arguments?.["path"] ?? tc.arguments?.["file"] ?? "");
519944
- if (p2)
519945
- this._worldFacts.files.set(p2, { exists: result.success, size: (result.output || "").length, hashSample: (result.output || "").slice(0, 32), lastSeenTurn: turn });
520144
+ if (p2) {
520145
+ const prev = this._worldFacts.files.get(p2);
520146
+ this._worldFacts.files.set(p2, {
520147
+ exists: result.success,
520148
+ size: (result.output || "").length,
520149
+ hashSample: (result.output || "").slice(0, 32),
520150
+ lastSeenTurn: turn,
520151
+ lastWriteTurn: prev?.lastWriteTurn,
520152
+ writeCount: prev?.writeCount
520153
+ });
520154
+ }
519946
520155
  if (this._fileSummaryStore && result.success && result.output && result.output.length > 100) {
519947
520156
  try {
519948
520157
  const existing = this._fileSummaryStore.get(p2);
@@ -542292,6 +542501,23 @@ async function ensureExpandedContext(modelName, backendUrl) {
542292
542501
  return { model: modelName, created: false, contextLabel: "", numCtx: 0 };
542293
542502
  }
542294
542503
  if (typeof existing === "string") {
542504
+ const lostTools = await wrapperLacksToolsCapability(backendUrl, existing).catch(() => false);
542505
+ if (lostTools) {
542506
+ try {
542507
+ const rebuilt = await createExpandedVariantNamedAsync(
542508
+ stripVariantTag(existing),
542509
+ modelName,
542510
+ specs,
542511
+ sizeGB,
542512
+ kvInfo?.kvBytesPerToken,
542513
+ kvInfo?.archMax
542514
+ );
542515
+ if (rebuilt) {
542516
+ return { model: rebuilt, created: true, contextLabel: ctx3.label, numCtx: ctx3.numCtx };
542517
+ }
542518
+ } catch {
542519
+ }
542520
+ }
542295
542521
  const repair = await repairExpandedVariantIfStale(
542296
542522
  existing,
542297
542523
  modelName,
@@ -542310,6 +542536,38 @@ async function ensureExpandedContext(modelName, backendUrl) {
542310
542536
  }
542311
542537
  return { model: modelName, created: false, contextLabel: ctx3.label, numCtx: ctx3.numCtx };
542312
542538
  }
542539
+ function guessBaseFromVariant(variantName, models) {
542540
+ const stripped = stripVariantTag(variantName);
542541
+ for (const m2 of models) {
542542
+ if (/^open-agents-/i.test(m2.name)) continue;
542543
+ const candidates = expandedModelCandidates(stripVariantTag(m2.name));
542544
+ if (candidates.includes(stripped)) {
542545
+ return m2.name;
542546
+ }
542547
+ }
542548
+ return null;
542549
+ }
542550
+ async function wrapperLacksToolsCapability(backendUrl, modelName) {
542551
+ try {
542552
+ const normalized = backendUrl.replace(/\/+$/, "");
542553
+ const showRes = await fetch(`${normalized}/api/show`, {
542554
+ method: "POST",
542555
+ headers: { "Content-Type": "application/json" },
542556
+ body: JSON.stringify({ name: modelName }),
542557
+ signal: AbortSignal.timeout(5e3)
542558
+ });
542559
+ if (!showRes.ok) return false;
542560
+ const showData = await showRes.json();
542561
+ const caps = Array.isArray(showData.capabilities) ? showData.capabilities : [];
542562
+ const hasTools = caps.some((c9) => /^tools?$/i.test(c9));
542563
+ if (hasTools) return false;
542564
+ const fromMatch = showData.modelfile?.match(/^FROM\s+(.+)$/m);
542565
+ const fromVal = fromMatch?.[1]?.trim() ?? "";
542566
+ return fromVal.startsWith("/") || /blobs\/sha256[-:]/.test(fromVal);
542567
+ } catch {
542568
+ return false;
542569
+ }
542570
+ }
542313
542571
  async function repairAllExpandedVariants(backendUrl) {
542314
542572
  const specs = await detectSystemSpecsAsync();
542315
542573
  const models = await fetchOllamaModels(backendUrl);
@@ -542327,25 +542585,47 @@ async function repairAllExpandedVariants(backendUrl) {
542327
542585
  summary.failed.push({ model: variant.name, reason: "missing-state" });
542328
542586
  continue;
542329
542587
  }
542330
- const baseModel = state.baseModel && !state.baseModel.startsWith("open-agents-") ? state.baseModel : null;
542588
+ let baseModel = state.baseModel && !state.baseModel.startsWith("open-agents-") ? state.baseModel : null;
542589
+ if (!baseModel) {
542590
+ baseModel = guessBaseFromVariant(variant.name, models);
542591
+ }
542331
542592
  if (!baseModel) {
542332
542593
  summary.failed.push({ model: variant.name, reason: "missing-base-model" });
542333
542594
  continue;
542334
542595
  }
542335
- const sizeGB = modelSizeGB(models, baseModel || variant.name);
542596
+ const sizeGB = modelSizeGB(models, baseModel);
542336
542597
  const kvInfo = await queryModelKVInfo(backendUrl, baseModel);
542337
542598
  const target = calculateExpandedVariantContextWindow(specs, sizeGB, kvInfo?.kvBytesPerToken, kvInfo?.archMax);
542599
+ const lostTools = await wrapperLacksToolsCapability(backendUrl, variant.name);
542338
542600
  try {
542339
- const result = await repairExpandedVariantIfStale(
542340
- variant.name,
542341
- baseModel,
542342
- backendUrl,
542343
- specs,
542344
- sizeGB,
542345
- target.numCtx,
542346
- kvInfo?.kvBytesPerToken,
542347
- kvInfo?.archMax
542348
- );
542601
+ let result;
542602
+ if (lostTools) {
542603
+ const created = await createExpandedVariantNamedAsync(
542604
+ stripVariantTag(variant.name),
542605
+ baseModel,
542606
+ specs,
542607
+ sizeGB,
542608
+ kvInfo?.kvBytesPerToken,
542609
+ kvInfo?.archMax
542610
+ );
542611
+ result = {
542612
+ repaired: !!created,
542613
+ currentNumCtx: state.currentNumCtx,
542614
+ baseModel,
542615
+ resolvedModel: created ?? variant.name
542616
+ };
542617
+ } else {
542618
+ result = await repairExpandedVariantIfStale(
542619
+ variant.name,
542620
+ baseModel,
542621
+ backendUrl,
542622
+ specs,
542623
+ sizeGB,
542624
+ target.numCtx,
542625
+ kvInfo?.kvBytesPerToken,
542626
+ kvInfo?.archMax
542627
+ );
542628
+ }
542349
542629
  if (result.repaired) {
542350
542630
  summary.repaired.push({
542351
542631
  model: result.resolvedModel,