foliko 2.0.29 → 2.0.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foliko",
3
- "version": "2.0.29",
3
+ "version": "2.0.30",
4
4
  "description": "简约的插件化 Agent 框架",
5
5
  "main": "src/index.js",
6
6
  "type": "commonjs",
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
- logger.info(`[_prepareSession] BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}, threshold=${this._compressionMessageThreshold}`);
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
- // AI SDK 数组格式
1196
- if (Array.isArray(m.content)) {
1197
- for (const item of m.content) {
1198
- if (!item) continue;
1199
- if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId === oldId) {
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 = self._countMessagesTokens(inputMessages);
1404
- const tokenLimit = self._maxContextTokens * SMART_COMPRESS_TOKEN_RATIO;
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 = self._repairInvalidToolCallsInMessages(inputMessages);
1423
- self._repairAndPrefixHistoricalIds(inputMessages);
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 上一步加进来的)
@@ -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,25 +194,6 @@ class ToolLoop extends EventEmitter {
201
194
  api.push({ role: 'user', content: text || '' });
202
195
  }
203
196
  } else if (msg.role === 'assistant') {
204
- // ★ 先收集此 assistant 消息中的 tool_call_id(推入 api 前更新 seen 集合)
205
- const currentToolCallIds = new Set();
206
- if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
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
197
  // ★ 检测是否有 reasoning 内容(thinking mode 必须携带)
224
198
  const hasReasoningInContent = Array.isArray(msg.content) && msg.content.some(p => p && p.type === 'reasoning');
225
199
  const hasReasoningTopLevel = msg.reasoning_content || msg.reasoning;
@@ -263,36 +237,16 @@ class ToolLoop extends EventEmitter {
263
237
  if (Array.isArray(msg.content)) {
264
238
  for (const part of msg.content) {
265
239
  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
240
  const output = part.output;
279
241
  let content;
280
242
  if (output?.type === 'text') content = output.value;
281
243
  else if (output?.type === 'json') content = JSON.stringify(output.value);
282
244
  else content = typeof output === 'string' ? output : JSON.stringify(output);
283
- api.push({ role: 'tool', tool_call_id: id, content });
245
+ api.push({ role: 'tool', tool_call_id: part.toolCallId, content });
284
246
  }
285
247
  } else if (msg.tool_call_id && typeof msg.content === 'string') {
286
- const id = msg.tool_call_id;
287
- // ★ 过滤:tool-result 必须匹配已出现过的 assistant 中的 tool_call_id
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
- }
248
+ // 已经是 OpenAI 格式(streaming 完成后的消息)
249
+ api.push(msg);
296
250
  }
297
251
  }
298
252
  }
@@ -494,12 +448,6 @@ class ToolLoop extends EventEmitter {
494
448
 
495
449
  let response;
496
450
  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
451
  response = await client.chat.completions.create({
504
452
  model,
505
453
  messages: apiMessages,
@@ -597,13 +545,6 @@ class ToolLoop extends EventEmitter {
597
545
 
598
546
  let stream;
599
547
  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
548
  stream = await client.chat.completions.create({
608
549
  model,
609
550
  messages: apiMessages,
@@ -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 = 1000;
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 = 120;
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
- return Math.ceil(chars / 4);
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
- return Math.ceil(chars / 4);
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
- return Math.ceil(chars / 4);
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
- return Math.ceil(chars / 4);
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
- return Math.ceil(chars / 4);
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
  }
@@ -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
- const content = `${this.fileEntries.map(e => JSON.stringify(e)).join('\n')}\n`;
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
- for (const e of this.fileEntries) {
195
- fs.appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`);
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`);
@@ -136,7 +136,8 @@ class JsonlSessionStorage {
136
136
  }
137
137
 
138
138
  static async _loadJsonlStorage(fsModule, filePath) {
139
- const content = fsModule.readFileSync(filePath, 'utf-8');
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.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
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
- fs.appendFileSync(this.filePath, JSON.stringify(entry) + '\n');
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 消息无对应 assistant 时被过滤(防止 orphaned tool-result)', () => {
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
- // orphaned tool-result 被过滤,只有 system 消息
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 消息', () => {