jinzd-ai-cli 0.4.51 → 0.4.53
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/{chunk-5JJKUQTS.js → chunk-6I5FUNPR.js} +25 -2
- package/dist/{chunk-VTMHZCWZ.js → chunk-IXDGWT2Z.js} +54 -7
- package/dist/{chunk-3QRYIBN3.js → chunk-W6AK76UM.js} +5 -1
- package/dist/{chunk-KGJYHTC2.js → chunk-YIMTDKUW.js} +5 -1
- package/dist/{hub-F4TORNUA.js → hub-4DNFD6JK.js} +1 -1
- package/dist/index.js +68 -33
- package/dist/{run-tests-Q2JYJVLK.js → run-tests-3NNL7Z2E.js} +1 -1
- package/dist/{run-tests-W2T5CK43.js → run-tests-NJQK4B43.js} +1 -1
- package/dist/{server-4K2VEKN7.js → server-PFHWO3HL.js} +104 -19
- package/dist/{task-orchestrator-ZKJOZB5S.js → task-orchestrator-C42TNHE6.js} +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
- **8 Built-in Providers** — Claude, Gemini, DeepSeek, OpenAI, Zhipu GLM, Kimi, OpenRouter (300+ models), **Ollama** (local models, no API key needed)
|
|
25
25
|
- **3 Interfaces** — Terminal CLI, browser Web UI (`aicli web`), Electron desktop app
|
|
26
|
-
- **Agentic Tool Calling** — AI autonomously runs shell commands, reads/writes files, searches code, fetches web, runs tests (up to
|
|
26
|
+
- **Agentic Tool Calling** — AI autonomously runs shell commands, reads/writes files, searches code, fetches web, runs tests (default 200 rounds, configurable up to 10000 via `config.maxToolRounds` or `--max-tool-rounds`)
|
|
27
27
|
- **Streaming Tool Use** — Real-time streaming of AI reasoning and tool calls as they happen
|
|
28
28
|
- **Sub-Agents** — Delegate complex subtasks to isolated child agents with independent tool loops
|
|
29
29
|
- **Extended Thinking** — Claude deep reasoning mode with `/think` toggle
|
package/README.zh-CN.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- **8 大内置 Provider** — Claude、Gemini、DeepSeek、OpenAI、智谱 GLM、Kimi、OpenRouter(300+ 模型)、**Ollama**(本地模型,无需 API Key)
|
|
17
17
|
- **三种使用方式** — 终端 CLI、浏览器 Web UI(`aicli web`)、Electron 桌面应用
|
|
18
|
-
- **Agentic 工具调用** — AI 自主执行 bash
|
|
18
|
+
- **Agentic 工具调用** — AI 自主执行 bash 命令、读写文件、搜索代码、抓取网页、运行测试(默认 200 轮,可通过 `config.maxToolRounds` 或 `--max-tool-rounds` 调整,上限 10000)
|
|
19
19
|
- **流式工具调用** — 实时流式展示 AI 推理过程和工具调用
|
|
20
20
|
- **子代理系统** — 将复杂子任务委派给独立子代理执行
|
|
21
21
|
- **深度推理** — Claude Extended Thinking,`/think` 一键切换
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ProviderNotFoundError,
|
|
8
8
|
RateLimitError,
|
|
9
9
|
schemaToJsonSchema
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-IXDGWT2Z.js";
|
|
11
11
|
import {
|
|
12
12
|
APP_NAME,
|
|
13
13
|
CONFIG_DIR_NAME,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
MCP_TOOL_PREFIX,
|
|
21
21
|
PLUGINS_DIR_NAME,
|
|
22
22
|
VERSION
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-YIMTDKUW.js";
|
|
24
24
|
|
|
25
25
|
// src/config/config-manager.ts
|
|
26
26
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -167,6 +167,16 @@ var ConfigSchema = z.object({
|
|
|
167
167
|
// 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
|
|
168
168
|
// 默认开启。设为 false 则仅在手动 /compact 时压缩
|
|
169
169
|
autoCompact: z.boolean().default(true),
|
|
170
|
+
// Agentic 工具调用循环单次对话最大轮次(默认 200)。
|
|
171
|
+
// 超过此值后 AI 被强制停止调用工具并生成总结。
|
|
172
|
+
// 建议范围:25(保守)~ 1000(宽松,接近 Claude Code)。
|
|
173
|
+
// CLI `--max-tool-rounds <n>` 可覆盖此值。
|
|
174
|
+
maxToolRounds: z.number().int().min(1).max(1e4).default(200),
|
|
175
|
+
// 单次工具输出(如 read_file、bash、grep_files)返回给 AI 的最大字符数上限。
|
|
176
|
+
// 默认 500_000 (~500K chars ≈ 6000-8000 行代码)。
|
|
177
|
+
// 实际上限还会受模型 contextWindow 动态约束(取 contextWindow/4 作为下限)。
|
|
178
|
+
// 设置为 0 或未配置时使用默认值;不建议设为小于 12_000 或大于模型 contextWindow/2。
|
|
179
|
+
maxToolOutputChars: z.number().int().min(0).default(5e5),
|
|
170
180
|
// 插件加载开关(安全控制)
|
|
171
181
|
// 默认 false:不自动加载 ~/.aicli/plugins/ 中的插件文件。
|
|
172
182
|
// 插件以完整 Node.js 权限在主进程中执行(可读写文件、访问网络、执行命令),
|
|
@@ -2527,6 +2537,19 @@ var SessionManager = class {
|
|
|
2527
2537
|
this._current = session;
|
|
2528
2538
|
return session;
|
|
2529
2539
|
}
|
|
2540
|
+
/**
|
|
2541
|
+
* 直接设置当前会话(用于从内存缓存恢复未保存的会话)。
|
|
2542
|
+
* 与 `loadSession` 不同,此方法不读取磁盘,也不抛出错误。
|
|
2543
|
+
* Web 多 Tab 场景下,SessionHandler 会维护一份未保存会话的内存缓存,
|
|
2544
|
+
* 切换 Tab 时通过此方法将缓存中的会话设为当前会话,避免"Session not found"。
|
|
2545
|
+
*/
|
|
2546
|
+
setCurrent(session) {
|
|
2547
|
+
this._current = session;
|
|
2548
|
+
}
|
|
2549
|
+
/** 清除当前会话引用(下次访问将触发 lazy 创建)。 */
|
|
2550
|
+
clearCurrent() {
|
|
2551
|
+
this._current = null;
|
|
2552
|
+
}
|
|
2530
2553
|
async save() {
|
|
2531
2554
|
if (!this._current) return;
|
|
2532
2555
|
mkdirSync2(this.historyDir, { recursive: true });
|
|
@@ -4,12 +4,13 @@ import {
|
|
|
4
4
|
} from "./chunk-4BKXL7SM.js";
|
|
5
5
|
import {
|
|
6
6
|
CONFIG_DIR_NAME,
|
|
7
|
+
DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
|
|
7
8
|
MEMORY_FILE_NAME,
|
|
8
9
|
SUBAGENT_ALLOWED_TOOLS,
|
|
9
10
|
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
10
11
|
SUBAGENT_MAX_ROUNDS_LIMIT,
|
|
11
12
|
runTestsTool
|
|
12
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YIMTDKUW.js";
|
|
13
14
|
|
|
14
15
|
// src/tools/builtin/bash.ts
|
|
15
16
|
import { execSync } from "child_process";
|
|
@@ -624,7 +625,7 @@ function tryExtractPdfText(absPath) {
|
|
|
624
625
|
var readFileTool = {
|
|
625
626
|
definition: {
|
|
626
627
|
name: "read_file",
|
|
627
|
-
description: "Read text file contents. Automatically detects binary files (PDF/images/etc) and returns a hint instead of garbled output.",
|
|
628
|
+
description: "Read text file contents. Automatically detects binary files (PDF/images/etc) and returns a hint instead of garbled output. For very large files, use offset/limit to read specific line ranges (1-based line numbers).",
|
|
628
629
|
parameters: {
|
|
629
630
|
path: {
|
|
630
631
|
type: "string",
|
|
@@ -636,6 +637,16 @@ var readFileTool = {
|
|
|
636
637
|
description: "Encoding format, defaults to utf-8",
|
|
637
638
|
enum: ["utf-8", "utf8", "ascii", "base64"],
|
|
638
639
|
required: false
|
|
640
|
+
},
|
|
641
|
+
offset: {
|
|
642
|
+
type: "number",
|
|
643
|
+
description: "Starting line number (1-based). When set, only lines from this line onwards are returned. Useful for paging through very large files.",
|
|
644
|
+
required: false
|
|
645
|
+
},
|
|
646
|
+
limit: {
|
|
647
|
+
type: "number",
|
|
648
|
+
description: "Maximum number of lines to return starting from offset. When omitted, reads to end of file. Combine with offset to read a specific line range.",
|
|
649
|
+
required: false
|
|
639
650
|
}
|
|
640
651
|
},
|
|
641
652
|
dangerous: false
|
|
@@ -643,6 +654,10 @@ var readFileTool = {
|
|
|
643
654
|
async execute(args) {
|
|
644
655
|
const filePath = String(args["path"] ?? "");
|
|
645
656
|
const encoding = args["encoding"] ?? "utf-8";
|
|
657
|
+
const rawOffset = args["offset"];
|
|
658
|
+
const rawLimit = args["limit"];
|
|
659
|
+
const offsetLine = typeof rawOffset === "number" && Number.isFinite(rawOffset) && rawOffset > 0 ? Math.floor(rawOffset) : void 0;
|
|
660
|
+
const limitLines = typeof rawLimit === "number" && Number.isFinite(rawLimit) && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
|
|
646
661
|
if (!filePath) throw new ToolError("read_file", "path is required");
|
|
647
662
|
const normalizedPath = resolve2(filePath);
|
|
648
663
|
if (!existsSync3(normalizedPath)) {
|
|
@@ -679,8 +694,8 @@ Use the bash tool to read in segments, e.g.:
|
|
|
679
694
|
if (ext === ".pdf") {
|
|
680
695
|
const pdfText = tryExtractPdfText(normalizedPath);
|
|
681
696
|
if (pdfText) {
|
|
682
|
-
const
|
|
683
|
-
return `[PDF extracted: ${filePath} | ${
|
|
697
|
+
const lines = pdfText.split("\n").length;
|
|
698
|
+
return `[PDF extracted: ${filePath} | ${lines} lines]
|
|
684
699
|
|
|
685
700
|
${pdfText}`;
|
|
686
701
|
}
|
|
@@ -724,8 +739,26 @@ This file contains binary data and cannot be read as text.
|
|
|
724
739
|
If needed, use the bash tool to run an appropriate conversion program.`;
|
|
725
740
|
}
|
|
726
741
|
const content = buf.toString(encoding);
|
|
727
|
-
const
|
|
728
|
-
|
|
742
|
+
const allLines = content.split("\n");
|
|
743
|
+
const totalLines = allLines.length;
|
|
744
|
+
if (offsetLine !== void 0 || limitLines !== void 0) {
|
|
745
|
+
const startIdx = offsetLine !== void 0 ? Math.max(0, offsetLine - 1) : 0;
|
|
746
|
+
if (startIdx >= totalLines) {
|
|
747
|
+
return `${sensitiveWarning}[File: ${filePath} | ${totalLines} lines | offset=${offsetLine} beyond EOF]
|
|
748
|
+
|
|
749
|
+
The file has only ${totalLines} lines. Requested offset ${offsetLine} is past end-of-file.`;
|
|
750
|
+
}
|
|
751
|
+
const endIdx = limitLines !== void 0 ? Math.min(totalLines, startIdx + limitLines) : totalLines;
|
|
752
|
+
const slice = allLines.slice(startIdx, endIdx).join("\n");
|
|
753
|
+
const actualStart = startIdx + 1;
|
|
754
|
+
const actualEnd = endIdx;
|
|
755
|
+
const remaining = totalLines - endIdx;
|
|
756
|
+
const rangeNote = remaining > 0 ? `${totalLines} lines total, showing ${actualStart}-${actualEnd} (${remaining} more lines after this range; read again with offset=${endIdx + 1} to continue)` : `${totalLines} lines total, showing ${actualStart}-${actualEnd} (end of file)`;
|
|
757
|
+
return `${sensitiveWarning}[File: ${filePath} | ${rangeNote}]
|
|
758
|
+
|
|
759
|
+
${slice}`;
|
|
760
|
+
}
|
|
761
|
+
return `${sensitiveWarning}[File: ${filePath} | ${totalLines} lines]
|
|
729
762
|
|
|
730
763
|
${content}`;
|
|
731
764
|
}
|
|
@@ -983,12 +1016,25 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
|
|
|
983
1016
|
|
|
984
1017
|
// src/tools/truncate.ts
|
|
985
1018
|
var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
|
|
1019
|
+
var activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
|
|
1020
|
+
function setMaxOutputCap(cap) {
|
|
1021
|
+
if (!cap || cap <= 0) {
|
|
1022
|
+
activeCap = DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP;
|
|
1023
|
+
} else {
|
|
1024
|
+
activeCap = Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, cap);
|
|
1025
|
+
}
|
|
1026
|
+
if (lastContextWindow > 0) {
|
|
1027
|
+
activeMaxChars = getMaxOutputChars(lastContextWindow);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
986
1030
|
function getMaxOutputChars(contextWindow) {
|
|
987
1031
|
if (!contextWindow || contextWindow <= 0) return DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
988
|
-
return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4),
|
|
1032
|
+
return Math.max(DEFAULT_MAX_TOOL_OUTPUT_CHARS, Math.min(Math.floor(contextWindow / 4), activeCap));
|
|
989
1033
|
}
|
|
990
1034
|
var activeMaxChars = DEFAULT_MAX_TOOL_OUTPUT_CHARS;
|
|
1035
|
+
var lastContextWindow = 0;
|
|
991
1036
|
function setContextWindow(contextWindow) {
|
|
1037
|
+
lastContextWindow = contextWindow;
|
|
992
1038
|
activeMaxChars = getMaxOutputChars(contextWindow);
|
|
993
1039
|
}
|
|
994
1040
|
function getActiveMaxChars() {
|
|
@@ -4139,6 +4185,7 @@ export {
|
|
|
4139
4185
|
renderDiff,
|
|
4140
4186
|
runHook,
|
|
4141
4187
|
checkPermission,
|
|
4188
|
+
setMaxOutputCap,
|
|
4142
4189
|
setContextWindow,
|
|
4143
4190
|
truncateOutput,
|
|
4144
4191
|
ToolExecutor,
|
|
@@ -6,7 +6,7 @@ import { platform } from "os";
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
|
|
8
8
|
// src/core/constants.ts
|
|
9
|
-
var VERSION = "0.4.
|
|
9
|
+
var VERSION = "0.4.53";
|
|
10
10
|
var APP_NAME = "ai-cli";
|
|
11
11
|
var CONFIG_DIR_NAME = ".aicli";
|
|
12
12
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -19,6 +19,8 @@ var MEMORY_FILE_NAME = "memory.md";
|
|
|
19
19
|
var MEMORY_MAX_CHARS = 1e4;
|
|
20
20
|
var DEV_STATE_FILE_NAME = "dev-state.md";
|
|
21
21
|
var DEFAULT_MAX_TOKENS = 8192;
|
|
22
|
+
var DEFAULT_MAX_TOOL_ROUNDS = 200;
|
|
23
|
+
var DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP = 5e5;
|
|
22
24
|
var MCP_TOOL_PREFIX = "mcp__";
|
|
23
25
|
var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
|
|
24
26
|
var MCP_CONNECT_TIMEOUT = 3e4;
|
|
@@ -487,6 +489,8 @@ export {
|
|
|
487
489
|
MEMORY_MAX_CHARS,
|
|
488
490
|
DEV_STATE_FILE_NAME,
|
|
489
491
|
DEFAULT_MAX_TOKENS,
|
|
492
|
+
DEFAULT_MAX_TOOL_ROUNDS,
|
|
493
|
+
DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
|
|
490
494
|
MCP_TOOL_PREFIX,
|
|
491
495
|
MCP_PROJECT_CONFIG_NAME,
|
|
492
496
|
MCP_CONNECT_TIMEOUT,
|
|
@@ -8,7 +8,7 @@ import { platform } from "os";
|
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
|
|
10
10
|
// src/core/constants.ts
|
|
11
|
-
var VERSION = "0.4.
|
|
11
|
+
var VERSION = "0.4.53";
|
|
12
12
|
var APP_NAME = "ai-cli";
|
|
13
13
|
var CONFIG_DIR_NAME = ".aicli";
|
|
14
14
|
var CONFIG_FILE_NAME = "config.json";
|
|
@@ -21,6 +21,8 @@ var MEMORY_FILE_NAME = "memory.md";
|
|
|
21
21
|
var MEMORY_MAX_CHARS = 1e4;
|
|
22
22
|
var DEV_STATE_FILE_NAME = "dev-state.md";
|
|
23
23
|
var DEFAULT_MAX_TOKENS = 8192;
|
|
24
|
+
var DEFAULT_MAX_TOOL_ROUNDS = 200;
|
|
25
|
+
var DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP = 5e5;
|
|
24
26
|
var MCP_TOOL_PREFIX = "mcp__";
|
|
25
27
|
var MCP_PROJECT_CONFIG_NAME = ".mcp.json";
|
|
26
28
|
var MCP_CONNECT_TIMEOUT = 3e4;
|
|
@@ -491,6 +493,8 @@ export {
|
|
|
491
493
|
MEMORY_MAX_CHARS,
|
|
492
494
|
DEV_STATE_FILE_NAME,
|
|
493
495
|
DEFAULT_MAX_TOKENS,
|
|
496
|
+
DEFAULT_MAX_TOOL_ROUNDS,
|
|
497
|
+
DEFAULT_MAX_TOOL_OUTPUT_CHARS_CAP,
|
|
494
498
|
MCP_TOOL_PREFIX,
|
|
495
499
|
MCP_PROJECT_CONFIG_NAME,
|
|
496
500
|
MCP_CONNECT_TIMEOUT,
|
|
@@ -385,7 +385,7 @@ ${content}`);
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
async function runTaskMode(config, providers, configManager, topic) {
|
|
388
|
-
const { TaskOrchestrator } = await import("./task-orchestrator-
|
|
388
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-C42TNHE6.js");
|
|
389
389
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
390
390
|
let interrupted = false;
|
|
391
391
|
const onSigint = () => {
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
saveDevState,
|
|
25
25
|
sessionHasMeaningfulContent,
|
|
26
26
|
setupProxy
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-6I5FUNPR.js";
|
|
28
28
|
import {
|
|
29
29
|
ToolExecutor,
|
|
30
30
|
ToolRegistry,
|
|
@@ -34,10 +34,11 @@ import {
|
|
|
34
34
|
lastResponseStore,
|
|
35
35
|
renderDiff,
|
|
36
36
|
setContextWindow,
|
|
37
|
+
setMaxOutputCap,
|
|
37
38
|
spawnAgentContext,
|
|
38
39
|
theme,
|
|
39
40
|
undoStack
|
|
40
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-IXDGWT2Z.js";
|
|
41
42
|
import {
|
|
42
43
|
fileCheckpoints
|
|
43
44
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
CONTEXT_PRESSURE_THRESHOLD,
|
|
51
52
|
CUSTOM_COMMANDS_DIR_NAME,
|
|
52
53
|
DEFAULT_MAX_TOKENS,
|
|
54
|
+
DEFAULT_MAX_TOOL_ROUNDS,
|
|
53
55
|
DESCRIPTION,
|
|
54
56
|
DEV_STATE_FILE_NAME,
|
|
55
57
|
MCP_PROJECT_CONFIG_NAME,
|
|
@@ -61,7 +63,7 @@ import {
|
|
|
61
63
|
SKILLS_DIR_NAME,
|
|
62
64
|
VERSION,
|
|
63
65
|
buildUserIdentityPrompt
|
|
64
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-YIMTDKUW.js";
|
|
65
67
|
|
|
66
68
|
// src/index.ts
|
|
67
69
|
import { program } from "commander";
|
|
@@ -2104,7 +2106,7 @@ ${hint}` : "")
|
|
|
2104
2106
|
usage: "/test [command|filter]",
|
|
2105
2107
|
async execute(args, ctx) {
|
|
2106
2108
|
try {
|
|
2107
|
-
const { executeTests } = await import("./run-tests-
|
|
2109
|
+
const { executeTests } = await import("./run-tests-3NNL7Z2E.js");
|
|
2108
2110
|
const argStr = args.join(" ").trim();
|
|
2109
2111
|
let testArgs = {};
|
|
2110
2112
|
if (argStr) {
|
|
@@ -3277,7 +3279,6 @@ ${content}
|
|
|
3277
3279
|
parts.push(...imageParts);
|
|
3278
3280
|
return { parts, hasImage: imageParts.length > 0, refs };
|
|
3279
3281
|
}
|
|
3280
|
-
var MAX_TOOL_ROUNDS = 25;
|
|
3281
3282
|
var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
|
|
3282
3283
|
var MAX_CONSECUTIVE_FREE_ROUNDS = 3;
|
|
3283
3284
|
var MAX_REPEATED_TOOL_CALLS = 2;
|
|
@@ -3328,6 +3329,7 @@ var Repl = class {
|
|
|
3328
3329
|
if (options?.allowedTools) this.allowedTools = options.allowedTools;
|
|
3329
3330
|
if (options?.blockedTools) this.blockedTools = options.blockedTools;
|
|
3330
3331
|
if (options?.resumeSessionId) this.resumeSessionId = options.resumeSessionId;
|
|
3332
|
+
if (options?.maxToolRoundsOverride !== void 0) this.maxToolRoundsOverride = options.maxToolRoundsOverride;
|
|
3331
3333
|
}
|
|
3332
3334
|
rl;
|
|
3333
3335
|
currentProvider;
|
|
@@ -3381,6 +3383,8 @@ var Repl = class {
|
|
|
3381
3383
|
* 防止 selectFromList 结束后 stdin.pause()/resume() 释放的残留字节被解析为新命令。
|
|
3382
3384
|
*/
|
|
3383
3385
|
selecting = false;
|
|
3386
|
+
/** CLI --max-tool-rounds 覆盖值;未指定时从 config.maxToolRounds 读取 */
|
|
3387
|
+
maxToolRoundsOverride;
|
|
3384
3388
|
// ── /add-dir 目录上下文支持 ────────────────────────────────────────────────
|
|
3385
3389
|
/**
|
|
3386
3390
|
* 扫描目录内容,返回格式化字符串(含目录树 + 关键文件内容)。
|
|
@@ -3751,10 +3755,11 @@ ${projectContext}`);
|
|
|
3751
3755
|
const envInfo = `OS: ${osName}
|
|
3752
3756
|
${shellInfo}
|
|
3753
3757
|
Working directory: ${process.cwd()}`;
|
|
3758
|
+
const effectiveMaxRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
|
|
3754
3759
|
const budgetInfo = `
|
|
3755
3760
|
|
|
3756
3761
|
# Tool Round Budget
|
|
3757
|
-
You have a maximum of ${
|
|
3762
|
+
You have a maximum of ${effectiveMaxRounds} tool call rounds per conversation turn. Plan your work accordingly:
|
|
3758
3763
|
- Use efficient approaches: batch edits (replaceAll), read multiple files in one round, avoid re-reading files you already read.
|
|
3759
3764
|
- On Windows, use PowerShell cmdlets (Invoke-RestMethod, Get-ChildItem) instead of Unix commands (curl, grep, find).
|
|
3760
3765
|
- If starting a long-running server process via bash, use background execution \u2014 do not block the tool round.
|
|
@@ -4009,6 +4014,8 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4009
4014
|
const welcomeModelInfo = welcomeProvider?.info.models.find((m) => m.id === this.currentModel);
|
|
4010
4015
|
const profileNickname = this.config.get("userProfile")?.nickname || this.config.get("userProfile")?.name;
|
|
4011
4016
|
this.renderer.printWelcome(this.currentProvider, this.currentModel, welcomeModelInfo?.contextWindow, profileNickname);
|
|
4017
|
+
const cfgOutputCap = this.config.get("maxToolOutputChars");
|
|
4018
|
+
if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
|
|
4012
4019
|
if (welcomeModelInfo?.contextWindow) setContextWindow(welcomeModelInfo.contextWindow);
|
|
4013
4020
|
if (this.resumeSessionId) {
|
|
4014
4021
|
const session = this.sessions.current;
|
|
@@ -4765,11 +4772,12 @@ Session '${this.resumeSessionId}' not found.
|
|
|
4765
4772
|
}
|
|
4766
4773
|
const apiMessages = [...messages];
|
|
4767
4774
|
const extraMessages = [];
|
|
4775
|
+
const maxToolRounds = this.maxToolRoundsOverride ?? this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
|
|
4768
4776
|
const baseSystemPrompt = (this.buildCurrentSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
|
|
4769
4777
|
const roundBudgetHint = this.planMode ? `
|
|
4770
4778
|
|
|
4771
4779
|
[Tool Round Budget \u2014 Plan Mode]
|
|
4772
|
-
You have a maximum of ${
|
|
4780
|
+
You have a maximum of ${maxToolRounds} tool call rounds. You are in READ-ONLY Plan Mode:
|
|
4773
4781
|
- Only use: read_file, list_dir, grep_files, glob_files, ask_user, write_todos
|
|
4774
4782
|
- Do NOT attempt to call bash, write_file, edit_file \u2014 they are disabled
|
|
4775
4783
|
- Do NOT write shell commands or code blocks as a substitute for tool calls
|
|
@@ -4779,11 +4787,11 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds. You are in READ-ONLY
|
|
|
4779
4787
|
- Every ${AUTO_PAUSE_INTERVAL} rounds the user will be asked whether to continue.` : `
|
|
4780
4788
|
|
|
4781
4789
|
[Tool Round Budget]
|
|
4782
|
-
You have a maximum of ${
|
|
4790
|
+
You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
|
|
4783
4791
|
- Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
|
|
4784
4792
|
- Do NOT read the same file more than once \u2014 use the content from previous reads.
|
|
4785
4793
|
- Prioritize the most critical tasks first in case rounds run out.
|
|
4786
|
-
- When remaining rounds are low
|
|
4794
|
+
- When remaining rounds are low, focus on completing the current task and summarizing.
|
|
4787
4795
|
- Every ${AUTO_PAUSE_INTERVAL} rounds the user will be asked whether to continue \u2014 use this as a natural checkpoint to report progress.`;
|
|
4788
4796
|
const systemPrompt = baseSystemPrompt + roundBudgetHint;
|
|
4789
4797
|
const modelParams = this.getModelParams();
|
|
@@ -4797,8 +4805,16 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
4797
4805
|
const roundToolHistory = [];
|
|
4798
4806
|
this.setupInterjectionListener();
|
|
4799
4807
|
try {
|
|
4800
|
-
|
|
4801
|
-
|
|
4808
|
+
const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
|
|
4809
|
+
const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
|
|
4810
|
+
const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
|
|
4811
|
+
const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
|
|
4812
|
+
const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
|
|
4813
|
+
let warnedNote = false;
|
|
4814
|
+
let warnedLow = false;
|
|
4815
|
+
let warnedCritical = false;
|
|
4816
|
+
for (let round = 0; round < maxToolRounds; round++) {
|
|
4817
|
+
this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
|
|
4802
4818
|
if (this.toolExecutor.pendingSlashCommand) {
|
|
4803
4819
|
const cmd = this.toolExecutor.pendingSlashCommand;
|
|
4804
4820
|
this.toolExecutor.pendingSlashCommand = null;
|
|
@@ -4813,26 +4829,29 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
4813
4829
|
`));
|
|
4814
4830
|
extraMessages.push({ role: "user", content: cmd });
|
|
4815
4831
|
}
|
|
4816
|
-
const roundsLeft =
|
|
4817
|
-
if (roundsLeft
|
|
4832
|
+
const roundsLeft = maxToolRounds - round;
|
|
4833
|
+
if (!warnedCritical && roundsLeft <= warnCriticalEff) {
|
|
4834
|
+
warnedCritical = true;
|
|
4818
4835
|
extraMessages.push({
|
|
4819
4836
|
role: "user",
|
|
4820
|
-
content: `\u{
|
|
4837
|
+
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.`
|
|
4821
4838
|
});
|
|
4822
|
-
|
|
4839
|
+
process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
|
|
4840
|
+
`));
|
|
4841
|
+
} else if (!warnedLow && roundsLeft <= warnLowEff) {
|
|
4842
|
+
warnedLow = true;
|
|
4823
4843
|
extraMessages.push({
|
|
4824
4844
|
role: "user",
|
|
4825
4845
|
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.`
|
|
4826
4846
|
});
|
|
4827
4847
|
process.stdout.write(theme.warning(` \u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining
|
|
4828
4848
|
`));
|
|
4829
|
-
} else if (roundsLeft
|
|
4849
|
+
} else if (!warnedNote && roundsLeft <= warnNoteAt) {
|
|
4850
|
+
warnedNote = true;
|
|
4830
4851
|
extraMessages.push({
|
|
4831
4852
|
role: "user",
|
|
4832
|
-
content: `\u{
|
|
4853
|
+
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.`
|
|
4833
4854
|
});
|
|
4834
|
-
process.stdout.write(theme.error(` \u{1F6A8} Critical: ${roundsLeft} rounds remaining
|
|
4835
|
-
`));
|
|
4836
4855
|
}
|
|
4837
4856
|
if (this._userInterjection) {
|
|
4838
4857
|
const msg = this._userInterjection;
|
|
@@ -4891,7 +4910,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
4891
4910
|
const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
|
|
4892
4911
|
const coarseHallucination = !this.planMode && hasWriteTools && !alreadyWrote && !!result.content && detectsHallucinatedFileOp(result.content);
|
|
4893
4912
|
const phantomPaths = (coarseHallucination || alreadyWrote) && !this.planMode && hasWriteTools && result.content ? findPhantomClaims(result.content, extraMessages) : [];
|
|
4894
|
-
if ((phantomPaths.length > 0 || coarseHallucination) && round <
|
|
4913
|
+
if ((phantomPaths.length > 0 || coarseHallucination) && round < maxToolRounds - 1) {
|
|
4895
4914
|
const providerName = this.currentProvider;
|
|
4896
4915
|
const detail = phantomPaths.length > 0 ? ` phantom files: ${phantomPaths.join(", ")}` : "";
|
|
4897
4916
|
process.stderr.write(
|
|
@@ -4906,7 +4925,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
4906
4925
|
{ role: "assistant", content: result.content },
|
|
4907
4926
|
{ role: "user", content: correctionMsg }
|
|
4908
4927
|
);
|
|
4909
|
-
spinner.start(`Retrying... (round ${round + 2}/${
|
|
4928
|
+
spinner.start(`Retrying... (round ${round + 2}/${maxToolRounds})`);
|
|
4910
4929
|
continue;
|
|
4911
4930
|
}
|
|
4912
4931
|
spinner.stop();
|
|
@@ -5096,11 +5115,11 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5096
5115
|
extraMessages.push({ role: "user", content: msg });
|
|
5097
5116
|
}
|
|
5098
5117
|
const effectiveRound = round + 1;
|
|
5099
|
-
const remaining =
|
|
5118
|
+
const remaining = maxToolRounds - effectiveRound;
|
|
5100
5119
|
if (AUTO_PAUSE_INTERVAL > 0 && effectiveRound > 0 && effectiveRound % AUTO_PAUSE_INTERVAL === 0 && remaining > 0) {
|
|
5101
5120
|
spinner.stop();
|
|
5102
5121
|
process.stdout.write("\n");
|
|
5103
|
-
process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${
|
|
5122
|
+
process.stdout.write(theme.warning(`\u23F8 Auto-pause: ${effectiveRound}/${maxToolRounds} rounds used, ${remaining} remaining
|
|
5104
5123
|
`));
|
|
5105
5124
|
const recentHistory = roundToolHistory.slice(-AUTO_PAUSE_INTERVAL);
|
|
5106
5125
|
if (recentHistory.length > 0) {
|
|
@@ -5131,7 +5150,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5131
5150
|
process.stdout.write(theme.warning("\u26A1 Stopped by user at auto-pause checkpoint\n"));
|
|
5132
5151
|
extraMessages.push({
|
|
5133
5152
|
role: "user",
|
|
5134
|
-
content: `The user has stopped the task at round ${effectiveRound}/${
|
|
5153
|
+
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.`
|
|
5135
5154
|
});
|
|
5136
5155
|
break;
|
|
5137
5156
|
} else if (pauseResponse && pauseResponse !== "y" && pauseResponse !== "Y" && pauseResponse !== "") {
|
|
@@ -5144,7 +5163,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5144
5163
|
}
|
|
5145
5164
|
const nextRound = round + 2;
|
|
5146
5165
|
spinner.start(
|
|
5147
|
-
nextRound <=
|
|
5166
|
+
nextRound <= maxToolRounds ? `Thinking... (round ${nextRound}/${maxToolRounds})` : "Thinking..."
|
|
5148
5167
|
);
|
|
5149
5168
|
}
|
|
5150
5169
|
spinner.stop();
|
|
@@ -5154,7 +5173,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5154
5173
|
...extraMessages,
|
|
5155
5174
|
{
|
|
5156
5175
|
role: "user",
|
|
5157
|
-
content: `You have used all ${
|
|
5176
|
+
content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
|
|
5158
5177
|
1. What work has been completed so far
|
|
5159
5178
|
2. What tasks remain unfinished
|
|
5160
5179
|
3. What the user can do next (e.g. send another request to continue)`
|
|
@@ -5178,7 +5197,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5178
5197
|
);
|
|
5179
5198
|
spinner.stop();
|
|
5180
5199
|
if ("content" in summaryResult) {
|
|
5181
|
-
this.renderer.renderError(`Reached maximum tool call rounds (${
|
|
5200
|
+
this.renderer.renderError(`Reached maximum tool call rounds (${maxToolRounds}). Here is a summary:`);
|
|
5182
5201
|
this.renderer.renderResponse(summaryResult.content);
|
|
5183
5202
|
lastResponseStore.content = summaryResult.content;
|
|
5184
5203
|
session.addMessage({ role: "assistant", content: summaryResult.content, timestamp: /* @__PURE__ */ new Date() });
|
|
@@ -5188,13 +5207,13 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
5188
5207
|
}
|
|
5189
5208
|
} else {
|
|
5190
5209
|
this.renderer.renderError(
|
|
5191
|
-
`Reached maximum tool call rounds (${
|
|
5210
|
+
`Reached maximum tool call rounds (${maxToolRounds}). Stopping.
|
|
5192
5211
|
Tip: You can continue the conversation by asking the AI to proceed.`
|
|
5193
5212
|
);
|
|
5194
5213
|
}
|
|
5195
5214
|
} catch {
|
|
5196
5215
|
this.renderer.renderError(
|
|
5197
|
-
`Reached maximum tool call rounds (${
|
|
5216
|
+
`Reached maximum tool call rounds (${maxToolRounds}). Stopping.
|
|
5198
5217
|
Tip: You can continue the conversation by asking the AI to proceed.`
|
|
5199
5218
|
);
|
|
5200
5219
|
}
|
|
@@ -5428,7 +5447,7 @@ process.on("unhandledRejection", (reason) => {
|
|
|
5428
5447
|
process.exit(1);
|
|
5429
5448
|
});
|
|
5430
5449
|
program.name("ai-cli").description("Cross-platform REPL-style AI CLI with multi-provider support").version(VERSION);
|
|
5431
|
-
program.option("--provider <name>", "AI provider to use (claude/gemini/deepseek/zhipu/kimi or any custom provider)").option("-m, --model <name>", "Model to use").option("-p, --prompt <text>", "Headless mode: send a single prompt and exit (reads stdin as context if piped)").option("--system <prompt>", "System prompt override (headless mode)").option("--json", "Output result as JSON object (headless mode only)").option("--output-format <format>", "Output format: text (default) | streaming-json (NDJSON per-chunk, headless only)").option("--no-stream", "Disable streaming output").option("--allowed-tools <tools>", "Comma-separated whitelist of tools (e.g. bash,read_file)").option("--blocked-tools <tools>", "Comma-separated blacklist of tools (e.g. bash,write_file)").option("--resume <id>", "Resume a previous session by ID (prefix match supported)").action(async (options) => {
|
|
5450
|
+
program.option("--provider <name>", "AI provider to use (claude/gemini/deepseek/zhipu/kimi or any custom provider)").option("-m, --model <name>", "Model to use").option("-p, --prompt <text>", "Headless mode: send a single prompt and exit (reads stdin as context if piped)").option("--system <prompt>", "System prompt override (headless mode)").option("--json", "Output result as JSON object (headless mode only)").option("--output-format <format>", "Output format: text (default) | streaming-json (NDJSON per-chunk, headless only)").option("--no-stream", "Disable streaming output").option("--allowed-tools <tools>", "Comma-separated whitelist of tools (e.g. bash,read_file)").option("--blocked-tools <tools>", "Comma-separated blacklist of tools (e.g. bash,write_file)").option("--resume <id>", "Resume a previous session by ID (prefix match supported)").option("--max-tool-rounds <n>", "Max agentic tool-call rounds per conversation (default: 200)").action(async (options) => {
|
|
5432
5451
|
if (options.prompt !== void 0) {
|
|
5433
5452
|
await runHeadless(options);
|
|
5434
5453
|
} else {
|
|
@@ -5474,7 +5493,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5474
5493
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5475
5494
|
process.exit(1);
|
|
5476
5495
|
}
|
|
5477
|
-
const { startWebServer } = await import("./server-
|
|
5496
|
+
const { startWebServer } = await import("./server-PFHWO3HL.js");
|
|
5478
5497
|
await startWebServer({ port, host: options.host });
|
|
5479
5498
|
});
|
|
5480
5499
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -5707,7 +5726,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5707
5726
|
}),
|
|
5708
5727
|
config.get("customProviders")
|
|
5709
5728
|
);
|
|
5710
|
-
const { startHub } = await import("./hub-
|
|
5729
|
+
const { startHub } = await import("./hub-4DNFD6JK.js");
|
|
5711
5730
|
await startHub(
|
|
5712
5731
|
{
|
|
5713
5732
|
topic: topic ?? "",
|
|
@@ -5943,7 +5962,23 @@ Provider '${options.provider}' is not configured. Run: ai-cli config
|
|
|
5943
5962
|
process.stderr.write(`[info] Tool blacklist: ${[...blockedTools].join(", ")}
|
|
5944
5963
|
`);
|
|
5945
5964
|
}
|
|
5946
|
-
|
|
5965
|
+
let maxToolRoundsOverride;
|
|
5966
|
+
if (options.maxToolRounds !== void 0) {
|
|
5967
|
+
const parsed = parseInt(String(options.maxToolRounds), 10);
|
|
5968
|
+
if (!Number.isFinite(parsed) || parsed < 1) {
|
|
5969
|
+
console.error(`[error] --max-tool-rounds must be a positive integer, got: ${options.maxToolRounds}`);
|
|
5970
|
+
process.exit(1);
|
|
5971
|
+
}
|
|
5972
|
+
maxToolRoundsOverride = parsed;
|
|
5973
|
+
process.stderr.write(`[info] Max tool rounds override: ${parsed}
|
|
5974
|
+
`);
|
|
5975
|
+
}
|
|
5976
|
+
const repl = new Repl(providers, sessions, config, events, {
|
|
5977
|
+
allowedTools,
|
|
5978
|
+
blockedTools,
|
|
5979
|
+
resumeSessionId: options.resume,
|
|
5980
|
+
maxToolRoundsOverride
|
|
5981
|
+
});
|
|
5947
5982
|
if (options.model) {
|
|
5948
5983
|
const defaultModels = config.get("defaultModels");
|
|
5949
5984
|
const provider = options.provider ?? config.getDefaultProvider();
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
hadPreviousWriteToolCalls,
|
|
16
16
|
loadDevState,
|
|
17
17
|
setupProxy
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6I5FUNPR.js";
|
|
19
19
|
import {
|
|
20
20
|
AuthManager
|
|
21
21
|
} from "./chunk-BYNY5JPB.js";
|
|
@@ -30,10 +30,11 @@ import {
|
|
|
30
30
|
renderDiff,
|
|
31
31
|
runHook,
|
|
32
32
|
setContextWindow,
|
|
33
|
+
setMaxOutputCap,
|
|
33
34
|
spawnAgentContext,
|
|
34
35
|
truncateOutput,
|
|
35
36
|
undoStack
|
|
36
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-IXDGWT2Z.js";
|
|
37
38
|
import "./chunk-4BKXL7SM.js";
|
|
38
39
|
import {
|
|
39
40
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
CONTEXT_FILE_CANDIDATES,
|
|
43
44
|
CUSTOM_COMMANDS_DIR_NAME,
|
|
44
45
|
DEFAULT_MAX_TOKENS,
|
|
46
|
+
DEFAULT_MAX_TOOL_ROUNDS,
|
|
45
47
|
DESCRIPTION,
|
|
46
48
|
MCP_PROJECT_CONFIG_NAME,
|
|
47
49
|
MEMORY_FILE_NAME,
|
|
@@ -52,7 +54,7 @@ import {
|
|
|
52
54
|
SKILLS_DIR_NAME,
|
|
53
55
|
VERSION,
|
|
54
56
|
buildUserIdentityPrompt
|
|
55
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-YIMTDKUW.js";
|
|
56
58
|
|
|
57
59
|
// src/web/server.ts
|
|
58
60
|
import express from "express";
|
|
@@ -466,7 +468,6 @@ function loadMemoryContent(configDir) {
|
|
|
466
468
|
import { existsSync as existsSync3, readFileSync as readFileSync3, appendFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
467
469
|
import { join as join2, resolve } from "path";
|
|
468
470
|
import { execSync } from "child_process";
|
|
469
|
-
var MAX_TOOL_ROUNDS = 25;
|
|
470
471
|
var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
|
|
471
472
|
var MAX_CONSECUTIVE_FREE_ROUNDS = 5;
|
|
472
473
|
var SessionHandler = class _SessionHandler {
|
|
@@ -492,6 +493,15 @@ var SessionHandler = class _SessionHandler {
|
|
|
492
493
|
activeSystemPrompt;
|
|
493
494
|
/** Directories added via /add-dir */
|
|
494
495
|
addedDirs = /* @__PURE__ */ new Set();
|
|
496
|
+
/**
|
|
497
|
+
* 未保存会话的内存缓存(per-handler)。
|
|
498
|
+
* 当客户端通过 `session new` 创建新会话时,会话仅存在于 SessionManager._current 中,
|
|
499
|
+
* 未写入磁盘(saveIfNeeded 跳过空会话)。Web UI 多 Tab 场景下每个 Tab 都会创建自己的
|
|
500
|
+
* 空会话,切换 Tab 时 `session load <id>` 会读取磁盘并报"Session not found"。
|
|
501
|
+
* 此 Map 在本 SessionHandler 生命周期内持有这些未保存会话,load 时优先查询。
|
|
502
|
+
* 会话一旦保存到磁盘(有消息后),会从此 Map 中移除。
|
|
503
|
+
*/
|
|
504
|
+
unsavedSessions = /* @__PURE__ */ new Map();
|
|
495
505
|
constructor(ws, shared) {
|
|
496
506
|
this.ws = ws;
|
|
497
507
|
this.config = shared.config;
|
|
@@ -598,6 +608,8 @@ var SessionHandler = class _SessionHandler {
|
|
|
598
608
|
/** 根据当前模型 context window 更新工具输出截断上限 */
|
|
599
609
|
updateContextWindow() {
|
|
600
610
|
try {
|
|
611
|
+
const cfgOutputCap = this.config.get("maxToolOutputChars");
|
|
612
|
+
if (typeof cfgOutputCap === "number" && cfgOutputCap > 0) setMaxOutputCap(cfgOutputCap);
|
|
601
613
|
const provider = this.providers.get(this.currentProvider);
|
|
602
614
|
const modelInfo = provider?.info.models.find((m) => m.id === this.currentModel);
|
|
603
615
|
if (modelInfo?.contextWindow) setContextWindow(modelInfo.contextWindow);
|
|
@@ -607,13 +619,16 @@ var SessionHandler = class _SessionHandler {
|
|
|
607
619
|
/** Save session only if it exists and has messages (never persist empty "Untitled" sessions). */
|
|
608
620
|
saveIfNeeded() {
|
|
609
621
|
if (this.sessions.current && this.sessions.current.messages.length > 0) {
|
|
622
|
+
const id = this.sessions.current.id;
|
|
610
623
|
this.sessions.save();
|
|
624
|
+
this.unsavedSessions.delete(id);
|
|
611
625
|
}
|
|
612
626
|
}
|
|
613
627
|
/** Lazily create a session if none exists yet (deferred from constructor). */
|
|
614
628
|
ensureSession() {
|
|
615
629
|
if (!this.sessions.current) {
|
|
616
|
-
this.sessions.createSession(this.currentProvider, this.currentModel);
|
|
630
|
+
const created = this.sessions.createSession(this.currentProvider, this.currentModel);
|
|
631
|
+
this.unsavedSessions.set(created.id, created);
|
|
617
632
|
}
|
|
618
633
|
}
|
|
619
634
|
// ── Chat handling ────────────────────────────────────────────────
|
|
@@ -733,33 +748,56 @@ var SessionHandler = class _SessionHandler {
|
|
|
733
748
|
const session = this.sessions.current;
|
|
734
749
|
const apiMessages = [...messages];
|
|
735
750
|
const extraMessages = [];
|
|
751
|
+
const maxToolRounds = this.config.get("maxToolRounds") ?? DEFAULT_MAX_TOOL_ROUNDS;
|
|
736
752
|
const baseSystemPrompt = (this.buildSystemPrompt() ?? "") + TOOL_CALL_REMINDER;
|
|
737
753
|
const roundBudgetHint = `
|
|
738
754
|
|
|
739
755
|
[Tool Round Budget]
|
|
740
|
-
You have a maximum of ${
|
|
756
|
+
You have a maximum of ${maxToolRounds} tool call rounds for this task. Plan efficiently:
|
|
741
757
|
- Prefer batch operations (e.g. global find-and-replace) over repetitive single edits.
|
|
742
758
|
- Prioritize the most critical tasks first in case rounds run out.
|
|
743
|
-
- When remaining rounds are low
|
|
759
|
+
- When remaining rounds are low, focus on completing the current task and summarizing.`;
|
|
744
760
|
const systemPrompt = baseSystemPrompt + roundBudgetHint;
|
|
745
761
|
const modelParams = this.getModelParams();
|
|
746
762
|
const roundUsage = { inputTokens: 0, outputTokens: 0 };
|
|
747
763
|
const supportsStreamingTools = typeof provider.chatWithToolsStream === "function";
|
|
748
764
|
let consecutiveFreeRounds = 0;
|
|
765
|
+
const warnNoteAt = Math.max(10, Math.floor(maxToolRounds * 0.2));
|
|
766
|
+
const warnLowAt = Math.max(5, Math.floor(maxToolRounds * 0.1));
|
|
767
|
+
const warnCriticalAt = Math.max(3, Math.floor(maxToolRounds * 0.05));
|
|
768
|
+
const warnLowEff = Math.min(warnLowAt, warnNoteAt - 1);
|
|
769
|
+
const warnCriticalEff = Math.min(warnCriticalAt, warnLowEff - 1);
|
|
770
|
+
let warnedNote = false;
|
|
771
|
+
let warnedLow = false;
|
|
772
|
+
let warnedCritical = false;
|
|
749
773
|
const ac = new AbortController();
|
|
750
774
|
this.abortController = ac;
|
|
751
775
|
try {
|
|
752
|
-
for (let round = 0; round <
|
|
776
|
+
for (let round = 0; round < maxToolRounds; round++) {
|
|
753
777
|
if (ac.signal.aborted) break;
|
|
754
|
-
this.toolExecutor.setRoundInfo(round + 1,
|
|
755
|
-
this.send({ type: "round_progress", current: round + 1, total:
|
|
756
|
-
const roundsLeft =
|
|
757
|
-
if (roundsLeft
|
|
778
|
+
this.toolExecutor.setRoundInfo(round + 1, maxToolRounds);
|
|
779
|
+
this.send({ type: "round_progress", current: round + 1, total: maxToolRounds });
|
|
780
|
+
const roundsLeft = maxToolRounds - round;
|
|
781
|
+
if (!warnedCritical && roundsLeft <= warnCriticalEff) {
|
|
782
|
+
warnedCritical = true;
|
|
783
|
+
extraMessages.push({
|
|
784
|
+
role: "user",
|
|
785
|
+
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.`
|
|
786
|
+
});
|
|
787
|
+
this.send({ type: "info", message: `\u{1F6A8} Critical: ${roundsLeft} rounds remaining` });
|
|
788
|
+
} else if (!warnedLow && roundsLeft <= warnLowEff) {
|
|
789
|
+
warnedLow = true;
|
|
758
790
|
extraMessages.push({
|
|
759
791
|
role: "user",
|
|
760
792
|
content: `\u26A0\uFE0F Budget warning: Only ${roundsLeft} tool rounds remaining. Prioritize completing the most critical task. If you cannot finish everything, summarize what's done and what remains.`
|
|
761
793
|
});
|
|
762
794
|
this.send({ type: "info", message: `\u26A0\uFE0F Low budget: ${roundsLeft} rounds remaining` });
|
|
795
|
+
} else if (!warnedNote && roundsLeft <= warnNoteAt) {
|
|
796
|
+
warnedNote = true;
|
|
797
|
+
extraMessages.push({
|
|
798
|
+
role: "user",
|
|
799
|
+
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.`
|
|
800
|
+
});
|
|
763
801
|
}
|
|
764
802
|
if (this.userInterjection) {
|
|
765
803
|
const msg = this.userInterjection;
|
|
@@ -795,7 +833,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
795
833
|
if (result.content && !result.toolCalls) {
|
|
796
834
|
const hasWriteTools = toolDefs.some((t) => t.name === "write_file" || t.name === "edit_file");
|
|
797
835
|
const alreadyWrote = hadPreviousWriteToolCalls(extraMessages);
|
|
798
|
-
if (hasWriteTools && !alreadyWrote && detectsHallucinatedFileOp(result.content) && round <
|
|
836
|
+
if (hasWriteTools && !alreadyWrote && detectsHallucinatedFileOp(result.content) && round < maxToolRounds - 1) {
|
|
799
837
|
this.send({ type: "info", message: "\u26A0 Hallucinated completion detected, forcing retry..." });
|
|
800
838
|
extraMessages.push(
|
|
801
839
|
{ role: "assistant", content: result.content },
|
|
@@ -844,7 +882,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
844
882
|
...extraMessages,
|
|
845
883
|
{
|
|
846
884
|
role: "user",
|
|
847
|
-
content: `You have used all ${
|
|
885
|
+
content: `You have used all ${maxToolRounds} tool call rounds. Do not call any more tools. Summarize in text:
|
|
848
886
|
1. What work has been completed so far
|
|
849
887
|
2. What tasks remain unfinished
|
|
850
888
|
3. What the user can do next`
|
|
@@ -866,7 +904,7 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
866
904
|
if ("content" in summaryResult && summaryResult.content) {
|
|
867
905
|
this.send({
|
|
868
906
|
type: "response_done",
|
|
869
|
-
content: `\u26A0 Reached maximum tool call rounds (${
|
|
907
|
+
content: `\u26A0 Reached maximum tool call rounds (${maxToolRounds}).
|
|
870
908
|
|
|
871
909
|
${summaryResult.content}`,
|
|
872
910
|
usage: roundUsage
|
|
@@ -876,7 +914,7 @@ ${summaryResult.content}`,
|
|
|
876
914
|
} catch {
|
|
877
915
|
this.send({
|
|
878
916
|
type: "error",
|
|
879
|
-
message: `Reached maximum tool call rounds (${
|
|
917
|
+
message: `Reached maximum tool call rounds (${maxToolRounds}). You can continue by asking the AI to proceed.`
|
|
880
918
|
});
|
|
881
919
|
}
|
|
882
920
|
this.sessionTokenUsage.inputTokens += roundUsage.inputTokens;
|
|
@@ -1043,7 +1081,8 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1043
1081
|
const sub = args[0];
|
|
1044
1082
|
if (sub === "new") {
|
|
1045
1083
|
this.saveIfNeeded();
|
|
1046
|
-
this.sessions.createSession(this.currentProvider, this.currentModel);
|
|
1084
|
+
const created = this.sessions.createSession(this.currentProvider, this.currentModel);
|
|
1085
|
+
this.unsavedSessions.set(created.id, created);
|
|
1047
1086
|
this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1048
1087
|
this.send({ type: "info", message: "New session created." });
|
|
1049
1088
|
this.sendStatus();
|
|
@@ -1051,6 +1090,20 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1051
1090
|
} else if (sub === "load" && args[1]) {
|
|
1052
1091
|
const targetId = args[1];
|
|
1053
1092
|
this.saveIfNeeded();
|
|
1093
|
+
const cachedExact = this.unsavedSessions.get(targetId);
|
|
1094
|
+
const cached = cachedExact ?? [...this.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
|
|
1095
|
+
if (cached) {
|
|
1096
|
+
this.sessions.setCurrent(cached);
|
|
1097
|
+
this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1098
|
+
this.send({
|
|
1099
|
+
type: "info",
|
|
1100
|
+
message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
|
|
1101
|
+
});
|
|
1102
|
+
this.sendSessionMessages();
|
|
1103
|
+
this.sendStatus();
|
|
1104
|
+
this.sendSessionList();
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1054
1107
|
const list = this.sessions.listSessions();
|
|
1055
1108
|
const found = list.find((s) => s.id.startsWith(targetId));
|
|
1056
1109
|
if (found) {
|
|
@@ -1061,12 +1114,30 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1061
1114
|
this.sendStatus();
|
|
1062
1115
|
this.sendSessionList();
|
|
1063
1116
|
} else {
|
|
1064
|
-
this.
|
|
1117
|
+
const recreated = this.sessions.createSession(this.currentProvider, this.currentModel);
|
|
1118
|
+
this.unsavedSessions.set(recreated.id, recreated);
|
|
1119
|
+
this.sessionTokenUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1120
|
+
this.send({
|
|
1121
|
+
type: "info",
|
|
1122
|
+
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
1123
|
+
});
|
|
1124
|
+
this.sendStatus();
|
|
1125
|
+
this.sendSessionList();
|
|
1065
1126
|
}
|
|
1066
1127
|
} else if (sub === "list") {
|
|
1067
1128
|
this.sendSessionList();
|
|
1068
1129
|
} else if (sub === "delete" && args[1]) {
|
|
1069
1130
|
const targetId = args[1];
|
|
1131
|
+
const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
1132
|
+
if (cachedKey) {
|
|
1133
|
+
this.unsavedSessions.delete(cachedKey);
|
|
1134
|
+
if (this.sessions.current?.id === cachedKey) {
|
|
1135
|
+
this.sessions.clearCurrent();
|
|
1136
|
+
}
|
|
1137
|
+
this.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
1138
|
+
this.sendSessionList();
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1070
1141
|
const list = this.sessions.listSessions();
|
|
1071
1142
|
const found = list.find((s) => s.id.startsWith(targetId));
|
|
1072
1143
|
if (found) {
|
|
@@ -1085,6 +1156,12 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1085
1156
|
const list = this.sessions.listSessions();
|
|
1086
1157
|
let deleted = 0;
|
|
1087
1158
|
for (const targetId of ids) {
|
|
1159
|
+
const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
1160
|
+
if (cachedKey) {
|
|
1161
|
+
this.unsavedSessions.delete(cachedKey);
|
|
1162
|
+
deleted++;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1088
1165
|
const found = list.find((s) => s.id.startsWith(targetId));
|
|
1089
1166
|
if (found) {
|
|
1090
1167
|
this.sessions.deleteSession(found.id);
|
|
@@ -1100,6 +1177,14 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1100
1177
|
this.send({ type: "error", message: "Title cannot be empty." });
|
|
1101
1178
|
break;
|
|
1102
1179
|
}
|
|
1180
|
+
const cachedKey = this.unsavedSessions.has(targetId) ? targetId : [...this.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
1181
|
+
if (cachedKey) {
|
|
1182
|
+
const session = this.unsavedSessions.get(cachedKey);
|
|
1183
|
+
session.title = newTitle;
|
|
1184
|
+
this.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
1185
|
+
this.sendSessionList();
|
|
1186
|
+
break;
|
|
1187
|
+
}
|
|
1103
1188
|
const list = this.sessions.listSessions();
|
|
1104
1189
|
const found = list.find((s) => s.id.startsWith(targetId));
|
|
1105
1190
|
if (found) {
|
|
@@ -1606,7 +1691,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1606
1691
|
case "test": {
|
|
1607
1692
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1608
1693
|
try {
|
|
1609
|
-
const { executeTests } = await import("./run-tests-
|
|
1694
|
+
const { executeTests } = await import("./run-tests-3NNL7Z2E.js");
|
|
1610
1695
|
const argStr = args.join(" ").trim();
|
|
1611
1696
|
let testArgs = {};
|
|
1612
1697
|
if (argStr) {
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
getDangerLevel,
|
|
5
5
|
googleSearchContext,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-IXDGWT2Z.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
9
|
import {
|
|
10
10
|
SUBAGENT_ALLOWED_TOOLS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-YIMTDKUW.js";
|
|
12
12
|
|
|
13
13
|
// src/hub/task-orchestrator.ts
|
|
14
14
|
import { createInterface } from "readline";
|