foliko 2.0.22 → 2.0.24

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
@@ -11,12 +11,7 @@
11
11
  const { EventEmitter } = require('../common/events');
12
12
  const { logger } = require('../common/logger');
13
13
  const fs = require('fs');
14
- const {
15
- tool: aiTool,
16
- ToolLoopAgent,
17
- isLoopFinished,
18
- InvalidToolInputError,
19
- } = require('ai');
14
+ const { ToolLoop } = require('./tool-loop');
20
15
  const { z } = require('zod');
21
16
  const { autoSplitToolResult } = require('../../plugins/executors/data-splitter');
22
17
  const { normalizeToolOutputs, validateAll, sanitizeForLLM } = require('../utils/message-validator');
@@ -60,7 +55,10 @@ class AgentChatHandler extends EventEmitter {
60
55
 
61
56
  // DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关
62
57
  // config.thinkingMode = true 可启用,默认为禁用
63
- const enableThinking = config.thinkingMode === true && isThinkingModel(this.model);
58
+ // 紧急关闭:设环境变量 FOLIKO_DISABLE_THINKING=1 强制关闭(绕过所有检测)
59
+ const forceDisabled = process.env.FOLIKO_DISABLE_THINKING === '1' ||
60
+ process.env.FOLIKO_DISABLE_THINKING === 'true';
61
+ const enableThinking = !forceDisabled && config.thinkingMode === true && isThinkingModel(this.model);
64
62
  this._thinkingMode = enableThinking;
65
63
 
66
64
  if (enableThinking) {
@@ -192,15 +190,10 @@ class AgentChatHandler extends EventEmitter {
192
190
 
193
191
  // ==================== 工具管理(委托给 ToolExecutor) ====================
194
192
 
195
- /** AI tools 格式缓存 (避免每次 chat 重建) */
196
- _aiToolsCache = null;
197
- _aiToolsCacheKey = ''; // 工具列表的哈希摘要
198
-
199
193
  registerTool(tool) {
200
194
  this._toolExecutor.registerTool(tool);
201
195
  // 工具列表变了,清除缓存
202
196
  this._toolsTokensCacheVersion = 0;
203
- this._aiToolsCache = null; // 清除 AI tools 缓存
204
197
  return this;
205
198
  }
206
199
 
@@ -298,6 +291,40 @@ class AgentChatHandler extends EventEmitter {
298
291
  }
299
292
  }
300
293
 
294
+ /**
295
+ * ★ DeepSeek thinking mode:把 reasoning 文本注入 assistant message
296
+ * 关键:必须作为 `{type: 'reasoning', text: '...'}` part 放入 content 数组,
297
+ * AI SDK 的 openai-compatible provider 才能识别并在下次 API 调用时
298
+ * 序列化为 `reasoning_content` 字段。
299
+ * 错误做法:直接 `msg.reasoning_content = '...'` → AI SDK 不认识这个字段,丢弃 → 报错
300
+ *
301
+ * @private
302
+ */
303
+ _injectReasoningContent(msg, reasoningText) {
304
+ if (!msg || !reasoningText) return;
305
+ if (typeof msg.content === 'string') {
306
+ // 旧格式:content 是字符串。转为数组,reasoning part 放前面
307
+ msg.content = [
308
+ { type: 'reasoning', text: reasoningText },
309
+ { type: 'text', text: msg.content },
310
+ ];
311
+ return;
312
+ }
313
+ if (Array.isArray(msg.content)) {
314
+ // 查找已有 reasoning part(合并而非重复)
315
+ const existing = msg.content.find(c => c && c.type === 'reasoning');
316
+ if (existing) {
317
+ existing.text = reasoningText;
318
+ } else {
319
+ // reasoning 放最前(按时间顺序)
320
+ msg.content.unshift({ type: 'reasoning', text: reasoningText });
321
+ }
322
+ return;
323
+ }
324
+ // content 不存在或不是 string/array:新建
325
+ msg.content = [{ type: 'reasoning', text: reasoningText }];
326
+ }
327
+
301
328
  // ==================== AI 调用 ====================
302
329
 
303
330
  setAIClient(client) {
@@ -448,20 +475,13 @@ class AgentChatHandler extends EventEmitter {
448
475
  }
449
476
 
450
477
  /**
451
- * AI SDK repairToolCall: 修复无效的 tool-call 输入
452
- * 当模型返回的 JSON 参数无法解析时,尝试修复
478
+ * 修复无效的 tool-call 输入(JSON 解析失败时尝试补全花括号)
453
479
  */
454
480
  _repairToolCall({ toolCall, tools, error }) {
455
- // 只处理 InvalidToolInputError(JSON 解析失败)
456
- if (!InvalidToolInputError.isInstance(error)) return null;
457
481
  const input = toolCall.input;
458
482
  if (typeof input !== 'string') return null;
459
483
  const trimmed = input.trim();
460
- // 空输入 修复为 {}
461
- if (trimmed === '') {
462
- return { ...toolCall, input: '{}' };
463
- }
464
- // 尝试补全花括号
484
+ if (trimmed === '') return { ...toolCall, input: '{}' };
465
485
  let fixed = null;
466
486
  if (!trimmed.startsWith('{')) {
467
487
  fixed = '{' + trimmed + '}';
@@ -474,15 +494,39 @@ class AgentChatHandler extends EventEmitter {
474
494
  return null;
475
495
  }
476
496
 
477
- _createToolLoopAgent(model, tools) {
478
- return new ToolLoopAgent({
479
- model,
480
- instructions: this._systemPrompt,
481
- tools,
482
- stopWhen: isLoopFinished(),
483
- prepareStep: this._createPrepareStep(),
484
- experimental_repairToolCall: this._repairToolCall.bind(this),
485
- });
497
+ /**
498
+ * 获取工具 Map(OpenAI tool 格式,供 ToolLoop 使用)
499
+ * @returns {{ name => { name, description, execute, parameters } }}
500
+ */
501
+ _getToolMap() {
502
+ const map = {};
503
+ const allTools = this.agent.framework.getTools();
504
+ for (const toolDef of allTools) {
505
+ map[toolDef.name] = {
506
+ name: toolDef.name,
507
+ description: toolDef.description || '',
508
+ parameters: toolDef.inputSchema || toolDef.parameters || null,
509
+ execute: async (args) => {
510
+ const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
511
+ this.emit('tool-call', { name: toolDef.name, args: cleanedArgs });
512
+ try {
513
+ const result = await toolDef.execute(cleanedArgs, this.agent.framework);
514
+ // 自动检测大工具结果,透明分拆
515
+ let finalResult = result;
516
+ try {
517
+ const splitCheck = await autoSplitToolResult(toolDef.name, result, this.agent.framework);
518
+ if (splitCheck.wasSplit && splitCheck.result) finalResult = splitCheck.result;
519
+ } catch { /* 分拆失败不中断 */ }
520
+ this.emit('tool-result', { name: toolDef.name, args: cleanedArgs, result: finalResult });
521
+ return finalResult;
522
+ } catch (err) {
523
+ this.emit('tool-error', { name: toolDef.name, args: cleanedArgs, error: err.message });
524
+ return { success: false, error: err.message };
525
+ }
526
+ },
527
+ };
528
+ }
529
+ return map;
486
530
  }
487
531
 
488
532
  /**
@@ -690,128 +734,65 @@ class AgentChatHandler extends EventEmitter {
690
734
  }
691
735
  const systemPrompt = framework.prompts.build();
692
736
  //await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
693
- const tools = this._getAITools(aiTool);
694
- const agent = new ToolLoopAgent({
695
- model: this._aiClient,
696
- instructions: systemPrompt,
737
+ const tools = this._getToolMap();
738
+ const toolLoop = new ToolLoop({
697
739
  tools,
698
- stopWhen: isLoopFinished(),
740
+ maxSteps: this._maxSteps || 20,
699
741
  prepareStep: this._createPrepareStep(),
700
- experimental_repairToolCall: this._repairToolCall.bind(this),
742
+ repairToolCall: this._repairToolCall.bind(this),
743
+ abortSignal: framework.getAbortSignal?.(),
744
+ thinkingMode: this._thinkingMode,
701
745
  });
702
746
 
703
- // AI SDK 错误回调:仅记录日志,不在这里处理(统一由 catch/yield 处理)
704
- const handleSDKError = ({ error }) => {
705
- //logger.debug(`[AgentChat] SDK onError: ${error?.message}`);
706
- };
707
-
708
747
  // DeepSeek thinking mode: 收集 reasoning_content
709
748
  const isThinking = this._thinkingMode;
710
749
  let reasoningContent = '';
711
750
 
712
- const result = await framework.runInSession(sessionId, {}, async () => {
713
- return agent.stream({
714
- messages,
715
- ...this.providerOptions,
716
- onError: handleSDKError,
717
- abortSignal: framework.getAbortSignal?.(),
718
- });
751
+ const stream = toolLoop.stream({
752
+ messages,
753
+ systemPrompt,
754
+ model: this.model,
755
+ client: this._aiClient,
756
+ ...this.providerOptions,
719
757
  });
720
-
721
- const stream = result.fullStream;
758
+ // 遍历流式响应
722
759
  let fullText = '';
723
- const iterator = stream[Symbol.asyncIterator] ? stream : stream.fullStream;
724
- // 追踪发送的 tool-call ID,过滤 API 返回的不匹配 tool-result
725
- const sentToolCallIds = new Set();
760
+ let usageRecv = {};
726
761
 
727
- for await (const part of iterator || stream) {
762
+ for await (const part of stream) {
728
763
  if (part.type === 'text-delta') {
729
- const text = part.text || part.textDelta || '';
730
- fullText += text;
731
- yield { type: 'text', text };
732
- } else if (part.type === 'reasoning') {
764
+ fullText += part.text;
765
+ yield { type: 'text', text: part.text };
766
+ } else if (part.type === 'thinking') {
733
767
  reasoningContent += part.text || '';
734
768
  yield { type: 'thinking', text: part.text };
735
769
  } else if (part.type === 'tool-call') {
736
- const toolCallId = part.toolCallId;
737
- if (toolCallId) sentToolCallIds.add(toolCallId);
738
770
  yield { type: 'tool-call', toolName: part.toolName, input: part.input };
739
771
  } else if (part.type === 'tool-result') {
740
- // 过滤 toolCallId 不匹配的 tool-result(API bug)
741
- const resultToolCallId = part.toolCallId;
742
- if (resultToolCallId && !sentToolCallIds.has(resultToolCallId)) {
743
- logger.debug(`[stream]过滤不匹配的 tool-result: ${resultToolCallId}`);
744
- continue;
745
- }
746
- yield { type: 'tool-result', toolName: part.toolName, result: part.output };
772
+ yield { type: 'tool-result', toolName: part.toolName, result: part.result };
747
773
  } else if (part.type === 'error') {
748
- // 统一错误消息
749
- const errMsg = part.error?.message || 'AI 服务错误';
774
+ const errMsg = part.error || 'AI 服务错误';
750
775
  yield { type: 'error', error: errMsg.split('\n')[0] };
776
+ } else if (part.type === 'usage') {
777
+ usageRecv = { inputTokens: part.inputTokens || 0, outputTokens: part.outputTokens || 0 };
778
+ yield { type: 'usage', inputTokens: usageRecv.inputTokens, outputTokens: usageRecv.outputTokens };
751
779
  }
752
780
  }
753
-
754
- // DeepSeek thinking mode: 标记 reasoning_content 已处理
755
- // 在 finishMessages 中标记,避免重复添加
756
- const finishMessages = (await result.response).messages;
757
- if (isThinking && reasoningContent) {
758
- // DeepSeek 要求:当有 tool_calls 时,必须将 reasoning_content 添加到消息中
759
- // AI SDK 的 finishMessage 不会包含 reasoning_content,需要手动添加
760
- for (const msg of finishMessages) {
761
- if (msg.role === 'assistant') {
762
- // 检查是否有 tool_calls
763
- const hasToolCalls = msg.tool_calls?.length > 0 ||
764
- (Array.isArray(msg.content) && msg.content.some(c => c.type === 'tool-call' || c.type === 'tool-use'));
765
- if (hasToolCalls) {
766
- // DeepSeek API 要求:带 tool_calls 时必须传递 reasoning_content
767
- msg.reasoning_content = reasoningContent;
768
- }
769
- }
770
- }
771
- }
772
- // 写入端 sanitize:AI SDK v6 响应里允许 error-text/error-json(工具失败标记),
773
- // 但 ModelMessage 输入 schema 不接受;持久化前必须规范化
774
- normalizeToolOutputs(finishMessages);
775
-
776
- messages.push(...finishMessages);
777
-
778
- // 获取或估算 token 用量
779
- const usage = await result.totalUsage;
780
- let inputTokens = 0, outputTokens = 0;
781
-
782
- if (usage && (usage.promptTokens > 0 || usage.completionTokens > 0 || usage.prompt_tokens > 0 || usage.completion_tokens > 0)) {
783
- // totalUsage 有真实数据(DeepSeek 等 API)
784
- inputTokens = usage.promptTokens || usage.prompt_tokens || 0;
785
- outputTokens = usage.completionTokens || usage.completion_tokens || 0;
786
- this._updateMessageStoreUsage(messageStore, usage);
787
- } else {
788
- // totalUsage 不可用或为空(MiniMax 等 API 返回 {promptTokens:0,completionTokens:0})
789
- // 用 TokenCounter 从实际消息内容估算
790
- inputTokens = this._tokenCounter.countMessages(messages);
791
- outputTokens = this._tokenCounter.countText(fullText);
792
- }
793
-
794
- // 确保 messageStore.usage 始终有数据
795
- const usageData = { promptTokens: inputTokens, completionTokens: outputTokens };
796
- messageStore.usage = usageData;
781
+ // 持久化用量
782
+ messageStore.usage = usageRecv;
797
783
 
798
784
  // 同时通过 framework 事件广播(绕开 queue 事件链路)
799
785
  if (this.agent && this.agent.framework) {
800
786
  this.agent.framework.emit('agent:usage', {
801
787
  sessionId,
802
- usage: usageData,
788
+ usage: usageRecv,
803
789
  });
804
790
  }
805
791
 
806
792
  // yield usage 数据,让 UI 层能直接获取
807
- yield {
808
- type: 'usage',
809
- sessionId,
810
- inputTokens,
811
- outputTokens,
812
- };
793
+ // (usage 已在 stream 循环中 yield,无需重复)
813
794
 
814
- const userMsg = messages[messages.length - finishMessages.length - 1];
795
+ const userMsg = messages.length > 0 ? messages[messages.length - 1] : null;
815
796
  this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
816
797
 
817
798
  // ★ 修复:成功路径才持久化。AI 失败时若 finally 也 save,
@@ -907,7 +888,6 @@ class AgentChatHandler extends EventEmitter {
907
888
  */
908
889
  async _processMessage(sessionId, message, options) {
909
890
  const framework = this.agent.framework;
910
- // ★ 提到外层作用域,catch 中要回滚 userMessage 时需要访问
911
891
  let messageStore = null;
912
892
  let messages = null;
913
893
  let userMessage = null;
@@ -922,7 +902,7 @@ class AgentChatHandler extends EventEmitter {
922
902
  throw new Error('AI 客户端未配置');
923
903
  }
924
904
  const systemPrompt = framework.prompts.build();
925
- const tools = this._getAITools(aiTool);
905
+ const tools = this._getToolMap();
926
906
 
927
907
  // DeepSeek thinking mode: 处理参数
928
908
  const apiOptions = { ...this.providerOptions };
@@ -930,46 +910,37 @@ class AgentChatHandler extends EventEmitter {
930
910
  delete apiOptions.temperature;
931
911
  }
932
912
 
933
- const agent = new ToolLoopAgent({
934
- model: this._aiClient,
935
- instructions: systemPrompt,
913
+ const toolLoop = new ToolLoop({
936
914
  tools,
937
- stopWhen: isLoopFinished(),
915
+ maxSteps: this._maxSteps || 20,
938
916
  prepareStep: this._createPrepareStep(),
939
- experimental_repairToolCall: this._repairToolCall.bind(this),
917
+ repairToolCall: this._repairToolCall.bind(this),
918
+ abortSignal: framework.getAbortSignal?.(),
919
+ thinkingMode: this._thinkingMode,
940
920
  });
941
921
 
942
- const result = await framework.runInSession(sessionId, {}, async () => {
943
- return agent.generate({ messages, ...apiOptions, abortSignal: framework.getAbortSignal?.() });
922
+ const result = await toolLoop.generate({
923
+ messages,
924
+ systemPrompt,
925
+ model: this.model,
926
+ client: this._aiClient,
927
+ ...apiOptions,
944
928
  });
929
+ // result = { text, response: { messages }, steps, usage, reasoningText }
945
930
 
946
931
  if (result.usage && (result.usage.promptTokens > 0 || result.usage.completionTokens > 0)) {
947
932
  this._updateMessageStoreUsage(messageStore, result.usage);
948
933
  } else {
949
- // fallback: 用 TokenCounter 估算
950
934
  const inputTokens = this._tokenCounter.countMessages(messages);
951
935
  const outputTokens = this._tokenCounter.countText(result.text || '');
952
936
  messageStore.usage = { promptTokens: inputTokens, completionTokens: outputTokens };
953
937
  }
954
938
 
955
- // DeepSeek thinking mode: 处理 reasoning_content
956
- const isThinking = this._thinkingMode;
957
- if (isThinking && result.reasoningText) {
958
- // 有 reasoning_text 需要添加到消息中(当有 tool_calls 时)
959
- const finishMessages = result.response.messages;
960
- for (const msg of finishMessages) {
961
- if (msg.role === 'assistant') {
962
- const hasToolCalls = msg.tool_calls?.length > 0 ||
963
- (Array.isArray(msg.content) && msg.content.some(c => c.type === 'tool-call' || c.type === 'tool-use'));
964
- if (hasToolCalls) {
965
- msg.reasoning_content = result.reasoningText;
966
- }
967
- }
968
- }
969
- }
970
-
971
- messages.push(...result.response.messages);
972
- const userMsg = messages[messages.length - result.response.messages.length - 1];
939
+ // ToolLoop 已经直接在 messages 末尾添加了新消息,不需要再 push
940
+ // 计算 original 的 user message
941
+ const userMsg = result.text
942
+ ? messages.slice(-1)[0]
943
+ : messages[messages.length - 1];
973
944
  this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
974
945
 
975
946
  // ★ 修复:AI 调用成功后才持久化(之前 finally 总是 save,
@@ -1343,78 +1314,6 @@ class AgentChatHandler extends EventEmitter {
1343
1314
  return createHash('sha256').update(toolDescriptors).digest('hex').slice(0, 32);
1344
1315
  }
1345
1316
 
1346
- /**
1347
- * 获取 AI 工具格式(带缓存)
1348
- * 使用 AI SDK 的 tool() 格式
1349
- * @private
1350
- */
1351
- _getAITools(toolFn) {
1352
- const currentKey = this._computeToolsCacheKey();
1353
- if (this._aiToolsCache && this._aiToolsCacheKey === currentKey) {
1354
- return this._aiToolsCache;
1355
- }
1356
-
1357
- const tools = {};
1358
- const allTools = this.agent.framework.getTools();
1359
- for (const toolDef of allTools) {
1360
- const toolName = toolDef.name;
1361
-
1362
- // 使用 AI SDK 的 tool() 格式
1363
- const toolConfig = {
1364
- name: toolName,
1365
- description: toolDef.description || '',
1366
- execute: async (args) => {
1367
- // 清理参数并验证类型
1368
- const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
1369
-
1370
- // 执行工具
1371
- this.emit('tool-call', { name: toolName, args: cleanedArgs });
1372
- // logger.info(`[Tool] Call: ${toolName}`);
1373
- try {
1374
- const result = await toolDef.execute(cleanedArgs, this.agent.framework);
1375
-
1376
- // 自动检测大工具结果,透明分拆
1377
- // 仅在数据分拆插件已加载时生效
1378
- let finalResult = result;
1379
- try {
1380
- const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);
1381
- if (splitCheck.wasSplit && splitCheck.result) {
1382
- finalResult = splitCheck.result;
1383
- // logger.info(
1384
- // `[AutoSplit] 工具 "${toolName}" 结果过大,已自动分拆处理`
1385
- // );
1386
- }
1387
- } catch (splitErr) {
1388
- // 分拆失败不阻断主流程,继续使用原始结果
1389
- logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);
1390
- }
1391
-
1392
- this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });
1393
- return finalResult;
1394
- } catch (err) {
1395
- this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
1396
- // 返回错误信息字符串,而不是抛出异常
1397
- return { success: false, error: err.message }
1398
- }
1399
- },
1400
- };
1401
-
1402
- // 支持 inputSchema 或 parameters
1403
- if (toolDef.inputSchema) {
1404
- toolConfig.inputSchema = toolDef.inputSchema;
1405
- } else if (toolDef.parameters) {
1406
- toolConfig.parameters = toolDef.parameters;
1407
- }
1408
-
1409
- // AI SDK 6.x 使用对象形式,键为工具名
1410
- tools[toolName] = toolFn(toolConfig);
1411
- }
1412
-
1413
- this._aiToolsCache = tools;
1414
- this._aiToolsCacheKey = currentKey;
1415
- return tools;
1416
- }
1417
-
1418
1317
  /**
1419
1318
  * 清理工具参数并根据 inputSchema 验证类型
1420
1319
  * @private
@@ -1432,6 +1331,14 @@ class AgentChatHandler extends EventEmitter {
1432
1331
  if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
1433
1332
  continue;
1434
1333
  }
1334
+ // ★ 自动修复:LLM 有时把 JSON 对象参数序列化为字符串传进来
1335
+ // 如 ext_call 的 args: "{id: ...}" → 自动 parse 为 {id: ...}
1336
+ if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) {
1337
+ try {
1338
+ cleaned[key] = JSON.parse(value);
1339
+ continue;
1340
+ } catch { /* 不是 JSON,当普通字符串处理 */ }
1341
+ }
1435
1342
  cleaned[key] = value;
1436
1343
  }
1437
1344
 
@@ -1469,6 +1376,10 @@ class AgentChatHandler extends EventEmitter {
1469
1376
  inputMessages.push(...trimmed);
1470
1377
  }
1471
1378
 
1379
+ // ★ _buildApiMessages 现在会自动补 placeholder reasoning_content
1380
+ // (对于 thinking mode 下无 reasoning 的旧消息,补 '...')
1381
+ // 所以不再需要 _stripStaleToolCalls 提前清理
1382
+
1472
1383
  // 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
1473
1384
  const repairedCount = this._repairInvalidToolCallsInMessages(inputMessages);
1474
1385
  this._repairAndPrefixHistoricalIds(inputMessages);
@@ -1500,6 +1411,62 @@ class AgentChatHandler extends EventEmitter {
1500
1411
  };
1501
1412
  }
1502
1413
 
1414
+ /**
1415
+ * ★ 清理"过时"的 tool_call:移除缺 reasoning 的 assistant 消息中的 tool_calls
1416
+ * 场景:旧 session(修复前保存)里的 assistant 消息只有 tool_call 没有 reasoning part
1417
+ * 截断/压缩后这些消息被发给 DeepSeek,DeepSeek 报 "reasoning_content must be passed back"
1418
+ * 处理:移除 tool_calls(tool result 会变 orphan,validateAll 清理掉)
1419
+ * 保留消息的 text/reasoning 内容(如果有),LLM 仍能看到"我之前说了什么"
1420
+ *
1421
+ * @private
1422
+ */
1423
+ _stripStaleToolCalls(messages) {
1424
+ if (!Array.isArray(messages)) return 0;
1425
+ const isThinking = this._thinkingMode;
1426
+ if (!isThinking) return 0; // 非 thinking 模式不需要处理
1427
+ let stripped = 0;
1428
+ for (const msg of messages) {
1429
+ if (msg.role !== 'assistant') continue;
1430
+
1431
+ // ★ 检查两种格式:
1432
+ // 1. content array 里含 tool-call part(AI SDK 标准)
1433
+ // 2. 顶层 tool_calls 字段(OpenAI 兼容格式,AI SDK 旧版本/未规范化)
1434
+ const contentParts = Array.isArray(msg.content) ? msg.content : [];
1435
+ const hasToolCallPart = contentParts.some(c => c && (c.type === 'tool-call' || c.type === 'tool-use'));
1436
+ const hasTopLevelToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
1437
+ if (!hasToolCallPart && !hasTopLevelToolCalls) continue;
1438
+
1439
+ // ★ 找 reasoning:两种格式都可能
1440
+ const hasReasoningPart = contentParts.some(c => c && c.type === 'reasoning' && c.text);
1441
+ // 顶层 reasoning_content(旧 fix 留下的字段,已被 AI SDK 不识别)
1442
+ const hasTopLevelReasoning = typeof msg.reasoning_content === 'string' && msg.reasoning_content.length > 0;
1443
+ if (hasReasoningPart || hasTopLevelReasoning) continue;
1444
+
1445
+ // 缺 reasoning:清理
1446
+ if (hasToolCallPart) {
1447
+ msg.content = contentParts.filter(c => {
1448
+ if (c && (c.type === 'tool-call' || c.type === 'tool-use')) {
1449
+ stripped++;
1450
+ return false;
1451
+ }
1452
+ return true;
1453
+ });
1454
+ }
1455
+ if (hasTopLevelToolCalls) {
1456
+ stripped += msg.tool_calls.length;
1457
+ delete msg.tool_calls;
1458
+ }
1459
+ // 清理孤立的 reasoning_content(如果之前 fix 留下的)
1460
+ if (hasTopLevelReasoning) {
1461
+ delete msg.reasoning_content;
1462
+ }
1463
+ }
1464
+ if (stripped > 0) {
1465
+ logger.warn(`[_stripStaleToolCalls] removed ${stripped} stale tool-call(s) (no reasoning_content, would fail DeepSeek)`);
1466
+ }
1467
+ return stripped;
1468
+ }
1469
+
1503
1470
  /**
1504
1471
  * 压缩上下文(用于 prepareStep)
1505
1472
  * @private
package/src/agent/sub.js CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  const { BaseAgent } = require('./base');
10
10
  const { cleanResponse } = require('../utils');
11
- const { generateText, tool, stepCountIs, ToolLoopAgent } = require('ai');
11
+ const { ToolLoop } = require('./tool-loop');
12
12
  const { z } = require('zod');
13
13
  const { logger } = require('../common/logger');
14
14
  const { validateAll } = require('../utils/message-validator');
@@ -61,36 +61,40 @@ class SubAgent extends BaseAgent {
61
61
  addTool(toolDef) { this.tools[toolDef.name] = toolDef; return this; }
62
62
  getTools() { return Object.values(this.tools); }
63
63
 
64
- _getAIProvider() {
65
- const { createAI } = require('../llm/provider');
66
- // 优先从 framework 的 ai 插件拿最新配置(避免构造时拿到 stale apiKey)
64
+ _getOpenAIClient() {
65
+ const { LLMProvider } = require('../llm/provider');
66
+ // 优先从 framework 的 ai 插件拿最新配置
67
67
  const aiPlugin = this.framework?.pluginManager?.get('ai');
68
68
  const aiConfig = aiPlugin ? aiPlugin.getConfig() : {};
69
- return createAI({
69
+ const provider = new LLMProvider({
70
70
  provider: aiConfig.provider || this.provider,
71
71
  model: aiConfig.model || this.model,
72
72
  apiKey: aiConfig.apiKey || this.apiKey,
73
73
  baseURL: aiConfig.baseURL || this.baseURL,
74
74
  });
75
+ return provider.createModel();
75
76
  }
76
77
 
77
- _buildAITools() {
78
- const tools = {};
79
- const all_tools = this.framework?.getTools() || [];
80
- let parentTools = [];
78
+ _buildToolMap() {
79
+ const map = {};
80
+ const allTools = this.framework?.getTools() || [];
81
81
  if (Array.isArray(this.parentTools)) {
82
- parentTools = this.parentTools.map(key => this.bindTools[key.toLocaleLowerCase()] || key);
83
- for (const toolName of parentTools) {
84
- const toolDef = all_tools.find(t => t.name === toolName);
85
- if (toolDef) tools[toolDef.name] = toolDef;
86
- }
87
- for (const tool of all_tools.filter(a => this.defaulTools.includes(a.name))) {
88
- tools[tool.name] = tool;
82
+ const parentToolNames = this.parentTools.map(key => this.bindTools[key.toLocaleLowerCase()] || key);
83
+ // 父工具 + 默认工具
84
+ const names = new Set([...parentToolNames, ...this.defaulTools]);
85
+ for (const tool of allTools) {
86
+ if (names.has(tool.name)) {
87
+ map[tool.name] = tool;
88
+ }
89
89
  }
90
90
  } else {
91
- all_tools.forEach(item => { tools[item.name] = item; });
91
+ allTools.forEach(t => { map[t.name] = t; });
92
+ }
93
+ // 覆盖 / 合并插件自身的工具
94
+ for (const [name, toolDef] of Object.entries(this.tools)) {
95
+ map[name] = toolDef;
92
96
  }
93
- return { ...tools, ...this.tools };
97
+ return map;
94
98
  }
95
99
 
96
100
  _buildSystemPrompt() {
@@ -130,26 +134,25 @@ class SubAgent extends BaseAgent {
130
134
  const maxSteps = options?.maxSteps || 30;
131
135
  const maxRetries = options?.maxRetries ?? this.maxRetries;
132
136
  const retryDelay = options?.retryDelay ?? this.retryDelay;
133
- const aiProvider = this._getAIProvider();
137
+ const client = this._getOpenAIClient();
134
138
 
135
139
  let messages = Array.isArray(taskOrMessages) ? [...taskOrMessages] : [{ role: 'user', content: taskOrMessages }];
136
140
 
137
141
  let lastError;
138
142
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
139
143
  try {
140
- const tools = this.disableTools ? {} : this._buildAITools();
144
+ const tools = this.disableTools ? {} : this._buildToolMap();
141
145
  const systemPrompt = this.disableTools ? this._customSystemPrompt : this._buildSystemPrompt();
142
146
  const validated = this._validateMessagesPairing(messages);
143
147
  if (validated.length !== messages.length) { messages.length = 0; messages.push(...validated); }
144
148
 
145
- const agent = new ToolLoopAgent({
146
- model: aiProvider(this.model),
147
- instructions: systemPrompt,
149
+ const loop = new ToolLoop({
148
150
  tools,
149
- stopWhen: stepCountIs(maxSteps),
151
+ maxSteps,
152
+ prepareStep: this._prepareStep ? this._prepareStep.bind(this) : undefined,
150
153
  });
151
154
 
152
- const result = await agent.generate({ messages, ...this.providerOptions, abortSignal: options.signal });
155
+ const result = await loop.generate({ messages, systemPrompt, model: this.model, client, ...this.providerOptions });
153
156
  messages.push(...result.response.messages);
154
157
  const full_text = cleanResponse(result.text);
155
158
  this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });