autopilot-ai 1.2.0 → 1.2.2

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.
@@ -173,6 +173,9 @@ interface KimiConfig {
173
173
  compiledContext?: boolean;
174
174
  /** Number of recent user turns to retain image content; older images are dropped. */
175
175
  imageHistoryTurns?: number;
176
+ /** Hard safety ceiling on tool iterations per turn, across automatic
177
+ * 200-call counter resets. Defaults to 1000. */
178
+ maxTotalToolIterations?: number;
176
179
  /** Enable local structured memory (SQLite + embeddings). */
177
180
  memoryEnabled?: boolean;
178
181
  /** Path to memory database. Defaults to .kimiflare/memory.db in repo root, or ~/.local/share/kimiflare/memory.db. */
package/dist/sdk/index.js CHANGED
@@ -1169,7 +1169,7 @@ var init_types = __esm({
1169
1169
  // src/hooks/settings.ts
1170
1170
  import { existsSync, readFileSync as readFileSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "fs";
1171
1171
  import { homedir as homedir10 } from "os";
1172
- import { join as join17, dirname as dirname7 } from "path";
1172
+ import { join as join17, dirname as dirname8 } from "path";
1173
1173
  import { createHash } from "crypto";
1174
1174
  function globalSettingsPath() {
1175
1175
  const xdg = process.env.XDG_CONFIG_HOME || join17(homedir10(), ".config");
@@ -2385,7 +2385,7 @@ var RETENTION = {
2385
2385
  memoryMaxEntries: 1e3
2386
2386
  };
2387
2387
  async function rotateJsonl(path, maxBytes, rotations) {
2388
- const { rename } = await import("fs/promises");
2388
+ const { rename: rename3 } = await import("fs/promises");
2389
2389
  let s;
2390
2390
  try {
2391
2391
  s = await stat(path);
@@ -2397,7 +2397,7 @@ async function rotateJsonl(path, maxBytes, rotations) {
2397
2397
  const src = i === 1 ? path : `${path}.${i - 1}`;
2398
2398
  const dst = `${path}.${i}`;
2399
2399
  try {
2400
- await rename(src, dst);
2400
+ await rename3(src, dst);
2401
2401
  } catch {
2402
2402
  }
2403
2403
  }
@@ -3452,6 +3452,7 @@ function buildSystemPrompt(opts) {
3452
3452
  }
3453
3453
 
3454
3454
  // src/agent/loop.ts
3455
+ var MAX_LOOP_RECOVERIES = 1;
3455
3456
  var BudgetExhaustedError = class extends Error {
3456
3457
  constructor(message = "Cumulative input token budget exhausted") {
3457
3458
  super(message);
@@ -3459,7 +3460,7 @@ var BudgetExhaustedError = class extends Error {
3459
3460
  }
3460
3461
  };
3461
3462
  var AgentLoopError = class extends Error {
3462
- constructor(message = "Agent got stuck repeating the same tool calls") {
3463
+ constructor(message = "Agent got stuck repeating the same tool calls and was stopped after a recovery attempt") {
3463
3464
  super(message);
3464
3465
  this.name = "AgentLoopError";
3465
3466
  }
@@ -3516,6 +3517,12 @@ async function runAgentTurn(opts) {
3516
3517
  const turnStart = performance.now();
3517
3518
  logger.info("turn:start", { sessionId: opts.sessionId, codeMode: opts.codeMode ?? false });
3518
3519
  const max = opts.maxToolIterations ?? 200;
3520
+ const limitBehavior = opts.toolLimitBehavior ?? (opts.continueOnLimit ? "continue" : "throw");
3521
+ const requestedCeiling = opts.maxTotalToolIterations;
3522
+ const hardCeiling = Math.max(
3523
+ max,
3524
+ requestedCeiling !== void 0 && Number.isFinite(requestedCeiling) && requestedCeiling > 0 ? Math.floor(requestedCeiling) : max * 5
3525
+ );
3519
3526
  const codeMode = opts.codeMode ?? false;
3520
3527
  const fireStopHook = async () => {
3521
3528
  if (opts.signal.aborted) return;
@@ -3665,8 +3672,11 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3665
3672
  const WEB_FETCH_DOMAIN_THRESHOLD = 2;
3666
3673
  let cumulativePromptTokens = 0;
3667
3674
  let iter = 0;
3675
+ let totalIter = 0;
3668
3676
  let budgetExhausted = false;
3669
3677
  let loopExhausted = false;
3678
+ let loopRecoveries = 0;
3679
+ let finalizeReason = null;
3670
3680
  let currentTasks = [];
3671
3681
  let mutatingToolsSinceLastTasksSet = 0;
3672
3682
  const MUTATING_TOOLS2 = /* @__PURE__ */ new Set(["write", "edit", "bash"]);
@@ -3684,36 +3694,43 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3684
3694
  content: "You have reached the cumulative input token budget for this session. Please synthesize your findings and provide a final summary of what was accomplished."
3685
3695
  });
3686
3696
  }
3687
- if (loopExhausted) {
3697
+ if (finalizeReason === null && totalIter >= hardCeiling) {
3698
+ finalizeReason = "limit_ceiling";
3688
3699
  opts.messages.push({
3689
3700
  role: "system",
3690
- content: "You have repeatedly called the same tools with identical arguments and are stuck in a loop. Please synthesize what you know from the conversation history and provide a final answer."
3701
+ content: `You have reached the hard safety ceiling of ${hardCeiling} tool iterations for this turn. Do not call any more tools. Summarize what you accomplished, what remains unfinished, and the concrete next steps needed to complete the task.`
3691
3702
  });
3692
- }
3693
- if (iter >= max) {
3694
- if (opts.callbacks.onToolLimitReached) {
3695
- const decision = await opts.callbacks.onToolLimitReached();
3696
- if (decision === "continue") {
3697
- opts.messages.push({
3698
- role: "system",
3699
- content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
3700
- });
3701
- iter = 0;
3702
- } else {
3703
- await fireStopHook();
3704
- return;
3705
- }
3706
- } else if (opts.continueOnLimit) {
3703
+ opts.callbacks.onGuardrail?.({
3704
+ kind: "limit_ceiling",
3705
+ message: `Tool-iteration ceiling (${hardCeiling}) reached \u2014 wrapping up the turn with a summary.`,
3706
+ totalIterations: totalIter
3707
+ });
3708
+ } else if (finalizeReason === null && iter >= max) {
3709
+ if (limitBehavior === "continue") {
3707
3710
  opts.messages.push({
3708
3711
  role: "system",
3709
3712
  content: "You have reached the tool-call limit for this session. The counter has been reset so you can continue working. Please proceed with your task."
3710
3713
  });
3711
3714
  iter = 0;
3715
+ opts.callbacks.onGuardrail?.({
3716
+ kind: "limit_reset",
3717
+ message: `Tool-call limit (${max}) reached \u2014 counter reset automatically (${totalIter}/${hardCeiling} total).`,
3718
+ totalIterations: totalIter
3719
+ });
3720
+ } else if (limitBehavior === "stop") {
3721
+ opts.callbacks.onGuardrail?.({
3722
+ kind: "limit_stopped",
3723
+ message: `Tool-call limit (${max}) reached \u2014 turn stopped.`,
3724
+ totalIterations: totalIter
3725
+ });
3726
+ await fireStopHook();
3727
+ return;
3712
3728
  } else {
3713
3729
  throw new Error(`kimiflare: tool iteration limit reached (${max})`);
3714
3730
  }
3715
3731
  }
3716
3732
  iter++;
3733
+ totalIter++;
3717
3734
  turn++;
3718
3735
  const previousMessages = opts.messages.slice();
3719
3736
  const toolCalls = [];
@@ -3852,8 +3869,21 @@ IMPORTANT \u2014 explore through code, not direct tools: to read files, run shel
3852
3869
  }))
3853
3870
  } : {}
3854
3871
  };
3872
+ if (finalizeReason !== null) {
3873
+ delete assistantMsg.tool_calls;
3874
+ if (!assistantMsg.content) {
3875
+ assistantMsg.content = finalizeReason === "loop" ? "Stopped: I kept repeating blocked tool calls and could not make further progress on this task." : `Stopped: reached the ${hardCeiling}-iteration safety ceiling for this turn before finishing.`;
3876
+ }
3877
+ }
3855
3878
  opts.messages.push(assistantMsg);
3856
3879
  opts.callbacks.onAssistantFinal?.(assistantMsg);
3880
+ if (finalizeReason !== null) {
3881
+ logger.info("turn:finalized", { sessionId: opts.sessionId, reason: finalizeReason, totalIter });
3882
+ if (budgetExhausted) throw new BudgetExhaustedError();
3883
+ await fireStopHook();
3884
+ if (finalizeReason === "loop") throw new AgentLoopError();
3885
+ return;
3886
+ }
3857
3887
  if (toolCalls.length === 0) {
3858
3888
  if (opts.sessionId && lastUsage) {
3859
3889
  void logTurnDebug({
@@ -4485,30 +4515,30 @@ ${sandboxResult.output}` : sandboxResult.output;
4485
4515
  throw new BudgetExhaustedError();
4486
4516
  }
4487
4517
  if (loopExhausted) {
4488
- if (opts.callbacks.onLoopDetected) {
4489
- const decision = await opts.callbacks.onLoopDetected();
4490
- if (decision === "continue") {
4491
- opts.messages.push({
4492
- role: "system",
4493
- content: "You were stuck calling the same tools with identical arguments. The guardrail has been reset so you can continue. Try a different approach."
4494
- });
4495
- loopExhausted = false;
4496
- recentToolCalls.length = 0;
4497
- continue;
4498
- }
4499
- if (decision === "synthesize") {
4500
- opts.messages.push({
4501
- role: "system",
4502
- content: "You were stuck calling the same tools with identical arguments. Please synthesize and conclude your findings so far. Do not call any more tools."
4503
- });
4504
- loopExhausted = false;
4505
- recentToolCalls.length = 0;
4506
- continue;
4507
- }
4508
- await fireStopHook();
4509
- return;
4518
+ loopExhausted = false;
4519
+ recentToolCalls.length = 0;
4520
+ if (loopRecoveries < MAX_LOOP_RECOVERIES) {
4521
+ loopRecoveries++;
4522
+ opts.messages.push({
4523
+ role: "system",
4524
+ content: "Every tool call in your last step was blocked because you kept repeating the same calls with identical arguments. Do not repeat them. Either take a genuinely different approach (different tools, arguments, or files), or, if you already have enough information, stop calling tools and give your final answer."
4525
+ });
4526
+ opts.callbacks.onGuardrail?.({
4527
+ kind: "loop_recovery",
4528
+ message: "Loop guardrail: repeated tool calls were blocked \u2014 asked the agent to change approach."
4529
+ });
4530
+ continue;
4510
4531
  }
4511
- throw new AgentLoopError();
4532
+ finalizeReason = "loop";
4533
+ opts.messages.push({
4534
+ role: "system",
4535
+ content: "You are still repeating blocked tool calls after being asked to change approach, so this turn is ending. Do not call any more tools. Summarize what you accomplished, what is blocking you, and what the user could do next."
4536
+ });
4537
+ opts.callbacks.onGuardrail?.({
4538
+ kind: "loop_stopped",
4539
+ message: "Loop guardrail: the agent kept repeating blocked tool calls \u2014 ending the turn with a summary."
4540
+ });
4541
+ continue;
4512
4542
  }
4513
4543
  }
4514
4544
  }
@@ -6722,6 +6752,7 @@ async function loadConfig() {
6722
6752
  cacheStablePrompts: persisted.cacheStablePrompts ?? cacheStablePrompts,
6723
6753
  compiledContext: persisted.compiledContext ?? compiledContext,
6724
6754
  imageHistoryTurns: imageHistoryTurns === void 0 || Number.isNaN(imageHistoryTurns) ? persisted.imageHistoryTurns : imageHistoryTurns,
6755
+ maxTotalToolIterations: readNumberEnv("KIMIFLARE_MAX_TOTAL_TOOL_ITERATIONS") ?? persisted.maxTotalToolIterations,
6725
6756
  memoryEnabled: readBooleanEnv("KIMIFLARE_MEMORY_ENABLED") ?? persisted.memoryEnabled ?? false,
6726
6757
  memoryDbPath: process.env.KIMIFLARE_MEMORY_DB_PATH ?? persisted.memoryDbPath,
6727
6758
  memoryMaxAgeDays: readNumberEnv("KIMIFLARE_MEMORY_MAX_AGE_DAYS") ?? persisted.memoryMaxAgeDays,
@@ -9505,11 +9536,11 @@ async function loadSession(filePath) {
9505
9536
  }
9506
9537
 
9507
9538
  // src/usage-tracker.ts
9508
- import { readFile as readFile8, writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
9539
+ import { readFile as readFile8, rename as rename2 } from "fs/promises";
9509
9540
  import { homedir as homedir8 } from "os";
9510
9541
  import { join as join15 } from "path";
9511
9542
  import { EventEmitter as EventEmitter2 } from "events";
9512
- import { randomUUID } from "crypto";
9543
+ import { randomUUID as randomUUID2 } from "crypto";
9513
9544
 
9514
9545
  // src/pricing.ts
9515
9546
  var PRICE_IN_PER_M = 0.95;
@@ -9530,6 +9561,59 @@ function calculateCost(promptTokens, completionTokens, cachedTokens = 0, model)
9530
9561
  return { uncachedIn, cachedIn, out, total };
9531
9562
  }
9532
9563
 
9564
+ // src/util/atomic-file.ts
9565
+ import { mkdir as mkdir6, rename, rm, stat as stat4, writeFile as writeFile7 } from "fs/promises";
9566
+ import { dirname as dirname6 } from "path";
9567
+ import { randomUUID } from "crypto";
9568
+ async function writeFileAtomic(path, data) {
9569
+ await mkdir6(dirname6(path), { recursive: true });
9570
+ const tmp = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
9571
+ try {
9572
+ await writeFile7(tmp, data, "utf8");
9573
+ await rename(tmp, path);
9574
+ } catch (err) {
9575
+ await rm(tmp, { force: true }).catch(() => void 0);
9576
+ throw err;
9577
+ }
9578
+ }
9579
+ var LOCK_STALE_MS = 15e3;
9580
+ var LOCK_WAIT_MS = 5e3;
9581
+ var LOCK_RETRY_MS = 25;
9582
+ async function withFileLock(lockPath, fn) {
9583
+ await mkdir6(dirname6(lockPath), { recursive: true });
9584
+ const deadline = Date.now() + LOCK_WAIT_MS;
9585
+ let held = false;
9586
+ while (!held) {
9587
+ try {
9588
+ await mkdir6(lockPath);
9589
+ held = true;
9590
+ } catch (err) {
9591
+ if (err.code !== "EEXIST") break;
9592
+ const age = await stat4(lockPath).then((s) => Date.now() - s.mtimeMs, () => 0);
9593
+ if (age > LOCK_STALE_MS) {
9594
+ await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
9595
+ continue;
9596
+ }
9597
+ if (Date.now() > deadline) break;
9598
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
9599
+ }
9600
+ }
9601
+ try {
9602
+ return await fn();
9603
+ } finally {
9604
+ if (held) await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
9605
+ }
9606
+ }
9607
+ function recoverJsonPrefix(raw) {
9608
+ for (let i = raw.lastIndexOf("}"); i > 0; i = raw.lastIndexOf("}", i - 1)) {
9609
+ try {
9610
+ return JSON.parse(raw.slice(0, i + 1));
9611
+ } catch {
9612
+ }
9613
+ }
9614
+ return void 0;
9615
+ }
9616
+
9533
9617
  // src/usage-tracker.ts
9534
9618
  var LOG_VERSION2 = 1;
9535
9619
  var usageEvents = new EventEmitter2();
@@ -9553,21 +9637,29 @@ function cutoffDate(daysBack) {
9553
9637
  return d.toISOString().slice(0, 10);
9554
9638
  }
9555
9639
  async function loadLog() {
9640
+ let raw;
9556
9641
  try {
9557
- const raw = await readFile8(usagePath(), "utf8");
9558
- const parsed = JSON.parse(raw);
9559
- if (parsed.version === LOG_VERSION2) return parsed;
9642
+ raw = await readFile8(usagePath(), "utf8");
9643
+ } catch {
9644
+ return { version: LOG_VERSION2, days: [], sessions: [] };
9645
+ }
9646
+ let parsed;
9647
+ try {
9648
+ parsed = JSON.parse(raw);
9560
9649
  } catch {
9650
+ await rename2(usagePath(), `${usagePath()}.corrupt-${Date.now()}`).catch(() => void 0);
9651
+ parsed = recoverJsonPrefix(raw);
9561
9652
  }
9653
+ if (parsed && parsed.version === LOG_VERSION2) return parsed;
9562
9654
  return { version: LOG_VERSION2, days: [], sessions: [] };
9563
9655
  }
9564
9656
  async function saveLog(log2) {
9565
- await mkdir6(usageDir(), { recursive: true });
9566
- await writeFile7(usagePath(), JSON.stringify(log2, null, 2), "utf8");
9657
+ await writeFileAtomic(usagePath(), JSON.stringify(log2, null, 2));
9567
9658
  }
9568
9659
  var writeChain = Promise.resolve();
9569
9660
  function withLock(fn) {
9570
- const next = writeChain.then(fn, fn);
9661
+ const locked = () => withFileLock(`${usagePath()}.lock`, fn);
9662
+ const next = writeChain.then(locked, locked);
9571
9663
  writeChain = next.catch(() => void 0);
9572
9664
  return next;
9573
9665
  }
@@ -9597,8 +9689,7 @@ async function upsertHistoryDay(day) {
9597
9689
  entries.push(day);
9598
9690
  }
9599
9691
  const lines = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
9600
- await mkdir6(usageDir(), { recursive: true });
9601
- await writeFile7(historyPath(), lines, "utf8");
9692
+ await writeFileAtomic(historyPath(), lines);
9602
9693
  }
9603
9694
  function getOrCreateDay(log2, date) {
9604
9695
  let day = log2.days.find((d) => d.date === date);
@@ -9657,7 +9748,7 @@ async function recordUsage(sessionId, usage, lookup, model) {
9657
9748
  );
9658
9749
  const estimatedCost = cost.total;
9659
9750
  const cachedTokens = usage.prompt_tokens_details?.cached_tokens ?? 0;
9660
- const turnId = randomUUID();
9751
+ const turnId = randomUUID2();
9661
9752
  const generationId = lookup?.meta.generationId;
9662
9753
  const inlineCost = typeof usage.cost === "number" && Number.isFinite(usage.cost) ? usage.cost : void 0;
9663
9754
  await withLock(async () => {
@@ -9760,7 +9851,7 @@ async function reconcileTurnCost(sessionId, turnId, lookup, fetchImpl2 = fetch,
9760
9851
  // src/models/openrouter-catalog.ts
9761
9852
  import { readFile as readFile9, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
9762
9853
  import { homedir as homedir9 } from "os";
9763
- import { join as join16, dirname as dirname6 } from "path";
9854
+ import { join as join16, dirname as dirname7 } from "path";
9764
9855
  var CACHE_VERSION = 4;
9765
9856
  var DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
9766
9857
  function catalogCachePath() {
@@ -9826,7 +9917,7 @@ async function readCache() {
9826
9917
  async function writeCache(models) {
9827
9918
  const path = catalogCachePath();
9828
9919
  try {
9829
- await mkdir7(dirname6(path), { recursive: true });
9920
+ await mkdir7(dirname7(path), { recursive: true });
9830
9921
  const cache = { version: CACHE_VERSION, fetchedAt: (/* @__PURE__ */ new Date()).toISOString(), models };
9831
9922
  await writeFile8(path, JSON.stringify(cache, null, 2), "utf8");
9832
9923
  } catch {
@@ -10261,6 +10352,10 @@ var InternalSession = class {
10261
10352
  onWarning: (msg) => {
10262
10353
  this.emit({ type: "warning", message: msg });
10263
10354
  },
10355
+ onGuardrail: (ev) => {
10356
+ this.emit({ type: "warning", message: ev.message });
10357
+ if (ev.kind === "limit_stopped") this.emit({ type: "status", status: "error" });
10358
+ },
10264
10359
  askPermission: async (req) => {
10265
10360
  if (mode === "auto") return "allow";
10266
10361
  if (mode === "plan") {
@@ -10296,10 +10391,6 @@ var InternalSession = class {
10296
10391
  });
10297
10392
  return decision;
10298
10393
  },
10299
- onToolLimitReached: async () => {
10300
- this.emit({ type: "status", status: "error" });
10301
- return "stop";
10302
- },
10303
10394
  onKimiMdStale: () => {
10304
10395
  this.onKimiMdStale?.();
10305
10396
  }
@@ -10317,6 +10408,9 @@ var InternalSession = class {
10317
10408
  hooks: this.hooks,
10318
10409
  // M6.1: Stop fires at end-of-turn when enabled.
10319
10410
  maxToolIterations,
10411
+ // SDK callers bound turns with `maxToolIterations`: stop cleanly at the
10412
+ // limit rather than resetting (never waits on a prompt either way).
10413
+ toolLimitBehavior: "stop",
10320
10414
  reasoningEffort: this.reasoningEffort,
10321
10415
  coauthor,
10322
10416
  sessionId: this.sessionId,