mocode-ai 0.1.4 → 0.1.6
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 +196 -188
- package/dist/agent/core.js +309 -0
- package/dist/agent/index.js +62 -249
- package/dist/agent/mode.js +46 -0
- package/dist/agent/spawn.js +123 -0
- package/dist/commands/config.js +8 -26
- package/dist/config/file.js +48 -0
- package/dist/config/index.js +81 -36
- package/dist/context/classifier.js +83 -0
- package/dist/context/encoders/index.js +10 -0
- package/dist/context/encoders/passthrough.js +23 -0
- package/dist/context/index.js +10 -0
- package/dist/context/pipeline.js +84 -0
- package/dist/context/registry.js +30 -0
- package/dist/context/types.js +12 -0
- package/dist/index.js +12 -2
- package/dist/llm/index.js +11 -9
- package/dist/memory/index.js +2 -2
- package/dist/memory/reflect.js +11 -11
- package/dist/memory/store.js +4 -4
- package/dist/repl/index.js +269 -109
- package/dist/sandbox/command.js +41 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/jail.js +75 -0
- package/dist/sandbox/policy.js +58 -0
- package/dist/sandbox/root.js +19 -0
- package/dist/session/compact.js +1 -1
- package/dist/skills/index.js +2 -2
- package/dist/tools/builtins/ask-human.js +7 -7
- package/dist/tools/builtins/codegraph.js +113 -0
- package/dist/tools/builtins/edit-file.js +3 -3
- package/dist/tools/builtins/glob.js +10 -5
- package/dist/tools/builtins/grep.js +14 -9
- package/dist/tools/builtins/index.js +7 -1
- package/dist/tools/builtins/memory-forget.js +2 -2
- package/dist/tools/builtins/memory-list.js +2 -2
- package/dist/tools/builtins/memory-save.js +7 -7
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/memory-update.js +6 -6
- package/dist/tools/builtins/read-file.js +5 -4
- package/dist/tools/builtins/run-command.js +47 -8
- package/dist/tools/builtins/switch-mode.js +46 -0
- package/dist/tools/builtins/task.js +62 -0
- package/dist/tools/builtins/use-skill.js +2 -2
- package/dist/tools/builtins/web-fetch.js +16 -3
- package/dist/tools/builtins/web-search.js +6 -6
- package/dist/tools/builtins/write-file.js +3 -3
- package/dist/tools/constants.js +16 -0
- package/dist/tools/registry.js +18 -4
- package/dist/ui/content.js +25 -17
- package/dist/ui/diff.js +28 -26
- package/dist/ui/intervention.js +24 -3
- package/dist/ui/layout.js +295 -81
- package/dist/ui/markdown.js +607 -0
- package/dist/ui/mouse.js +93 -0
- package/dist/ui/prompt.js +259 -5
- package/dist/ui/render.js +1 -1
- package/dist/ui/theme.js +146 -13
- package/package.json +2 -2
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// agent 核心循环(纯逻辑,无 TUI 依赖):流式 chat → 工具执行 → 回灌。
|
|
2
|
+
// 所有展示副作用经 AgentHooks 注入——主 agent 注入 TUI 渲染(layout + spinner + diff),
|
|
3
|
+
// 子 agent 注入静默/摘要 hooks(不写屏)。逻辑层共享,避免重复实现循环 / 分组 / abort 还原。
|
|
4
|
+
//
|
|
5
|
+
// 与 index.ts 的关系:index.ts 的 runAgent = runAgentCore + TUI hooks 薄封装(行为不变)。
|
|
6
|
+
// spawn.ts 的 spawnAgent = runAgentCore + 静默 hooks(子 agent)。
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
8
|
+
import { chat, planChatTools, } from '../llm/index.js';
|
|
9
|
+
import { executeTool } from '../tools/registry.js';
|
|
10
|
+
import { PLAN_DISABLED_TOOLS } from '../tools/constants.js';
|
|
11
|
+
import { getAgentMode, setAgentMode } from './mode.js';
|
|
12
|
+
import { maybeCompact, contextState } from '../session/index.js';
|
|
13
|
+
import { optimizeToolResult } from '../context/index.js';
|
|
14
|
+
import { config } from '../config/index.js';
|
|
15
|
+
import { jailResolve } from '../sandbox/index.js';
|
|
16
|
+
/** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
|
|
17
|
+
function parseArgs(raw) {
|
|
18
|
+
try {
|
|
19
|
+
return raw.trim() ? JSON.parse(raw) : {};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** 只读工具集:一轮多个时,连续的只读工具成组 Promise.all 并行(无副作用、互不依赖)。 */
|
|
26
|
+
const READ_TOOL_NAMES = new Set([
|
|
27
|
+
'read_file',
|
|
28
|
+
'glob',
|
|
29
|
+
'grep',
|
|
30
|
+
'codegraph',
|
|
31
|
+
'web_search',
|
|
32
|
+
'web_fetch',
|
|
33
|
+
]);
|
|
34
|
+
/** mutation 工具:写盘 + 在 executeTool 内记回滚 before 快照,必须串行保快照序。 */
|
|
35
|
+
const isMutationTool = (name) => name === 'edit_file' || name === 'write_file';
|
|
36
|
+
/** mutation 执行前读旧内容供 diff:write_file 取整文件旧内容(不存在→null=新建),
|
|
37
|
+
* edit_file 取 old_string 起始行号(供 diff 显示真实文件行号)。读不到则 diff 退化为相对行号。
|
|
38
|
+
* 非 mutation 或参数非法返 { preWriteOld: null, editStartLine: 1 }。失败不阻断。 */
|
|
39
|
+
function readDiffContext(tc, parsed) {
|
|
40
|
+
if (!parsed)
|
|
41
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
42
|
+
const p = String(parsed.path ?? '');
|
|
43
|
+
if (!p)
|
|
44
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
45
|
+
if (tc.name === 'write_file') {
|
|
46
|
+
try {
|
|
47
|
+
// jailResolve:沙箱越界(../../、绝对外圈、软链出圈)抛错 → catch 兜底返 null,不泄露牢外内容(TOCTOU)
|
|
48
|
+
return { preWriteOld: readFileSync(jailResolve(p), 'utf8'), editStartLine: 1 };
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return { preWriteOld: null, editStartLine: 1 }; // 文件不存在(新建)、不可读 或 沙箱越界(不泄露)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (tc.name === 'edit_file') {
|
|
55
|
+
const oldStr = String(parsed.old_string ?? '');
|
|
56
|
+
try {
|
|
57
|
+
// jailResolve:同上,沙箱越界抛错 → catch 兜底,不泄露牢外内容
|
|
58
|
+
const data = readFileSync(jailResolve(p), 'utf8');
|
|
59
|
+
const idx = oldStr ? data.indexOf(oldStr) : -1;
|
|
60
|
+
return {
|
|
61
|
+
preWriteOld: null,
|
|
62
|
+
editStartLine: idx >= 0 ? data.slice(0, idx).split('\n').length : 1,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { preWriteOld: null, editStartLine: 1 }; // 读不到:diff 退化为相对行号(含沙箱越界)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { preWriteOld: null, editStartLine: 1 };
|
|
70
|
+
}
|
|
71
|
+
/** 回灌 tool 结果到 history:经 Context Optimization Pipeline 编码(tree/search/log/...)后裁到单条上限。
|
|
72
|
+
* tool_call_id 与 assistant.tool_calls 按序配对。未注册 encoder 时回落 capToolResultForHistory(零行为变化)。
|
|
73
|
+
* TUI 渲染(hooks.onToolResult)用原始 output,与此解耦——屏上看全量,LLM 看编码后紧凑版。 */
|
|
74
|
+
function pushToolResult(history, tc, output) {
|
|
75
|
+
history.push({
|
|
76
|
+
role: 'tool',
|
|
77
|
+
tool_call_id: tc.id,
|
|
78
|
+
// optimizeToolResult:classifier 选 encoder → encode(保不变量压缩)→ capToolResultForHistory 兜底。
|
|
79
|
+
// tc.arguments 透传给 encoder(上下文感知编码,如 read_file 的 offset/limit)。永不抛错。
|
|
80
|
+
content: optimizeToolResult(tc.name, output, tc.arguments),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* agent 核心循环(纯逻辑):
|
|
85
|
+
* 流式调 LLM(经 hooks.onText 实时渲染)→ 有 tool_calls 就分组执行并回灌
|
|
86
|
+
* → 否则流式正文即最终回复。history 在调用间持久,由调用方持有。
|
|
87
|
+
* 步前经 session/maybeCompact 自动压缩(接近窗口上限时三层压缩);
|
|
88
|
+
* 工具结果进 history 前经 Context Optimization Pipeline(optimizeToolResult:类型化编码 + 长度裁剪)。
|
|
89
|
+
*
|
|
90
|
+
* 中断语义:signal 经 executeTool(name, args, signal) 串进工具;run_command/web_fetch 等 abort 即时杀
|
|
91
|
+
* (树杀子进程 / 取消 fetch),循环顶 if(signal.aborted) 兜底还原。不会留下未配对的 tool_call_id。
|
|
92
|
+
* abort 时 history 还原到本 turn 前(savedHistory 浅拷贝),模式还原,调 hooks.onAbort。
|
|
93
|
+
*
|
|
94
|
+
* 所有展示副作用经 hooks 注入;core 自身不直接调 layout / spinner(不依赖 ui/layout.ts)。
|
|
95
|
+
* 但 core 仍依赖 ui/render.ts 的纯函数(summarizeToolCall / truncateDisplay / fmtElapsed)——
|
|
96
|
+
* 这些是纯字符串格式化,无副作用,共享安全。
|
|
97
|
+
*/
|
|
98
|
+
export async function runAgentCore(opts) {
|
|
99
|
+
const { history, userInput, signal, onContextUpdate, hooks, skipRollback } = opts;
|
|
100
|
+
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
101
|
+
// 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
102
|
+
// 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
|
|
103
|
+
const savedHistory = history.slice();
|
|
104
|
+
// 中断还原:LLM 中途可能调 switch_mode 切了模式,abort 时连同模式一起还原回轮首。
|
|
105
|
+
const savedMode = getAgentMode();
|
|
106
|
+
// 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
|
|
107
|
+
const t0 = Date.now();
|
|
108
|
+
let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
|
|
109
|
+
history.push({ role: 'user', content: userInput });
|
|
110
|
+
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
111
|
+
let mode = 'idle';
|
|
112
|
+
let gotText = false;
|
|
113
|
+
let lastChar = '';
|
|
114
|
+
const onText = (s) => {
|
|
115
|
+
hooks.onText?.(s); // 主 agent:走 markdown 渲染写内容区
|
|
116
|
+
mode = 'text';
|
|
117
|
+
gotText = true;
|
|
118
|
+
if (s)
|
|
119
|
+
lastChar = s[s.length - 1];
|
|
120
|
+
};
|
|
121
|
+
const onToolCall = (name) => {
|
|
122
|
+
// 文本/思考已流完,模型转而生成 tool_call 参数(可能很长,如 write_file 整篇内容):
|
|
123
|
+
// 补换行(让随后的 ● 行与 diff 不黏在正文末尾)+ 启「生成中」内联 spinner,内容区不再干等。
|
|
124
|
+
if (lastChar && lastChar !== '\n') {
|
|
125
|
+
hooks.onTextEnd?.(); // 主 agent:layout.contentWrite('\n')
|
|
126
|
+
lastChar = '\n';
|
|
127
|
+
}
|
|
128
|
+
hooks.onToolCall?.(name); // 主 agent:spinner.start(`生成 ${name}…`)
|
|
129
|
+
};
|
|
130
|
+
// 中断还原:停 spinner + 补换行 + (已中断)提示 + history 还原到本 turn 前 + 模式还原。
|
|
131
|
+
// 两处共用:① await chat() 抛 AbortError 的 catch;② 工具被 abort 杀后循环顶检查。
|
|
132
|
+
const abortRestore = () => {
|
|
133
|
+
hooks.onAbort?.();
|
|
134
|
+
history.length = 0;
|
|
135
|
+
history.push(...savedHistory);
|
|
136
|
+
setAgentMode(savedMode);
|
|
137
|
+
};
|
|
138
|
+
try {
|
|
139
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
140
|
+
// 上一步工具被 abort 杀(run_command/web_fetch 等)→ signal.aborted,直接还原退出,不等 maybeCompact + chat()
|
|
141
|
+
if (signal?.aborted) {
|
|
142
|
+
abortRestore();
|
|
143
|
+
return { completed: false, finalText: null };
|
|
144
|
+
}
|
|
145
|
+
// 步前:接近窗口上限时自动压缩(三层)。此时 spinner 已停,通知行干净。
|
|
146
|
+
await maybeCompact(history);
|
|
147
|
+
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
148
|
+
mode = 'idle';
|
|
149
|
+
gotText = false;
|
|
150
|
+
lastChar = '';
|
|
151
|
+
let result;
|
|
152
|
+
try {
|
|
153
|
+
// 每步读实时模式:LLM 可能在上一步调 switch_mode 切了模式,这里立即用对应工具集
|
|
154
|
+
// (auto=chatTools 全量;plan=planChatTools 只读子集)。模式由 src/agent/mode.ts 单一持有。
|
|
155
|
+
// 调用方可传 toolsOverride 覆盖(子 agent 受限工具子集)。
|
|
156
|
+
result = await chat(history, { onText, onToolCall }, signal, opts.toolsOverride ??
|
|
157
|
+
(getAgentMode() === 'plan' ? planChatTools : undefined));
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
// 中断(用户运行中 Ctrl+C):chat() 抛 AbortError(signal.aborted)→ 还原 history + 模式 + return(不抛)。
|
|
161
|
+
// 工具执行现已串 signal:run_command/web_fetch 被 abort 即时杀,循环顶检查兜底(不会留未配对 tool_call_id)。
|
|
162
|
+
if (signal?.aborted ||
|
|
163
|
+
(e instanceof Error &&
|
|
164
|
+
(e.name === 'AbortError' || e.name === 'APIUserAbortError'))) {
|
|
165
|
+
abortRestore();
|
|
166
|
+
return { completed: false, finalText: null };
|
|
167
|
+
}
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
170
|
+
contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
171
|
+
hooks.onChatDone?.(); // 主 agent:spinner.stop()
|
|
172
|
+
// lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
|
|
173
|
+
onContextUpdate?.();
|
|
174
|
+
if (result.toolCalls.length > 0) {
|
|
175
|
+
// 流式正文末尾补换行(若 onToolCall 已补则 lastChar='\n',此处 no-op);防 ● 行黏在正文行尾
|
|
176
|
+
if (mode !== 'idle' && lastChar !== '\n')
|
|
177
|
+
hooks.onTextEnd?.();
|
|
178
|
+
// 带工具调用的 assistant 消息原样回灌(OpenAI 格式要求)
|
|
179
|
+
history.push({
|
|
180
|
+
role: 'assistant',
|
|
181
|
+
content: result.content,
|
|
182
|
+
tool_calls: result.toolCalls.map((tc) => ({
|
|
183
|
+
id: tc.id,
|
|
184
|
+
type: 'function',
|
|
185
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
186
|
+
})),
|
|
187
|
+
});
|
|
188
|
+
// 工具分组执行(保 tool_calls 原顺序):连续的只读工具(READ_TOOL_NAMES)成组并发——一次性启动全部
|
|
189
|
+
// executeTool(调用即开始 I/O),再按原顺序逐个 await + 渲染(● 头与 ↳ 结果紧邻,修并行时"全 ● 后全 ↳"分离)。
|
|
190
|
+
// mutation(write_file/edit_file)及 run_command/use_skill 各为单步串行屏障——mutation 串行保
|
|
191
|
+
// recordMutation 调用序 = 回滚快照序(executeTool 内写前记 before 快照,同文件多次写需按序)。
|
|
192
|
+
// 渲染与 history 回灌一律按原顺序;并发只影响执行时序,tool_call_id 仍按序配对。
|
|
193
|
+
// executeTool 永不抛错(调度器 try/catch 返字符串),故 await 单个 promise 不会抛(永远 resolve 为字符串)。
|
|
194
|
+
const calls = result.toolCalls;
|
|
195
|
+
let i = 0;
|
|
196
|
+
while (i < calls.length) {
|
|
197
|
+
if (READ_TOOL_NAMES.has(calls[i].name)) {
|
|
198
|
+
// 收集连续只读组(≥1),并发执行:先一次性启动所有(executeTool 调用即开始 I/O),
|
|
199
|
+
// 再按原顺序逐个 await + 渲染——● 头与 ↳ 结果紧邻、顺序 = tool_calls 序(修"全 ● 后全 ↳"分离 bug)。
|
|
200
|
+
// 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
|
|
201
|
+
let j = i;
|
|
202
|
+
while (j < calls.length && READ_TOOL_NAMES.has(calls[j].name))
|
|
203
|
+
j++;
|
|
204
|
+
const batch = calls.slice(i, j);
|
|
205
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
206
|
+
for (let k = 0; k < batch.length; k++) {
|
|
207
|
+
const tc = batch[k];
|
|
208
|
+
hooks.onToolHeader?.(tc);
|
|
209
|
+
hooks.onToolStart?.(tc.name);
|
|
210
|
+
const output = await started[k];
|
|
211
|
+
hooks.onToolDone?.();
|
|
212
|
+
hooks.onToolResult?.(tc, output, null, null, 1); // 只读工具无 diff
|
|
213
|
+
pushToolResult(history, tc, output);
|
|
214
|
+
}
|
|
215
|
+
i = j;
|
|
216
|
+
}
|
|
217
|
+
else if (calls[i].name === 'task') {
|
|
218
|
+
// task 并发组:连续的 task 调用成组并发(子 agent 并行跑,各自独立 history)。
|
|
219
|
+
// 一次性启动全部(executeTool 即 spawnAgent,子 agent 开始跑),再并发 await + 渲染。
|
|
220
|
+
// task 是长任务,并发 fan-out 总耗时 ≈ 最慢一个子 agent。
|
|
221
|
+
// task 与 mutation/run_command 之间串行屏障(task 子 agent 可能有文件改动,不能和 write_file 乱序)。
|
|
222
|
+
// 渲染:先批量打印所有 ● 头 + 启 spinner(让用户看到多个 task 同时在跑),再逐个 await 出结果。
|
|
223
|
+
// (若像只读组那样「header → await → result」串行,长 task 的第二个 header 要等第一个跑完才出现,
|
|
224
|
+
// 视觉上只有一个在跑——与并发事实不符。)
|
|
225
|
+
//
|
|
226
|
+
// plan 模式防御 backstop(与单步串行分支同语义):schema 已剔除 task,正常不会进这里;
|
|
227
|
+
// 防后端幻觉调用——不执行(绝不派生子 agent,子 agent 可能有 mutation,违反只读),直接返错回灌。
|
|
228
|
+
if (getAgentMode() === 'plan') {
|
|
229
|
+
const tc = calls[i];
|
|
230
|
+
hooks.onToolHeader?.(tc);
|
|
231
|
+
const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
|
|
232
|
+
hooks.onToolResult?.(tc, err, null, null, 1);
|
|
233
|
+
pushToolResult(history, tc, err);
|
|
234
|
+
i++;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
let j = i;
|
|
238
|
+
while (j < calls.length && calls[j].name === 'task')
|
|
239
|
+
j++;
|
|
240
|
+
const batch = calls.slice(i, j);
|
|
241
|
+
const started = batch.map((tc) => executeTool(tc.name, tc.arguments, signal, { skipRollback }));
|
|
242
|
+
// 先批量打印所有头 + 启 spinner(多 task 并发,spinner 只显一个,但 ● 头都打出来)
|
|
243
|
+
for (const tc of batch) {
|
|
244
|
+
hooks.onToolHeader?.(tc);
|
|
245
|
+
}
|
|
246
|
+
hooks.onToolStart?.(batch[0].name); // spinner:多 task 共用一个「执行 task…」
|
|
247
|
+
// 逐个 await 出结果(按 tool_calls 原序,保 tool_call_id 配对);结果到即渲染 ↳
|
|
248
|
+
for (let k = 0; k < batch.length; k++) {
|
|
249
|
+
const tc = batch[k];
|
|
250
|
+
const output = await started[k];
|
|
251
|
+
hooks.onToolResult?.(tc, output, null, null, 1); // task 结果是摘要,无 diff
|
|
252
|
+
pushToolResult(history, tc, output);
|
|
253
|
+
}
|
|
254
|
+
hooks.onToolDone?.();
|
|
255
|
+
i = j;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// 单步串行(mutation / run_command / use_skill)——逐个执行,保快照序
|
|
259
|
+
const tc = calls[i];
|
|
260
|
+
// plan 模式防御 backstop:schema 已剔除这些工具,正常不会进这里;防后端幻觉调用——
|
|
261
|
+
// 不执行,直接返错回灌(让模型看到「plan 模式禁用」并停止),绝不写盘 / 跑命令。
|
|
262
|
+
if (getAgentMode() === 'plan' && PLAN_DISABLED_TOOLS.has(tc.name)) {
|
|
263
|
+
hooks.onToolHeader?.(tc);
|
|
264
|
+
const err = `错误:计划模式下禁用工具 ${tc.name}(仅读探查,不改动文件 / 不跑命令)`;
|
|
265
|
+
hooks.onToolResult?.(tc, err, null, null, 1);
|
|
266
|
+
pushToolResult(history, tc, err);
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
hooks.onToolHeader?.(tc);
|
|
271
|
+
const parsed = isMutationTool(tc.name)
|
|
272
|
+
? parseArgs(tc.arguments)
|
|
273
|
+
: null;
|
|
274
|
+
const { preWriteOld, editStartLine } = readDiffContext(tc, parsed);
|
|
275
|
+
hooks.onToolStart?.(tc.name);
|
|
276
|
+
const output = await executeTool(tc.name, tc.arguments, signal, { skipRollback });
|
|
277
|
+
hooks.onToolDone?.();
|
|
278
|
+
hooks.onToolResult?.(tc, output, parsed, preWriteOld, editStartLine);
|
|
279
|
+
pushToolResult(history, tc, output);
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 工具步末尾补一空行:与下一轮的思考 / 正文分隔(否则 ↳ 后紧接 ▎ 思考,无空行不好看;
|
|
284
|
+
// 与正文→● 的 1 空行对称)。工具结果已以 \n 收尾,此处再补 \n 恰好 1 空行。
|
|
285
|
+
hooks.onToolBatchEnd?.();
|
|
286
|
+
continue; // 带着工具结果再调一次 LLM
|
|
287
|
+
}
|
|
288
|
+
if (mode !== 'idle' && lastChar !== '\n')
|
|
289
|
+
hooks.onTextEnd?.(); // 流式末尾补换行
|
|
290
|
+
// 没有工具调用:流式正文即最终回复(已实时打印)
|
|
291
|
+
if (!gotText)
|
|
292
|
+
hooks.onNoReply?.();
|
|
293
|
+
history.push({ role: 'assistant', content: result.content });
|
|
294
|
+
done = true;
|
|
295
|
+
return { completed: true, finalText: result.content };
|
|
296
|
+
}
|
|
297
|
+
hooks.onMaxSteps?.();
|
|
298
|
+
done = true;
|
|
299
|
+
return { completed: true, finalText: null };
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
// 跑完(正常 / 达上限)在回复末尾打耗时摘要行(仿 Claude Code);中断 done=false 不打。
|
|
303
|
+
if (done) {
|
|
304
|
+
hooks.onDone?.(Date.now() - t0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// ── 导出共享辅助(主 agent 的 TUI hooks 实现要用)──────────────────────────
|
|
309
|
+
export { parseArgs, readDiffContext, isMutationTool, READ_TOOL_NAMES };
|