lcagent-cli 0.1.6 → 0.1.8
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 +138 -1
- package/dist/app/bootstrap.d.ts +4 -1
- package/dist/app/bootstrap.js +2 -2
- package/dist/bin/cli.js +81 -8
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +2 -0
- package/dist/core/engine.d.ts +3 -2
- package/dist/core/engine.js +100 -10
- package/dist/core/loop.d.ts +7 -2
- package/dist/core/loop.js +9 -2
- package/dist/core/message.d.ts +9 -0
- package/dist/core/message.js +9 -0
- package/dist/core/systemPrompt.js +8 -0
- package/dist/tools/editFile.d.ts +13 -0
- package/dist/tools/editFile.js +305 -15
- package/dist/tools/editUtils.d.ts +15 -0
- package/dist/tools/editUtils.js +142 -0
- package/dist/tools/execute.js +56 -7
- package/dist/tools/permissions.d.ts +12 -0
- package/dist/tools/permissions.js +105 -0
- package/dist/tools/runShell.js +61 -10
- package/dist/tools/types.d.ts +28 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
- `lcagent provider [name]`:查看或修改模型 provider
|
|
11
11
|
- `lcagent model [name]`:查看或修改默认模型
|
|
12
12
|
- `lcagent doctor`:检查当前模型配置和接口连通性
|
|
13
|
+
- `lcagent config set approvalMode auto|manual`:切换工具权限模式
|
|
14
|
+
- `lcagent config set autoContinueOnMaxTurns true|false`:切换超限自动续跑
|
|
13
15
|
- `lcagent config show`
|
|
14
16
|
- `lcagent config set <key> <value>`
|
|
15
17
|
|
|
@@ -20,6 +22,32 @@
|
|
|
20
22
|
- `grep`
|
|
21
23
|
- `run_shell`
|
|
22
24
|
|
|
25
|
+
其中 `edit_file` 现已支持:
|
|
26
|
+
|
|
27
|
+
- 单文件 `unified diff / patch` 应用
|
|
28
|
+
- 精确文本替换
|
|
29
|
+
- 直引号 / 弯引号不一致时的归一化匹配
|
|
30
|
+
- 命中弯引号内容时自动保留原文件引号风格
|
|
31
|
+
- 多处命中时提示使用 `occurrence` 或 `replaceAll`
|
|
32
|
+
- 替换失败时返回更具体的上下文诊断
|
|
33
|
+
|
|
34
|
+
`edit_file` 现在有两种模式:
|
|
35
|
+
|
|
36
|
+
- 文本替换模式:传 `path + oldText + newText`
|
|
37
|
+
- 新建/整文件写入模式:传 `path + content`;若覆盖已有文件,额外传 `replaceEntireFile=true`
|
|
38
|
+
- Patch 模式:传 `patch`,也可以额外传 `path`
|
|
39
|
+
|
|
40
|
+
Patch 模式当前限制:
|
|
41
|
+
|
|
42
|
+
- 只支持单文件 unified diff
|
|
43
|
+
- patch 需要能对当前文件内容干净应用
|
|
44
|
+
- 如果 `path` 与 patch 头里的目标文件不一致,会直接报错
|
|
45
|
+
|
|
46
|
+
Windows 下如果模型需要创建目录或文件,优先建议:
|
|
47
|
+
|
|
48
|
+
- 用 `run_shell` 执行 Windows 兼容命令,而不是 Bash 专用语法
|
|
49
|
+
- 或者直接用 `edit_file` 的 `content` 模式创建文件
|
|
50
|
+
|
|
23
51
|
## 安装
|
|
24
52
|
|
|
25
53
|
```bash
|
|
@@ -28,6 +56,21 @@ npm install
|
|
|
28
56
|
npm run build
|
|
29
57
|
```
|
|
30
58
|
|
|
59
|
+
如果你想先确认本地最小闭环是否正常,可以直接运行:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm run smoke
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
这条命令不会访问模型服务,只会验证:
|
|
66
|
+
|
|
67
|
+
- `lcagent tools`
|
|
68
|
+
- `lcagent config show`
|
|
69
|
+
- `read_file`
|
|
70
|
+
- `grep`
|
|
71
|
+
- `edit_file`
|
|
72
|
+
- `run_shell`
|
|
73
|
+
|
|
31
74
|
## 发布到公网 npm
|
|
32
75
|
|
|
33
76
|
先登录 npm 官方仓库:
|
|
@@ -137,16 +180,111 @@ npm run start -- tools
|
|
|
137
180
|
npm run start -- provider
|
|
138
181
|
npm run start -- model
|
|
139
182
|
npm run start -- doctor
|
|
183
|
+
npm run start -- config set approvalMode manual
|
|
140
184
|
```
|
|
141
185
|
|
|
186
|
+
注意:`chat` 和 `-p` 都依赖一个可用的模型配置。
|
|
187
|
+
|
|
188
|
+
- 如果你走 `anthropic`,需要先配置 `ANTHROPIC_API_KEY` 或 `lcagent config set apiKey ...`
|
|
189
|
+
- 如果你走本地模型,建议先执行 `init-local`,再执行 `doctor`,最后再进入 `chat`
|
|
190
|
+
|
|
191
|
+
## 最小闭环验收顺序
|
|
192
|
+
|
|
193
|
+
推荐按下面顺序验证当前版本:
|
|
194
|
+
|
|
195
|
+
1. 本地静态能力:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm run check
|
|
199
|
+
npm run build
|
|
200
|
+
npm run smoke
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
2. 本地模型接入(OpenAI-compatible):
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
npm run start -- init-local --base-url http://127.0.0.1:8000/v1 --model your-model-name
|
|
207
|
+
npm run start -- doctor
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
如果你使用 Anthropic 官方接口,则把上面两条替换成:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
export ANTHROPIC_API_KEY="your-key"
|
|
214
|
+
npm run start -- doctor
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
3. 交互式 chat:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npm run start -- chat
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
进到 chat 后,建议先做一个最小读文件任务,例如:
|
|
224
|
+
|
|
225
|
+
- `读取 README.md 的前 40 行并总结项目结构`
|
|
226
|
+
- `搜索项目里有哪些 edit_file 相关实现`
|
|
227
|
+
|
|
228
|
+
如果 `doctor` 可通过、`chat` 能读文件、`npm run smoke` 通过,就说明当前最小闭环已经稳定可用。
|
|
229
|
+
|
|
230
|
+
如果 `doctor` 能跑但 `chat` 提示缺少 API key,通常不是程序损坏,而是当前配置仍停留在默认的 `anthropic` 且没有完成认证。先执行 `config show` 检查当前 provider,再按上面的配置步骤修正即可。
|
|
231
|
+
|
|
232
|
+
## 权限模式
|
|
233
|
+
|
|
234
|
+
`lcagent` 当前支持两种最小权限模式:
|
|
235
|
+
|
|
236
|
+
- `auto`:工具直接执行
|
|
237
|
+
- `manual`:`read_file` / `grep` 直接放行,`edit_file` / `run_shell` 在执行前询问确认
|
|
238
|
+
|
|
239
|
+
其中 `manual` 模式已经裁剪复用了 Claude Code 权限层里的两类思路:
|
|
240
|
+
|
|
241
|
+
- 权限模式分层思路
|
|
242
|
+
- 危险文件 / 危险目录名单(如 `.git`、`.vscode`、`.claude`、shell 配置文件)
|
|
243
|
+
|
|
244
|
+
如果 `edit_file` 命中的目标位于这些高风险路径下,确认提示会给出更高风险说明。
|
|
245
|
+
|
|
246
|
+
如果当前不是交互式终端,`manual` 模式下的写入/执行工具会被拒绝,并返回明确原因。
|
|
247
|
+
|
|
142
248
|
当模型触发工具时,CLI 会打印:
|
|
143
249
|
|
|
144
250
|
- `[tool-call] 工具名`:后面跟模型传入的原始 JSON 参数
|
|
145
251
|
- `[tool-result] 工具名`:显示工具执行结果
|
|
146
252
|
- `[tool-result] 工具名 (error)`:显示工具失败原因
|
|
253
|
+
- 同时附带 `cwd`、执行耗时、审批状态、失败阶段等元数据,便于排查是校验失败、审批拒绝还是工具执行异常
|
|
147
254
|
|
|
148
255
|
这样可以直接看出是模型参数字段名不对、路径不对,还是工具执行本身报错。
|
|
149
256
|
|
|
257
|
+
## 超限续跑
|
|
258
|
+
|
|
259
|
+
当前默认配置下,`lcagent` 在单段运行达到 `maxTurns` 后,不会立刻报错退出,而是会自动续跑下一段:
|
|
260
|
+
|
|
261
|
+
- `maxTurns`:每一段最多运行多少轮,默认 `8`
|
|
262
|
+
- `autoContinueOnMaxTurns`:达到上限后是否自动续跑,默认 `true`
|
|
263
|
+
- `maxContinuations`:最多自动续跑多少段,默认 `3`
|
|
264
|
+
|
|
265
|
+
也就是说,默认情况下最多会跑 `1 + 3 = 4` 段。
|
|
266
|
+
|
|
267
|
+
如果自动续跑次数也耗尽了,CLI 不会只打印一条生硬错误,而是会额外输出:
|
|
268
|
+
|
|
269
|
+
- 最近的 assistant 文本进展
|
|
270
|
+
- 最近几次工具调用
|
|
271
|
+
- 最近几次工具结果
|
|
272
|
+
- 一个建议的“继续”提示词
|
|
273
|
+
|
|
274
|
+
常用配置示例:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
npm run start -- config set maxTurns 12
|
|
278
|
+
npm run start -- config set autoContinueOnMaxTurns true
|
|
279
|
+
npm run start -- config set maxContinuations 4
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
如果你想恢复原来那种“达到上限就停止”的行为:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
npm run start -- config set autoContinueOnMaxTurns false
|
|
286
|
+
```
|
|
287
|
+
|
|
150
288
|
## 设计目标
|
|
151
289
|
|
|
152
290
|
- 保留 Claude Code 式的核心代理循环
|
|
@@ -157,7 +295,6 @@ npm run start -- doctor
|
|
|
157
295
|
|
|
158
296
|
- 当前是最小版,没有流式输出
|
|
159
297
|
- 工具权限策略还是简化版
|
|
160
|
-
- `edit_file` 只做简单字符串替换
|
|
161
298
|
- `grep` 是纯 Node 实现,不如 ripgrep 快
|
|
162
299
|
- 当前按 Anthropic Messages API 直接调用,需要有效 API Key
|
|
163
300
|
|
package/dist/app/bootstrap.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { loadConfig } from '../config/store.js';
|
|
2
2
|
import { AgentEngine } from '../core/engine.js';
|
|
3
3
|
import { getDefaultTools } from '../tools/registry.js';
|
|
4
|
-
|
|
4
|
+
import type { ToolContext } from '../tools/types.js';
|
|
5
|
+
export declare function createApp(cwd: string, options?: {
|
|
6
|
+
requestApproval?: ToolContext['requestApproval'];
|
|
7
|
+
}): Promise<{
|
|
5
8
|
config: Awaited<ReturnType<typeof loadConfig>>;
|
|
6
9
|
engine: AgentEngine;
|
|
7
10
|
tools: ReturnType<typeof getDefaultTools>;
|
package/dist/app/bootstrap.js
CHANGED
|
@@ -4,7 +4,7 @@ import { buildSystemPrompt } from '../core/systemPrompt.js';
|
|
|
4
4
|
import { ModelClient } from '../core/model.js';
|
|
5
5
|
import { AgentEngine } from '../core/engine.js';
|
|
6
6
|
import { getDefaultTools } from '../tools/registry.js';
|
|
7
|
-
export async function createApp(cwd) {
|
|
7
|
+
export async function createApp(cwd, options) {
|
|
8
8
|
const config = await loadConfig();
|
|
9
9
|
const apiKey = config.apiKey ??
|
|
10
10
|
process.env.MODEL_API_KEY ??
|
|
@@ -21,7 +21,7 @@ export async function createApp(cwd) {
|
|
|
21
21
|
baseUrl: config.baseUrl,
|
|
22
22
|
model: config.model,
|
|
23
23
|
maxTokens: config.maxTokens,
|
|
24
|
-
}), tools, buildSystemPrompt(cwd), cwd);
|
|
24
|
+
}), tools, buildSystemPrompt(cwd), cwd, options?.requestApproval);
|
|
25
25
|
return {
|
|
26
26
|
config,
|
|
27
27
|
engine,
|
package/dist/bin/cli.js
CHANGED
|
@@ -54,6 +54,33 @@ function formatToolInput(input) {
|
|
|
54
54
|
return String(input);
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
function formatToolResultMeta(meta) {
|
|
58
|
+
if (!meta) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const parts = [`cwd=${meta.cwd}`, `duration=${meta.durationMs}ms`];
|
|
62
|
+
if (meta.approval.required) {
|
|
63
|
+
const approvalState = meta.approval.approved === false
|
|
64
|
+
? 'denied'
|
|
65
|
+
: meta.approval.approved === true
|
|
66
|
+
? 'approved'
|
|
67
|
+
: 'pending';
|
|
68
|
+
parts.push(`approval=${approvalState}`);
|
|
69
|
+
if (meta.approval.risk) {
|
|
70
|
+
parts.push(`risk=${meta.approval.risk}`);
|
|
71
|
+
}
|
|
72
|
+
if (meta.approval.targets && meta.approval.targets.length > 0) {
|
|
73
|
+
parts.push(`targets=${meta.approval.targets.join(',')}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
parts.push(`approval=${meta.approval.mode === 'manual' ? 'not-required' : 'auto'}`);
|
|
78
|
+
}
|
|
79
|
+
if (meta.failureStage) {
|
|
80
|
+
parts.push(`stage=${meta.failureStage}`);
|
|
81
|
+
}
|
|
82
|
+
return parts.join(' | ');
|
|
83
|
+
}
|
|
57
84
|
function printEvent(event) {
|
|
58
85
|
switch (event.type) {
|
|
59
86
|
case 'status':
|
|
@@ -64,14 +91,54 @@ function printEvent(event) {
|
|
|
64
91
|
break;
|
|
65
92
|
case 'tool_call':
|
|
66
93
|
console.log(`\n[tool-call] ${event.toolName}`);
|
|
94
|
+
console.log(`cwd=${event.cwd} | approvalMode=${event.approvalMode}`);
|
|
67
95
|
console.log(formatToolInput(event.input));
|
|
68
96
|
break;
|
|
69
97
|
case 'tool_result':
|
|
70
98
|
console.log(`\n[tool-result] ${event.toolName}${event.isError ? ' (error)' : ''}`);
|
|
99
|
+
const metaLine = formatToolResultMeta(event.meta);
|
|
100
|
+
if (metaLine) {
|
|
101
|
+
console.log(metaLine);
|
|
102
|
+
}
|
|
71
103
|
console.log(event.result);
|
|
72
104
|
break;
|
|
73
105
|
}
|
|
74
106
|
}
|
|
107
|
+
async function promptForToolApproval(request, rl) {
|
|
108
|
+
if (!input.isTTY || !output.isTTY) {
|
|
109
|
+
return {
|
|
110
|
+
approved: false,
|
|
111
|
+
reason: `Denied ${request.toolName}: manual approval requires an interactive terminal.`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const lines = [
|
|
115
|
+
'',
|
|
116
|
+
`[approval] ${request.toolName}`,
|
|
117
|
+
`- risk: ${request.risk}`,
|
|
118
|
+
`- summary: ${request.summary}`,
|
|
119
|
+
`- reason: ${request.reason}`,
|
|
120
|
+
];
|
|
121
|
+
if (request.targets.length > 0) {
|
|
122
|
+
lines.push(`- targets: ${request.targets.join(', ')}`);
|
|
123
|
+
}
|
|
124
|
+
console.log(lines.join('\n'));
|
|
125
|
+
const temporaryRl = rl ?? createInterface({ input, output });
|
|
126
|
+
try {
|
|
127
|
+
const answer = (await questionAsync(temporaryRl, 'Allow this tool call? [y/N] ')).trim().toLowerCase();
|
|
128
|
+
if (answer === 'y' || answer === 'yes') {
|
|
129
|
+
return { approved: true };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
approved: false,
|
|
133
|
+
reason: `User denied ${request.toolName}.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
if (!rl) {
|
|
138
|
+
temporaryRl.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
75
142
|
async function probeUrl(url) {
|
|
76
143
|
try {
|
|
77
144
|
const response = await fetchWithTimeout(url, {
|
|
@@ -229,17 +296,21 @@ async function runDoctor() {
|
|
|
229
296
|
});
|
|
230
297
|
}
|
|
231
298
|
async function printAgentRun(prompt) {
|
|
232
|
-
const { engine } = await createApp(process.cwd()
|
|
299
|
+
const { engine } = await createApp(process.cwd(), {
|
|
300
|
+
requestApproval: request => promptForToolApproval(request),
|
|
301
|
+
});
|
|
233
302
|
for await (const event of engine.submit(prompt)) {
|
|
234
303
|
printEvent(event);
|
|
235
304
|
}
|
|
236
305
|
}
|
|
237
306
|
async function runChat() {
|
|
238
|
-
const { engine, config } = await createApp(process.cwd());
|
|
239
|
-
console.log(`lcagent chat started with provider: ${config.provider}, model: ${config.model}`);
|
|
240
|
-
console.log('Type `exit` or `quit` to leave.');
|
|
241
307
|
const rl = createInterface({ input, output });
|
|
242
308
|
try {
|
|
309
|
+
const { engine, config } = await createApp(process.cwd(), {
|
|
310
|
+
requestApproval: request => promptForToolApproval(request, rl),
|
|
311
|
+
});
|
|
312
|
+
console.log(`lcagent chat started with provider: ${config.provider}, model: ${config.model}, approvalMode: ${config.approvalMode}`);
|
|
313
|
+
console.log('Type `exit` or `quit` to leave.');
|
|
243
314
|
while (true) {
|
|
244
315
|
const line = (await questionAsync(rl, '> ')).trim();
|
|
245
316
|
if (!line) {
|
|
@@ -358,11 +429,13 @@ configCommand
|
|
|
358
429
|
.argument('<value>', 'Config value')
|
|
359
430
|
.action(async (key, value) => {
|
|
360
431
|
const current = await loadConfig();
|
|
361
|
-
const nextValue = key === 'maxTurns' || key === 'maxTokens'
|
|
432
|
+
const nextValue = key === 'maxTurns' || key === 'maxTokens' || key === 'maxContinuations'
|
|
362
433
|
? Number(value)
|
|
363
|
-
: key === '
|
|
364
|
-
? value
|
|
365
|
-
:
|
|
434
|
+
: key === 'autoContinueOnMaxTurns'
|
|
435
|
+
? value === 'true'
|
|
436
|
+
: key === 'approvalMode' || key === 'provider'
|
|
437
|
+
? value
|
|
438
|
+
: value;
|
|
366
439
|
const next = agentConfigSchema.parse({
|
|
367
440
|
...current,
|
|
368
441
|
[key]: nextValue,
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const agentConfigSchema: z.ZodObject<{
|
|
|
8
8
|
baseUrl: z.ZodDefault<z.ZodString>;
|
|
9
9
|
model: z.ZodDefault<z.ZodString>;
|
|
10
10
|
maxTurns: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
autoContinueOnMaxTurns: z.ZodDefault<z.ZodBoolean>;
|
|
12
|
+
maxContinuations: z.ZodDefault<z.ZodNumber>;
|
|
11
13
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
12
14
|
approvalMode: z.ZodDefault<z.ZodEnum<{
|
|
13
15
|
auto: "auto";
|
package/dist/config/schema.js
CHANGED
|
@@ -5,6 +5,8 @@ export const agentConfigSchema = z.object({
|
|
|
5
5
|
baseUrl: z.string().url().default('https://api.anthropic.com'),
|
|
6
6
|
model: z.string().trim().min(1).default('claude-3-7-sonnet-latest'),
|
|
7
7
|
maxTurns: z.number().int().positive().default(8),
|
|
8
|
+
autoContinueOnMaxTurns: z.boolean().default(true),
|
|
9
|
+
maxContinuations: z.number().int().min(0).default(3),
|
|
8
10
|
maxTokens: z.number().int().positive().default(2048),
|
|
9
11
|
approvalMode: z.enum(['auto', 'manual']).default('auto'),
|
|
10
12
|
});
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import type { ToolContext, ToolDefinition } from '../tools/types.js';
|
|
1
2
|
import type { AgentConfig } from '../config/schema.js';
|
|
2
3
|
import type { AgentSession } from '../app/session.js';
|
|
3
4
|
import { type AgentEvent } from './message.js';
|
|
4
5
|
import type { ModelClient } from './model.js';
|
|
5
|
-
import type { ToolDefinition } from '../tools/types.js';
|
|
6
6
|
export declare class AgentEngine {
|
|
7
7
|
private readonly session;
|
|
8
8
|
private readonly config;
|
|
@@ -10,6 +10,7 @@ export declare class AgentEngine {
|
|
|
10
10
|
private readonly tools;
|
|
11
11
|
private readonly systemPrompt;
|
|
12
12
|
private readonly cwd;
|
|
13
|
-
|
|
13
|
+
private readonly requestApproval?;
|
|
14
|
+
constructor(session: AgentSession, config: AgentConfig, modelClient: ModelClient, tools: ToolDefinition[], systemPrompt: string, cwd: string, requestApproval?: ToolContext['requestApproval']);
|
|
14
15
|
submit(prompt: string): AsyncGenerator<AgentEvent, void>;
|
|
15
16
|
}
|
package/dist/core/engine.js
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
|
-
import { createUserTextMessage } from './message.js';
|
|
1
|
+
import { createAutoContinuationMessage, createUserTextMessage } from './message.js';
|
|
2
2
|
import { runAgentLoop } from './loop.js';
|
|
3
|
+
function truncate(value, maxLength = 140) {
|
|
4
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
5
|
+
if (normalized.length <= maxLength) {
|
|
6
|
+
return normalized;
|
|
7
|
+
}
|
|
8
|
+
return `${normalized.slice(0, maxLength - 1)}…`;
|
|
9
|
+
}
|
|
10
|
+
function buildRecentProgressSummary(messages) {
|
|
11
|
+
const recentToolNames = [];
|
|
12
|
+
const recentToolResults = [];
|
|
13
|
+
let latestAssistantText = null;
|
|
14
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
15
|
+
const message = messages[index];
|
|
16
|
+
if (!latestAssistantText && message.role === 'assistant') {
|
|
17
|
+
const textBlock = message.content.find(block => block.type === 'text' && block.text.trim().length > 0);
|
|
18
|
+
if (textBlock && textBlock.type === 'text') {
|
|
19
|
+
latestAssistantText = truncate(textBlock.text);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (message.role === 'assistant') {
|
|
23
|
+
const toolUses = message.content.filter(block => block.type === 'tool_use');
|
|
24
|
+
for (let toolIndex = toolUses.length - 1; toolIndex >= 0; toolIndex -= 1) {
|
|
25
|
+
const toolUse = toolUses[toolIndex];
|
|
26
|
+
recentToolNames.push(toolUse.name);
|
|
27
|
+
if (recentToolNames.length >= 3) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (message.role === 'user') {
|
|
33
|
+
const toolResults = message.content.filter(block => block.type === 'tool_result');
|
|
34
|
+
for (let resultIndex = toolResults.length - 1; resultIndex >= 0; resultIndex -= 1) {
|
|
35
|
+
const toolResult = toolResults[resultIndex];
|
|
36
|
+
recentToolResults.push(truncate(toolResult.content));
|
|
37
|
+
if (recentToolResults.length >= 3) {
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (latestAssistantText &&
|
|
43
|
+
recentToolNames.length >= 3 &&
|
|
44
|
+
recentToolResults.length >= 3) {
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const lines = ['Recent progress summary:'];
|
|
49
|
+
if (latestAssistantText) {
|
|
50
|
+
lines.push(`- Last assistant note: ${latestAssistantText}`);
|
|
51
|
+
}
|
|
52
|
+
if (recentToolNames.length > 0) {
|
|
53
|
+
lines.push(`- Recent tools: ${recentToolNames.reverse().join(' -> ')}`);
|
|
54
|
+
}
|
|
55
|
+
if (recentToolResults.length > 0) {
|
|
56
|
+
lines.push(`- Recent tool results: ${recentToolResults.reverse().join(' | ')}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('- Suggested next prompt: 继续,基于当前上下文完成剩余任务,并优先处理尚未完成的步骤。');
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
}
|
|
3
61
|
export class AgentEngine {
|
|
4
62
|
session;
|
|
5
63
|
config;
|
|
@@ -7,23 +65,55 @@ export class AgentEngine {
|
|
|
7
65
|
tools;
|
|
8
66
|
systemPrompt;
|
|
9
67
|
cwd;
|
|
10
|
-
|
|
68
|
+
requestApproval;
|
|
69
|
+
constructor(session, config, modelClient, tools, systemPrompt, cwd, requestApproval) {
|
|
11
70
|
this.session = session;
|
|
12
71
|
this.config = config;
|
|
13
72
|
this.modelClient = modelClient;
|
|
14
73
|
this.tools = tools;
|
|
15
74
|
this.systemPrompt = systemPrompt;
|
|
16
75
|
this.cwd = cwd;
|
|
76
|
+
this.requestApproval = requestApproval;
|
|
17
77
|
}
|
|
18
78
|
async *submit(prompt) {
|
|
19
79
|
this.session.messages.push(createUserTextMessage(prompt));
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
80
|
+
let continuationCount = 0;
|
|
81
|
+
while (true) {
|
|
82
|
+
const result = yield* runAgentLoop({
|
|
83
|
+
config: this.config,
|
|
84
|
+
modelClient: this.modelClient,
|
|
85
|
+
messages: this.session.messages,
|
|
86
|
+
tools: this.tools,
|
|
87
|
+
systemPrompt: this.systemPrompt,
|
|
88
|
+
cwd: this.cwd,
|
|
89
|
+
requestApproval: this.requestApproval,
|
|
90
|
+
});
|
|
91
|
+
if (result.reason !== 'max_turns') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!this.config.autoContinueOnMaxTurns ||
|
|
95
|
+
continuationCount >= this.config.maxContinuations) {
|
|
96
|
+
yield {
|
|
97
|
+
type: 'status',
|
|
98
|
+
message: `Reached maxTurns=${this.config.maxTurns} and stopped after ${continuationCount} auto-continuation(s). ` +
|
|
99
|
+
'Increase maxTurns or maxContinuations if you want longer autonomous runs.',
|
|
100
|
+
};
|
|
101
|
+
yield {
|
|
102
|
+
type: 'status',
|
|
103
|
+
message: buildRecentProgressSummary(this.session.messages),
|
|
104
|
+
};
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
continuationCount += 1;
|
|
108
|
+
yield {
|
|
109
|
+
type: 'status',
|
|
110
|
+
message: `Reached maxTurns=${this.config.maxTurns}; auto-continuing segment ${continuationCount}/${this.config.maxContinuations}...`,
|
|
111
|
+
};
|
|
112
|
+
this.session.messages.push(createAutoContinuationMessage({
|
|
113
|
+
maxTurns: this.config.maxTurns,
|
|
114
|
+
segment: continuationCount,
|
|
115
|
+
maxContinuations: this.config.maxContinuations,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
28
118
|
}
|
|
29
119
|
}
|
package/dist/core/loop.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { AgentConfig } from '../config/schema.js';
|
|
2
2
|
import type { AgentEvent, AgentMessage } from './message.js';
|
|
3
|
-
import type { ToolDefinition } from '../tools/types.js';
|
|
3
|
+
import type { ToolContext, ToolDefinition } from '../tools/types.js';
|
|
4
4
|
import type { ModelClient } from './model.js';
|
|
5
|
+
export type AgentLoopResult = {
|
|
6
|
+
reason: 'completed' | 'max_turns';
|
|
7
|
+
turnCount: number;
|
|
8
|
+
};
|
|
5
9
|
export declare function runAgentLoop(params: {
|
|
6
10
|
config: AgentConfig;
|
|
7
11
|
modelClient: ModelClient;
|
|
@@ -9,4 +13,5 @@ export declare function runAgentLoop(params: {
|
|
|
9
13
|
tools: ToolDefinition[];
|
|
10
14
|
systemPrompt: string;
|
|
11
15
|
cwd: string;
|
|
12
|
-
|
|
16
|
+
requestApproval?: ToolContext['requestApproval'];
|
|
17
|
+
}): AsyncGenerator<AgentEvent, AgentLoopResult>;
|
package/dist/core/loop.js
CHANGED
|
@@ -3,6 +3,7 @@ export async function* runAgentLoop(params) {
|
|
|
3
3
|
const toolContext = {
|
|
4
4
|
cwd: params.cwd,
|
|
5
5
|
approvalMode: params.config.approvalMode,
|
|
6
|
+
requestApproval: params.requestApproval,
|
|
6
7
|
};
|
|
7
8
|
for (let turn = 0; turn < params.config.maxTurns; turn += 1) {
|
|
8
9
|
yield { type: 'status', message: `Thinking (turn ${turn + 1}/${params.config.maxTurns})...` };
|
|
@@ -23,13 +24,15 @@ export async function* runAgentLoop(params) {
|
|
|
23
24
|
}
|
|
24
25
|
}
|
|
25
26
|
if (toolUses.length === 0) {
|
|
26
|
-
return;
|
|
27
|
+
return { reason: 'completed', turnCount: turn + 1 };
|
|
27
28
|
}
|
|
28
29
|
for (const toolUse of toolUses) {
|
|
29
30
|
yield {
|
|
30
31
|
type: 'tool_call',
|
|
31
32
|
toolName: toolUse.name,
|
|
32
33
|
input: toolUse.input,
|
|
34
|
+
cwd: params.cwd,
|
|
35
|
+
approvalMode: params.config.approvalMode,
|
|
33
36
|
};
|
|
34
37
|
const result = await executeToolCall(toolUse, params.tools, toolContext);
|
|
35
38
|
yield {
|
|
@@ -37,6 +40,7 @@ export async function* runAgentLoop(params) {
|
|
|
37
40
|
toolName: toolUse.name,
|
|
38
41
|
result: result.content,
|
|
39
42
|
isError: result.isError,
|
|
43
|
+
meta: result.meta,
|
|
40
44
|
};
|
|
41
45
|
const toolResultBlock = {
|
|
42
46
|
type: 'tool_result',
|
|
@@ -50,5 +54,8 @@ export async function* runAgentLoop(params) {
|
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
|
-
|
|
57
|
+
return {
|
|
58
|
+
reason: 'max_turns',
|
|
59
|
+
turnCount: params.config.maxTurns,
|
|
60
|
+
};
|
|
54
61
|
}
|
package/dist/core/message.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ToolExecutionMeta } from '../tools/types.js';
|
|
1
2
|
export type TextBlock = {
|
|
2
3
|
type: 'text';
|
|
3
4
|
text: string;
|
|
@@ -29,10 +30,18 @@ export type AgentEvent = {
|
|
|
29
30
|
type: 'tool_call';
|
|
30
31
|
toolName: string;
|
|
31
32
|
input: unknown;
|
|
33
|
+
cwd: string;
|
|
34
|
+
approvalMode: 'auto' | 'manual';
|
|
32
35
|
} | {
|
|
33
36
|
type: 'tool_result';
|
|
34
37
|
toolName: string;
|
|
35
38
|
result: string;
|
|
36
39
|
isError?: boolean;
|
|
40
|
+
meta?: ToolExecutionMeta;
|
|
37
41
|
};
|
|
38
42
|
export declare function createUserTextMessage(text: string): AgentMessage;
|
|
43
|
+
export declare function createAutoContinuationMessage(params: {
|
|
44
|
+
maxTurns: number;
|
|
45
|
+
segment: number;
|
|
46
|
+
maxContinuations: number;
|
|
47
|
+
}): AgentMessage;
|
package/dist/core/message.js
CHANGED
|
@@ -4,3 +4,12 @@ export function createUserTextMessage(text) {
|
|
|
4
4
|
content: [{ type: 'text', text }],
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
|
+
export function createAutoContinuationMessage(params) {
|
|
8
|
+
const text = [
|
|
9
|
+
`Automatic continuation ${params.segment}/${params.maxContinuations} after reaching maxTurns=${params.maxTurns}.`,
|
|
10
|
+
'Continue from the current state without repeating completed work.',
|
|
11
|
+
'Prioritize the remaining unfinished steps and finish the task if possible.',
|
|
12
|
+
'If the task is already complete, answer directly without additional tool calls.',
|
|
13
|
+
].join(' ');
|
|
14
|
+
return createUserTextMessage(text);
|
|
15
|
+
}
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
export function buildSystemPrompt(cwd) {
|
|
2
|
+
const platform = process.platform;
|
|
3
|
+
const platformGuidance = platform === 'win32'
|
|
4
|
+
? 'Current platform is Windows. For run_shell, use cmd/PowerShell-compatible syntax. Avoid Bash-only constructs such as mkdir -p, brace expansion {a,b}, and heredoc blocks.'
|
|
5
|
+
: 'Current platform is Unix-like. Use standard POSIX shell syntax for run_shell.';
|
|
2
6
|
return [
|
|
3
7
|
'You are a coding agent running inside a local CLI.',
|
|
4
8
|
'Prefer using tools when they help answer precisely.',
|
|
5
9
|
'Do not invent file contents or command output when tools can verify them.',
|
|
6
10
|
'Before editing code, read the relevant files.',
|
|
11
|
+
'Use valid structured tool calls only. Never output literal pseudo-tags like <tool_call> or XML wrappers.',
|
|
12
|
+
'Prefer edit_file for creating or updating files instead of shell redirection when possible.',
|
|
7
13
|
'Explain briefly and act directly.',
|
|
8
14
|
`Current working directory: ${cwd}`,
|
|
15
|
+
`Current platform: ${platform}`,
|
|
16
|
+
platformGuidance,
|
|
9
17
|
'Available tools may read files, edit files, search files, and run shell commands.',
|
|
10
18
|
].join('\n');
|
|
11
19
|
}
|
package/dist/tools/editFile.d.ts
CHANGED
|
@@ -4,10 +4,23 @@ declare const inputSchema: z.ZodObject<{
|
|
|
4
4
|
path: z.ZodOptional<z.ZodString>;
|
|
5
5
|
filePath: z.ZodOptional<z.ZodString>;
|
|
6
6
|
file_path: z.ZodOptional<z.ZodString>;
|
|
7
|
+
content: z.ZodOptional<z.ZodString>;
|
|
8
|
+
fileContent: z.ZodOptional<z.ZodString>;
|
|
9
|
+
file_content: z.ZodOptional<z.ZodString>;
|
|
10
|
+
replaceEntireFile: z.ZodOptional<z.ZodBoolean>;
|
|
11
|
+
replace_entire_file: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
patch: z.ZodOptional<z.ZodString>;
|
|
13
|
+
diff: z.ZodOptional<z.ZodString>;
|
|
14
|
+
unifiedDiff: z.ZodOptional<z.ZodString>;
|
|
15
|
+
unified_diff: z.ZodOptional<z.ZodString>;
|
|
7
16
|
oldText: z.ZodOptional<z.ZodString>;
|
|
8
17
|
old_text: z.ZodOptional<z.ZodString>;
|
|
9
18
|
newText: z.ZodOptional<z.ZodString>;
|
|
10
19
|
new_text: z.ZodOptional<z.ZodString>;
|
|
20
|
+
replaceAll: z.ZodOptional<z.ZodBoolean>;
|
|
21
|
+
replace_all: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
occurrence: z.ZodOptional<z.ZodNumber>;
|
|
23
|
+
occurrence_index: z.ZodOptional<z.ZodNumber>;
|
|
11
24
|
}, z.core.$strip>;
|
|
12
25
|
export declare const editFileTool: ToolDefinition<z.infer<typeof inputSchema>>;
|
|
13
26
|
export {};
|