foliko 2.0.21 → 2.0.23
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/.editorconfig +56 -56
- package/.lintstagedrc +7 -7
- package/.prettierignore +29 -29
- package/.prettierrc +11 -11
- package/Dockerfile +63 -63
- package/install.ps1 +129 -129
- package/install.sh +121 -121
- package/package.json +3 -5
- package/plugins/core/skill-manager/index.js +34 -23
- package/plugins/core/sub-agent/index.js +103 -14
- package/plugins/core/workflow/context.js +941 -941
- package/plugins/core/workflow/engine.js +66 -66
- package/plugins/core/workflow/js-runner.js +318 -318
- package/plugins/core/workflow/json-runner.js +323 -323
- package/plugins/core/workflow/stages/choice.js +74 -74
- package/plugins/core/workflow/stages/each.js +123 -123
- package/plugins/core/workflow/stages/parallel.js +69 -69
- package/plugins/executors/extension/extension-registry.js +72 -1
- package/plugins/executors/extension/index.js +68 -9
- package/plugins/io/file-system/index.js +377 -153
- package/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +207 -222
- package/src/agent/sub.js +29 -26
- package/src/agent/tool-loop.js +648 -0
- package/src/llm/provider.js +12 -28
- package/src/plugin/base.js +17 -14
- package/src/plugin/manager.js +19 -0
- package/src/tool/router.js +2 -2
- package/src/utils/message-validator.js +186 -0
- package/tests/core/chat-tool.test.js +187 -0
- package/tests/core/disable-thinking.test.js +64 -0
- package/tests/core/edit-file.test.js +194 -0
- package/tests/core/ext-call-empty-args.test.js +136 -0
- package/tests/core/reasoning-content.test.js +129 -0
- package/tests/core/sanitize-for-llm.test.js +152 -0
- package/tests/core/search.test.js +212 -0
- package/tests/core/skill-input-schema.test.js +150 -0
- package/tests/core/strip-stale-tool-calls.test.js +154 -0
- package/tests/core/sub-agent-parse.test.js +247 -0
- package/tests/core/sub-agent-skills.test.js +197 -0
- package/tests/core/tool-loop.test.js +208 -0
- package/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.jsonl +0 -76
package/src/agent/chat.js
CHANGED
|
@@ -11,15 +11,10 @@
|
|
|
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
|
-
const { normalizeToolOutputs, validateAll } = require('../utils/message-validator');
|
|
17
|
+
const { normalizeToolOutputs, validateAll, sanitizeForLLM } = require('../utils/message-validator');
|
|
23
18
|
const { cleanResponse } = require('../utils');
|
|
24
19
|
const { ChatQueueManager } = require('../common/queue');
|
|
25
20
|
const { TokenCounter } = require('../llm/tokens');
|
|
@@ -60,7 +55,10 @@ class AgentChatHandler extends EventEmitter {
|
|
|
60
55
|
|
|
61
56
|
// DeepSeek thinking mode: 支持通过 config.thinkingMode 控制开关
|
|
62
57
|
// config.thinkingMode = true 可启用,默认为禁用
|
|
63
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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.
|
|
694
|
-
const
|
|
695
|
-
model: this._aiClient,
|
|
696
|
-
instructions: systemPrompt,
|
|
737
|
+
const tools = this._getToolMap();
|
|
738
|
+
const toolLoop = new ToolLoop({
|
|
697
739
|
tools,
|
|
698
|
-
|
|
740
|
+
maxSteps: this._maxSteps || 20,
|
|
699
741
|
prepareStep: this._createPrepareStep(),
|
|
700
|
-
|
|
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
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
724
|
-
// 追踪发送的 tool-call ID,过滤 API 返回的不匹配 tool-result
|
|
725
|
-
const sentToolCallIds = new Set();
|
|
760
|
+
let usageRecv = {};
|
|
726
761
|
|
|
727
|
-
for await (const part of
|
|
762
|
+
for await (const part of stream) {
|
|
728
763
|
if (part.type === 'text-delta') {
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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.
|
|
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
|
|
934
|
-
model: this._aiClient,
|
|
935
|
-
instructions: systemPrompt,
|
|
913
|
+
const toolLoop = new ToolLoop({
|
|
936
914
|
tools,
|
|
937
|
-
|
|
915
|
+
maxSteps: this._maxSteps || 20,
|
|
938
916
|
prepareStep: this._createPrepareStep(),
|
|
939
|
-
|
|
917
|
+
repairToolCall: this._repairToolCall.bind(this),
|
|
918
|
+
abortSignal: framework.getAbortSignal?.(),
|
|
919
|
+
thinkingMode: this._thinkingMode,
|
|
940
920
|
});
|
|
941
921
|
|
|
942
|
-
const result = await
|
|
943
|
-
|
|
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
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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,
|
|
@@ -1083,6 +1054,15 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1083
1054
|
logger.warn(`[_prepareSession] 去重 ${deduped} 个重复 tool-call ID`);
|
|
1084
1055
|
}
|
|
1085
1056
|
|
|
1057
|
+
// ★ 防御性 sanitize:确保所有 message 符合 AI SDK ModelMessage[] schema
|
|
1058
|
+
// 修复/丢弃:content 类型错误、part 缺 type、tool-result.output 异常等
|
|
1059
|
+
const sanitized = sanitizeForLLM(messages);
|
|
1060
|
+
if (sanitized.fixed > 0 || sanitized.dropped > 0) {
|
|
1061
|
+
logger.warn(`[_prepareSession] sanitize: fixed=${sanitized.fixed}, dropped=${sanitized.dropped}`);
|
|
1062
|
+
messages.length = 0;
|
|
1063
|
+
messages.push(...sanitized.messages);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1086
1066
|
// 单次遍历完成所有验证 + 规范化(validateAll 内部处理 error-text/error-json)
|
|
1087
1067
|
const validated = validateAll(messages);
|
|
1088
1068
|
if (validated.length !== messages.length) {
|
|
@@ -1334,78 +1314,6 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1334
1314
|
return createHash('sha256').update(toolDescriptors).digest('hex').slice(0, 32);
|
|
1335
1315
|
}
|
|
1336
1316
|
|
|
1337
|
-
/**
|
|
1338
|
-
* 获取 AI 工具格式(带缓存)
|
|
1339
|
-
* 使用 AI SDK 的 tool() 格式
|
|
1340
|
-
* @private
|
|
1341
|
-
*/
|
|
1342
|
-
_getAITools(toolFn) {
|
|
1343
|
-
const currentKey = this._computeToolsCacheKey();
|
|
1344
|
-
if (this._aiToolsCache && this._aiToolsCacheKey === currentKey) {
|
|
1345
|
-
return this._aiToolsCache;
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
const tools = {};
|
|
1349
|
-
const allTools = this.agent.framework.getTools();
|
|
1350
|
-
for (const toolDef of allTools) {
|
|
1351
|
-
const toolName = toolDef.name;
|
|
1352
|
-
|
|
1353
|
-
// 使用 AI SDK 的 tool() 格式
|
|
1354
|
-
const toolConfig = {
|
|
1355
|
-
name: toolName,
|
|
1356
|
-
description: toolDef.description || '',
|
|
1357
|
-
execute: async (args) => {
|
|
1358
|
-
// 清理参数并验证类型
|
|
1359
|
-
const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
|
|
1360
|
-
|
|
1361
|
-
// 执行工具
|
|
1362
|
-
this.emit('tool-call', { name: toolName, args: cleanedArgs });
|
|
1363
|
-
// logger.info(`[Tool] Call: ${toolName}`);
|
|
1364
|
-
try {
|
|
1365
|
-
const result = await toolDef.execute(cleanedArgs, this.agent.framework);
|
|
1366
|
-
|
|
1367
|
-
// 自动检测大工具结果,透明分拆
|
|
1368
|
-
// 仅在数据分拆插件已加载时生效
|
|
1369
|
-
let finalResult = result;
|
|
1370
|
-
try {
|
|
1371
|
-
const splitCheck = await autoSplitToolResult(toolName, result, this.agent.framework);
|
|
1372
|
-
if (splitCheck.wasSplit && splitCheck.result) {
|
|
1373
|
-
finalResult = splitCheck.result;
|
|
1374
|
-
// logger.info(
|
|
1375
|
-
// `[AutoSplit] 工具 "${toolName}" 结果过大,已自动分拆处理`
|
|
1376
|
-
// );
|
|
1377
|
-
}
|
|
1378
|
-
} catch (splitErr) {
|
|
1379
|
-
// 分拆失败不阻断主流程,继续使用原始结果
|
|
1380
|
-
logger.warn(`[AutoSplit] 自动分拆跳过: ${splitErr.message}`);
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
this.emit('tool-result', { name: toolName, args: cleanedArgs, result: finalResult });
|
|
1384
|
-
return finalResult;
|
|
1385
|
-
} catch (err) {
|
|
1386
|
-
this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
|
|
1387
|
-
// 返回错误信息字符串,而不是抛出异常
|
|
1388
|
-
return { success: false, error: err.message }
|
|
1389
|
-
}
|
|
1390
|
-
},
|
|
1391
|
-
};
|
|
1392
|
-
|
|
1393
|
-
// 支持 inputSchema 或 parameters
|
|
1394
|
-
if (toolDef.inputSchema) {
|
|
1395
|
-
toolConfig.inputSchema = toolDef.inputSchema;
|
|
1396
|
-
} else if (toolDef.parameters) {
|
|
1397
|
-
toolConfig.parameters = toolDef.parameters;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
// AI SDK 6.x 使用对象形式,键为工具名
|
|
1401
|
-
tools[toolName] = toolFn(toolConfig);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
this._aiToolsCache = tools;
|
|
1405
|
-
this._aiToolsCacheKey = currentKey;
|
|
1406
|
-
return tools;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
1317
|
/**
|
|
1410
1318
|
* 清理工具参数并根据 inputSchema 验证类型
|
|
1411
1319
|
* @private
|
|
@@ -1423,6 +1331,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1423
1331
|
if (value === undefined || typeof value === 'function' || typeof value === 'symbol') {
|
|
1424
1332
|
continue;
|
|
1425
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
|
+
}
|
|
1426
1342
|
cleaned[key] = value;
|
|
1427
1343
|
}
|
|
1428
1344
|
|
|
@@ -1460,10 +1376,23 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1460
1376
|
inputMessages.push(...trimmed);
|
|
1461
1377
|
}
|
|
1462
1378
|
|
|
1379
|
+
// ★ _buildApiMessages 现在会自动补 placeholder reasoning_content
|
|
1380
|
+
// (对于 thinking mode 下无 reasoning 的旧消息,补 '...')
|
|
1381
|
+
// 所以不再需要 _stripStaleToolCalls 提前清理
|
|
1382
|
+
|
|
1463
1383
|
// 单次遍历完成:修复 invalid tool-call + 修复错误 text + 重映射历史 ID
|
|
1464
1384
|
const repairedCount = this._repairInvalidToolCallsInMessages(inputMessages);
|
|
1465
1385
|
this._repairAndPrefixHistoricalIds(inputMessages);
|
|
1466
1386
|
|
|
1387
|
+
// ★ 防御性 sanitize:确保所有 message 符合 AI SDK ModelMessage[] schema
|
|
1388
|
+
// 每次 LLM step 前都跑一次(messages 数组里可能有 AI SDK 上一步加进来的)
|
|
1389
|
+
const sanitized = sanitizeForLLM(inputMessages);
|
|
1390
|
+
if (sanitized.fixed > 0 || sanitized.dropped > 0) {
|
|
1391
|
+
logger.warn(`[PrepareStep] sanitize: fixed=${sanitized.fixed}, dropped=${sanitized.dropped}`);
|
|
1392
|
+
inputMessages.length = 0;
|
|
1393
|
+
inputMessages.push(...sanitized.messages);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1467
1396
|
// 单次遍历完成所有验证 + 规范化
|
|
1468
1397
|
const validated = validateAll(inputMessages);
|
|
1469
1398
|
if (validated.length !== inputMessages.length) {
|
|
@@ -1482,6 +1411,62 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1482
1411
|
};
|
|
1483
1412
|
}
|
|
1484
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
|
+
|
|
1485
1470
|
/**
|
|
1486
1471
|
* 压缩上下文(用于 prepareStep)
|
|
1487
1472
|
* @private
|