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
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ToolLoop — 自实现的工具调用循环
|
|
5
|
+
* 替代 AI SDK 的 ToolLoopAgent,完全本地可控
|
|
6
|
+
*
|
|
7
|
+
* 职责:
|
|
8
|
+
* - 管理 messages → LLM → tool_call → execute → tool_result → LLM 循环
|
|
9
|
+
* - 支持流式和非流式
|
|
10
|
+
* - 支持 prepareStep 回调(压缩、sanitize)
|
|
11
|
+
* - 支持中断信号
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { EventEmitter } = require('../common/events');
|
|
15
|
+
const { logger } = require('../common/logger');
|
|
16
|
+
const log = logger.child('ToolLoop');
|
|
17
|
+
|
|
18
|
+
class ToolLoop extends EventEmitter {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
super();
|
|
21
|
+
this.tools = config.tools || {}; // { name => { execute, description } }
|
|
22
|
+
this.maxSteps = config.maxSteps || 100000;
|
|
23
|
+
this.prepareStep = config.prepareStep || (async () => {});
|
|
24
|
+
this.abortSignal = config.abortSignal || null;
|
|
25
|
+
this._repairFn = config.repairToolCall || null;
|
|
26
|
+
this._thinkingMode = config.thinkingMode === true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_checkAbort() {
|
|
30
|
+
if (this.abortSignal?.aborted) throw new Error('AbortError');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 提取 DeepSeek thinking 配置,清理 AI SDK 残留字段
|
|
35
|
+
*/
|
|
36
|
+
_prepareApiOptions(options) {
|
|
37
|
+
const apiOptions = { ...options };
|
|
38
|
+
if (apiOptions.deepseek?.thinking) {
|
|
39
|
+
apiOptions.thinking = apiOptions.deepseek.thinking;
|
|
40
|
+
}
|
|
41
|
+
delete apiOptions.deepseek;
|
|
42
|
+
delete apiOptions.providerOptions;
|
|
43
|
+
return apiOptions;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 执行工具调用循环
|
|
48
|
+
* @param {Array} toolCalls - 工具调用列表
|
|
49
|
+
* @param {Array} messages - 消息数组(会被修改)
|
|
50
|
+
* @param {Object} opts - { yieldEvents: boolean }
|
|
51
|
+
*/
|
|
52
|
+
async *_executeToolCalls(toolCalls, messages, { yieldEvents = false } = {}) {
|
|
53
|
+
for (const tc of toolCalls) {
|
|
54
|
+
this._checkAbort();
|
|
55
|
+
const toolName = tc.function.name;
|
|
56
|
+
const tool = this.tools[toolName];
|
|
57
|
+
if (!tool) {
|
|
58
|
+
const errMsg = '工具不存在: ' + toolName;
|
|
59
|
+
this._addToolResult(messages, tc.id, toolName, null, errMsg);
|
|
60
|
+
if (yieldEvents) yield { type: 'error', error: errMsg };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let args;
|
|
65
|
+
try {
|
|
66
|
+
args = JSON.parse(tc.function.arguments);
|
|
67
|
+
} catch (parseErr) {
|
|
68
|
+
try {
|
|
69
|
+
args = this._repairArgs(toolName, tc.function.arguments, parseErr);
|
|
70
|
+
} catch {
|
|
71
|
+
const errMsg = '参数解析失败: ' + parseErr.message;
|
|
72
|
+
this._addToolResult(messages, tc.id, toolName, null, errMsg);
|
|
73
|
+
if (yieldEvents) yield { type: 'error', error: errMsg };
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (yieldEvents) yield { type: 'tool-call', toolName, input: args };
|
|
79
|
+
this.emit('tool-call', { name: toolName, args });
|
|
80
|
+
try {
|
|
81
|
+
const result = await tool.execute(args);
|
|
82
|
+
this.emit('tool-result', { name: toolName, args, result });
|
|
83
|
+
this._addToolResult(messages, tc.id, toolName, result);
|
|
84
|
+
if (yieldEvents) yield { type: 'tool-result', toolName, result };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.emit('tool-error', { name: toolName, args, error: err.message });
|
|
87
|
+
this._addToolResult(messages, tc.id, toolName, null, err.message);
|
|
88
|
+
if (yieldEvents) yield { type: 'error', error: err.message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 处理流式响应块
|
|
95
|
+
* @returns {Object} { finishReason, hasContent, hasReasoning, hasToolCalls, error }
|
|
96
|
+
*/
|
|
97
|
+
_processStreamingChunk(chunk, toolCallAccum, state) {
|
|
98
|
+
const result = { finishReason: null, hasContent: false, hasReasoning: false, hasToolCalls: false, error: null };
|
|
99
|
+
|
|
100
|
+
if (chunk.usage) {
|
|
101
|
+
state.totalUsage = {
|
|
102
|
+
promptTokens: chunk.usage.prompt_tokens || 0,
|
|
103
|
+
completionTokens: chunk.usage.completion_tokens || 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const delta = chunk.choices?.[0]?.delta;
|
|
108
|
+
const choice = chunk.choices?.[0];
|
|
109
|
+
|
|
110
|
+
if (choice?.finish_reason) {
|
|
111
|
+
result.finishReason = choice.finish_reason;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (chunk.error) {
|
|
115
|
+
result.error = chunk.error.message || '流式错误';
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!delta) return result;
|
|
120
|
+
|
|
121
|
+
// Reasoning content
|
|
122
|
+
if (delta.reasoning_content || delta.reasoning) {
|
|
123
|
+
result.hasReasoning = true;
|
|
124
|
+
const text = delta.reasoning_content || delta.reasoning || '';
|
|
125
|
+
state.fullReasoning += text;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Text delta
|
|
129
|
+
if (delta.content) {
|
|
130
|
+
result.hasContent = true;
|
|
131
|
+
state.fullText += delta.content;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Tool calls delta
|
|
135
|
+
if (delta.tool_calls) {
|
|
136
|
+
result.hasToolCalls = true;
|
|
137
|
+
for (const tc of delta.tool_calls) {
|
|
138
|
+
const idx = tc.index;
|
|
139
|
+
if (!toolCallAccum[idx]) {
|
|
140
|
+
toolCallAccum[idx] = {
|
|
141
|
+
id: tc.id || '',
|
|
142
|
+
type: 'function',
|
|
143
|
+
function: { name: '', arguments: '' },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (tc.id) toolCallAccum[idx].id = tc.id;
|
|
147
|
+
if (tc.function?.name) toolCallAccum[idx].function.name += tc.function.name;
|
|
148
|
+
if (tc.function?.arguments) toolCallAccum[idx].function.arguments += tc.function.arguments;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 修复工具参数 JSON
|
|
157
|
+
* 当 LLM 返回的 JSON 无法解析时尝试修复
|
|
158
|
+
*/
|
|
159
|
+
_repairArgs(toolName, argsStr, error) {
|
|
160
|
+
if (this._repairFn) {
|
|
161
|
+
const repaired = this._repairFn({ toolCall: { toolName, input: argsStr }, error });
|
|
162
|
+
if (repaired && repaired.input) {
|
|
163
|
+
return typeof repaired.input === 'string' ? JSON.parse(repaired.input) : repaired.input;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// 基础修复
|
|
167
|
+
const trimmed = (argsStr || '').trim();
|
|
168
|
+
if (trimmed === '') return {};
|
|
169
|
+
if (!trimmed.startsWith('{')) {
|
|
170
|
+
try { return JSON.parse('{' + trimmed + '}'); } catch { /* ignore */ }
|
|
171
|
+
}
|
|
172
|
+
if (trimmed.endsWith('}') || !trimmed.includes('}')) {
|
|
173
|
+
try { return JSON.parse(trimmed + '}'); } catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 构建 OpenAI API 格式的 messages 数组
|
|
180
|
+
* 从内部格式(AI SDK ModelMessage)转换
|
|
181
|
+
*/
|
|
182
|
+
_buildApiMessages(messages, systemPrompt) {
|
|
183
|
+
const api = [];
|
|
184
|
+
if (systemPrompt) api.push({ role: 'system', content: systemPrompt });
|
|
185
|
+
|
|
186
|
+
for (const msg of messages) {
|
|
187
|
+
if (msg.role === 'system') {
|
|
188
|
+
api.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' });
|
|
189
|
+
} else if (msg.role === 'user') {
|
|
190
|
+
if (typeof msg.content === 'string') {
|
|
191
|
+
api.push({ role: 'user', content: msg.content });
|
|
192
|
+
} else if (Array.isArray(msg.content)) {
|
|
193
|
+
const text = msg.content.filter(p => p && p.type === 'text').map(p => p.text).join('');
|
|
194
|
+
api.push({ role: 'user', content: text || '' });
|
|
195
|
+
}
|
|
196
|
+
} else if (msg.role === 'assistant') {
|
|
197
|
+
// ★ 检测是否有 reasoning 内容(thinking mode 必须携带)
|
|
198
|
+
const hasReasoningInContent = Array.isArray(msg.content) && msg.content.some(p => p && p.type === 'reasoning');
|
|
199
|
+
const hasReasoningTopLevel = msg.reasoning_content || msg.reasoning;
|
|
200
|
+
|
|
201
|
+
if (typeof msg.content === 'string') {
|
|
202
|
+
if (msg.tool_calls && Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) {
|
|
203
|
+
// thinking mode 下,无 reasoning 则自动补 placeholder
|
|
204
|
+
const reasonVal = hasReasoningTopLevel || (this._thinkingMode ? '...' : '');
|
|
205
|
+
api.push({ role: 'assistant', content: msg.content || null, tool_calls: msg.tool_calls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
|
|
206
|
+
} else {
|
|
207
|
+
api.push({ role: 'assistant', content: msg.content });
|
|
208
|
+
}
|
|
209
|
+
} else if (Array.isArray(msg.content)) {
|
|
210
|
+
let text = '';
|
|
211
|
+
const toolCalls = [];
|
|
212
|
+
for (const part of msg.content) {
|
|
213
|
+
if (!part) continue;
|
|
214
|
+
if (part.type === 'text') text += part.text;
|
|
215
|
+
else if (part.type === 'tool-call' || part.type === 'tool-use') {
|
|
216
|
+
toolCalls.push({
|
|
217
|
+
id: part.toolCallId,
|
|
218
|
+
type: 'function',
|
|
219
|
+
function: {
|
|
220
|
+
name: part.toolName,
|
|
221
|
+
arguments: typeof part.input === 'string' ? part.input : JSON.stringify(part.input ?? {}),
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (toolCalls.length > 0) {
|
|
228
|
+
const reasonVal = hasReasoningTopLevel || (this._thinkingMode ? '...' : '');
|
|
229
|
+
api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
|
|
230
|
+
} else {
|
|
231
|
+
api.push({ role: 'assistant', content: text });
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
api.push({ role: 'assistant', content: String(msg.content ?? '') });
|
|
235
|
+
}
|
|
236
|
+
} else if (msg.role === 'tool') {
|
|
237
|
+
if (Array.isArray(msg.content)) {
|
|
238
|
+
for (const part of msg.content) {
|
|
239
|
+
if (!part || (part.type !== 'tool-result' && part.type !== 'tool_result')) continue;
|
|
240
|
+
const output = part.output;
|
|
241
|
+
let content;
|
|
242
|
+
if (output?.type === 'text') content = output.value;
|
|
243
|
+
else if (output?.type === 'json') content = JSON.stringify(output.value);
|
|
244
|
+
else content = typeof output === 'string' ? output : JSON.stringify(output);
|
|
245
|
+
api.push({ role: 'tool', tool_call_id: part.toolCallId, content });
|
|
246
|
+
}
|
|
247
|
+
} else if (msg.tool_call_id && typeof msg.content === 'string') {
|
|
248
|
+
// 已经是 OpenAI 格式(streaming 完成后的消息)
|
|
249
|
+
api.push(msg);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return api;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 构建 OpenAI API 格式的 tools 数组
|
|
258
|
+
* 从内部的 tools map 转换
|
|
259
|
+
*/
|
|
260
|
+
_buildApiTools(tools) {
|
|
261
|
+
if (!tools || Object.keys(tools).length === 0) return undefined;
|
|
262
|
+
return Object.values(tools).map(t => {
|
|
263
|
+
const paramSchema = t.parameters || t.inputSchema;
|
|
264
|
+
// ★ 确保 schema 是纯 JSON Schema 对象(不是 zod 实例)
|
|
265
|
+
// OpenAI API 要求 tools 的 parameters 必须是 {type: 'object', ...}
|
|
266
|
+
// zod 实例的 JSON 序列化物没有 type: 'object',API 会抛 400
|
|
267
|
+
const jsonSchema = this._toJsonSchema(paramSchema);
|
|
268
|
+
return {
|
|
269
|
+
type: 'function',
|
|
270
|
+
function: {
|
|
271
|
+
name: t.name,
|
|
272
|
+
description: t.description || '',
|
|
273
|
+
parameters: jsonSchema,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 将 zod schema 或任意 schema 转为纯 JSON Schema 对象
|
|
281
|
+
* 递归处理所有嵌套类型(ZodArray → {type:'array', items} 等)
|
|
282
|
+
* 确保 OpenAI API 能正确识别参数结构
|
|
283
|
+
*/
|
|
284
|
+
_toJsonSchema(schema) {
|
|
285
|
+
if (!schema) return { type: 'object', properties: {} };
|
|
286
|
+
if (schema.properties) return schema; // 已经是 JSON Schema
|
|
287
|
+
if (typeof schema.safeParse !== 'function') return { type: 'object', properties: {} };
|
|
288
|
+
return this._zodToJson(schema);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* 递归转换 zod 实例为 JSON Schema
|
|
293
|
+
*/
|
|
294
|
+
_zodToJson(zod) {
|
|
295
|
+
if (!zod || !zod._def) return {};
|
|
296
|
+
const def = zod._def;
|
|
297
|
+
const tn = def.typeName || '';
|
|
298
|
+
|
|
299
|
+
// 先解包包装类型
|
|
300
|
+
if (tn === 'ZodOptional') {
|
|
301
|
+
const inner = this._zodToJson(def.innerType);
|
|
302
|
+
return inner;
|
|
303
|
+
}
|
|
304
|
+
if (tn === 'ZodDefault') {
|
|
305
|
+
const inner = this._zodToJson(def.innerType);
|
|
306
|
+
if (def.defaultValue !== undefined) inner.default = def.defaultValue;
|
|
307
|
+
return inner;
|
|
308
|
+
}
|
|
309
|
+
if (tn === 'ZodNullable') {
|
|
310
|
+
const inner = this._zodToJson(def.innerType);
|
|
311
|
+
inner.type = [inner.type, 'null'];
|
|
312
|
+
return inner;
|
|
313
|
+
}
|
|
314
|
+
if (tn === 'ZodBranded') return this._zodToJson(def.type);
|
|
315
|
+
if (tn === 'ZodEffects') return this._zodToJson(def.schema || def.innerType);
|
|
316
|
+
if (tn === 'ZodReadonly') return this._zodToJson(def.innerType);
|
|
317
|
+
|
|
318
|
+
// 基本类型
|
|
319
|
+
if (tn === 'ZodString') {
|
|
320
|
+
const r = { type: 'string' };
|
|
321
|
+
if (def.description) r.description = def.description;
|
|
322
|
+
return r;
|
|
323
|
+
}
|
|
324
|
+
if (tn === 'ZodNumber' || tn === 'ZodInt' || tn === 'ZodBigInt') {
|
|
325
|
+
const r = { type: 'number' };
|
|
326
|
+
if (def.description) r.description = def.description;
|
|
327
|
+
return r;
|
|
328
|
+
}
|
|
329
|
+
if (tn === 'ZodBoolean') {
|
|
330
|
+
const r = { type: 'boolean' };
|
|
331
|
+
if (def.description) r.description = def.description;
|
|
332
|
+
return r;
|
|
333
|
+
}
|
|
334
|
+
if (tn === 'ZodNull') return { type: 'null' };
|
|
335
|
+
if (tn === 'ZodAny' || tn === 'ZodUnknown') return {};
|
|
336
|
+
|
|
337
|
+
// 枚举
|
|
338
|
+
if (tn === 'ZodEnum' || tn === 'ZodNativeEnum') {
|
|
339
|
+
return { type: 'string', enum: def.values || Object.values(def.values || {}), description: def.description };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 数组
|
|
343
|
+
if (tn === 'ZodArray') {
|
|
344
|
+
const r = { type: 'array', items: this._zodToJson(def.type) };
|
|
345
|
+
if (def.description) r.description = def.description;
|
|
346
|
+
if (def.minLength) r.minItems = def.minLength.value;
|
|
347
|
+
if (def.maxLength) r.maxItems = def.maxLength.value;
|
|
348
|
+
return r;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// 对象
|
|
352
|
+
if (tn === 'ZodObject') {
|
|
353
|
+
const shape = typeof def.shape === 'function' ? def.shape() : def.shape;
|
|
354
|
+
if (!shape) return { type: 'object' };
|
|
355
|
+
const properties = {};
|
|
356
|
+
const required = [];
|
|
357
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
358
|
+
properties[key] = this._zodToJson(field);
|
|
359
|
+
// 判断必填:没有 isOptional 方法 或 isOptional() 返回 false
|
|
360
|
+
if (typeof field.isOptional !== 'function' || !field.isOptional()) {
|
|
361
|
+
required.push(key);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return { type: 'object', properties, required: required.length > 0 ? required : undefined, description: def.description };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 联合
|
|
368
|
+
if (tn === 'ZodUnion') {
|
|
369
|
+
const options = def.options?.map(o => this._zodToJson(o)) || [];
|
|
370
|
+
return { anyOf: options, description: def.description };
|
|
371
|
+
}
|
|
372
|
+
if (tn === 'ZodDiscriminatedUnion') {
|
|
373
|
+
const options = Object.values(def.optionsMap || {}).map(o => this._zodToJson(o));
|
|
374
|
+
return { anyOf: options, description: def.description };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// 记录
|
|
378
|
+
if (tn === 'ZodRecord') {
|
|
379
|
+
return { type: 'object', additionalProperties: this._zodToJson(def.valueType), description: def.description };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 元组
|
|
383
|
+
if (tn === 'ZodTuple') {
|
|
384
|
+
const items = (def.items || []).map(item => this._zodToJson(item));
|
|
385
|
+
return { type: 'array', items, description: def.description };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 函数(OpenAI 不支持函数参数 schema,返回空对象)
|
|
389
|
+
if (tn === 'ZodFunction') {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Map
|
|
394
|
+
if (tn === 'ZodMap') {
|
|
395
|
+
return { type: 'object', additionalProperties: this._zodToJson(def.valueType), description: def.description };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Literal
|
|
399
|
+
if (tn === 'ZodLiteral') {
|
|
400
|
+
const val = def.value;
|
|
401
|
+
if (typeof val === 'string') return { type: 'string', const: val };
|
|
402
|
+
if (typeof val === 'number') return { type: 'number', const: val };
|
|
403
|
+
if (typeof val === 'boolean') return { type: 'boolean', const: val };
|
|
404
|
+
return {};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 兜底
|
|
408
|
+
return { description: def.description };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 以内部格式添加 tool result 到 messages
|
|
413
|
+
* 使用 { type: 'tool-result' } part 格式(兼容 sanitizeForLLM / validateAll)
|
|
414
|
+
*/
|
|
415
|
+
_addToolResult(messages, toolCallId, toolName, content, error) {
|
|
416
|
+
const output = error
|
|
417
|
+
? { type: 'text', value: JSON.stringify({ error }) }
|
|
418
|
+
: { type: 'text', value: typeof content === 'string' ? content : JSON.stringify(content) };
|
|
419
|
+
messages.push({
|
|
420
|
+
role: 'tool',
|
|
421
|
+
content: [{ type: 'tool-result', toolCallId, toolName, output }],
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* 非流式生成
|
|
427
|
+
* @returns {{ text, response: { messages }, steps, usage, reasoningText }}
|
|
428
|
+
*/
|
|
429
|
+
async generate({ messages, systemPrompt, model, client, ...options }) {
|
|
430
|
+
this._checkAbort();
|
|
431
|
+
let step = 0;
|
|
432
|
+
let fullText = '';
|
|
433
|
+
let fullReasoning = '';
|
|
434
|
+
let totalUsage = null;
|
|
435
|
+
|
|
436
|
+
while (step < this.maxSteps) {
|
|
437
|
+
this._checkAbort();
|
|
438
|
+
|
|
439
|
+
// 1. 准备步骤(压缩、sanitize)
|
|
440
|
+
if (this.prepareStep) {
|
|
441
|
+
await this.prepareStep({ stepNumber: step, messages });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// 2. 构建 API 请求
|
|
445
|
+
const apiMessages = this._buildApiMessages(messages, systemPrompt);
|
|
446
|
+
const apiTools = this._buildApiTools(this.tools);
|
|
447
|
+
const apiOptions = this._prepareApiOptions(options);
|
|
448
|
+
|
|
449
|
+
let response;
|
|
450
|
+
try {
|
|
451
|
+
response = await client.chat.completions.create({
|
|
452
|
+
model,
|
|
453
|
+
messages: apiMessages,
|
|
454
|
+
tools: apiTools,
|
|
455
|
+
...apiOptions,
|
|
456
|
+
stream: false,
|
|
457
|
+
});
|
|
458
|
+
} catch (apiErr) {
|
|
459
|
+
log.error(`[ToolLoop] API call failed: ${apiErr.message}`);
|
|
460
|
+
const recent = apiMessages.slice(-3).map(m => ({
|
|
461
|
+
role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
|
|
462
|
+
}));
|
|
463
|
+
log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
|
|
464
|
+
throw apiErr;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 3. 解析响应
|
|
468
|
+
const choice = response.choices?.[0];
|
|
469
|
+
if (!choice) throw new Error('API 响应没有 choices');
|
|
470
|
+
const message = choice.message;
|
|
471
|
+
|
|
472
|
+
// 跟踪用量
|
|
473
|
+
if (response.usage) {
|
|
474
|
+
totalUsage = {
|
|
475
|
+
promptTokens: response.usage.prompt_tokens || 0,
|
|
476
|
+
completionTokens: response.usage.completion_tokens || 0,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// 收集 reasoning
|
|
481
|
+
const reasoningContent = message.reasoning_content || message.reasoning || '';
|
|
482
|
+
if (reasoningContent) fullReasoning += reasoningContent;
|
|
483
|
+
|
|
484
|
+
// 4. 处理工具调用
|
|
485
|
+
if (choice.finish_reason === 'tool_calls' && message.tool_calls && message.tool_calls.length > 0) {
|
|
486
|
+
// 插入 assistant 消息(保留原格式,后面的 _processMessage 会存盘)
|
|
487
|
+
const assistantMsg = {
|
|
488
|
+
role: 'assistant',
|
|
489
|
+
content: message.content || '',
|
|
490
|
+
};
|
|
491
|
+
if (message.tool_calls.length > 0) {
|
|
492
|
+
assistantMsg.tool_calls = message.tool_calls;
|
|
493
|
+
}
|
|
494
|
+
// ★ 保存 reasoning_content(下次 API 调用时需要传回)
|
|
495
|
+
if (reasoningContent) {
|
|
496
|
+
assistantMsg.reasoning_content = reasoningContent;
|
|
497
|
+
}
|
|
498
|
+
messages.push(assistantMsg);
|
|
499
|
+
|
|
500
|
+
// 执行工具
|
|
501
|
+
for await (const _ of this._executeToolCalls(message.tool_calls, messages)) {
|
|
502
|
+
// 非流式模式,不 yield 事件
|
|
503
|
+
}
|
|
504
|
+
step++;
|
|
505
|
+
} else {
|
|
506
|
+
// 没有工具调用 → 完成
|
|
507
|
+
fullText = message.content || '';
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
text: fullText,
|
|
514
|
+
response: { messages },
|
|
515
|
+
steps: step,
|
|
516
|
+
usage: totalUsage,
|
|
517
|
+
reasoningText: fullReasoning || undefined,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 流式生成
|
|
523
|
+
* yield { type: 'text-delta'|'thinking'|'tool-call'|'tool-result'|'usage'|'error', ... }
|
|
524
|
+
* 最终通过 result.response.messages 拿完整消息列表
|
|
525
|
+
*/
|
|
526
|
+
async *stream({ messages, systemPrompt, model, client, ...options }) {
|
|
527
|
+
this._checkAbort();
|
|
528
|
+
let step = 0;
|
|
529
|
+
let fullText = '';
|
|
530
|
+
let fullReasoning = '';
|
|
531
|
+
let totalUsage = null;
|
|
532
|
+
|
|
533
|
+
while (step < this.maxSteps) {
|
|
534
|
+
this._checkAbort();
|
|
535
|
+
|
|
536
|
+
// 1. 准备步骤
|
|
537
|
+
if (this.prepareStep) {
|
|
538
|
+
await this.prepareStep({ stepNumber: step, messages });
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// 2. 构建请求
|
|
542
|
+
const apiMessages = this._buildApiMessages(messages, systemPrompt);
|
|
543
|
+
const apiTools = this._buildApiTools(this.tools);
|
|
544
|
+
const apiOptions = this._prepareApiOptions(options);
|
|
545
|
+
|
|
546
|
+
let stream;
|
|
547
|
+
try {
|
|
548
|
+
stream = await client.chat.completions.create({
|
|
549
|
+
model,
|
|
550
|
+
messages: apiMessages,
|
|
551
|
+
tools: apiTools,
|
|
552
|
+
...apiOptions,
|
|
553
|
+
stream: true,
|
|
554
|
+
stream_options: { include_usage: true },
|
|
555
|
+
});
|
|
556
|
+
} catch (apiErr) {
|
|
557
|
+
log.error(`[ToolLoop] stream call failed: ${apiErr.message}`);
|
|
558
|
+
log.error(`[ToolLoop] model=${model} msgs=${apiMessages.length}`);
|
|
559
|
+
const recent = apiMessages.slice(-3).map(m => ({
|
|
560
|
+
role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
|
|
561
|
+
}));
|
|
562
|
+
log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
|
|
563
|
+
throw apiErr;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 3. 处理流式响应(累计 tool_calls delta)
|
|
567
|
+
const toolCallAccum = {};
|
|
568
|
+
let finishReason = null;
|
|
569
|
+
const state = { fullText: '', fullReasoning: '', totalUsage: null };
|
|
570
|
+
|
|
571
|
+
for await (const chunk of stream) {
|
|
572
|
+
this._checkAbort();
|
|
573
|
+
|
|
574
|
+
const result = this._processStreamingChunk(chunk, toolCallAccum, state);
|
|
575
|
+
|
|
576
|
+
if (result.error) {
|
|
577
|
+
yield { type: 'error', error: result.error };
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// 追踪 finish_reason
|
|
582
|
+
if (result.finishReason) {
|
|
583
|
+
finishReason = result.finishReason;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Reasoning content
|
|
587
|
+
if (result.hasReasoning) {
|
|
588
|
+
yield { type: 'thinking', text: state.fullReasoning.slice(fullReasoning.length) };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Text delta
|
|
592
|
+
if (result.hasContent) {
|
|
593
|
+
const textStart = fullText.length;
|
|
594
|
+
fullText += state.fullText.slice(textStart);
|
|
595
|
+
yield { type: 'text-delta', text: state.fullText.slice(textStart) };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Tool calls delta
|
|
599
|
+
if (result.hasToolCalls) {
|
|
600
|
+
// toolCallAccum 已通过 _processStreamingChunk 更新,无需额外操作
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 同步状态
|
|
605
|
+
totalUsage = state.totalUsage;
|
|
606
|
+
fullReasoning = state.fullReasoning;
|
|
607
|
+
|
|
608
|
+
const toolCalls = Object.values(toolCallAccum).filter(tc => tc.id);
|
|
609
|
+
|
|
610
|
+
if (finishReason === 'tool_calls' && toolCalls.length > 0) {
|
|
611
|
+
// 插入 assistant 消息(含 tool_calls)
|
|
612
|
+
const assistantMsg = { role: 'assistant', content: fullText || '', tool_calls: toolCalls };
|
|
613
|
+
// ★ 保存 reasoning_content(下次 API 调用时需要传回)
|
|
614
|
+
if (fullReasoning) assistantMsg.reasoning_content = fullReasoning;
|
|
615
|
+
messages.push(assistantMsg);
|
|
616
|
+
fullText = '';
|
|
617
|
+
fullReasoning = ''; // ★ 重置,下轮单独收集
|
|
618
|
+
|
|
619
|
+
// 执行工具
|
|
620
|
+
for await (const event of this._executeToolCalls(toolCalls, messages, { yieldEvents: true })) {
|
|
621
|
+
yield event;
|
|
622
|
+
}
|
|
623
|
+
step++;
|
|
624
|
+
} else {
|
|
625
|
+
// 无工具调用 → 完成(与 generate() 保持一致:确保 fullText 有值)
|
|
626
|
+
fullText = state.fullText || fullText;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// 输出用量
|
|
632
|
+
yield {
|
|
633
|
+
type: 'usage',
|
|
634
|
+
inputTokens: totalUsage?.promptTokens || 0,
|
|
635
|
+
outputTokens: totalUsage?.completionTokens || 0,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
return {
|
|
639
|
+
text: fullText,
|
|
640
|
+
response: { messages },
|
|
641
|
+
steps: step,
|
|
642
|
+
usage: totalUsage,
|
|
643
|
+
reasoningText: fullReasoning || undefined,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
module.exports = { ToolLoop };
|