@huicai/cli 2026.711.0 → 2026.711.24616
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/LICENSE +43 -21
- package/package.json +4 -3
- package/src/advisor.js +1 -46
- package/src/agent.js +1 -518
- package/src/api.js +1 -359
- package/src/autofix.js +1 -90
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -99
- package/src/clipboard.js +1 -34
- package/src/codeindex.js +1 -229
- package/src/colors.js +1 -47
- package/src/commands.js +1 -1294
- package/src/compaction.js +1 -144
- package/src/config.js +1 -86
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -119
- package/src/context.js +1 -152
- package/src/cron.js +1 -64
- package/src/daemon.js +1 -62
- package/src/device.js +1 -88
- package/src/diffview.js +1 -115
- package/src/doctor.js +1 -72
- package/src/executors.js +1 -391
- package/src/forks.js +1 -59
- package/src/gitflow.js +1 -140
- package/src/hooks.js +1 -207
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -102
- package/src/kbsync.js +1 -141
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -161
- package/src/permissions.js +1 -120
- package/src/render.js +1 -138
- package/src/repl.js +1 -271
- package/src/scan.js +1 -84
- package/src/schedule.js +1 -48
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -183
- package/src/todo.js +1 -75
- package/src/tools.js +1 -147
- package/src/transcript.js +1 -94
- package/src/tui/app.js +1 -666
- package/src/usage.js +1 -43
- package/src/watch.js +1 -74
package/src/agent.js
CHANGED
|
@@ -1,518 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 核心机制:循环与工具都在客户端,平台只做模型推理与计费。
|
|
3
|
-
//
|
|
4
|
-
// 两种运行形态(按 config.model 自动判定):
|
|
5
|
-
// - 原始模型:本机工具循环(内置文件/命令工具 + todo_write + memory_write + extraTools[MCP/子智能体]);
|
|
6
|
-
// - 伪模型 agent-/workflow-/datahuman-:平台编排直连(不传 tools,纯流式多轮)——即智能体/数字人/工作流
|
|
7
|
-
// 分享页的对话能力,CLI/扩展/REPL/TUI 全部可用。
|
|
8
|
-
//
|
|
9
|
-
// 关键机制:
|
|
10
|
-
// - 连续的只读工具调用并行执行(结果仍按原顺序回灌);
|
|
11
|
-
// - 工具输出截断保头尾注入上下文;工具参数 JSON 解析失败时把错误回灌模型(而非静默空参数);
|
|
12
|
-
// - 中断时为未执行的 tool_calls 合成占位结果(协议配对完整);
|
|
13
|
-
// - pullQueued:用户回合中排队的消息在工具轮间隙/回合末注入;
|
|
14
|
-
// - 思考过程(reasoning)流式上抛(events.onReasoning / REPL 灰字),config.showReasoning=false 关闭;
|
|
15
|
-
// - 上下文压缩为**非破坏式**:摘要存 compaction 状态(随会话持久化),原消息/页面显示不受影响,
|
|
16
|
-
// 仅发给模型时经 buildModelMessages 组装视图;
|
|
17
|
-
// - 文本化工具调用防泄漏(与平台侧同款):模型把工具调用写成纯文本标记时(DeepSeek 系退化)
|
|
18
|
-
// 过滤不显示、不进会话,并注入纠正指令重试(一次要求改走 tool_calls 通道,二次禁工具强制作答)。
|
|
19
|
-
|
|
20
|
-
import fs from 'node:fs';
|
|
21
|
-
import path from 'node:path';
|
|
22
|
-
import { streamChat } from './api.js';
|
|
23
|
-
import { MarkupFilter, stripMarkup } from './markup-filter.js';
|
|
24
|
-
import { TOOL_SPECS, READONLY_TOOLS, runTool } from './tools.js';
|
|
25
|
-
import { toolLabel, indent, renderFileDiff } from './render.js';
|
|
26
|
-
import { compactIfNeeded, buildModelMessages } from './compaction.js';
|
|
27
|
-
import { makeTodoTool, formatTodos, TODO_TOOL_NAME } from './todo.js';
|
|
28
|
-
import { makeMemoryTool } from './memory.js';
|
|
29
|
-
import { loadHooks, runHooks } from './hooks.js';
|
|
30
|
-
import * as ide from './ide.js';
|
|
31
|
-
import { c } from './colors.js';
|
|
32
|
-
|
|
33
|
-
const DEFAULT_MAX_ITERS = 120; // 中大型项目需更多工具轮(读多文件/多次检索/多处编辑);仍可 config.maxTurns 或 --max-turns 覆盖
|
|
34
|
-
const TOOL_CONCURRENCY = 8; // 并行只读工具的并发上限:大仓库里模型一次触发大量 grep/glob 时限并发,降争用、提稳定与总吞吐
|
|
35
|
-
const MAX_TOOL_RESULT_CHARS = 20_000;
|
|
36
|
-
const CAP_HEAD = 16_000;
|
|
37
|
-
const CAP_TAIL = 3_000;
|
|
38
|
-
|
|
39
|
-
// 「文本化工具调用」被拦截后的纠正指令:与平台侧 ToolCallingOrchestrator 同款两级策略。
|
|
40
|
-
// 第一次:要求改走规范 tool_calls 通道重试;第二次:禁用工具、强制直接作答(保证用户拿到干净答复)。
|
|
41
|
-
const MARKUP_RETRY_PROMPT =
|
|
42
|
-
'你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。'
|
|
43
|
-
+ '请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,'
|
|
44
|
-
+ '绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。';
|
|
45
|
-
const MARKUP_FORCE_ANSWER_PROMPT =
|
|
46
|
-
'请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,'
|
|
47
|
-
+ '不要输出任何工具调用或协议标记。';
|
|
48
|
-
// 预算收敛:临近工具轮数上限时注入,禁用工具并要求直接作答,保证中大型项目里到达上限也能给出干净的最终答复(而非被硬截断)。
|
|
49
|
-
const BUDGET_FINAL_PROMPT =
|
|
50
|
-
'已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。';
|
|
51
|
-
// 自我纠错:短时间内多次工具失败时注入,打破「反复用同样会失败的调用」的死循环。
|
|
52
|
-
const SELF_CORRECT_NOTE =
|
|
53
|
-
'你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?'
|
|
54
|
-
+ '先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。';
|
|
55
|
-
// 判断一条工具结果是否为「失败」(用于自我纠错计数)。
|
|
56
|
-
function looksLikeToolFailure(result) {
|
|
57
|
-
const s = String(result);
|
|
58
|
-
return s.startsWith('工具执行失败') || s.startsWith('命令启动失败')
|
|
59
|
-
|| s.includes('不是合法 JSON') || s.includes('未找到要替换的内容')
|
|
60
|
-
|| s.includes('路径超出项目目录') || s.includes('是目录而非文件');
|
|
61
|
-
}
|
|
62
|
-
// 计划模式(/plan 开启):每轮追加到系统提示词末尾,令模型只读调研并产出方案,写/执行由权限层拦截。
|
|
63
|
-
const PLAN_MODE_NOTE =
|
|
64
|
-
'\n\n# 计划模式(当前开启)\n'
|
|
65
|
-
+ '你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,'
|
|
66
|
-
+ '不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。'
|
|
67
|
-
+ '请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,'
|
|
68
|
-
+ '用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。';
|
|
69
|
-
// 沙箱模式(/sandbox 开启):禁 shell 与联网。
|
|
70
|
-
const SANDBOX_MODE_NOTE =
|
|
71
|
-
'\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)与联网(fetch_url),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。';
|
|
72
|
-
|
|
73
|
-
/** /goal 设定的目标:每轮追加到系统提示词末尾,令模型停止前自检是否达成。 */
|
|
74
|
-
function goalNote(goal) {
|
|
75
|
-
return '\n\n# 当前目标(用户设定)\n' + String(goal).trim()
|
|
76
|
-
+ '\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。';
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 从消息 content(字符串或多模态数组)提取纯文本,用于钩子上下文。 */
|
|
80
|
-
function contentText(content) {
|
|
81
|
-
if (typeof content === 'string') return content;
|
|
82
|
-
if (Array.isArray(content)) return content.filter((p) => p?.type === 'text').map((p) => p.text || '').join('\n');
|
|
83
|
-
return '';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** 有序的有限并发 map:并发不超过 limit,结果按输入下标回填(与 Promise.all 相同的顺序语义,但并发受限)。 */
|
|
87
|
-
async function mapLimit(items, limit, fn) {
|
|
88
|
-
const results = new Array(items.length);
|
|
89
|
-
let next = 0;
|
|
90
|
-
const worker = async () => {
|
|
91
|
-
while (next < items.length) {
|
|
92
|
-
const i = next++;
|
|
93
|
-
results[i] = await fn(items[i], i);
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
97
|
-
return results;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** 伪模型(平台编排)判定:agent-/workflow-/datahuman- 前缀。 */
|
|
101
|
-
export function isPlatformModel(model) {
|
|
102
|
-
return /^(agent|workflow|datahuman)-/.test(String(model || ''));
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function shortLine(s, max = 200) {
|
|
106
|
-
const first = String(s).split('\n')[0];
|
|
107
|
-
return first.length > max ? first.slice(0, max) + '…' : first;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** 工具输出超长时截断(保头尾),防止单条结果撑爆上下文。 */
|
|
111
|
-
export function capToolResult(text) {
|
|
112
|
-
const s = String(text);
|
|
113
|
-
if (s.length <= MAX_TOOL_RESULT_CHARS) return s;
|
|
114
|
-
const omitted = s.length - CAP_HEAD - CAP_TAIL;
|
|
115
|
-
return s.slice(0, CAP_HEAD) + `\n…【工具输出过长,中间已省略 ${omitted} 字符】…\n` + s.slice(-CAP_TAIL);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* 跑一个用户回合:mutate messages(追加 assistant/tool 消息),流式打印正文与工具进展。
|
|
120
|
-
*
|
|
121
|
-
* @param {object} p
|
|
122
|
-
* @param {{spec:object,run:Function,readOnly:boolean}[]} [p.extraTools] 动态外部工具(MCP / 子智能体)
|
|
123
|
-
* @param {number} [p.depth] 子智能体递归深度(0=顶层;todo/memory 仅顶层提供)
|
|
124
|
-
* @param {{write:(s:string)=>void}} [p.io] 输出通道(子智能体传静默通道)
|
|
125
|
-
* @param {object} [p.events] 结构化事件:onDelta/onReasoning/onAssistantText/onToolStart/onToolResult/onFileDiff/onSkip/onInfo/onTodo/onMemory/onTurnEnd
|
|
126
|
-
* @param {{list:any[]}} [p.todoState] 任务清单状态(调用方持有可跨轮保留)
|
|
127
|
-
* @param {{upTo?:number, summary?:string}} [p.compaction] 压缩状态(随会话持久化;原消息不被修改)
|
|
128
|
-
* @param {() => (string|null)} [p.pullQueued] 取一条用户排队消息(无则 null)
|
|
129
|
-
* @param {number} [p.maxIters] 本回合最大工具轮数(默认 config.maxTurns 或 60)
|
|
130
|
-
* @returns {Promise<{aborted?:boolean, error?:string, usage:{prompt:number,completion:number}, lastPromptTokens:number}>}
|
|
131
|
-
*/
|
|
132
|
-
export async function runTurn({
|
|
133
|
-
config, messages, permissions, cwd, ask, signal,
|
|
134
|
-
extraTools = [], depth = 0, io, events, todoState, compaction, pullQueued, maxIters, fileJournal,
|
|
135
|
-
}) {
|
|
136
|
-
const out = (io && io.write) || ((s) => process.stdout.write(s));
|
|
137
|
-
const ev = events || null; // 结构化事件(ink TUI / 扩展用);为空则走 out() 字符串输出(普通 REPL)
|
|
138
|
-
const info = (t) => {
|
|
139
|
-
if (ev) ev.onInfo?.(t);
|
|
140
|
-
else out(c.gray(' · ' + t) + '\n');
|
|
141
|
-
};
|
|
142
|
-
let prompt = 0;
|
|
143
|
-
let completion = 0;
|
|
144
|
-
let lastPromptTokens = 0;
|
|
145
|
-
const iterMax = Number(maxIters || config.maxTurns) || DEFAULT_MAX_ITERS;
|
|
146
|
-
const platformMode = isPlatformModel(config.model);
|
|
147
|
-
|
|
148
|
-
// 按任务自动选模型:非伪模型 + 配置了 strong 模型 + 本回合看起来复杂(长提示/难点关键词/计划模式)→ 升配
|
|
149
|
-
let effectiveModel = config.model;
|
|
150
|
-
if (depth === 0 && config.autoModelStrong && !platformMode) {
|
|
151
|
-
const lastUser = [...messages].reverse().find((m) => m.role === 'user');
|
|
152
|
-
const t = contentText(lastUser?.content);
|
|
153
|
-
const hard = config.planMode || t.length > 400
|
|
154
|
-
|| /重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(t);
|
|
155
|
-
if (hard && config.autoModelStrong !== config.model) {
|
|
156
|
-
effectiveModel = config.autoModelStrong;
|
|
157
|
-
if (config.debug) info(`[automodel] 复杂回合升配 → ${effectiveModel}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
const compState = compaction || {}; // 未传则本回合内独立生效(不持久化)
|
|
161
|
-
|
|
162
|
-
// 顶层注入 todo_write / memory_write(平台编排模式无本机工具,均不注入)
|
|
163
|
-
const dynamicTools = [...extraTools];
|
|
164
|
-
if (depth === 0 && !platformMode && !dynamicTools.some((t) => t.spec?.function?.name === TODO_TOOL_NAME)) {
|
|
165
|
-
const state = todoState || { list: [] };
|
|
166
|
-
dynamicTools.push(
|
|
167
|
-
makeTodoTool({
|
|
168
|
-
state,
|
|
169
|
-
emit: (list) => {
|
|
170
|
-
if (ev) {
|
|
171
|
-
if (ev.onTodo) ev.onTodo(list.map((t) => ({ ...t })));
|
|
172
|
-
else ev.onInfo?.('任务清单:\n' + formatTodos(list));
|
|
173
|
-
} else {
|
|
174
|
-
out(c.gray(indent(formatTodos(list), ' ')) + '\n');
|
|
175
|
-
}
|
|
176
|
-
},
|
|
177
|
-
}),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
if (depth === 0 && !platformMode && config.memoryEnabled !== false
|
|
181
|
-
&& !dynamicTools.some((t) => t.spec?.function?.name === 'memory_write')) {
|
|
182
|
-
dynamicTools.push(
|
|
183
|
-
makeMemoryTool({
|
|
184
|
-
cwd,
|
|
185
|
-
emit: (t) => {
|
|
186
|
-
if (ev) {
|
|
187
|
-
if (ev.onMemory) ev.onMemory(t);
|
|
188
|
-
else ev.onInfo?.('已记入长期记忆:' + shortLine(t, 80));
|
|
189
|
-
} else {
|
|
190
|
-
out(c.gray(' ✎ 已记入长期记忆:' + shortLine(t, 80)) + '\n');
|
|
191
|
-
}
|
|
192
|
-
},
|
|
193
|
-
}),
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const extraByName = new Map();
|
|
198
|
-
for (const t of dynamicTools) extraByName.set(t.spec.function.name, t);
|
|
199
|
-
const specs = platformMode ? undefined : [...TOOL_SPECS, ...dynamicTools.map((t) => t.spec)];
|
|
200
|
-
|
|
201
|
-
let markupRetries = 0; // 「文本化工具调用」纠正次数(首次要求改走 tool_calls 通道,二次禁工具收敛)
|
|
202
|
-
let toolsDisabled = false;
|
|
203
|
-
let toolFailures = 0; // 工具失败计数(自我纠错:累计≥3 注入一次「换策略」提示)
|
|
204
|
-
let correction = null; // 待注入的纠正指令:仅随本轮请求发给模型,不进会话记录(与平台侧临时 convo 等价)
|
|
205
|
-
|
|
206
|
-
// 工具事件钩子:仅顶层原始模型加载(平台伪模型由服务端编排,无本机工具)
|
|
207
|
-
const hooks = (depth === 0 && !platformMode) ? loadHooks(cwd) : null;
|
|
208
|
-
const hooksActive = !!(hooks && hooks.all.length);
|
|
209
|
-
|
|
210
|
-
// SessionStart(会话首个用户回合触发一次)+ UserPromptSubmit 钩子:可阻断,或注入附加上下文
|
|
211
|
-
if (hooksActive) {
|
|
212
|
-
const parts = [];
|
|
213
|
-
if (messages.filter((m) => m.role === 'user').length <= 1) {
|
|
214
|
-
const ss = runHooks('SessionStart', { cwd }, hooks);
|
|
215
|
-
if (ss.injectContext) parts.push(ss.injectContext);
|
|
216
|
-
for (const o of ss.outputs) parts.push(o.stdout);
|
|
217
|
-
}
|
|
218
|
-
const lastUser = [...messages].reverse().find((m) => m.role === 'user');
|
|
219
|
-
const r = runHooks('UserPromptSubmit', { cwd, prompt: contentText(lastUser?.content) }, hooks);
|
|
220
|
-
if (r.block) {
|
|
221
|
-
info('提交被 UserPromptSubmit 钩子阻断:' + r.reason);
|
|
222
|
-
return { error: '提交被钩子阻断:' + r.reason, usage: { prompt, completion }, lastPromptTokens };
|
|
223
|
-
}
|
|
224
|
-
if (r.injectContext) parts.push(r.injectContext);
|
|
225
|
-
for (const o of r.outputs) parts.push(o.stdout);
|
|
226
|
-
if (parts.length) correction = '【钩子提供的附加上下文】\n' + parts.join('\n');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
for (let iter = 0; iter < iterMax; iter++) {
|
|
230
|
-
// 预算收敛:最后一轮禁用工具并提示直接作答,保证中大型项目里到达上限也能给出干净的最终答复(而非被硬截断)
|
|
231
|
-
const lastIter = iter === iterMax - 1;
|
|
232
|
-
if (lastIter && !platformMode && !toolsDisabled && !correction) correction = BUDGET_FINAL_PROMPT;
|
|
233
|
-
// 非破坏式上下文压缩:仅更新 compState(摘要独立存储),messages/页面内容不动
|
|
234
|
-
const compacted = await compactIfNeeded(messages, config, { state: compState, lastPromptTokens });
|
|
235
|
-
if (compacted > 0) info(`已压缩上下文(${compacted} 条较早消息已摘要,页面与会话记录不受影响)`);
|
|
236
|
-
|
|
237
|
-
let streamedAny = false;
|
|
238
|
-
let streamedReason = false;
|
|
239
|
-
const mf = new MarkupFilter(); // 文本化工具标记防泄漏:每轮模型输出独立实例
|
|
240
|
-
const emitDelta = (t) => {
|
|
241
|
-
if (!t) return;
|
|
242
|
-
if (!streamedAny && streamedReason && !ev) out('\n'); // 思考→正文换行
|
|
243
|
-
streamedAny = true;
|
|
244
|
-
if (ev) ev.onDelta?.(t);
|
|
245
|
-
else out(t);
|
|
246
|
-
};
|
|
247
|
-
const modelMessages = buildModelMessages(messages, compState);
|
|
248
|
-
// 会话级临时指令(计划模式 / 目标)追加到系统提示词末尾(构造新数组,绝不改动原 messages/会话)
|
|
249
|
-
let requestMessages = modelMessages;
|
|
250
|
-
if (!platformMode && modelMessages.length && modelMessages[0]?.role === 'system') {
|
|
251
|
-
let extra = '';
|
|
252
|
-
if (config.planMode) extra += PLAN_MODE_NOTE;
|
|
253
|
-
if (config.sandbox) extra += SANDBOX_MODE_NOTE;
|
|
254
|
-
if (config.goal && String(config.goal).trim()) extra += goalNote(config.goal);
|
|
255
|
-
if (extra) {
|
|
256
|
-
requestMessages = [
|
|
257
|
-
{ ...modelMessages[0], content: String(modelMessages[0].content || '') + extra },
|
|
258
|
-
...modelMessages.slice(1),
|
|
259
|
-
];
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const reqStart = Date.now();
|
|
263
|
-
let res;
|
|
264
|
-
try {
|
|
265
|
-
res = await streamChat(
|
|
266
|
-
{
|
|
267
|
-
baseUrl: config.baseUrl,
|
|
268
|
-
apiKey: config.apiKey,
|
|
269
|
-
model: effectiveModel,
|
|
270
|
-
messages: correction ? [...requestMessages, { role: 'user', content: correction }] : requestMessages,
|
|
271
|
-
tools: (toolsDisabled || lastIter) ? undefined : specs,
|
|
272
|
-
reasoningEffort: config.reasoningEffort, // 推理强度透传(/effort;模型不支持则忽略)
|
|
273
|
-
// 对话落库审计:来源与会话号(由 config 透传;扩展侧为 vscode)
|
|
274
|
-
client: config.client,
|
|
275
|
-
sessionId: config.sessionId,
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
onContent: (t) => emitDelta(mf.feed(t)),
|
|
279
|
-
onReasoning: (t) => {
|
|
280
|
-
if (config.showReasoning === false) return;
|
|
281
|
-
if (ev) {
|
|
282
|
-
ev.onReasoning?.(t);
|
|
283
|
-
} else {
|
|
284
|
-
if (!streamedReason) out(c.gray('\n[思考] '));
|
|
285
|
-
out(c.gray(t));
|
|
286
|
-
}
|
|
287
|
-
streamedReason = true;
|
|
288
|
-
},
|
|
289
|
-
onRetry: (n, waitMs, reason) => info(`模型响应异常,${Math.round(waitMs / 1000)}s 后自动重试(第 ${n} 次):${reason}`),
|
|
290
|
-
},
|
|
291
|
-
signal,
|
|
292
|
-
);
|
|
293
|
-
} catch (e) {
|
|
294
|
-
if (e.name === 'AbortError') {
|
|
295
|
-
if (ev) ev.onInfo?.('已中断');
|
|
296
|
-
else out(c.gray('\n[已中断]\n'));
|
|
297
|
-
return { aborted: true, usage: { prompt, completion }, lastPromptTokens };
|
|
298
|
-
}
|
|
299
|
-
if (ev) ev.onInfo?.('✗ 模型调用失败:' + e.message);
|
|
300
|
-
else out('\n' + c.red('✗ 模型调用失败:') + e.message + '\n');
|
|
301
|
-
return { error: e.message, usage: { prompt, completion }, lastPromptTokens };
|
|
302
|
-
}
|
|
303
|
-
emitDelta(mf.flush()); // 放行轮末仍持有的未确认内容(吞噬中则丢弃)
|
|
304
|
-
|
|
305
|
-
if (res.usage) {
|
|
306
|
-
prompt += res.usage.prompt_tokens || 0;
|
|
307
|
-
completion += res.usage.completion_tokens || 0;
|
|
308
|
-
lastPromptTokens = res.usage.prompt_tokens || lastPromptTokens;
|
|
309
|
-
}
|
|
310
|
-
if (config.debug) {
|
|
311
|
-
info(`[debug] 轮${iter + 1} · ${config.model} · in ${res.usage?.prompt_tokens || 0} / out ${res.usage?.completion_tokens || 0} tok`
|
|
312
|
-
+ ` · ${Date.now() - reqStart}ms · ${(res.toolCalls || []).length} 工具调用`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// 全量内容按同一状态机二次清洗(与流式放行结果一致):协议标记绝不进入会话记录与后续上下文
|
|
316
|
-
const { text: cleanText, sawMarkup: sawInFull } = stripMarkup(res.content);
|
|
317
|
-
const markupRound = sawInFull || mf.sawMarkup;
|
|
318
|
-
correction = null; // 上一轮注入的纠正指令已被消费
|
|
319
|
-
|
|
320
|
-
const toolCalls = platformMode ? [] : res.toolCalls;
|
|
321
|
-
if (!toolCalls.length) {
|
|
322
|
-
// 「文本化工具调用」拦截后的纠正:本轮"正文"实为协议标记(已被过滤,未显示,工具也未执行)。
|
|
323
|
-
// 平台编排模式(伪模型)由服务端过滤与纠正,客户端不再重试。
|
|
324
|
-
if (markupRound && !cleanText.trim() && !platformMode) {
|
|
325
|
-
if (markupRetries < 2) {
|
|
326
|
-
markupRetries++;
|
|
327
|
-
if (markupRetries === 2) toolsDisabled = true;
|
|
328
|
-
correction = markupRetries === 1 ? MARKUP_RETRY_PROMPT : MARKUP_FORCE_ANSWER_PROMPT;
|
|
329
|
-
info(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${markupRetries} 次)`);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
info('模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)');
|
|
333
|
-
}
|
|
334
|
-
if (streamedAny && !ev) out('\n');
|
|
335
|
-
// 记录最终答复进上下文,使后续用户回合能看到助手上一次回复(多轮对话记忆)
|
|
336
|
-
if (cleanText) messages.push({ role: 'assistant', content: cleanText });
|
|
337
|
-
if (ev) ev.onTurnEnd?.(cleanText);
|
|
338
|
-
// 用户在回合中排队了新消息 → 直接续跑,无需重新发起回合
|
|
339
|
-
const queued = pullQueued?.();
|
|
340
|
-
if (queued) {
|
|
341
|
-
messages.push({ role: 'user', content: queued });
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
// Stop 钩子:回合结束触发(不阻断,仅采集输出,如收尾自检/通知)
|
|
345
|
-
if (hooksActive) {
|
|
346
|
-
const st = runHooks('Stop', { cwd, result: cleanText }, hooks);
|
|
347
|
-
for (const o of st.outputs) info('钩子·Stop:' + shortLine(o.stdout, 160));
|
|
348
|
-
}
|
|
349
|
-
return { usage: { prompt, completion }, lastPromptTokens };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (streamedAny && !ev) out('\n');
|
|
353
|
-
if (ev && cleanText) ev.onAssistantText?.(cleanText); // 工具轮前的正文先落定
|
|
354
|
-
messages.push({ role: 'assistant', content: cleanText, tool_calls: toolCalls });
|
|
355
|
-
|
|
356
|
-
// 预解析全部调用(名称/参数/只读性);参数 JSON 解析失败记录错误(回灌模型重试,而非静默空参数)
|
|
357
|
-
const parsed = toolCalls.map((tc, idx) => {
|
|
358
|
-
const name = tc.function?.name || '';
|
|
359
|
-
let args = {};
|
|
360
|
-
let argsError = null;
|
|
361
|
-
const raw = tc.function?.arguments || '';
|
|
362
|
-
if (raw) {
|
|
363
|
-
try {
|
|
364
|
-
args = JSON.parse(raw);
|
|
365
|
-
} catch (e) {
|
|
366
|
-
argsError = `工具参数不是合法 JSON(${e.message}):${String(raw).slice(0, 200)}…请修正参数后重新调用。`;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
const extra = extraByName.get(name);
|
|
370
|
-
// IDE 集成下,write_file/edit_file 的批准发生在编辑器的内联 diff 审阅里,跳过 CLI 自身的 y/n(避免双重确认)
|
|
371
|
-
const deferToIde = ide.isActive() && (name === 'write_file' || name === 'edit_file');
|
|
372
|
-
const readOnly = extra ? !!extra.readOnly : READONLY_TOOLS.has(name);
|
|
373
|
-
return { tc, idx, name, args, argsError, extra, readOnly, deferToIde };
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
// 中断时为剩余未执行的 tool_calls 合成占位结果,保证 assistant.tool_calls 配对完整(协议要求)
|
|
377
|
-
const abortRest = (from) => {
|
|
378
|
-
for (let k = from; k < parsed.length; k++) {
|
|
379
|
-
messages.push({ role: 'tool', tool_call_id: parsed[k].tc.id || `call_${k}`, content: '(用户中断,该工具未执行)' });
|
|
380
|
-
}
|
|
381
|
-
if (ev) ev.onInfo?.('已中断');
|
|
382
|
-
else out(c.gray('\n[已中断]\n'));
|
|
383
|
-
return { aborted: true, usage: { prompt, completion }, lastPromptTokens };
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
const execOne = async (item) => {
|
|
387
|
-
if (item.argsError) return item.argsError;
|
|
388
|
-
let result;
|
|
389
|
-
try {
|
|
390
|
-
result = item.extra ? await item.extra.run(item.args) : await runTool(item.name, item.args, cwd, config);
|
|
391
|
-
} catch (e) {
|
|
392
|
-
result = `工具执行失败:${e.message}`;
|
|
393
|
-
}
|
|
394
|
-
return capToolResult(String(result));
|
|
395
|
-
};
|
|
396
|
-
const emitStart = (item) => {
|
|
397
|
-
if (ev) ev.onToolStart?.(item.name, item.args);
|
|
398
|
-
else out(c.cyan(' ⏺ ') + toolLabel(item.name, item.args) + '\n');
|
|
399
|
-
};
|
|
400
|
-
const emitResult = (item, result) => {
|
|
401
|
-
if (looksLikeToolFailure(result)) toolFailures++;
|
|
402
|
-
if (ev) ev.onToolResult?.(item.name, String(result));
|
|
403
|
-
else out(c.gray(' ' + shortLine(result)) + '\n');
|
|
404
|
-
messages.push({ role: 'tool', tool_call_id: item.tc.id, content: String(result) });
|
|
405
|
-
};
|
|
406
|
-
const emitSkip = (item, reason) => {
|
|
407
|
-
if (ev) ev.onSkip?.(item.name, item.args);
|
|
408
|
-
else out(c.gray(` ⊘ ${toolLabel(item.name, item.args)} — 已跳过\n`));
|
|
409
|
-
messages.push({ role: 'tool', tool_call_id: item.tc.id, content: reason || '未授权,未执行。' });
|
|
410
|
-
};
|
|
411
|
-
// 写文件类工具(edit_file/write_file)成功后向 UI 展示「更改的行」(display-only,不进模型上下文)。
|
|
412
|
-
// 以「执行前后文件内容对比」判定:未改动/失败/被拒 → before==after → 空 diff,天然不显示。
|
|
413
|
-
const isFileWrite = (name) => name === 'edit_file' || name === 'write_file';
|
|
414
|
-
const readSafe = (abs) => { try { return fs.readFileSync(abs, 'utf8'); } catch { return ''; } };
|
|
415
|
-
const emitFileDiff = (item, before) => {
|
|
416
|
-
const abs = path.resolve(cwd, String(item.args.path));
|
|
417
|
-
const diff = renderFileDiff(before, readSafe(abs), { path: String(item.args.path) });
|
|
418
|
-
if (!diff) return;
|
|
419
|
-
if (ev) ev.onFileDiff?.(diff);
|
|
420
|
-
else out(diff + '\n');
|
|
421
|
-
};
|
|
422
|
-
|
|
423
|
-
let i = 0;
|
|
424
|
-
while (i < parsed.length) {
|
|
425
|
-
if (signal?.aborted) return abortRest(i);
|
|
426
|
-
const cur = parsed[i];
|
|
427
|
-
|
|
428
|
-
// 参数损坏:不请求授权、不执行,直接把解析错误回灌(模型可修正后重试)
|
|
429
|
-
if (cur.argsError) {
|
|
430
|
-
emitStart(cur);
|
|
431
|
-
emitResult(cur, cur.argsError);
|
|
432
|
-
i++;
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (cur.readOnly) {
|
|
437
|
-
// 连续只读段:并行执行(权限对只读即时放行),结果按原顺序回灌
|
|
438
|
-
let j = i;
|
|
439
|
-
while (j < parsed.length && parsed[j].readOnly && !parsed[j].argsError) j++;
|
|
440
|
-
const batch = parsed.slice(i, j);
|
|
441
|
-
// 只读段并行执行,但并发受限(大仓库里模型可能一次触发很多 grep/glob,无限并发会抢占磁盘/CPU 反而更慢);结果仍按原顺序回灌
|
|
442
|
-
const settled = await mapLimit(batch, TOOL_CONCURRENCY, async (item) => {
|
|
443
|
-
const perm = await permissions.request(item.name, item.args, cwd, true);
|
|
444
|
-
if (!perm.allow) return { item, denied: perm.reason || '未授权,未执行。' };
|
|
445
|
-
emitStart(item);
|
|
446
|
-
return { item, result: await execOne(item) };
|
|
447
|
-
});
|
|
448
|
-
for (const s of settled) {
|
|
449
|
-
if (s.denied != null) emitSkip(s.item, s.denied);
|
|
450
|
-
else emitResult(s.item, s.result);
|
|
451
|
-
}
|
|
452
|
-
i = j;
|
|
453
|
-
if (signal?.aborted) return abortRest(i);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// 写/执行类:顺序执行,逐个审批
|
|
458
|
-
const perm = await permissions.request(cur.name, cur.args, cwd, cur.readOnly || cur.deferToIde);
|
|
459
|
-
if (signal?.aborted) return abortRest(i);
|
|
460
|
-
if (!perm.allow) {
|
|
461
|
-
emitSkip(cur, perm.reason);
|
|
462
|
-
i++;
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
// PreToolUse 钩子:可阻断该工具(非零退出码),阻断原因回灌模型
|
|
466
|
-
if (hooksActive) {
|
|
467
|
-
const pre = runHooks('PreToolUse', { cwd, toolName: cur.name, args: cur.args }, hooks);
|
|
468
|
-
for (const o of pre.outputs) info('钩子·PreToolUse:' + shortLine(o.stdout, 120));
|
|
469
|
-
if (pre.block) {
|
|
470
|
-
emitSkip(cur, 'PreToolUse 钩子阻断:' + pre.reason);
|
|
471
|
-
i++;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (pre.modifiedArgs) {
|
|
475
|
-
cur.args = { ...cur.args, ...pre.modifiedArgs }; // 结构化钩子改写工具参数
|
|
476
|
-
info('钩子·PreToolUse 已修改工具参数');
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
emitStart(cur);
|
|
480
|
-
// 检查点文件日志:写入前记录原文与是否已存在(供 /rewind 回滚;过大文件仅标记不存原文)
|
|
481
|
-
const before = isFileWrite(cur.name) && cur.args?.path
|
|
482
|
-
? readSafe(path.resolve(cwd, String(cur.args.path)))
|
|
483
|
-
: null;
|
|
484
|
-
if (fileJournal && before != null && cur.args?.path) {
|
|
485
|
-
const existed = fs.existsSync(path.resolve(cwd, String(cur.args.path)));
|
|
486
|
-
fileJournal.push(before.length > 2 * 1024 * 1024
|
|
487
|
-
? { path: String(cur.args.path), tooLarge: true }
|
|
488
|
-
: { path: String(cur.args.path), before, existed });
|
|
489
|
-
}
|
|
490
|
-
let result = await execOne(cur);
|
|
491
|
-
// PostToolUse 钩子:不阻断,输出附加到工具结果(模型可见,如格式化/测试结果)
|
|
492
|
-
if (hooksActive) {
|
|
493
|
-
const post = runHooks('PostToolUse', { cwd, toolName: cur.name, args: cur.args, result: String(result) }, hooks);
|
|
494
|
-
if (post.outputs.length) {
|
|
495
|
-
for (const o of post.outputs) info('钩子·PostToolUse:' + shortLine(o.stdout, 120));
|
|
496
|
-
result = String(result) + '\n\n[PostToolUse 钩子输出]\n' + post.outputs.map((o) => o.stdout).join('\n');
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
emitResult(cur, result);
|
|
500
|
-
if (before != null) emitFileDiff(cur, before);
|
|
501
|
-
i++;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// 自我纠错:本回合累计工具失败达阈值,注入一次「换策略」提示(打破反复失败的死循环)
|
|
505
|
-
if (toolFailures >= 3 && !toolsDisabled && !correction) {
|
|
506
|
-
correction = SELF_CORRECT_NOTE;
|
|
507
|
-
toolFailures = 0;
|
|
508
|
-
info('检测到连续工具失败,已提示智能体换一种更可靠的方法');
|
|
509
|
-
}
|
|
510
|
-
// 工具轮之间注入用户排队消息(协议合法:tool 配对已完整),模型下一轮即可看到
|
|
511
|
-
const queued = pullQueued?.();
|
|
512
|
-
if (queued) messages.push({ role: 'user', content: queued });
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (ev) ev.onInfo?.('已达最大工具轮数,停止');
|
|
516
|
-
else out(c.yellow('\n[已达最大工具轮数,停止]\n'));
|
|
517
|
-
return { usage: { prompt, completion }, lastPromptTokens };
|
|
518
|
-
}
|
|
1
|
+
import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function b(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function k(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function x(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:I,ask:E,signal:U,extraTools:v=[],depth:A=0,io:O,events:C,todoState:R,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=C||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=k(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=R||{list:[]};X.push(p({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:I,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null;const rt=0!==A||G?null:y(I),st=!(!rt||!rt.all.length);if(st){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:I},rt);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:I,prompt:k(o?.content)},rt);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(j,$,{state:V,lastPromptTokens:z});p>0&&W(`已压缩上下文(${p} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},k=g(j,V);let E=k;if(!G&&k.length&&"system"===k[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)与联网(fetch_url),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=b($.goal)),t&&(E=[{...k[0],content:String(k[0].content||"")+t},...k.slice(1)])}const v=Date.now();let A;try{A=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:nt?[...E,{role:"user",content:nt}]:E,tools:ot||c?void 0:Z,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),A.usage&&(L+=A.usage.prompt_tokens||0,Q+=A.usage.completion_tokens||0,z=A.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${A.usage?.prompt_tokens||0} / out ${A.usage?.completion_tokens||0} tok · ${Date.now()-v}ms · ${(A.toolCalls||[]).length} 工具调用`);const{text:O,sawMarkup:C}=r(A.content),R=C||h.sawMarkup;nt=null;const N=G?[]:A.toolCalls;if(!N.length){if(R&&!O.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}f&&!K&&F("\n"),O&&j.push({role:"assistant",content:O}),K&&K.onTurnEnd?.(O);const t=D?.();if(t){j.push({role:"user",content:t});continue}if(st){const t=w("Stop",{cwd:I,result:O},rt);for(const o of t.outputs)W("钩子·Stop:"+P(o.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&O&&K.onAssistantText?.(O),j.push({role:"assistant",content:O,tool_calls:N});const J=N.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),X=t=>{for(let o=t;o<J.length;o++)j.push({role:"tool",tool_call_id:J[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},at=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},lt=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},it=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},ct=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},ut=t=>"edit_file"===t||"write_file"===t,mt=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},gt=(t,e)=>{const n=o.resolve(I,String(t.args.path)),r=u(e,mt(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let pt=0;for(;pt<J.length;){if(U?.aborted)return X(pt);const e=J[pt];if(e.argsError){lt(e),it(e,e.argsError),pt++;continue}if(e.readOnly){let t=pt;for(;t<J.length&&J[t].readOnly&&!J[t].argsError;)t++;const o=J.slice(pt,t),e=await x(o,8,async t=>{const o=await M.request(t.name,t.args,I,!0);return o.allow?(lt(t),{item:t,result:await at(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?ct(t.item,t.denied):it(t.item,t.result);if(pt=t,U?.aborted)return X(pt);continue}const n=await M.request(e.name,e.args,I,e.readOnly||e.deferToIde);if(U?.aborted)return X(pt);if(!n.allow){ct(e,n.reason),pt++;continue}if(st){const t=w("PreToolUse",{cwd:I,toolName:e.name,args:e.args},rt);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){ct(e,"PreToolUse 钩子阻断:"+t.reason),pt++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}lt(e);const r=ut(e.name)&&e.args?.path?mt(o.resolve(I,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(I,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await at(e);if(st){const t=w("PostToolUse",{cwd:I,toolName:e.name,args:e.args,result:String(s)},rt);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}it(e,s),null!=r&>(e,r),pt++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const ft=D?.();ft&&j.push({role:"user",content:ft})}return K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}
|