foliko 2.0.11 → 2.0.13

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/src/agent/chat.js CHANGED
@@ -15,9 +15,11 @@ const {
15
15
  tool: aiTool,
16
16
  ToolLoopAgent,
17
17
  isLoopFinished,
18
+ InvalidToolInputError,
18
19
  } = require('ai');
19
20
  const { z } = require('zod');
20
21
  const { autoSplitToolResult } = require('../../plugins/executors/data-splitter');
22
+ const { normalizeToolOutputs, validateAll } = require('../utils/message-validator');
21
23
  const { cleanResponse } = require('../utils');
22
24
  const { ChatQueueManager } = require('../common/queue');
23
25
  const { TokenCounter } = require('../llm/tokens');
@@ -232,12 +234,70 @@ class AgentChatHandler extends EventEmitter {
232
234
 
233
235
  /**
234
236
  * 验证工具调用
235
- * @private
237
+ * @deprecated Use validateAll() instead
236
238
  */
237
239
  _validateToolCalls(messages) {
238
240
  return this._toolExecutor.validateToolCalls(messages);
239
241
  }
240
242
 
243
+ /**
244
+ * 规范化 tool-result output 字段。
245
+ * 委托给 message-validator 版本(避免重复逻辑)。
246
+ * @private
247
+ */
248
+ _normalizeToolOutputs(messages) {
249
+ return normalizeToolOutputs(messages);
250
+ }
251
+
252
+ /**
253
+ * 最终兜底:暴力清理所有 orphaned tool 消息。
254
+ * @deprecated 由 validateAll 替代
255
+ */
256
+ _stripOrphanedToolMessages(messages) {
257
+ const validIds = new Set();
258
+ for (const msg of messages) {
259
+ if (msg.role !== 'assistant') continue;
260
+ if (Array.isArray(msg.content)) {
261
+ for (const item of msg.content) {
262
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) validIds.add(item.toolCallId);
263
+ }
264
+ }
265
+ if (Array.isArray(msg.tool_calls)) {
266
+ for (const tc of msg.tool_calls) { if (tc.id) validIds.add(tc.id); }
267
+ }
268
+ }
269
+
270
+ let removed = 0;
271
+ for (let i = messages.length - 1; i >= 0; i--) {
272
+ const msg = messages[i];
273
+ if (msg.role !== 'tool') continue;
274
+
275
+ let isOrphaned = false;
276
+ if (Array.isArray(msg.content)) {
277
+ msg.content = msg.content.filter(item => {
278
+ if (item && (item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !validIds.has(item.toolCallId)) {
279
+ return false;
280
+ }
281
+ return true;
282
+ });
283
+ if (msg.content.length === 0) isOrphaned = true;
284
+ } else if (msg.tool_call_id && !validIds.has(msg.tool_call_id)) {
285
+ isOrphaned = true;
286
+ } else if (!msg.tool_call_id && validIds.size === 0) {
287
+ isOrphaned = true;
288
+ }
289
+
290
+ if (isOrphaned) {
291
+ messages.splice(i, 1);
292
+ removed++;
293
+ }
294
+ }
295
+
296
+ if (removed > 0) {
297
+ logger.debug(`[_stripOrphanedToolMessages] removed ${removed} orphaned tool message(s)`);
298
+ }
299
+ }
300
+
241
301
  // ==================== AI 调用 ====================
242
302
 
243
303
  setAIClient(client) {
@@ -268,6 +328,152 @@ class AgentChatHandler extends EventEmitter {
268
328
  * 创建 ToolLoopAgent
269
329
  * @private
270
330
  */
331
+ /**
332
+ * 为历史消息中的 tool-call ID 加上前缀,避免与新生成的 ID 冲突。
333
+ * 历史消息的 tool-call ID 可能与当前请求新生成的 ID 重复,
334
+ * 导致 AI API 返回 "tool result's tool id not found" 错误。
335
+ */
336
+ /**
337
+ * 单次遍历完成:
338
+ * 1. 修复历史错误转换的 text tool-call("参数不完整,已跳过")
339
+ * 2. 给所有历史 tool-call ID 加上前缀(避免与新 ID 冲突)
340
+ * 3. 重映射对应的 tool-result ID
341
+ * @param {Array} messages - 消息数组(会被修改)
342
+ * @param {Map} [idMap] - 可选,外部传入的 ID 映射(用于跨调用共享)
343
+ */
344
+ _repairAndPrefixHistoricalIds(messages, idMap = new Map()) {
345
+ const HISTORICAL_PREFIX = 'h_';
346
+
347
+ for (const msg of messages) {
348
+ if (msg.role === 'assistant') {
349
+ if (Array.isArray(msg.content)) {
350
+ for (let i = 0; i < msg.content.length; i++) {
351
+ const item = msg.content[i];
352
+ if (!item) continue;
353
+
354
+ // 1. 修复被错误转成 text 的 tool-call
355
+ if (item.type === 'text') {
356
+ const text = item.text;
357
+ if (typeof text === 'string') {
358
+ const match = text.match(/^(工具调用 (.+?) 参数不完整,已跳过)$/);
359
+ if (match) {
360
+ const newId = `${HISTORICAL_PREFIX}restored_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
361
+ idMap.set(`restored_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, newId);
362
+ msg.content[i] = {
363
+ type: 'tool-call',
364
+ toolName: match[1],
365
+ toolCallId: newId,
366
+ input: '{}',
367
+ };
368
+ continue;
369
+ }
370
+ }
371
+ }
372
+
373
+ // 2. 重映射有历史 ID 的 tool-call
374
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.toolCallId) {
375
+ if (!item.toolCallId.startsWith(HISTORICAL_PREFIX)) {
376
+ const newId = HISTORICAL_PREFIX + item.toolCallId;
377
+ idMap.set(item.toolCallId, newId);
378
+ item.toolCallId = newId;
379
+ }
380
+ }
381
+ }
382
+ }
383
+ // tool_calls 数组格式
384
+ if (Array.isArray(msg.tool_calls)) {
385
+ for (const tc of msg.tool_calls) {
386
+ if (tc.id && !tc.id.startsWith(HISTORICAL_PREFIX)) {
387
+ const newId = HISTORICAL_PREFIX + tc.id;
388
+ idMap.set(tc.id, newId);
389
+ tc.id = newId;
390
+ }
391
+ }
392
+ }
393
+ } else if (msg.role === 'tool') {
394
+ // 3. 重映射 tool-result 的 ID
395
+ if (Array.isArray(msg.content)) {
396
+ for (const item of msg.content) {
397
+ if ((item.type === 'tool-result' || item.type === 'tool_result') && item.toolCallId && !item.toolCallId.startsWith(HISTORICAL_PREFIX)) {
398
+ const newId = idMap.get(item.toolCallId);
399
+ if (newId) item.toolCallId = newId;
400
+ }
401
+ }
402
+ }
403
+ if (msg.tool_call_id && !msg.tool_call_id.startsWith(HISTORICAL_PREFIX)) {
404
+ const newId = idMap.get(msg.tool_call_id);
405
+ if (newId) msg.tool_call_id = newId;
406
+ }
407
+ }
408
+ }
409
+
410
+ return idMap;
411
+ }
412
+
413
+ /**
414
+ * 修复消息数组中 AI SDK 标记为 invalid 的 tool-call
415
+ * @param {Array} messages - 消息数组(会被修改)
416
+ * @returns {number} 修复数量
417
+ */
418
+ _repairInvalidToolCallsInMessages(messages) {
419
+ let count = 0;
420
+ for (const msg of messages) {
421
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;
422
+ for (const item of msg.content) {
423
+ if ((item.type === 'tool-call' || item.type === 'tool-use') && item.invalid) {
424
+ const input = item.input;
425
+ if (typeof input !== 'string') continue;
426
+ const trimmed = input.trim();
427
+ // 空输入 → {}
428
+ if (trimmed === '') {
429
+ item.input = '{}';
430
+ delete item.invalid;
431
+ count++;
432
+ continue;
433
+ }
434
+ // 尝试补全花括号
435
+ let fixed = null;
436
+ if (!trimmed.startsWith('{')) {
437
+ fixed = '{' + trimmed + '}';
438
+ } else if (!trimmed.endsWith('}')) {
439
+ fixed = trimmed + '}';
440
+ }
441
+ if (fixed) {
442
+ try { JSON.parse(fixed); item.input = fixed; delete item.invalid; count++; } catch {}
443
+ }
444
+ }
445
+ }
446
+ }
447
+ return count;
448
+ }
449
+
450
+ /**
451
+ * AI SDK repairToolCall: 修复无效的 tool-call 输入
452
+ * 当模型返回的 JSON 参数无法解析时,尝试修复
453
+ */
454
+ _repairToolCall({ toolCall, tools, error }) {
455
+ // 只处理 InvalidToolInputError(JSON 解析失败)
456
+ if (!InvalidToolInputError.isInstance(error)) return null;
457
+ const input = toolCall.input;
458
+ if (typeof input !== 'string') return null;
459
+ const trimmed = input.trim();
460
+ // 空输入 → 修复为 {}
461
+ if (trimmed === '') {
462
+ return { ...toolCall, input: '{}' };
463
+ }
464
+ // 尝试补全花括号
465
+ let fixed = null;
466
+ if (!trimmed.startsWith('{')) {
467
+ fixed = '{' + trimmed + '}';
468
+ } else if (!trimmed.endsWith('}')) {
469
+ fixed = trimmed + '}';
470
+ }
471
+ if (fixed) {
472
+ try { JSON.parse(fixed); return { ...toolCall, input: fixed }; } catch {}
473
+ }
474
+ return null;
475
+ }
476
+
271
477
  _createToolLoopAgent(model, tools) {
272
478
  return new ToolLoopAgent({
273
479
  model,
@@ -275,6 +481,7 @@ class AgentChatHandler extends EventEmitter {
275
481
  tools,
276
482
  stopWhen: isLoopFinished(),
277
483
  prepareStep: this._createPrepareStep(),
484
+ experimental_repairToolCall: this._repairToolCall.bind(this),
278
485
  });
279
486
  }
280
487
 
@@ -474,7 +681,6 @@ class AgentChatHandler extends EventEmitter {
474
681
  if (!this._aiClient) {
475
682
  throw new Error('AI 客户端未配置');
476
683
  }
477
- this._validateToolCalls(messages);
478
684
  const systemPrompt = framework.prompts.build();
479
685
  //await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
480
686
  const tools = this._getAITools(aiTool);
@@ -484,6 +690,7 @@ class AgentChatHandler extends EventEmitter {
484
690
  tools,
485
691
  stopWhen: isLoopFinished(),
486
692
  prepareStep: this._createPrepareStep(),
693
+ experimental_repairToolCall: this._repairToolCall.bind(this),
487
694
  });
488
695
 
489
696
  // AI SDK 错误回调:仅记录日志,不在这里处理(统一由 catch/yield 处理)
@@ -557,7 +764,8 @@ class AgentChatHandler extends EventEmitter {
557
764
  }
558
765
  // 写入端 sanitize:AI SDK v6 响应里允许 error-text/error-json(工具失败标记),
559
766
  // 但 ModelMessage 输入 schema 不接受;持久化前必须规范化
560
- this._normalizeToolOutputs(finishMessages);
767
+ normalizeToolOutputs(finishMessages);
768
+
561
769
  messages.push(...finishMessages);
562
770
 
563
771
  // 获取或估算 token 用量
@@ -668,7 +876,6 @@ class AgentChatHandler extends EventEmitter {
668
876
  if (!this._aiClient) {
669
877
  throw new Error('AI 客户端未配置');
670
878
  }
671
- this._validateToolCalls(messages);
672
879
  const systemPrompt = framework.prompts.build();
673
880
  const tools = this._getAITools(aiTool);
674
881
 
@@ -684,6 +891,7 @@ class AgentChatHandler extends EventEmitter {
684
891
  tools,
685
892
  stopWhen: isLoopFinished(),
686
893
  prepareStep: this._createPrepareStep(),
894
+ experimental_repairToolCall: this._repairToolCall.bind(this),
687
895
  });
688
896
 
689
897
  const result = await framework.runInSession(sessionId, {}, async () => {
@@ -787,65 +995,29 @@ class AgentChatHandler extends EventEmitter {
787
995
  logger.debug(`[_prepareSession] AFTER compress: messages=${messages.length}`);
788
996
  }
789
997
 
790
- // 验证工具调用
791
- const validated = this._validateMessagesPairing(messages);
998
+ // 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
999
+ this._repairInvalidToolCallsInMessages(messages);
1000
+ this._repairAndPrefixHistoricalIds(messages);
1001
+
1002
+ // 单次遍历完成所有验证 + 规范化(validateAll 内部处理 error-text/error-json)
1003
+ const validated = validateAll(messages);
792
1004
  if (validated.length !== messages.length) {
793
- // 删除了消息,缓存失效
794
1005
  this._invalidateMessageTokensCache(messageStore);
795
1006
  messages.length = 0;
796
1007
  messages.push(...validated);
797
1008
  }
798
1009
  messages.push(userMessage);
799
- this._validateToolCalls(messages);
800
-
801
- // 读取端 sanitize:修复历史 chat log 中可能存在的 error-text/error-json
802
- this._normalizeToolOutputs(messages);
803
-
804
- // 最终兜底:暴力清理所有 orphaned tool 消息,确保 AI SDK 不报错
805
- this._stripOrphanedToolMessages(messages);
806
1010
 
807
1011
  return { messageStore, messages };
808
1012
  }
809
1013
 
810
1014
  /**
811
1015
  * 规范化 tool-result 的 output 字段。
812
- * AI SDK v6 响应中允许 'error-text' / 'error-json'(表示工具执行失败),
813
- * 但 ModelMessage 输入 schema(AI SDK 发送 prompt 时使用)不接受这两类。
814
- * 必须在写入磁盘和读取发送前都做转换。
1016
+ * 委托给 message-validator 版本(避免重复逻辑)。
815
1017
  * @private
816
1018
  */
817
1019
  _normalizeToolOutputs(messages) {
818
- const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
819
- let fixed = 0;
820
- for (const msg of messages) {
821
- if (msg.role !== 'tool' || !Array.isArray(msg.content)) continue;
822
- for (const part of msg.content) {
823
- if (!part || part.type !== 'tool-result') continue;
824
- const out = part.output;
825
- if (out === undefined || out === null || typeof out !== 'object') {
826
- const v = out === undefined ? 'undefined' : (typeof out === 'string' ? out : JSON.stringify(out));
827
- part.output = { type: 'text', value: String(v) };
828
- fixed++;
829
- continue;
830
- }
831
- if (VALID_OUTPUT_TYPES.has(out.type)) continue;
832
- if (out.type === 'error-text' || out.type === 'error_text') {
833
- part.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
834
- fixed++;
835
- continue;
836
- }
837
- if (out.type === 'error-json' || out.type === 'error_json') {
838
- part.output = { type: 'json', value: out.value ?? null };
839
- fixed++;
840
- continue;
841
- }
842
- // 未知 type 降级为 text
843
- try { part.output = { type: 'text', value: JSON.stringify(out) }; }
844
- catch { part.output = { type: 'text', value: String(out) }; }
845
- fixed++;
846
- }
847
- }
848
- return fixed;
1020
+ return normalizeToolOutputs(messages);
849
1021
  }
850
1022
 
851
1023
  /**
@@ -1090,19 +1262,20 @@ class AgentChatHandler extends EventEmitter {
1090
1262
  inputMessages.push(...trimmed);
1091
1263
  }
1092
1264
 
1093
- // 验证 tool-call tool-result 配对
1094
- const validated = this._validateMessagesPairing(inputMessages);
1265
+ // 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
1266
+ const repairedCount = this._repairInvalidToolCallsInMessages(inputMessages);
1267
+ this._repairAndPrefixHistoricalIds(inputMessages);
1268
+
1269
+ // 单次遍历完成所有验证 + 规范化
1270
+ const validated = validateAll(inputMessages);
1095
1271
  if (validated.length !== inputMessages.length) {
1096
1272
  logger.debug(
1097
- `[PrepareStep] After pairing validation: ${inputMessages.length} -> ${validated.length}`
1273
+ `[PrepareStep] After validation: ${inputMessages.length} -> ${validated.length}`
1098
1274
  );
1099
1275
  inputMessages.length = 0;
1100
1276
  inputMessages.push(...validated);
1101
1277
  }
1102
1278
 
1103
- // 验证 tool-call 格式
1104
- this._validateToolCalls(inputMessages);
1105
-
1106
1279
  return { messages: inputMessages };
1107
1280
  } catch (err) {
1108
1281
  logger.error('prepareStep error:', err.message, err.stack);
@@ -114,17 +114,21 @@ function validateToolCalls(messages, options = {}) {
114
114
  if (typeof input !== 'string') continue;
115
115
 
116
116
  const trimmed = input.trim();
117
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
118
- if (item.toolCallId) {
119
- invalidatedToolCallIds.add(item.toolCallId);
120
- }
117
+ // 只有真正无法解析为 JSON 的才标记为 invalid
118
+ let invalid = false;
119
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
120
+ try { JSON.parse(trimmed); } catch { invalid = true; }
121
+ } else if (trimmed !== '') {
122
+ invalid = true;
123
+ }
124
+
125
+ if (invalid) {
126
+ if (item.toolCallId) invalidatedToolCallIds.add(item.toolCallId);
121
127
  if (shouldLog) {
122
128
  logger.warn(
123
- `[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId ||
124
- '?'}, converting to text`
129
+ `[validateToolCalls] invalid input for "${item.toolName}", toolCallId=${item.toolCallId || '?'}, converting to text`
125
130
  );
126
131
  }
127
- // 把无效的 tool-call 转换成 text 文本
128
132
  item.type = 'text';
129
133
  item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
130
134
  delete item.toolCallId;
@@ -155,24 +159,143 @@ function validateToolCalls(messages, options = {}) {
155
159
  }
156
160
  }
157
161
 
158
- if (shouldLog && fixedCount > 0) {
159
- // logger.info(`[validateToolCalls] Fixed ${fixedCount} incomplete tool calls/results`);
160
- }
162
+ if (shouldLog && fixedCount > 0) {}
161
163
 
162
164
  return fixedCount;
163
165
  }
164
166
 
165
167
  /**
166
- * 一次遍历完成配对验证 + tool-call 格式验证
167
- * 相比分别调用减少消息数组迭代次数
168
+ * 单次遍历完成所有验证和规范化:
169
+ * 1. 修复 tool-call 格式(不完整 JSON → text)
170
+ * 2. 收集有效 toolCallId(content[] 和 tool_calls[])
171
+ * 3. 规范化 error-text/error-json → text/json
172
+ * 4. 移除 orphaned tool-result(没有对应 tool-call 的)
173
+ * 5. 移除整个 orphaned tool 消息(content 被清空的)
174
+ *
175
+ * 比分别调用减少 3-4 次消息数组遍历
168
176
  * @param {Array} messages - 消息数组(会被修改)
169
177
  * @returns {Array} 过滤后的消息数组
170
178
  */
171
179
  function validateAll(messages) {
172
- // 1. 先修复 tool-call 格式(避免无效 ID 干扰后续配对验证)
173
- validateToolCalls(messages);
174
- // 2. 再移除 orphaned tool-result
175
- return validateMessagesPairing(messages);
180
+ const VALID_OUTPUT_TYPES = new Set(['text', 'json', 'execution-denied', 'content']);
181
+ const validToolCallIds = new Set();
182
+ const invalidatedIds = new Set();
183
+
184
+ // 第一遍:收集有效 ID + 修复格式 + 规范化 error 类型
185
+ for (const msg of messages) {
186
+ if (msg.role === 'assistant') {
187
+ const content = msg.content;
188
+ if (Array.isArray(content)) {
189
+ for (const item of content) {
190
+ if (item && (item.type === 'tool-call' || item.type === 'tool-use')) {
191
+ const input = item.input;
192
+ // 检查 input 是否为不完整的 JSON:空字符串、或只有 { 或 }
193
+ // 有效 input 应能被 JSON.parse(即使是 {})
194
+ let invalid = false;
195
+ if (typeof input === 'string') {
196
+ const trimmed = input.trim();
197
+ // 有效 input:能解析为 JSON 的字符串(包括 '{}')
198
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
199
+ try { JSON.parse(trimmed); } catch { invalid = true; }
200
+ } else if (trimmed !== '') {
201
+ // 非空且不是 {} 开头:无效
202
+ invalid = true;
203
+ }
204
+ // 空字符串 '' 单独放过(无参数工具是合法的)
205
+ } else if (input === undefined || input === null) {
206
+ // input 为 null/undefined 时:只有当工具 schema 要求必填时才应该报错
207
+ // 但我们无法在此判断工具是否必填,统一放行让后续验证处理
208
+ // invalid = true;
209
+ }
210
+ if (invalid) {
211
+ logger.debug(`[validateAll] INVALID detected: toolName=${item.toolName}, input=${JSON.stringify(input)}, typeof=${typeof input}`);
212
+ // 无效输入 → 转 text,标记为 invalid
213
+ item.type = 'text';
214
+ item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
215
+ if (item.toolCallId) invalidatedIds.add(item.toolCallId);
216
+ delete item.toolCallId;
217
+ delete item.toolName;
218
+ delete item.input;
219
+ } else if (item.toolCallId) {
220
+ validToolCallIds.add(item.toolCallId);
221
+ }
222
+ } else if (item && item.type === 'tool-result') {
223
+ // 规范化 tool-result output
224
+ const out = item.output;
225
+ if (out === undefined || out === null || typeof out !== 'object') {
226
+ item.output = { type: 'text', value: String(out ?? 'undefined') };
227
+ } else if (!VALID_OUTPUT_TYPES.has(out.type)) {
228
+ if (out.type === 'error-text' || out.type === 'error_text') {
229
+ item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
230
+ } else if (out.type === 'error-json' || out.type === 'error_json') {
231
+ item.output = { type: 'json', value: out.value ?? null };
232
+ } else {
233
+ try { item.output = { type: 'text', value: JSON.stringify(out) }; }
234
+ catch { item.output = { type: 'text', value: String(out) }; }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ // tool_calls[] 格式(OpenAI 兼容)
241
+ if (Array.isArray(msg.tool_calls)) {
242
+ for (const tc of msg.tool_calls) {
243
+ if (tc.id && !invalidatedIds.has(tc.id)) validToolCallIds.add(tc.id);
244
+ }
245
+ }
246
+ } else if (msg.role === 'tool') {
247
+ // 规范化 tool 消息 output
248
+ if (Array.isArray(msg.content)) {
249
+ for (const item of msg.content) {
250
+ if (item && item.type === 'tool-result') {
251
+ const out = item.output;
252
+ if (out === undefined || out === null || typeof out !== 'object') {
253
+ item.output = { type: 'text', value: String(out ?? 'undefined') };
254
+ } else if (!VALID_OUTPUT_TYPES.has(out.type)) {
255
+ if (out.type === 'error-text' || out.type === 'error_text') {
256
+ item.output = { type: 'text', value: typeof out.value === 'string' ? out.value : String(out.value ?? '') };
257
+ } else if (out.type === 'error-json' || out.type === 'error_json') {
258
+ item.output = { type: 'json', value: out.value ?? null };
259
+ } else {
260
+ try { item.output = { type: 'text', value: JSON.stringify(out) }; }
261
+ catch { item.output = { type: 'text', value: String(out) }; }
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ // 第二遍:过滤 orphaned tool-result + orphaned tool messages
271
+ const toolMessageIndicesToRemove = new Set();
272
+ for (let i = 0; i < messages.length; i++) {
273
+ const msg = messages[i];
274
+ if (msg.role !== 'tool') continue;
275
+
276
+ let hasValidResult = false;
277
+ if (Array.isArray(msg.content)) {
278
+ const originalLen = msg.content.length;
279
+ msg.content = msg.content.filter((item) => {
280
+ if (!item || (item.type !== 'tool-result' && item.type !== 'tool_result')) return true;
281
+ const id = item.toolCallId;
282
+ if (!id || (!validToolCallIds.has(id) && !invalidatedIds.has(id))) return false;
283
+ hasValidResult = true;
284
+ return true;
285
+ });
286
+ if (msg.content.length === 0 && originalLen > 0) {
287
+ toolMessageIndicesToRemove.add(i);
288
+ }
289
+ } else if (msg.tool_call_id && !validToolCallIds.has(msg.tool_call_id)) {
290
+ toolMessageIndicesToRemove.add(i);
291
+ }
292
+ }
293
+
294
+ if (toolMessageIndicesToRemove.size > 0) {
295
+ messages = messages.filter((_, i) => !toolMessageIndicesToRemove.has(i));
296
+ }
297
+
298
+ return messages;
176
299
  }
177
300
 
178
301
  // AI SDK v6 ToolResultOutput 允许的类型(用于 ModelMessage[] 输入 schema)