foliko 2.0.29 → 2.0.31
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/docs/plugin-dev-guide.md +850 -0
- package/package.json +1 -1
- package/src/agent/chat.js +19 -54
- package/src/agent/tool-loop.js +22 -80
- package/src/common/constants.js +2 -2
- package/src/llm/tokens.js +43 -5
- package/src/session/session.js +32 -4
- package/src/storage/jsonl.js +5 -3
- package/tests/core/tool-loop.test.js +4 -3
|
@@ -0,0 +1,850 @@
|
|
|
1
|
+
# Plugin 开发指南
|
|
2
|
+
|
|
3
|
+
> 本指南详细说明如何在 Foliko 框架中开发插件。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 目录
|
|
8
|
+
|
|
9
|
+
- [1. 概述](#1-概述)
|
|
10
|
+
- [2. 插件存放位置](#2-插件存放位置)
|
|
11
|
+
- [3. 用户插件开发](#3-用户插件开发)
|
|
12
|
+
- [4. 内置插件开发](#4-内置插件开发)
|
|
13
|
+
- [5. 插件属性](#5-插件属性)
|
|
14
|
+
- [6. 生命周期](#6-生命周期)
|
|
15
|
+
- [7. 工具注册](#7-工具注册)
|
|
16
|
+
- [8. 扩展工具](#8-扩展工具)
|
|
17
|
+
- [9. 子 Agent](#9-子-agent)
|
|
18
|
+
- [10. 工作流](#10-工作流)
|
|
19
|
+
- [11. Prompt Parts](#11-prompt-parts)
|
|
20
|
+
- [12. Skill 管理](#12-skill-管理)
|
|
21
|
+
- [13. 依赖管理](#13-依赖管理)
|
|
22
|
+
- [14. 系统插件访问](#14-系统插件访问)
|
|
23
|
+
- [15. 最佳实践](#15-最佳实践)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. 概述
|
|
28
|
+
|
|
29
|
+
Foliko 是一个基于插件的 Agent 框架,核心保持简单,通过插件扩展功能。
|
|
30
|
+
|
|
31
|
+
### 插件 vs 技能
|
|
32
|
+
|
|
33
|
+
| 类型 | 存储位置 | 说明 |
|
|
34
|
+
|------|----------|------|
|
|
35
|
+
| **Plugin(插件)** | `.foliko/plugins/` | 扩展功能模块 |
|
|
36
|
+
| **Skill(技能)** | `.foliko/skills/` | 为 Agent 提供指导的知识包 |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. 插件存放位置
|
|
41
|
+
|
|
42
|
+
### 用户插件 `.foliko/plugins/`
|
|
43
|
+
|
|
44
|
+
用户自定义插件放在项目根目录的 `.foliko/plugins/` 下,**自动加载**,无需手动注册。
|
|
45
|
+
|
|
46
|
+
**推荐使用文件夹结构**(适合复杂插件):
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
项目目录/
|
|
50
|
+
└── .foliko/
|
|
51
|
+
└── plugins/
|
|
52
|
+
├── my-plugin/ ✅ 文件夹结构(推荐)
|
|
53
|
+
│ ├── package.json # 可选,main 字段指定入口
|
|
54
|
+
│ ├── index.js # 默认入口
|
|
55
|
+
│ └── node_modules/ # 可选,插件私有依赖
|
|
56
|
+
├── another-plugin/ ✅ 另一个文件夹插件
|
|
57
|
+
│ └── index.js
|
|
58
|
+
└── legacy.js ✅ 单文件仍支持
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### 内置插件 `plugins/`
|
|
62
|
+
|
|
63
|
+
框架内置插件位于仓库根目录的 `plugins/` 目录,需要 `require` Plugin 基类。
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. 用户插件开发
|
|
68
|
+
|
|
69
|
+
### 文件夹结构(推荐)
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
.foliko/plugins/my-plugin/
|
|
73
|
+
├── package.json # 可选
|
|
74
|
+
└── index.js # 入口
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
// .foliko/plugins/my-plugin/index.js
|
|
79
|
+
const { z } = require('zod');
|
|
80
|
+
|
|
81
|
+
module.exports = function (Plugin) {
|
|
82
|
+
return class MyPlugin extends Plugin {
|
|
83
|
+
constructor(config = {}) {
|
|
84
|
+
super();
|
|
85
|
+
this.name = 'my-plugin';
|
|
86
|
+
this.version = '1.0.0';
|
|
87
|
+
this.description = '我的工具插件';
|
|
88
|
+
this.priority = 10;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 方式1:声明式工具定义
|
|
92
|
+
tools = {
|
|
93
|
+
my_tool: {
|
|
94
|
+
description: '我的工具',
|
|
95
|
+
inputSchema: z.object({
|
|
96
|
+
param: z.string().describe('参数描述'),
|
|
97
|
+
}),
|
|
98
|
+
execute: async (args) => {
|
|
99
|
+
return { success: true, result: args.param };
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// 方式2:生命周期方法
|
|
105
|
+
onStart(framework) {
|
|
106
|
+
// 注册额外工具
|
|
107
|
+
this.tool.register({
|
|
108
|
+
name: 'another_tool',
|
|
109
|
+
description: '另一个工具',
|
|
110
|
+
inputSchema: z.object({}),
|
|
111
|
+
execute: async (args) => ({ success: true }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
uninstall(framework) {
|
|
116
|
+
// 清理资源
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 单文件结构(仍支持)
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
// .foliko/plugins/my-plugin.js
|
|
126
|
+
const { z } = require('zod');
|
|
127
|
+
|
|
128
|
+
module.exports = function (Plugin) {
|
|
129
|
+
return class MyPlugin extends Plugin {
|
|
130
|
+
constructor(config = {}) {
|
|
131
|
+
super();
|
|
132
|
+
this.name = 'my-plugin';
|
|
133
|
+
this.version = '1.0.0';
|
|
134
|
+
this.description = '我的工具插件';
|
|
135
|
+
this.priority = 10;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
tools = {
|
|
139
|
+
my_tool: {
|
|
140
|
+
description: '我的工具',
|
|
141
|
+
inputSchema: z.object({
|
|
142
|
+
param: z.string().describe('参数描述'),
|
|
143
|
+
}),
|
|
144
|
+
execute: async (args) => {
|
|
145
|
+
return { success: true, result: args.param };
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
onStart(framework) {}
|
|
151
|
+
uninstall(framework) {}
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**注意**:如果文件夹和同名 `.js` 文件同时存在,**文件夹优先**。
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 4. 内置插件开发
|
|
161
|
+
|
|
162
|
+
内置插件需要显式引入 Plugin 基类:
|
|
163
|
+
|
|
164
|
+
```javascript
|
|
165
|
+
// plugins/my-plugin.js
|
|
166
|
+
const { Plugin } = require('../src/plugin/base');
|
|
167
|
+
const { z } = require('zod');
|
|
168
|
+
|
|
169
|
+
class MyPlugin extends Plugin {
|
|
170
|
+
constructor(config = {}) {
|
|
171
|
+
super();
|
|
172
|
+
this.name = 'my-plugin';
|
|
173
|
+
this.version = '1.0.0';
|
|
174
|
+
this.description = '我的工具插件';
|
|
175
|
+
this.priority = 10;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
tools = {
|
|
179
|
+
my_tool: {
|
|
180
|
+
description: '我的工具',
|
|
181
|
+
inputSchema: z.object({
|
|
182
|
+
param: z.string().describe('参数描述'),
|
|
183
|
+
}),
|
|
184
|
+
execute: async (args) => {
|
|
185
|
+
return { success: true, result: args.param };
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
onStart(framework) {
|
|
191
|
+
// 注册额外工具
|
|
192
|
+
this.tool.register({
|
|
193
|
+
name: 'another_tool',
|
|
194
|
+
description: '另一个工具',
|
|
195
|
+
inputSchema: z.object({}),
|
|
196
|
+
execute: async (args) => ({ success: true }),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
uninstall(framework) {}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = MyPlugin;
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## 5. 插件属性
|
|
209
|
+
|
|
210
|
+
| 属性 | 类型 | 必须 | 说明 |
|
|
211
|
+
|------|------|------|------|
|
|
212
|
+
| `name` | string | ✅ | 唯一名称 |
|
|
213
|
+
| `version` | string | ❌ | 版本号,默认 '1.0.0' |
|
|
214
|
+
| `description` | string | ❌ | 插件描述 |
|
|
215
|
+
| `priority` | number | ❌ | 加载优先级,默认 100 |
|
|
216
|
+
| `tools` | object | ❌ | 声明式工具定义 |
|
|
217
|
+
| `agents` | array | ❌ | 声明式子 Agent 定义 |
|
|
218
|
+
| `prompts` | array | ❌ | 声明式 prompt parts |
|
|
219
|
+
| `enabled` | boolean | ❌ | 是否启用,默认 true |
|
|
220
|
+
| `system` | boolean | ❌ | 是否系统插件,默认 false |
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 6. 生命周期
|
|
225
|
+
|
|
226
|
+
| 方法 | 调用时机 | 必须实现 | 说明 |
|
|
227
|
+
|------|----------|----------|------|
|
|
228
|
+
| `install(framework)` | 插件安装时 | ✅ | 初始化,获取 framework |
|
|
229
|
+
| `onStart(framework)` | start/enable/reload 时 | 推荐 | 注册工具、扩展、子代理 |
|
|
230
|
+
| `onStop()` | disable 时 | 可选 | 清理资源 |
|
|
231
|
+
| `reload(framework)` | 热重载时 | 可选 | prompts 自动清理/注册 |
|
|
232
|
+
| `uninstall(framework)` | 卸载时 | 推荐 | 清理资源 |
|
|
233
|
+
|
|
234
|
+
### 生命周期流程
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
install() → onStart() → (reload()) → onStop() → uninstall()
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### onStart / onStop(推荐方式)
|
|
241
|
+
|
|
242
|
+
**推荐使用 `onStart` 注册工具和扩展**,基类自动处理生命周期:
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
class MyPlugin extends Plugin {
|
|
246
|
+
name = 'my-plugin';
|
|
247
|
+
|
|
248
|
+
onStart(framework) {
|
|
249
|
+
// 注册 AI SDK 工具(直接调用)
|
|
250
|
+
this.tool.register({
|
|
251
|
+
name: 'hello',
|
|
252
|
+
description: '打招呼',
|
|
253
|
+
inputSchema: framework.z.object({
|
|
254
|
+
name: framework.z.string().describe('姓名')
|
|
255
|
+
}),
|
|
256
|
+
execute: async (args) => ({ message: `Hello, ${args.name}!` }),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// 注册扩展工具(通过 ext_call 调用)
|
|
260
|
+
this.extension.register({
|
|
261
|
+
name: 'greet_ext',
|
|
262
|
+
description: '打招呼扩展',
|
|
263
|
+
inputSchema: framework.z.object({
|
|
264
|
+
name: framework.z.string()
|
|
265
|
+
}),
|
|
266
|
+
execute: async (args) => ({ message: `Hi, ${args.name}!` }),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
onStop() {
|
|
271
|
+
// 清理资源(如果需要)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 生命周期自动管理
|
|
277
|
+
|
|
278
|
+
| 操作 | 效果 |
|
|
279
|
+
|------|------|
|
|
280
|
+
| `disable` | 自动移除工具/扩展(保留注册记录) |
|
|
281
|
+
| `enable` | 自动重新注册工具/扩展 |
|
|
282
|
+
| `reload` | 移除后重新注册 |
|
|
283
|
+
| `uninstall` | 移除并清空注册记录 |
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## 7. 工具注册
|
|
288
|
+
|
|
289
|
+
### 方式1:`this.tools = {}`(声明式)
|
|
290
|
+
|
|
291
|
+
插件将工具定义在 `this.tools = {}` 中,ExtensionExecutor 会自动扫描并注册:
|
|
292
|
+
|
|
293
|
+
```javascript
|
|
294
|
+
class MyPlugin extends Plugin {
|
|
295
|
+
name = 'my-plugin';
|
|
296
|
+
|
|
297
|
+
tools = {
|
|
298
|
+
my_tool: {
|
|
299
|
+
description: '我的工具',
|
|
300
|
+
inputSchema: z.object({
|
|
301
|
+
param: z.string().describe('参数描述'),
|
|
302
|
+
}),
|
|
303
|
+
execute: async (args) => {
|
|
304
|
+
return { success: true, result: args.param };
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 方式2:`this.tool.register()`(推荐)
|
|
312
|
+
|
|
313
|
+
在 `onStart` 中使用,支持完整的生命周期管理:
|
|
314
|
+
|
|
315
|
+
```javascript
|
|
316
|
+
onStart(framework) {
|
|
317
|
+
this.tool.register({
|
|
318
|
+
name: 'my_tool',
|
|
319
|
+
description: '我的工具',
|
|
320
|
+
inputSchema: framework.z.object({
|
|
321
|
+
param: framework.z.string().describe('参数描述')
|
|
322
|
+
}),
|
|
323
|
+
execute: async (args) => {
|
|
324
|
+
return { success: true, result: args.param };
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 方式3:`this.registerTool()`(旧方式)
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
install(framework) {
|
|
334
|
+
this.registerTool('my_tool', {
|
|
335
|
+
description: '我的工具',
|
|
336
|
+
inputSchema: framework.z.object({
|
|
337
|
+
param: framework.z.string().describe('参数描述')
|
|
338
|
+
}),
|
|
339
|
+
execute: async (args) => ({ success: true })
|
|
340
|
+
});
|
|
341
|
+
return this;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### 工具操作
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
// 注册
|
|
349
|
+
this.tool.register({ name: 'xxx', ... });
|
|
350
|
+
|
|
351
|
+
// 移除
|
|
352
|
+
this.tool.remove('xxx');
|
|
353
|
+
|
|
354
|
+
// 执行
|
|
355
|
+
const result = await this.tool.execute('any-tool', { arg: 'value' });
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 8. 扩展工具
|
|
361
|
+
|
|
362
|
+
扩展工具通过 `ext_call` 间接调用,适合需要额外封装的工具。
|
|
363
|
+
|
|
364
|
+
### 注册扩展工具
|
|
365
|
+
|
|
366
|
+
```javascript
|
|
367
|
+
onStart(framework) {
|
|
368
|
+
this.extension.register({
|
|
369
|
+
name: 'my_ext_tool',
|
|
370
|
+
description: '扩展工具',
|
|
371
|
+
inputSchema: framework.z.object({
|
|
372
|
+
param: framework.z.string()
|
|
373
|
+
}),
|
|
374
|
+
execute: async (args) => ({ success: true, data: args.param })
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 调用扩展工具
|
|
380
|
+
|
|
381
|
+
```javascript
|
|
382
|
+
// 通过 ext_call 调用
|
|
383
|
+
const result = await ext_call({
|
|
384
|
+
plugin: 'my-plugin',
|
|
385
|
+
tool: 'my_ext_tool',
|
|
386
|
+
args: { param: 'value' }
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### 扩展工具操作
|
|
391
|
+
|
|
392
|
+
```javascript
|
|
393
|
+
// 注册
|
|
394
|
+
this.extension.register({ name: 'xxx', ... });
|
|
395
|
+
|
|
396
|
+
// 移除
|
|
397
|
+
this.extension.remove('xxx');
|
|
398
|
+
|
|
399
|
+
// 执行(当前插件)
|
|
400
|
+
const r1 = await this.extension.execute('xxx', { arg: 'value' });
|
|
401
|
+
|
|
402
|
+
// 执行(指定插件)
|
|
403
|
+
const r2 = await this.extension.execute('plugin-name', 'xxx', { arg: 'value' });
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## 9. 子 Agent
|
|
409
|
+
|
|
410
|
+
### 配置式注册
|
|
411
|
+
|
|
412
|
+
```javascript
|
|
413
|
+
class MyPlugin extends Plugin {
|
|
414
|
+
name = 'my-plugin';
|
|
415
|
+
|
|
416
|
+
agents = [
|
|
417
|
+
{
|
|
418
|
+
name: 'code-agent',
|
|
419
|
+
role: '代码专家',
|
|
420
|
+
description: '处理代码开发任务',
|
|
421
|
+
tools: {
|
|
422
|
+
compile: {
|
|
423
|
+
description: '编译代码',
|
|
424
|
+
inputSchema: z.object({
|
|
425
|
+
language: z.string(),
|
|
426
|
+
code: z.string(),
|
|
427
|
+
}),
|
|
428
|
+
execute: async (args) => ({ success: true }),
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
parentTools: ['read_file', 'write_file'],
|
|
432
|
+
},
|
|
433
|
+
];
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### 手动注册
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
onStart(framework) {
|
|
441
|
+
this.agent.register({
|
|
442
|
+
name: 'code-agent',
|
|
443
|
+
role: '代码专家',
|
|
444
|
+
description: '处理代码开发任务',
|
|
445
|
+
tools: {
|
|
446
|
+
compile: {
|
|
447
|
+
description: '编译代码',
|
|
448
|
+
inputSchema: framework.z.object({
|
|
449
|
+
language: framework.z.string(),
|
|
450
|
+
code: framework.z.string(),
|
|
451
|
+
}),
|
|
452
|
+
execute: async (args) => ({ success: true }),
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
parentTools: ['read_file', 'write_file'],
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### 子 Agent 配置说明
|
|
461
|
+
|
|
462
|
+
| 字段 | 类型 | 说明 |
|
|
463
|
+
|------|------|------|
|
|
464
|
+
| `name` | string | 子 Agent 名称(唯一标识) |
|
|
465
|
+
| `role` | string | 角色描述 |
|
|
466
|
+
| `description` | string | 详细描述 |
|
|
467
|
+
| `tools` | object | 自定义工具,只属于此子 Agent |
|
|
468
|
+
| `parentTools` | array | 从父 Agent 继承的工具名称列表 |
|
|
469
|
+
|
|
470
|
+
### 子 Agent 操作
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
// 获取
|
|
474
|
+
const agent = this.agent.get('my-agent');
|
|
475
|
+
|
|
476
|
+
// 移除
|
|
477
|
+
this.agent.remove('my-agent');
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## 10. 工作流
|
|
483
|
+
|
|
484
|
+
### 注册工作流
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
onStart(framework) {
|
|
488
|
+
this.workflow.register('my-workflow', {
|
|
489
|
+
name: 'my-workflow',
|
|
490
|
+
description: '我的工作流',
|
|
491
|
+
inputSchema: framework.z.object({
|
|
492
|
+
msg: framework.z.string()
|
|
493
|
+
}),
|
|
494
|
+
steps: [
|
|
495
|
+
{ type: 'tool', tool: 'hello', args: { name: '{{inputs.msg}}' } },
|
|
496
|
+
{ type: 'action', code: 'return { result: inputs.msg + "!" }' }
|
|
497
|
+
]
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### 工作流操作
|
|
503
|
+
|
|
504
|
+
```javascript
|
|
505
|
+
// 执行
|
|
506
|
+
const result = await this.workflow.execute('my-workflow', { msg: 'hello' });
|
|
507
|
+
|
|
508
|
+
// 移除
|
|
509
|
+
this.workflow.remove('my-workflow');
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 11. Prompt Parts
|
|
515
|
+
|
|
516
|
+
### 配置式注册
|
|
517
|
+
|
|
518
|
+
```javascript
|
|
519
|
+
class MyPlugin extends Plugin {
|
|
520
|
+
name = 'my-plugin';
|
|
521
|
+
|
|
522
|
+
prompts = [
|
|
523
|
+
{
|
|
524
|
+
name: 'my-prompt',
|
|
525
|
+
scope: 'global', // 'global' | 'main' | agentId
|
|
526
|
+
priority: 50, // 数字越小越靠前
|
|
527
|
+
description: '我的提示词',
|
|
528
|
+
provider() {
|
|
529
|
+
return '这是动态生成的提示词内容';
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
];
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### 手动注册
|
|
537
|
+
|
|
538
|
+
```javascript
|
|
539
|
+
onStart(framework) {
|
|
540
|
+
this.prompt.register('my-prompt', () => 'Prompt content', {
|
|
541
|
+
scope: 'global',
|
|
542
|
+
priority: 50
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Prompt 操作
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
// 移除
|
|
551
|
+
this.prompt.remove('my-prompt');
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
## 12. Skill 管理
|
|
557
|
+
|
|
558
|
+
### 注册 Skill
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
onStart(framework) {
|
|
562
|
+
this.skill.register(
|
|
563
|
+
'my-skill',
|
|
564
|
+
'# 我的技能\n\n这是一个技能说明',
|
|
565
|
+
{
|
|
566
|
+
description: '技能描述',
|
|
567
|
+
tags: ['test'],
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
commands: [
|
|
571
|
+
{
|
|
572
|
+
name: 'do',
|
|
573
|
+
description: '执行操作',
|
|
574
|
+
options: [
|
|
575
|
+
{ flags: '-x <x>', description: '参数', required: true },
|
|
576
|
+
],
|
|
577
|
+
execute: async (args) => ({ x: args.x }),
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
### Skill 操作
|
|
586
|
+
|
|
587
|
+
```javascript
|
|
588
|
+
// 检查是否存在
|
|
589
|
+
this.skill.has('my-skill');
|
|
590
|
+
|
|
591
|
+
// 获取详情
|
|
592
|
+
this.skill.get('my-skill');
|
|
593
|
+
this.skill.details('my-skill');
|
|
594
|
+
|
|
595
|
+
// 列出
|
|
596
|
+
this.skill.list();
|
|
597
|
+
this.skill.listOwned(); // 本插件注册的
|
|
598
|
+
|
|
599
|
+
// 执行
|
|
600
|
+
await this.skill.execute('my-skill:do', { x: 1 });
|
|
601
|
+
|
|
602
|
+
// 移除
|
|
603
|
+
this.skill.remove('my-skill');
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## 13. 依赖管理
|
|
609
|
+
|
|
610
|
+
### 依赖安装位置
|
|
611
|
+
|
|
612
|
+
| 插件类型 | 安装位置 | 说明 |
|
|
613
|
+
|----------|----------|------|
|
|
614
|
+
| **文件夹插件** | `.foliko/plugins/插件名/node_modules/` | 插件自包含,可独立迁移 |
|
|
615
|
+
| **单文件插件** | `.foliko/node_modules/` | 共享依赖 |
|
|
616
|
+
|
|
617
|
+
### 安装工具
|
|
618
|
+
|
|
619
|
+
```javascript
|
|
620
|
+
// 安装到默认位置 .foliko/node_modules
|
|
621
|
+
install({ package: 'zod' });
|
|
622
|
+
|
|
623
|
+
// 安装到指定目录(文件夹插件)
|
|
624
|
+
install({ package: 'axios', path: '.foliko/plugins/my-plugin' });
|
|
625
|
+
|
|
626
|
+
// 从 package.json 安装
|
|
627
|
+
install({ file: './my-plugin/package.json', path: '.foliko/plugins/my-plugin' });
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### 重要依赖版本要求
|
|
631
|
+
|
|
632
|
+
**zod 必须使用 `"zod": "^3.25.76"` 版本**
|
|
633
|
+
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## 14. 系统插件访问
|
|
637
|
+
|
|
638
|
+
### 可访问的系统插件
|
|
639
|
+
|
|
640
|
+
| 插件名 | 说明 | 可用方法 |
|
|
641
|
+
|--------|------|----------|
|
|
642
|
+
| `storage` | 键值对存储 | `get(key)`, `set(key, value)`, `delete(key)` |
|
|
643
|
+
| `session` | 会话管理 | `getSession(id)`, `getHistory(id)` |
|
|
644
|
+
| `audit` | 审计日志 | `log(type, data)` |
|
|
645
|
+
| `scheduler` | 定时任务 | `addTask()`, `removeTask()` |
|
|
646
|
+
| `file-system` | 文件操作 | 工具已注册: `read_file`, `write_file` |
|
|
647
|
+
| `tools` | 工具管理 | `list()`, `reload()` |
|
|
648
|
+
| `ai` | AI 配置 | `getAIClient()` |
|
|
649
|
+
|
|
650
|
+
### 访问示例
|
|
651
|
+
|
|
652
|
+
```javascript
|
|
653
|
+
install(framework) {
|
|
654
|
+
// 访问 storage 插件
|
|
655
|
+
const storage = this._framework.pluginManager.get('storage');
|
|
656
|
+
|
|
657
|
+
// 访问 session 插件
|
|
658
|
+
const session = this._framework.pluginManager.get('session');
|
|
659
|
+
|
|
660
|
+
// 访问 audit 插件
|
|
661
|
+
const audit = this._framework.pluginManager.get('audit');
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Storage 插件 API
|
|
666
|
+
|
|
667
|
+
```javascript
|
|
668
|
+
const storage = this._framework.pluginManager.get('storage');
|
|
669
|
+
|
|
670
|
+
// 方式1:通过工具调用(推荐)
|
|
671
|
+
// storage_get({ key: 'xxx', namespace: 'yyy' })
|
|
672
|
+
// storage_set({ key: 'xxx', value: {...}, namespace: 'yyy' })
|
|
673
|
+
|
|
674
|
+
// 方式2:直接调用内部方法
|
|
675
|
+
const value = storage.getStore().get('key');
|
|
676
|
+
storage.setDirect('key', value);
|
|
677
|
+
storage.deleteDirect('key');
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### Session 插件 API
|
|
681
|
+
|
|
682
|
+
```javascript
|
|
683
|
+
const session = this._framework.pluginManager.get('session');
|
|
684
|
+
|
|
685
|
+
// 获取会话
|
|
686
|
+
const sess = session.getSession(sessionId);
|
|
687
|
+
|
|
688
|
+
// 获取历史消息
|
|
689
|
+
const messages = session.getHistory(sessionId);
|
|
690
|
+
|
|
691
|
+
// 添加消息
|
|
692
|
+
session.addMessage(sessionId, { role: 'user', content: 'hello' });
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## 15. 最佳实践
|
|
698
|
+
|
|
699
|
+
### 核心规则
|
|
700
|
+
|
|
701
|
+
| 规则 | 说明 |
|
|
702
|
+
|------|------|
|
|
703
|
+
| **优先使用 this.tools = {}** | ExtensionExecutor 自动扫描并注册到系统提示词 |
|
|
704
|
+
| **优先使用文件夹结构** | 复杂插件推荐用文件夹,便于管理依赖和代码 |
|
|
705
|
+
| **必须用 inputSchema** | `inputSchema: z.object({})`(不是 parameters!) |
|
|
706
|
+
| **必须用 zod 定义参数** | `param: z.string().describe('描述')` |
|
|
707
|
+
| **install 必须返回 this** | 确保链式调用 |
|
|
708
|
+
| **文件夹与文件重名时** | 文件夹优先,同名 `.js` 文件会被忽略 |
|
|
709
|
+
| **工具自动出现在系统提示词** | 定义在 `this.tools` 中会自动注册 |
|
|
710
|
+
|
|
711
|
+
### 开发流程
|
|
712
|
+
|
|
713
|
+
1. **创建插件**(优先使用文件夹结构)
|
|
714
|
+
2. **安装依赖**:`install { package: "包名", path: ".foliko/plugins/插件名" }`
|
|
715
|
+
3. **热重载**:`reload_plugins`
|
|
716
|
+
4. **检查加载状态**
|
|
717
|
+
|
|
718
|
+
### 引入第三方库
|
|
719
|
+
|
|
720
|
+
**必须先安装依赖,再热重载!**
|
|
721
|
+
|
|
722
|
+
```javascript
|
|
723
|
+
// .foliko/plugins/my-plugin/index.js
|
|
724
|
+
const { z } = require('zod');
|
|
725
|
+
|
|
726
|
+
module.exports = function (Plugin) {
|
|
727
|
+
return class MyPlugin extends Plugin {
|
|
728
|
+
name = 'my-plugin';
|
|
729
|
+
|
|
730
|
+
tools = {
|
|
731
|
+
fetch_data: {
|
|
732
|
+
description: '获取远程数据',
|
|
733
|
+
inputSchema: z.object({
|
|
734
|
+
url: z.string().describe('API URL'),
|
|
735
|
+
}),
|
|
736
|
+
execute: async (args) => {
|
|
737
|
+
// 安装到插件自己的目录
|
|
738
|
+
await this._framework.callTool('install', {
|
|
739
|
+
package: 'axios',
|
|
740
|
+
path: '.foliko/plugins/my-plugin',
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// 引入插件目录下的 node_modules
|
|
744
|
+
const axios = require('.foliko/plugins/my-plugin/node_modules/axios');
|
|
745
|
+
const response = await axios.get(args.url);
|
|
746
|
+
return { success: true, data: response.data };
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
};
|
|
751
|
+
};
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### 错误处理
|
|
755
|
+
|
|
756
|
+
```javascript
|
|
757
|
+
onStart(framework) {
|
|
758
|
+
try {
|
|
759
|
+
this.tool.register({ ... });
|
|
760
|
+
} catch (error) {
|
|
761
|
+
framework.logger.error(`[${this.name}] Failed to register tool: ${error.message}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
uninstall(framework) {
|
|
766
|
+
// 确保清理所有资源
|
|
767
|
+
this.tool.remove('my_tool');
|
|
768
|
+
this.extension.remove('my_ext');
|
|
769
|
+
this.agent.remove('my-agent');
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 示例插件
|
|
776
|
+
|
|
777
|
+
### 完整示例:天气插件
|
|
778
|
+
|
|
779
|
+
```javascript
|
|
780
|
+
// .foliko/plugins/weather/index.js
|
|
781
|
+
const { z } = require('zod');
|
|
782
|
+
|
|
783
|
+
module.exports = function (Plugin) {
|
|
784
|
+
return class WeatherPlugin extends Plugin {
|
|
785
|
+
constructor() {
|
|
786
|
+
super();
|
|
787
|
+
this.name = 'weather';
|
|
788
|
+
this.version = '1.0.0';
|
|
789
|
+
this.description = '天气查询插件';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
tools = {
|
|
793
|
+
get_weather: {
|
|
794
|
+
description: '查询指定城市的天气',
|
|
795
|
+
inputSchema: z.object({
|
|
796
|
+
city: z.string().describe('城市名称'),
|
|
797
|
+
units: z.enum(['celsius', 'fahrenheit']).optional().describe('温度单位,默认 celsius'),
|
|
798
|
+
}),
|
|
799
|
+
execute: async (args) => {
|
|
800
|
+
// 这里应该是真实的 API 调用
|
|
801
|
+
return {
|
|
802
|
+
city: args.city,
|
|
803
|
+
temperature: 25,
|
|
804
|
+
units: args.units || 'celsius',
|
|
805
|
+
condition: 'Sunny',
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
onStart(framework) {
|
|
812
|
+
// 注册额外的工具
|
|
813
|
+
this.tool.register({
|
|
814
|
+
name: 'get_forecast',
|
|
815
|
+
description: '获取天气预报',
|
|
816
|
+
inputSchema: framework.z.object({
|
|
817
|
+
city: framework.z.string().describe('城市名称'),
|
|
818
|
+
days: framework.z.number().min(1).max(7).default(3).describe('预报天数'),
|
|
819
|
+
}),
|
|
820
|
+
execute: async (args) => {
|
|
821
|
+
return {
|
|
822
|
+
city: args.city,
|
|
823
|
+
forecast: Array.from({ length: args.days }, (_, i) => ({
|
|
824
|
+
day: i + 1,
|
|
825
|
+
temp: 20 + Math.floor(Math.random() * 10),
|
|
826
|
+
condition: ['Sunny', 'Cloudy', 'Rainy'][Math.floor(Math.random() * 3)],
|
|
827
|
+
})),
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
onStop() {
|
|
834
|
+
// 清理资源
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
uninstall(framework) {
|
|
838
|
+
// 完全清理
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
};
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## 相关文档
|
|
847
|
+
|
|
848
|
+
- [扩展系统](./extensions.md) - ext_call、ext_skill 统一调用
|
|
849
|
+
- [Public API](./public-api.md) - 框架公共 API 参考
|
|
850
|
+
- [架构文档](./architecture.md) - 系统架构设计
|
package/package.json
CHANGED
package/src/agent/chat.js
CHANGED
|
@@ -112,6 +112,9 @@ class AgentChatHandler extends EventEmitter {
|
|
|
112
112
|
this._toolsTokensCacheVersion = 0;
|
|
113
113
|
this._systemPromptTokensCache = null;
|
|
114
114
|
this._systemPromptTokensCacheKey = null;
|
|
115
|
+
// 消息 token 缓存(避免重复计算同一条消息)
|
|
116
|
+
this._messageTokenCache = new Map();
|
|
117
|
+
this._messageTokenCacheMaxSize = 200;
|
|
115
118
|
|
|
116
119
|
// ChatQueueManager: 队列管理
|
|
117
120
|
this.queueManager = new ChatQueueManager({
|
|
@@ -1028,7 +1031,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1028
1031
|
const totalTokens = cachedMsgTokens + toolsTokens + systemPromptTokens;
|
|
1029
1032
|
const limit = this._maxContextTokens * SIMPLE_COMPRESS_TOKEN_RATIO;
|
|
1030
1033
|
|
|
1031
|
-
|
|
1034
|
+
//gger.info(`[_prepareSession] BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}, threshold=${this._compressionMessageThreshold}`);
|
|
1032
1035
|
|
|
1033
1036
|
if (totalTokens > limit || messages.length > this._compressionMessageThreshold) {
|
|
1034
1037
|
logger.info(
|
|
@@ -1117,8 +1120,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1117
1120
|
|
|
1118
1121
|
for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
|
|
1119
1122
|
const msg = messages[msgIdx];
|
|
1120
|
-
|
|
1121
|
-
// ★ assistant 消息:content 数组格式的 tool-call
|
|
1122
1123
|
if (msg.role === 'assistant' && Array.isArray(msg.content)) {
|
|
1123
1124
|
for (let itemIdx = 0; itemIdx < msg.content.length; itemIdx++) {
|
|
1124
1125
|
const item = msg.content[itemIdx];
|
|
@@ -1126,58 +1127,31 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1126
1127
|
if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
|
|
1127
1128
|
const id = item.toolCallId;
|
|
1128
1129
|
if (firstSeen.has(id)) {
|
|
1130
|
+
// 冲突:生成唯一新 ID(不依赖全局计数器,避免跨调用泄露)
|
|
1129
1131
|
const newId = `${id}_dup${msgIdx}_${itemIdx}`;
|
|
1130
1132
|
item.toolCallId = newId;
|
|
1133
|
+
// 同步修改紧随其后的 tool-result
|
|
1131
1134
|
const renamed = this._renameFollowingToolResult(messages, msgIdx, id, newId);
|
|
1132
1135
|
fixedCount += 1 + (renamed ? 1 : 0);
|
|
1136
|
+
// 不更新 firstSeen(原始 ID 已被第一次声明占用)
|
|
1133
1137
|
} else {
|
|
1134
1138
|
firstSeen.set(id, { msgIdx, itemIdx, kind: 'call' });
|
|
1135
1139
|
}
|
|
1136
1140
|
}
|
|
1137
1141
|
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
// ★ assistant 消息:tool_calls 数组格式(OpenAI 格式)
|
|
1141
|
-
if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
|
|
1142
|
-
for (let itemIdx = 0; itemIdx < msg.tool_calls.length; itemIdx++) {
|
|
1143
|
-
const tc = msg.tool_calls[itemIdx];
|
|
1144
|
-
if (!tc || !tc.id) continue;
|
|
1145
|
-
const id = tc.id;
|
|
1146
|
-
if (firstSeen.has(id)) {
|
|
1147
|
-
const newId = `${id}_dup${msgIdx}_${itemIdx}`;
|
|
1148
|
-
tc.id = newId;
|
|
1149
|
-
const renamed = this._renameFollowingToolResult(messages, msgIdx, id, newId);
|
|
1150
|
-
fixedCount += 1 + (renamed ? 1 : 0);
|
|
1151
|
-
} else {
|
|
1152
|
-
firstSeen.set(id, { msgIdx, itemIdx, kind: 'call' });
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// ★ tool 消息:content 数组格式(AI SDK 格式)
|
|
1158
|
-
// 注意:tool-result 与 assistant tool-call 配对使用同一 ID 是正常的,
|
|
1159
|
-
// 此处只记录第一次出现的 ID,不做重命名(重命名由 _renameFollowingToolResult 在上面的 assistant 分支处理)
|
|
1160
|
-
if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
1142
|
+
} else if (msg.role === 'tool' && Array.isArray(msg.content)) {
|
|
1161
1143
|
for (let itemIdx = 0; itemIdx < msg.content.length; itemIdx++) {
|
|
1162
1144
|
const item = msg.content[itemIdx];
|
|
1163
1145
|
if (!item) continue;
|
|
1164
1146
|
if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId) {
|
|
1165
1147
|
const id = item.toolCallId;
|
|
1148
|
+
// 记录 tool-result 第一次出现(不与 call 比较,因为可能已被上面的分支处理过)
|
|
1166
1149
|
if (!firstSeen.has(id)) {
|
|
1167
1150
|
firstSeen.set(id, { msgIdx, itemIdx, kind: 'result' });
|
|
1168
1151
|
}
|
|
1169
1152
|
}
|
|
1170
1153
|
}
|
|
1171
1154
|
}
|
|
1172
|
-
|
|
1173
|
-
// ★ tool 消息:tool_call_id 字符串格式(OpenAI 格式)
|
|
1174
|
-
// 同样只记录第一次出现,不做重命名
|
|
1175
|
-
if (msg.role === 'tool' && msg.tool_call_id && !Array.isArray(msg.content)) {
|
|
1176
|
-
const id = msg.tool_call_id;
|
|
1177
|
-
if (!firstSeen.has(id)) {
|
|
1178
|
-
firstSeen.set(id, { msgIdx, itemIdx: -1, kind: 'result' });
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
1155
|
}
|
|
1182
1156
|
return fixedCount;
|
|
1183
1157
|
}
|
|
@@ -1191,22 +1165,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1191
1165
|
for (let i = afterMsgIdx + 1; i < messages.length; i++) {
|
|
1192
1166
|
const m = messages[i];
|
|
1193
1167
|
if (m.role === 'assistant') return false;
|
|
1194
|
-
if (m.role === 'tool') {
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
item.toolCallId = newId;
|
|
1201
|
-
return true;
|
|
1202
|
-
}
|
|
1168
|
+
if (m.role === 'tool' && Array.isArray(m.content)) {
|
|
1169
|
+
for (const item of m.content) {
|
|
1170
|
+
if (!item) continue;
|
|
1171
|
+
if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId === oldId) {
|
|
1172
|
+
item.toolCallId = newId;
|
|
1173
|
+
return true;
|
|
1203
1174
|
}
|
|
1204
1175
|
}
|
|
1205
|
-
// OpenAI 字符串格式
|
|
1206
|
-
if (m.tool_call_id === oldId) {
|
|
1207
|
-
m.tool_call_id = newId;
|
|
1208
|
-
return true;
|
|
1209
|
-
}
|
|
1210
1176
|
}
|
|
1211
1177
|
}
|
|
1212
1178
|
return false;
|
|
@@ -1397,11 +1363,10 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1397
1363
|
* @private
|
|
1398
1364
|
*/
|
|
1399
1365
|
_createPrepareStep() {
|
|
1400
|
-
const self = this;
|
|
1401
1366
|
return async ({ stepNumber, messages: inputMessages }) => {
|
|
1402
1367
|
try {
|
|
1403
|
-
const tokenCount =
|
|
1404
|
-
const tokenLimit =
|
|
1368
|
+
const tokenCount = this._countMessagesTokens(inputMessages);
|
|
1369
|
+
const tokenLimit = this._maxContextTokens * SMART_COMPRESS_TOKEN_RATIO;
|
|
1405
1370
|
|
|
1406
1371
|
// 超过限制时,保留三分之一的消息
|
|
1407
1372
|
if (tokenCount > tokenLimit) {
|
|
@@ -1419,8 +1384,8 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1419
1384
|
// 所以不再需要 _stripStaleToolCalls 提前清理
|
|
1420
1385
|
|
|
1421
1386
|
// 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
|
|
1422
|
-
const repairedCount =
|
|
1423
|
-
|
|
1387
|
+
const repairedCount = this._repairInvalidToolCallsInMessages(inputMessages);
|
|
1388
|
+
this._repairAndPrefixHistoricalIds(inputMessages);
|
|
1424
1389
|
|
|
1425
1390
|
// ★ 防御性 sanitize:确保所有 message 符合 AI SDK ModelMessage[] schema
|
|
1426
1391
|
// 每次 LLM step 前都跑一次(messages 数组里可能有 AI SDK 上一步加进来的)
|
package/src/agent/tool-loop.js
CHANGED
|
@@ -183,13 +183,6 @@ class ToolLoop extends EventEmitter {
|
|
|
183
183
|
const api = [];
|
|
184
184
|
if (systemPrompt) api.push({ role: 'system', content: systemPrompt });
|
|
185
185
|
|
|
186
|
-
// ★ 单遍扫描:跟踪"已遇到过的 assistant 中的 tool_call_id",
|
|
187
|
-
// tool-result 只能匹配其之前的 assistant,不能匹配其之后的。
|
|
188
|
-
// 防止历史会话中的 orphaned tool-result 被错误地保留。
|
|
189
|
-
const seenAssistantToolCallIds = new Set();
|
|
190
|
-
// ★ 追踪已输出的 tool_call_id,防止重复
|
|
191
|
-
const seenOutputToolCallIds = new Set();
|
|
192
|
-
|
|
193
186
|
for (const msg of messages) {
|
|
194
187
|
if (msg.role === 'system') {
|
|
195
188
|
api.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' });
|
|
@@ -201,40 +194,13 @@ class ToolLoop extends EventEmitter {
|
|
|
201
194
|
api.push({ role: 'user', content: text || '' });
|
|
202
195
|
}
|
|
203
196
|
} else if (msg.role === 'assistant') {
|
|
204
|
-
// ★
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
for (const tc of msg.tool_calls) {
|
|
208
|
-
if (tc.id) currentToolCallIds.add(tc.id);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (Array.isArray(msg.content)) {
|
|
212
|
-
for (const part of msg.content) {
|
|
213
|
-
if (part && (part.type === 'tool-call' || part.type === 'tool-use') && part.toolCallId) {
|
|
214
|
-
currentToolCallIds.add(part.toolCallId);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// ★ 将这些 ID 加入 seenAssistantToolCallIds,之后的 tool-result 才能匹配
|
|
219
|
-
for (const id of currentToolCallIds) {
|
|
220
|
-
seenAssistantToolCallIds.add(id);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ★ 检测是否有 reasoning 内容(thinking mode 必须携带)
|
|
224
|
-
const hasReasoningInContent = Array.isArray(msg.content) && msg.content.some(p => p && p.type === 'reasoning');
|
|
225
|
-
const hasReasoningTopLevel = msg.reasoning_content || msg.reasoning;
|
|
197
|
+
// ★ 提取 text + tool_calls(兼容 content=string、content=array、顶层 tool_calls 三种格式)
|
|
198
|
+
let text = '';
|
|
199
|
+
const toolCalls = [];
|
|
226
200
|
|
|
227
201
|
if (typeof msg.content === 'string') {
|
|
228
|
-
|
|
229
|
-
// thinking mode 下,无 reasoning 则自动补 placeholder
|
|
230
|
-
const reasonVal = hasReasoningTopLevel || (this._thinkingMode ? '...' : '');
|
|
231
|
-
api.push({ role: 'assistant', content: msg.content || null, tool_calls: msg.tool_calls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
|
|
232
|
-
} else {
|
|
233
|
-
api.push({ role: 'assistant', content: msg.content });
|
|
234
|
-
}
|
|
202
|
+
text = msg.content;
|
|
235
203
|
} else if (Array.isArray(msg.content)) {
|
|
236
|
-
let text = '';
|
|
237
|
-
const toolCalls = [];
|
|
238
204
|
for (const part of msg.content) {
|
|
239
205
|
if (!part) continue;
|
|
240
206
|
if (part.type === 'text') text += part.text;
|
|
@@ -249,50 +215,39 @@ class ToolLoop extends EventEmitter {
|
|
|
249
215
|
});
|
|
250
216
|
}
|
|
251
217
|
}
|
|
218
|
+
}
|
|
252
219
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
220
|
+
// ★ 合并顶层 tool_calls(OpenAI 格式,来自 session 文件)
|
|
221
|
+
const existingIds = new Set(toolCalls.map(tc => tc.id));
|
|
222
|
+
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
223
|
+
for (const tc of msg.tool_calls) {
|
|
224
|
+
if (!existingIds.has(tc.id)) toolCalls.push(tc);
|
|
225
|
+
existingIds.add(tc.id);
|
|
258
226
|
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (toolCalls.length > 0) {
|
|
230
|
+
// ★ 核心:thinking mode 下,所有带 tool_calls 的消息都必须有 reasoning_content
|
|
231
|
+
// 无条件注入,不依赖 hasReasoningTopLevel 检测
|
|
232
|
+
const reasonVal = msg.reasoning_content || msg.reasoning || (this._thinkingMode ? '...' : '');
|
|
233
|
+
api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
|
|
259
234
|
} else {
|
|
260
|
-
api.push({ role: 'assistant', content:
|
|
235
|
+
api.push({ role: 'assistant', content: text || '' });
|
|
261
236
|
}
|
|
262
237
|
} else if (msg.role === 'tool') {
|
|
263
238
|
if (Array.isArray(msg.content)) {
|
|
264
239
|
for (const part of msg.content) {
|
|
265
240
|
if (!part || (part.type !== 'tool-result' && part.type !== 'tool_result')) continue;
|
|
266
|
-
const id = part.toolCallId;
|
|
267
|
-
// ★ 过滤:tool-result 必须匹配已出现过的 assistant 中的 tool_call_id
|
|
268
|
-
if (!seenAssistantToolCallIds.has(id)) {
|
|
269
|
-
log.warn(`[ToolLoop] 过滤无效 tool-result(顺序/无匹配): toolCallId=${id}`);
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
// ★ 过滤重复的 tool_call_id(同一 ID 多次出现)
|
|
273
|
-
if (seenOutputToolCallIds.has(id)) {
|
|
274
|
-
log.warn(`[ToolLoop] 跳过重复 tool-result: toolCallId=${id}`);
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
seenOutputToolCallIds.add(id);
|
|
278
241
|
const output = part.output;
|
|
279
242
|
let content;
|
|
280
243
|
if (output?.type === 'text') content = output.value;
|
|
281
244
|
else if (output?.type === 'json') content = JSON.stringify(output.value);
|
|
282
245
|
else content = typeof output === 'string' ? output : JSON.stringify(output);
|
|
283
|
-
api.push({ role: 'tool', tool_call_id:
|
|
246
|
+
api.push({ role: 'tool', tool_call_id: part.toolCallId, content });
|
|
284
247
|
}
|
|
285
248
|
} else if (msg.tool_call_id && typeof msg.content === 'string') {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
if (!seenAssistantToolCallIds.has(id)) {
|
|
289
|
-
log.warn(`[ToolLoop] 过滤无效 tool-result(顺序/无匹配): tool_call_id=${id}`);
|
|
290
|
-
} else if (seenOutputToolCallIds.has(id)) {
|
|
291
|
-
log.warn(`[ToolLoop] 跳过重复 tool-result: tool_call_id=${id}`);
|
|
292
|
-
} else {
|
|
293
|
-
seenOutputToolCallIds.add(id);
|
|
294
|
-
api.push(msg);
|
|
295
|
-
}
|
|
249
|
+
// 已经是 OpenAI 格式(streaming 完成后的消息)
|
|
250
|
+
api.push(msg);
|
|
296
251
|
}
|
|
297
252
|
}
|
|
298
253
|
}
|
|
@@ -494,12 +449,6 @@ class ToolLoop extends EventEmitter {
|
|
|
494
449
|
|
|
495
450
|
let response;
|
|
496
451
|
try {
|
|
497
|
-
// ★ DEBUG: 记录所有 tool 消息的 ID
|
|
498
|
-
for (const m of apiMessages) {
|
|
499
|
-
if (m.role === 'tool') {
|
|
500
|
-
log.warn(`[ToolLoop] [DEBUG] 发送 tool-result: tool_call_id=${m.tool_call_id}, content_len=${(m.content||'').length}`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
452
|
response = await client.chat.completions.create({
|
|
504
453
|
model,
|
|
505
454
|
messages: apiMessages,
|
|
@@ -597,13 +546,6 @@ class ToolLoop extends EventEmitter {
|
|
|
597
546
|
|
|
598
547
|
let stream;
|
|
599
548
|
try {
|
|
600
|
-
// ★ DEBUG: 记录所有 tool 消息的 ID
|
|
601
|
-
for (const m of apiMessages) {
|
|
602
|
-
if (m.role === 'tool') {
|
|
603
|
-
log.warn(`[ToolLoop] [DEBUG] 发送 tool-result: tool_call_id=${m.tool_call_id}, content_len=${(m.content||'').length}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
549
|
stream = await client.chat.completions.create({
|
|
608
550
|
model,
|
|
609
551
|
messages: apiMessages,
|
package/src/common/constants.js
CHANGED
|
@@ -24,7 +24,7 @@ const DEFAULT_MAX_OUTPUT_TOKENS = 8192;
|
|
|
24
24
|
const DEFAULT_TEMPERATURE = 0.3;
|
|
25
25
|
|
|
26
26
|
/** 默认 Max Steps(工具调用最大轮数) */
|
|
27
|
-
const DEFAULT_MAX_STEPS =
|
|
27
|
+
const DEFAULT_MAX_STEPS = 20;
|
|
28
28
|
|
|
29
29
|
/** 需要禁用 temperature 的 thinking mode 模型列表 */
|
|
30
30
|
const THINKING_MODELS = [
|
|
@@ -54,7 +54,7 @@ const DEFAULT_MAX_MESSAGES_PER_SESSION = 1000;
|
|
|
54
54
|
const KEEP_RECENT_MESSAGES = 20;
|
|
55
55
|
|
|
56
56
|
/** 压缩消息数量阈值 */
|
|
57
|
-
const COMPRESSION_MESSAGE_THRESHOLD =
|
|
57
|
+
const COMPRESSION_MESSAGE_THRESHOLD = 150;
|
|
58
58
|
|
|
59
59
|
/** 智能压缩启用 */
|
|
60
60
|
const ENABLE_SMART_COMPRESS = true;
|
package/src/llm/tokens.js
CHANGED
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
* Ported from pi's compaction/compaction.ts with enhancements
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
// 全局 token 缓存(避免重复计算相同内容)
|
|
9
|
+
const _tokenCache = new Map();
|
|
10
|
+
const _TOKEN_CACHE_MAX_SIZE = 500;
|
|
11
|
+
|
|
12
|
+
function _getCacheKey(message) {
|
|
13
|
+
if (message.id) return message.id;
|
|
14
|
+
if (message.role === 'tool') return `tool:${message.tool_call_id || message.toolName}`;
|
|
15
|
+
return `${message.role}:${JSON.stringify(message.content || '').slice(0, 100)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
function encode(text, bytesPerToken = 4) {
|
|
9
19
|
if (!text) return 0;
|
|
10
20
|
const bytes = Buffer.byteLength(String(text), 'utf8');
|
|
@@ -26,6 +36,12 @@ function safeJsonStringify(value) {
|
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
function estimateTokens(message) {
|
|
39
|
+
// 性能优化:使用缓存避免重复计算
|
|
40
|
+
const cacheKey = _getCacheKey(message);
|
|
41
|
+
if (_tokenCache.has(cacheKey)) {
|
|
42
|
+
return _tokenCache.get(cacheKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
let chars = 0;
|
|
30
46
|
|
|
31
47
|
switch (message.role) {
|
|
@@ -40,7 +56,9 @@ function estimateTokens(message) {
|
|
|
40
56
|
}
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
|
-
|
|
59
|
+
const result = Math.ceil(chars / 4);
|
|
60
|
+
_updateCache(cacheKey, result);
|
|
61
|
+
return result;
|
|
44
62
|
}
|
|
45
63
|
case 'assistant': {
|
|
46
64
|
const assistant = message;
|
|
@@ -53,7 +71,9 @@ function estimateTokens(message) {
|
|
|
53
71
|
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
54
72
|
}
|
|
55
73
|
}
|
|
56
|
-
|
|
74
|
+
const result = Math.ceil(chars / 4);
|
|
75
|
+
_updateCache(cacheKey, result);
|
|
76
|
+
return result;
|
|
57
77
|
}
|
|
58
78
|
case 'custom':
|
|
59
79
|
case 'toolResult': {
|
|
@@ -69,22 +89,40 @@ function estimateTokens(message) {
|
|
|
69
89
|
}
|
|
70
90
|
}
|
|
71
91
|
}
|
|
72
|
-
|
|
92
|
+
const result = Math.ceil(chars / 4);
|
|
93
|
+
_updateCache(cacheKey, result);
|
|
94
|
+
return result;
|
|
73
95
|
}
|
|
74
96
|
case 'bashExecution': {
|
|
75
97
|
chars = message.command.length + (message.output?.length || 0);
|
|
76
|
-
|
|
98
|
+
const result = Math.ceil(chars / 4);
|
|
99
|
+
_updateCache(cacheKey, result);
|
|
100
|
+
return result;
|
|
77
101
|
}
|
|
78
102
|
case 'branchSummary':
|
|
79
103
|
case 'compactionSummary': {
|
|
80
104
|
chars = message.summary.length;
|
|
81
|
-
|
|
105
|
+
const result = Math.ceil(chars / 4);
|
|
106
|
+
_updateCache(cacheKey, result);
|
|
107
|
+
return result;
|
|
82
108
|
}
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
return 0;
|
|
86
112
|
}
|
|
87
113
|
|
|
114
|
+
function _updateCache(key, value) {
|
|
115
|
+
if (_tokenCache.size >= _TOKEN_CACHE_MAX_SIZE) {
|
|
116
|
+
// 删除最旧的 20%
|
|
117
|
+
const keysToDelete = Math.floor(_TOKEN_CACHE_MAX_SIZE * 0.2);
|
|
118
|
+
const iterator = _tokenCache.keys();
|
|
119
|
+
for (let i = 0; i < keysToDelete; i++) {
|
|
120
|
+
_tokenCache.delete(iterator.next().value);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
_tokenCache.set(key, value);
|
|
124
|
+
}
|
|
125
|
+
|
|
88
126
|
function calculateContextTokens(usage) {
|
|
89
127
|
return usage.totalTokens || (usage.input || 0) + (usage.output || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
|
|
90
128
|
}
|
package/src/session/session.js
CHANGED
|
@@ -123,6 +123,11 @@ class SessionManager {
|
|
|
123
123
|
_loadEntriesFromFile(filePath) {
|
|
124
124
|
if (!fs.existsSync(filePath)) return [];
|
|
125
125
|
|
|
126
|
+
// 性能优化:使用流式读取大文件,减少内存占用
|
|
127
|
+
const stats = fs.statSync(filePath);
|
|
128
|
+
if (stats.size > 5 * 1024 * 1024) { // > 5MB 使用流
|
|
129
|
+
return this._loadEntriesFromFileStream(filePath);
|
|
130
|
+
}
|
|
126
131
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
127
132
|
const entries = [];
|
|
128
133
|
const lines = content.trim().split('\n');
|
|
@@ -152,6 +157,28 @@ class SessionManager {
|
|
|
152
157
|
return entries;
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
async _loadEntriesFromFileStream(filePath) {
|
|
161
|
+
// 性能优化:流式读取大文件
|
|
162
|
+
const entries = [];
|
|
163
|
+
const seenIds = new Set();
|
|
164
|
+
const readline = require('readline');
|
|
165
|
+
const rl = readline.createInterface({
|
|
166
|
+
input: fs.createReadStream(filePath),
|
|
167
|
+
crlfDelay: Infinity
|
|
168
|
+
});
|
|
169
|
+
for await (const line of rl) {
|
|
170
|
+
if (!line.trim()) continue;
|
|
171
|
+
try {
|
|
172
|
+
const entry = JSON.parse(line);
|
|
173
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
174
|
+
if (entry.id && seenIds.has(entry.id)) continue;
|
|
175
|
+
if (entry.id) seenIds.add(entry.id);
|
|
176
|
+
entries.push(entry);
|
|
177
|
+
} catch { /* skip */ }
|
|
178
|
+
}
|
|
179
|
+
return entries;
|
|
180
|
+
}
|
|
181
|
+
|
|
155
182
|
_buildIndex() {
|
|
156
183
|
this.byId.clear();
|
|
157
184
|
this.labelsById.clear();
|
|
@@ -175,7 +202,8 @@ class SessionManager {
|
|
|
175
202
|
|
|
176
203
|
_rewriteFile() {
|
|
177
204
|
if (!this.persist || !this.sessionFile) return;
|
|
178
|
-
|
|
205
|
+
// 性能优化:单次写入代替多次追加
|
|
206
|
+
const content = this.fileEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
179
207
|
fs.writeFileSync(this.sessionFile, content);
|
|
180
208
|
}
|
|
181
209
|
|
|
@@ -190,10 +218,10 @@ class SessionManager {
|
|
|
190
218
|
return;
|
|
191
219
|
}
|
|
192
220
|
|
|
221
|
+
// 性能优化:使用批量写入代替多次追加
|
|
193
222
|
if (!this.flushed) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
223
|
+
const batch = this.fileEntries.map(e => `${JSON.stringify(e)}\n`).join('');
|
|
224
|
+
fs.writeFileSync(this.sessionFile, batch);
|
|
197
225
|
this.flushed = true;
|
|
198
226
|
} else {
|
|
199
227
|
fs.appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
|
package/src/storage/jsonl.js
CHANGED
|
@@ -136,7 +136,8 @@ class JsonlSessionStorage {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
static async _loadJsonlStorage(fsModule, filePath) {
|
|
139
|
-
|
|
139
|
+
// 性能优化:使用异步读取,避免阻塞事件循环
|
|
140
|
+
const content = await fsModule.promises.readFile(filePath, 'utf-8');
|
|
140
141
|
const lines = content.split('\n').filter(line => line.trim());
|
|
141
142
|
if (lines.length === 0) {
|
|
142
143
|
throw invalidSession(filePath, 'missing session header');
|
|
@@ -195,7 +196,7 @@ class JsonlSessionStorage {
|
|
|
195
196
|
timestamp: new Date().toISOString(),
|
|
196
197
|
targetId: leafId
|
|
197
198
|
};
|
|
198
|
-
fs.
|
|
199
|
+
await this.fs.promises.appendFile(this.filePath, JSON.stringify(entry) + '\n');
|
|
199
200
|
this.entries.push(entry);
|
|
200
201
|
this.byId.set(entry.id, entry);
|
|
201
202
|
this.currentLeafId = leafId;
|
|
@@ -206,7 +207,8 @@ class JsonlSessionStorage {
|
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
async appendEntry(entry) {
|
|
209
|
-
|
|
210
|
+
// 性能优化:使用异步追加,减少阻塞时间
|
|
211
|
+
await this.fs.promises.appendFile(this.filePath, JSON.stringify(entry) + '\n');
|
|
210
212
|
this.entries.push(entry);
|
|
211
213
|
this.byId.set(entry.id, entry);
|
|
212
214
|
updateLabelCache(this.labelsById, entry);
|
|
@@ -59,13 +59,14 @@ describe('ToolLoop._buildApiMessages 格式转换', () => {
|
|
|
59
59
|
expect(api[3].content).toBe('{"data":"ok"}');
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('tool
|
|
62
|
+
it('tool 消息已经用 OpenAI 格式时保持', () => {
|
|
63
63
|
const api = loop._buildApiMessages([
|
|
64
64
|
{ role: 'tool', tool_call_id: 'c1', content: 'done' },
|
|
65
65
|
], 'system prompt');
|
|
66
|
-
//
|
|
67
|
-
expect(api).toHaveLength(1);
|
|
66
|
+
// 已经符合 OpenAI 格式的直接保留
|
|
68
67
|
expect(api[0].role).toBe('system');
|
|
68
|
+
expect(api[1].role).toBe('tool');
|
|
69
|
+
expect(api[1].tool_call_id).toBe('c1');
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
it('systemPrompt 为空时不加 system 消息', () => {
|