jinzd-ai-cli 0.4.179 → 0.4.181

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
@@ -18,7 +18,7 @@ import {
18
18
  saveDevState,
19
19
  sessionHasMeaningfulContent,
20
20
  setupProxy
21
- } from "./chunk-FLJ5RSDY.js";
21
+ } from "./chunk-6SY45V62.js";
22
22
  import {
23
23
  ToolExecutor,
24
24
  ToolRegistry,
@@ -37,10 +37,10 @@ import {
37
37
  spawnAgentContext,
38
38
  theme,
39
39
  undoStack
40
- } from "./chunk-VQQPCJ7I.js";
40
+ } from "./chunk-ORXPLVM7.js";
41
41
  import "./chunk-HDSKW7Q3.js";
42
42
  import "./chunk-ZWVIDFGY.js";
43
- import "./chunk-IWBPFOYL.js";
43
+ import "./chunk-D4ZTYYBU.js";
44
44
  import {
45
45
  SessionManager,
46
46
  getContentText
@@ -49,15 +49,25 @@ import {
49
49
  getConfigDirUsage,
50
50
  listRecentCrashes,
51
51
  writeCrashLog
52
- } from "./chunk-A4VW4QJ6.js";
52
+ } from "./chunk-ZWQ36YFI.js";
53
53
  import {
54
+ BudgetWarner,
54
55
  CONTENT_ONLY_STREAM_REMINDER,
56
+ ContextPressureMonitor,
57
+ EmptyResponseGuard,
58
+ FreeRoundTracker,
55
59
  HALLUCINATION_CORRECTION_MESSAGE,
56
60
  ProviderRegistry,
57
61
  TEE_FINAL_USER_NUDGE,
58
62
  TOOL_CALL_REMINDER,
63
+ ThinkTagFilter,
64
+ accumulateUsage,
59
65
  buildPhantomCorrectionMessage,
66
+ buildRoundBudgetHint,
67
+ buildRoundsExhaustedPrompt,
68
+ buildUserStopMessage,
60
69
  buildWriteRoundReminder,
70
+ consumeToolCallStream,
61
71
  detectMetaNarration,
62
72
  detectPseudoToolCalls,
63
73
  detectsHallucinatedFileOp,
@@ -67,18 +77,19 @@ import {
67
77
  hadPreviousWriteToolCalls,
68
78
  looksLikeDocumentBody,
69
79
  stripPseudoToolCalls,
70
- stripToolCallReminder
71
- } from "./chunk-2TWARH5X.js";
80
+ stripToolCallReminder,
81
+ summarizeRecentTools
82
+ } from "./chunk-5LK7H45B.js";
72
83
  import {
73
84
  getStatsSnapshot,
74
85
  getTopFailingTools,
75
86
  getTopUsedTools,
76
87
  installFlushOnExit
77
- } from "./chunk-M4BEF3S5.js";
88
+ } from "./chunk-4VB6UP4W.js";
78
89
  import "./chunk-HIU2SH4V.js";
79
90
  import {
80
91
  ConfigManager
81
- } from "./chunk-7ZJKEL2S.js";
92
+ } from "./chunk-P2VFMUR5.js";
82
93
  import {
83
94
  AuthError,
84
95
  ProviderError,
@@ -105,7 +116,7 @@ import {
105
116
  SKILLS_DIR_NAME,
106
117
  VERSION,
107
118
  buildUserIdentityPrompt
108
- } from "./chunk-GDZ7ITJM.js";
119
+ } from "./chunk-CPZ7KG3Y.js";
109
120
  import {
110
121
  formatGitContextForPrompt,
111
122
  getGitContext,
@@ -208,13 +219,6 @@ function isInterruptedSession(messages) {
208
219
  import chalk from "chalk";
209
220
  import { createWriteStream, mkdirSync } from "fs";
210
221
  import { dirname } from "path";
211
- function partialTagTail(s, tag) {
212
- const max = Math.min(s.length, tag.length - 1);
213
- for (let k = max; k > 0; k--) {
214
- if (s.endsWith(tag.slice(0, k))) return k;
215
- }
216
- return 0;
217
- }
218
222
  function fmtContextWindow(tokens) {
219
223
  if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
220
224
  if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
@@ -495,48 +499,15 @@ var Renderer = class {
495
499
  }
496
500
  let fullContent = "";
497
501
  let usage;
498
- let inThinking = false;
499
- let thinkBuf = "";
500
- const emitText = (raw) => {
501
- thinkBuf += raw;
502
- let out = "";
503
- while (thinkBuf.length > 0) {
504
- if (!inThinking) {
505
- const open = thinkBuf.indexOf("<think>");
506
- if (open === -1) {
507
- const keep = partialTagTail(thinkBuf, "<think>");
508
- out += thinkBuf.slice(0, thinkBuf.length - keep);
509
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
510
- break;
511
- }
512
- out += thinkBuf.slice(0, open);
513
- thinkBuf = thinkBuf.slice(open + "<think>".length);
514
- inThinking = true;
515
- } else {
516
- const close = thinkBuf.indexOf("</think>");
517
- if (close === -1) {
518
- const keep = partialTagTail(thinkBuf, "</think>");
519
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
520
- break;
521
- }
522
- thinkBuf = thinkBuf.slice(close + "</think>".length);
523
- inThinking = false;
524
- }
525
- }
526
- if (out) {
527
- process.stdout.write(out);
528
- if (fileStream) fileStream.write(out);
529
- fullContent += out;
530
- }
531
- };
532
- const flushTail = () => {
533
- if (!inThinking && thinkBuf) {
534
- process.stdout.write(thinkBuf);
535
- if (fileStream) fileStream.write(thinkBuf);
536
- fullContent += thinkBuf;
537
- thinkBuf = "";
538
- }
502
+ const thinkFilter = new ThinkTagFilter();
503
+ const writeVisible = (visible) => {
504
+ if (!visible) return;
505
+ process.stdout.write(visible);
506
+ if (fileStream) fileStream.write(visible);
507
+ fullContent += visible;
539
508
  };
509
+ const emitText = (raw) => writeVisible(thinkFilter.push(raw));
510
+ const flushTail = () => writeVisible(thinkFilter.flush());
540
511
  let interrupted = false;
541
512
  let streamErr = null;
542
513
  try {
@@ -1786,7 +1757,7 @@ No tools match "${filter}".
1786
1757
  const { join: join6 } = await import("path");
1787
1758
  const { existsSync: existsSync6 } = await import("fs");
1788
1759
  const { getGitRoot: getGitRoot2 } = await import("./git-context-7KIP4X2V.js");
1789
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-2XKWY6LU.js");
1760
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-5DFFSPWI.js");
1790
1761
  const { approveProject, hashMcpFile } = await import("./project-trust-IFM7FXEV.js");
1791
1762
  const cwd = process.cwd();
1792
1763
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2847,7 +2818,7 @@ ${hint}` : "")
2847
2818
  usage: "/test [command|filter]",
2848
2819
  async execute(args, ctx) {
2849
2820
  try {
2850
- const { executeTests } = await import("./run-tests-SU6NWCF6.js");
2821
+ const { executeTests } = await import("./run-tests-673ABQQE.js");
2851
2822
  const argStr = args.join(" ").trim();
2852
2823
  let testArgs = {};
2853
2824
  if (argStr) {
@@ -4694,17 +4665,8 @@ ${content}
4694
4665
  parts.push(...imageParts);
4695
4666
  return { parts, hasImage: imageParts.length > 0, refs };
4696
4667
  }
4697
- var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
4698
- var MAX_CONSECUTIVE_FREE_ROUNDS = 3;
4699
4668
  var MAX_REPEATED_TOOL_CALLS = 2;
4700
4669
  var DEFAULT_AUTO_PAUSE_INTERVAL = 50;
4701
- function partialTagTail2(s, tag) {
4702
- const max = Math.min(s.length, tag.length - 1);
4703
- for (let k = max; k > 0; k--) {
4704
- if (s.endsWith(tag.slice(0, k))) return k;
4705
- }
4706
- return 0;
4707
- }
4708
4670
  function fmtTokens(n) {
4709
4671
  if (n >= 1e6) return `${Math.round(n / 1e5) / 10}M`;
4710
4672
  if (n >= 1e3) return `${Math.round(n / 1024)}K`;
@@ -6407,14 +6369,12 @@ Session '${this.resumeSessionId}' not found.
6407
6369
  /**
6408
6370
  * 消费流式工具调用事件生成器,实时渲染文本内容和工具名称,
6409
6371
  * 累积完整工具调用参数后返回结构化结果。
6372
+ *
6373
+ * v0.4.181: 累积与决策(<think> 折叠 / 截断 JSON 修复 / done 守卫 /
6374
+ * index 键累积)委托给 core/agent-loop 的统一消费器(与 Web 端同一实现),
6375
+ * 此处只保留终端呈现(spinner / theme 渲染)。
6410
6376
  */
6411
6377
  async consumeToolStream(stream, spinner) {
6412
- const textParts = [];
6413
- const toolCallAccumulators = /* @__PURE__ */ new Map();
6414
- let usage;
6415
- let rawContent;
6416
- let reasoningContent;
6417
- let finishReason;
6418
6378
  let spinnerStopped = false;
6419
6379
  const stopSpinner = () => {
6420
6380
  if (!spinnerStopped) {
@@ -6422,135 +6382,36 @@ Session '${this.resumeSessionId}' not found.
6422
6382
  spinnerStopped = true;
6423
6383
  }
6424
6384
  };
6425
- let inThink = false;
6426
- let thinkShown = false;
6427
- let thinkBuf = "";
6428
- const emitText = (raw) => {
6429
- thinkBuf += raw;
6430
- let out = "";
6431
- while (thinkBuf.length > 0) {
6432
- if (!inThink) {
6433
- const open = thinkBuf.indexOf("<think>");
6434
- if (open === -1) {
6435
- const keep = partialTagTail2(thinkBuf, "<think>");
6436
- out += thinkBuf.slice(0, thinkBuf.length - keep);
6437
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
6438
- break;
6439
- }
6440
- out += thinkBuf.slice(0, open);
6441
- thinkBuf = thinkBuf.slice(open + "<think>".length);
6442
- inThink = true;
6443
- thinkShown = true;
6444
- } else {
6445
- const close = thinkBuf.indexOf("</think>");
6446
- if (close === -1) {
6447
- const keep = partialTagTail2(thinkBuf, "</think>");
6448
- thinkBuf = thinkBuf.slice(thinkBuf.length - keep);
6449
- break;
6450
- }
6451
- thinkBuf = thinkBuf.slice(close + "</think>".length);
6452
- inThink = false;
6453
- }
6454
- }
6455
- if (out) {
6456
- process.stdout.write(out);
6457
- textParts.push(out);
6458
- }
6459
- };
6460
- try {
6461
- for await (const event of stream) {
6462
- switch (event.type) {
6463
- case "text_delta":
6464
- stopSpinner();
6465
- emitText(event.delta);
6466
- break;
6467
- case "thinking_start":
6468
- stopSpinner();
6469
- process.stdout.write(theme.dim("<think>"));
6470
- break;
6471
- case "thinking_delta":
6472
- break;
6473
- case "thinking_end":
6474
- process.stdout.write(theme.dim("</think>"));
6475
- break;
6476
- case "tool_call_start":
6477
- stopSpinner();
6478
- process.stdout.write(
6479
- theme.dim(`
6480
- \u2699 Streaming: `) + theme.toolCall(event.name) + theme.dim("...\n")
6481
- );
6482
- toolCallAccumulators.set(event.index, {
6483
- id: event.id,
6484
- name: event.name,
6485
- arguments: ""
6486
- });
6487
- break;
6488
- case "tool_call_delta": {
6489
- const acc = toolCallAccumulators.get(event.index);
6490
- if (acc) {
6491
- acc.arguments += event.argumentsDelta;
6492
- }
6493
- break;
6494
- }
6495
- case "tool_call_end":
6496
- break;
6497
- case "done":
6498
- if (event.usage) usage = event.usage;
6499
- if (event.rawContent) rawContent = event.rawContent;
6500
- if (event.reasoningContent) reasoningContent = event.reasoningContent;
6501
- if (event.finishReason) finishReason = event.finishReason;
6502
- break;
6503
- }
6504
- }
6505
- } catch (err) {
6506
- if (err instanceof Error && (err.name === "AbortError" || err.message.includes("aborted"))) {
6385
+ const result = await consumeToolCallStream(stream, {
6386
+ onText: (visible) => {
6507
6387
  stopSpinner();
6508
- process.stdout.write(theme.dim("\n[interrupted]\n"));
6509
- return {
6510
- textContent: textParts.join(""),
6511
- toolCalls: [],
6512
- usage,
6513
- rawContent,
6514
- reasoningContent,
6515
- finishReason
6516
- };
6517
- }
6518
- throw err;
6519
- }
6520
- if (!inThink && thinkBuf) {
6521
- process.stdout.write(thinkBuf);
6522
- textParts.push(thinkBuf);
6523
- thinkBuf = "";
6524
- }
6525
- const toolCalls = [];
6526
- for (const [, acc] of toolCallAccumulators) {
6527
- let parsedArgs;
6528
- try {
6529
- parsedArgs = JSON.parse(acc.arguments || "{}");
6530
- } catch {
6531
- const truncated = acc.arguments.trimEnd();
6532
- const lastComma = truncated.lastIndexOf(",");
6533
- const fixed = lastComma > 0 ? truncated.slice(0, lastComma) + "}" : truncated.slice(0, truncated.indexOf("{") + 1) + "}";
6534
- try {
6535
- parsedArgs = JSON.parse(fixed);
6536
- } catch {
6537
- parsedArgs = {};
6538
- }
6388
+ process.stdout.write(visible);
6389
+ },
6390
+ onThinkingStart: () => {
6391
+ stopSpinner();
6392
+ process.stdout.write(theme.dim("<think>"));
6393
+ },
6394
+ // thinking_delta 内容折叠,不显示详情(与现有行为一致)
6395
+ onThinkingEnd: () => {
6396
+ process.stdout.write(theme.dim("</think>"));
6397
+ },
6398
+ onToolCallStart: (_index, _id, name) => {
6399
+ stopSpinner();
6400
+ process.stdout.write(
6401
+ theme.dim(`
6402
+ \u2699 Streaming: `) + theme.toolCall(name) + theme.dim("...\n")
6403
+ );
6404
+ },
6405
+ onWarn: (message) => {
6406
+ process.stderr.write(`[warn] ${message}
6407
+ `);
6539
6408
  }
6540
- toolCalls.push({
6541
- id: acc.id,
6542
- name: acc.name,
6543
- arguments: parsedArgs
6544
- });
6409
+ });
6410
+ if (result.aborted) {
6411
+ stopSpinner();
6412
+ process.stdout.write(theme.dim("\n[interrupted]\n"));
6545
6413
  }
6546
- return {
6547
- textContent: textParts.join(""),
6548
- toolCalls,
6549
- usage,
6550
- rawContent,
6551
- reasoningContent,
6552
- finishReason
6553
- };
6414
+ return result;
6554
6415
  }
6555
6416
  async handleChatWithTools(provider, messages, modelOverride) {
6556
6417
  const session = this.sessions.current;
@@ -6592,25 +6453,7 @@ Session '${this.resumeSessionId}' not found.
6592
6453
  const autoPauseIntervalRaw = this.config.get("autoPauseInterval");
6593
6454
  const autoPauseInterval = typeof autoPauseIntervalRaw === "number" ? autoPauseIntervalRaw : DEFAULT_AUTO_PAUSE_INTERVAL;
6594
6455
  const { stable: toolStable, volatile: toolVolatile } = this.buildCurrentSystemPrompt();
6595
- const pauseHint = autoPauseInterval > 0 ? `
6596
- - Every ${autoPauseInterval} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.` : "";
6597
- const roundBudgetHint = this.planMode ? `
6598
-
6599
- [Tool Round Budget \u2014 Plan Mode]
6600
- You have a maximum of ${maxToolRounds} tool call rounds. You are in READ-ONLY Plan Mode:
6601
- - Only use: read_file, list_dir, grep_files, glob_files, ask_user, write_todos
6602
- - Do NOT attempt to call bash, write_file, edit_file \u2014 they are disabled
6603
- - Do NOT write shell commands or code blocks as a substitute for tool calls
6604
- - Do NOT read the same file more than once
6605
- - Call write_todos ONCE to present your plan, then give a text summary
6606
- - If the user asks you to execute anything, respond: "Please type /plan execute to switch to execute mode."${pauseHint}` : `
6607
-
6608
- [Tool Round Budget]
6609
- You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
6610
- - Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
6611
- - Do NOT read the same file more than once \u2014 use the content from previous reads.
6612
- - Prioritize the most critical tasks first in case rounds run out.
6613
- - When remaining rounds are low, focus on completing the current task and summarizing.${pauseHint}`;
6456
+ const roundBudgetHint = buildRoundBudgetHint({ maxToolRounds, autoPauseInterval, planMode: this.planMode });
6614
6457
  const systemPrompt = toolStable + TOOL_CALL_REMINDER + roundBudgetHint + (mcpBudgetNote ? `
6615
6458
 
6616
6459
  ${mcpBudgetNote}` : "");
@@ -6620,22 +6463,15 @@ ${mcpBudgetNote}` : "");
6620
6463
  const spinner = this.renderer.showSpinner("Thinking...");
6621
6464
  const roundUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
6622
6465
  const supportsStreamingTools = useStreaming && typeof provider.chatWithToolsStream === "function";
6623
- let consecutiveFreeRounds = 0;
6624
6466
  let lastToolCallSignature = "";
6625
6467
  let repeatedToolCallCount = 0;
6626
- let emptyResponseRetries = 0;
6627
- let warnedCtx80 = false;
6628
6468
  const roundToolHistory = [];
6469
+ const budgetWarner = new BudgetWarner(maxToolRounds);
6470
+ const emptyGuard = new EmptyResponseGuard();
6471
+ const ctxMonitor = new ContextPressureMonitor();
6472
+ const freeRounds = new FreeRoundTracker();
6629
6473
  this.setupInterjectionListener();
6630
6474
  try {
6631
- const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
6632
- const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
6633
- const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
6634
- const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
6635
- const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
6636
- let warnedNote = false;
6637
- let warnedLow = false;
6638
- let warnedCritical = false;
6639
6475
  for (let round = 0; round < maxToolRounds; round++) {
6640
6476
  this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
6641
6477
  if (this.toolExecutor.pendingSlashCommand) {
@@ -6652,29 +6488,14 @@ ${mcpBudgetNote}` : "");
6652
6488
  `));
6653
6489
  extraMessages.push({ role: "user", content: cmd });
6654
6490
  }
6655
- const roundsLeft = maxToolRounds - round;
6656
- if (!warnedCritical && roundsLeft <= warnCriticalEff) {
6657
- warnedCritical = true;
6658
- extraMessages.push({
6659
- role: "user",
6660
- content: `\u{1F6A8} Critical budget: Only ${roundsLeft} rounds left! Wrap up NOW \u2014 complete the current operation and give a final summary. Do NOT start new tasks.`
6661
- });
6662
- process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
6491
+ const budgetWarning = budgetWarner.check(maxToolRounds - round);
6492
+ if (budgetWarning) {
6493
+ extraMessages.push({ role: "user", content: budgetWarning.injectMessage });
6494
+ if (budgetWarning.displayMessage) {
6495
+ const paint = budgetWarning.level === "critical" ? theme.error : theme.warning;
6496
+ process.stdout.write(paint(` ${budgetWarning.displayMessage}
6663
6497
  `));
6664
- } else if (!warnedLow && roundsLeft <= warnLowEff) {
6665
- warnedLow = true;
6666
- extraMessages.push({
6667
- role: "user",
6668
- content: `\u26A0\uFE0F Budget warning: Only ${roundsLeft} tool rounds remaining. Prioritize completing the most critical task. Use efficient approaches (batch edits, fewer reads). If you cannot finish everything, summarize what's done and what remains.`
6669
- });
6670
- process.stdout.write(theme.warning(` \u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining
6671
- `));
6672
- } else if (!warnedNote && roundsLeft <= warnNoteAt) {
6673
- warnedNote = true;
6674
- extraMessages.push({
6675
- role: "user",
6676
- content: `\u{1F4CA} Budget note: ${roundsLeft} tool rounds remaining out of ${maxToolRounds}. Plan your remaining work efficiently \u2014 use batch operations (e.g., replaceAll) when possible.`
6677
- });
6498
+ }
6678
6499
  }
6679
6500
  if (this._userInterjection) {
6680
6501
  const msg = this._userInterjection;
@@ -6686,13 +6507,13 @@ ${mcpBudgetNote}` : "");
6686
6507
  const ctxWindow = this.getContextWindowSize();
6687
6508
  if (ctxWindow > 0) {
6688
6509
  const reqTokens = this.estimateRequestTokens(systemPrompt, extraMessages);
6689
- const reqRatio = reqTokens / ctxWindow;
6690
- if (reqRatio >= 0.95) {
6510
+ const pressure = ctxMonitor.check(reqTokens, ctxWindow);
6511
+ if (pressure.action === "abort") {
6691
6512
  spinner.stop();
6692
6513
  process.stderr.write(
6693
6514
  theme.error(
6694
6515
  `
6695
- \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 aborting agentic loop before API rejection.
6516
+ \u26A0 Context at ${Math.round(pressure.ratio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 aborting agentic loop before API rejection.
6696
6517
  `
6697
6518
  )
6698
6519
  );
@@ -6712,20 +6533,16 @@ ${mcpBudgetNote}` : "");
6712
6533
  }
6713
6534
  }
6714
6535
  return;
6715
- } else if (reqRatio >= 0.8 && !warnedCtx80) {
6716
- warnedCtx80 = true;
6536
+ } else if (pressure.action === "warn") {
6717
6537
  spinner.stop();
6718
6538
  process.stdout.write(
6719
6539
  theme.warning(
6720
6540
  `
6721
- \u26A0 Context at ${Math.round(reqRatio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 asking AI to wrap up.
6541
+ \u26A0 Context at ${Math.round(pressure.ratio * 100)}% of ${fmtTokens(ctxWindow)} \u2014 asking AI to wrap up.
6722
6542
  `
6723
6543
  )
6724
6544
  );
6725
- extraMessages.push({
6726
- role: "user",
6727
- content: `\u26A0\uFE0F Context pressure: ~${Math.round(reqRatio * 100)}% of the ${fmtTokens(ctxWindow)} context window is used. Avoid reading more files or running broad scans. Finish the current critical step, then produce a final summary. Every unnecessary tool call now risks breaking the conversation.`
6728
- });
6545
+ extraMessages.push({ role: "user", content: pressure.injectMessage });
6729
6546
  spinner.start(`Thinking... (round ${round + 1}/${maxToolRounds})`);
6730
6547
  }
6731
6548
  }
@@ -6797,12 +6614,7 @@ ${mcpBudgetNote}` : "");
6797
6614
  (p, m) => p.chatWithTools({ ...chatRequest, model: m }, toolDefs)
6798
6615
  );
6799
6616
  }
6800
- if (result.usage) {
6801
- roundUsage.inputTokens += result.usage.inputTokens;
6802
- roundUsage.outputTokens += result.usage.outputTokens;
6803
- roundUsage.cacheCreationTokens += result.usage.cacheCreationTokens ?? 0;
6804
- roundUsage.cacheReadTokens += result.usage.cacheReadTokens ?? 0;
6805
- }
6617
+ accumulateUsage(roundUsage, result.usage);
6806
6618
  if ("content" in result) {
6807
6619
  const hasWriteTools = toolDefs.some((t) => t.name === "write_file" || t.name === "edit_file");
6808
6620
  const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
@@ -6831,32 +6643,25 @@ ${mcpBudgetNote}` : "");
6831
6643
  }
6832
6644
  if (!result.content || result.content.trim() === "") {
6833
6645
  const fr = "finishReason" in result ? result.finishReason : void 0;
6834
- const reasonLabel = fr === "length" ? "output limit reached (finish_reason=length)" : fr === "content_filter" ? "content blocked (finish_reason=content_filter)" : fr ? `empty response (finish_reason=${fr})` : "empty response";
6835
- if (emptyResponseRetries === 0 && round < maxToolRounds - 1) {
6836
- emptyResponseRetries++;
6646
+ const decision = emptyGuard.onEmpty(round < maxToolRounds - 1, fr);
6647
+ if (decision.action === "nudge") {
6837
6648
  spinner.stop();
6838
6649
  if (alreadyRendered) process.stdout.write("\n");
6839
- process.stderr.write(
6840
- theme.warning(`\u26A0 ${reasonLabel} \u2014 nudging AI to continue...
6841
- `)
6842
- );
6843
- extraMessages.push({
6844
- role: "user",
6845
- content: "Your previous response was empty \u2014 no text and no tool calls. This usually means the context window is nearly full. Please either: (1) continue the task by calling the next tool you need, or (2) give a concise final text summary of what has been accomplished so far and what remains. Do NOT repeat earlier long outputs."
6846
- });
6650
+ process.stderr.write(theme.warning(`${decision.displayMessage}
6651
+ `));
6652
+ extraMessages.push({ role: "user", content: decision.injectMessage });
6847
6653
  spinner.start(`Retrying... (round ${round + 2}/${maxToolRounds})`);
6848
6654
  continue;
6849
6655
  }
6850
6656
  spinner.stop();
6851
6657
  if (alreadyRendered) process.stdout.write("\n");
6852
- const frHint = fr === "length" ? "Output token limit hit \u2014 try /compact to reduce context, raise maxTokens, or /model to switch." : fr === "content_filter" ? "Content was blocked by the provider filter." : "Context window may be exhausted or max_tokens too low.";
6853
6658
  process.stderr.write(
6854
6659
  theme.error(`
6855
- \u26A0 AI returned empty responses twice in a row. Stopping agentic loop.
6660
+ ${decision.displayMessage}
6856
6661
  `)
6857
6662
  );
6858
6663
  process.stderr.write(
6859
- theme.dim(` ${frHint}
6664
+ theme.dim(` ${decision.hint}
6860
6665
  Try: /compact, /clear, or /model to switch.
6861
6666
 
6862
6667
  `)
@@ -6870,7 +6675,7 @@ ${mcpBudgetNote}` : "");
6870
6675
  }
6871
6676
  return;
6872
6677
  }
6873
- emptyResponseRetries = 0;
6678
+ emptyGuard.onNonEmpty();
6874
6679
  spinner.stop();
6875
6680
  const finalContent = result.content;
6876
6681
  if (!alreadyRendered) {
@@ -7181,14 +6986,8 @@ ${systemPromptVolatile}` : systemPrompt;
7181
6986
  });
7182
6987
  }
7183
6988
  }
7184
- const allFree = result.toolCalls.every((tc) => FREE_ROUND_TOOLS.has(tc.name));
7185
- if (allFree) {
7186
- consecutiveFreeRounds++;
7187
- if (consecutiveFreeRounds <= MAX_CONSECUTIVE_FREE_ROUNDS) {
7188
- round--;
7189
- }
7190
- } else {
7191
- consecutiveFreeRounds = 0;
6989
+ if (freeRounds.apply(result.toolCalls.map((tc) => tc.name))) {
6990
+ round--;
7192
6991
  }
7193
6992
  const currentSignature = result.toolCalls.map((tc) => `${tc.name}:${JSON.stringify(tc.arguments)}`).join("|");
7194
6993
  if (currentSignature === lastToolCallSignature) {
@@ -7226,15 +7025,8 @@ ${systemPromptVolatile}` : systemPrompt;
7226
7025
  process.stdout.write("\n");
7227
7026
  process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${maxToolRounds} rounds used, ${remaining} remaining
7228
7027
  `));
7229
- const recentHistory = roundToolHistory.slice(-autoPauseInterval);
7230
- if (recentHistory.length > 0) {
7231
- const toolCounts = /* @__PURE__ */ new Map();
7232
- for (const rh of recentHistory) {
7233
- for (const t of rh.tools) {
7234
- toolCounts.set(t, (toolCounts.get(t) || 0) + 1);
7235
- }
7236
- }
7237
- const summary = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]).map(([name, count]) => count > 1 ? `${name}\xD7${count}` : name).join(", ");
7028
+ const summary = summarizeRecentTools(roundToolHistory, autoPauseInterval);
7029
+ if (summary) {
7238
7030
  process.stdout.write(theme.dim(` Tools used: ${summary}
7239
7031
  `));
7240
7032
  }
@@ -7253,10 +7045,7 @@ ${systemPromptVolatile}` : systemPrompt;
7253
7045
  this.setupInterjectionListener();
7254
7046
  if (pauseResponse === "n" || pauseResponse === "N" || pauseResponse === "\x1B") {
7255
7047
  process.stdout.write(theme.warning("\u26A1 Stopped by user at auto-pause checkpoint\n"));
7256
- extraMessages.push({
7257
- role: "user",
7258
- content: `The user has stopped the task at round ${effectiveRound}/${maxToolRounds}. Do not call any more tools. Summarize what has been completed and what remains.`
7259
- });
7048
+ extraMessages.push({ role: "user", content: buildUserStopMessage(effectiveRound, maxToolRounds) });
7260
7049
  break;
7261
7050
  } else if (pauseResponse && pauseResponse !== "y" && pauseResponse !== "Y" && pauseResponse !== "") {
7262
7051
  process.stdout.write(theme.warning(`\u26A1 Redirect: "${pauseResponse}"
@@ -7276,13 +7065,7 @@ ${systemPromptVolatile}` : systemPrompt;
7276
7065
  spinner.start("Generating summary...");
7277
7066
  const summaryExtra = [
7278
7067
  ...extraMessages,
7279
- {
7280
- role: "user",
7281
- content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
7282
- 1. What work has been completed so far
7283
- 2. What tasks remain unfinished
7284
- 3. What the user can do next (e.g. send another request to continue)`
7285
- }
7068
+ { role: "user", content: buildRoundsExhaustedPrompt(maxToolRounds) }
7286
7069
  ];
7287
7070
  const summaryResult = await provider.chatWithTools(
7288
7071
  {
@@ -7633,7 +7416,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7633
7416
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7634
7417
  process.exit(1);
7635
7418
  }
7636
- const { startWebServer } = await import("./server-LOSA6ROG.js");
7419
+ const { startWebServer } = await import("./server-SLDE3AML.js");
7637
7420
  await startWebServer({ port, host: options.host });
7638
7421
  });
7639
7422
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7800,12 +7583,12 @@ program.command("sessions").description("List recent conversation sessions").opt
7800
7583
  console.log(footer + "\n");
7801
7584
  });
7802
7585
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7803
- const { runDoctorCli } = await import("./doctor-cli-E6FTBUEK.js");
7586
+ const { runDoctorCli } = await import("./doctor-cli-GQBR6RI6.js");
7804
7587
  await runDoctorCli({ json: !!options.json, resetStats: !!options.resetStats });
7805
7588
  });
7806
7589
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7807
7590
  try {
7808
- const batch = await import("./batch-OEAVIMNC.js");
7591
+ const batch = await import("./batch-DG5STMLZ.js");
7809
7592
  switch (action) {
7810
7593
  case "submit":
7811
7594
  if (!arg) {
@@ -7848,7 +7631,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7848
7631
  }
7849
7632
  });
7850
7633
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7851
- const { startMcpServer } = await import("./server-HHZHSKCO.js");
7634
+ const { startMcpServer } = await import("./server-RCUIX4R5.js");
7852
7635
  await startMcpServer({
7853
7636
  allowDestructive: !!options.allowDestructive,
7854
7637
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7857,7 +7640,7 @@ program.command("mcp-serve").description("Start an MCP server over STDIO, exposi
7857
7640
  });
7858
7641
  });
7859
7642
  program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number; diff fetched via `gh pr diff <num>`", (v) => parseInt(v, 10)).option("--base <ref>", "Base ref for `git diff <ref>...HEAD` (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").action(async (options) => {
7860
- const { runCi } = await import("./ci-2EUCJ23L.js");
7643
+ const { runCi } = await import("./ci-UXVUG3LC.js");
7861
7644
  const result = await runCi({
7862
7645
  pr: options.pr,
7863
7646
  base: options.base,
@@ -8002,7 +7785,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
8002
7785
  }),
8003
7786
  config.get("customProviders")
8004
7787
  );
8005
- const { startHub } = await import("./hub-5UDU2CL6.js");
7788
+ const { startHub } = await import("./hub-N75OQVNY.js");
8006
7789
  await startHub(
8007
7790
  {
8008
7791
  topic: topic ?? "",