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.
Files changed (42) hide show
  1. package/.editorconfig +56 -56
  2. package/.lintstagedrc +7 -7
  3. package/.prettierignore +29 -29
  4. package/.prettierrc +11 -11
  5. package/Dockerfile +63 -63
  6. package/install.ps1 +129 -129
  7. package/install.sh +121 -121
  8. package/package.json +3 -5
  9. package/plugins/core/skill-manager/index.js +34 -23
  10. package/plugins/core/sub-agent/index.js +103 -14
  11. package/plugins/core/workflow/context.js +941 -941
  12. package/plugins/core/workflow/engine.js +66 -66
  13. package/plugins/core/workflow/js-runner.js +318 -318
  14. package/plugins/core/workflow/json-runner.js +323 -323
  15. package/plugins/core/workflow/stages/choice.js +74 -74
  16. package/plugins/core/workflow/stages/each.js +123 -123
  17. package/plugins/core/workflow/stages/parallel.js +69 -69
  18. package/plugins/executors/extension/extension-registry.js +72 -1
  19. package/plugins/executors/extension/index.js +68 -9
  20. package/plugins/io/file-system/index.js +377 -153
  21. package/skills/find-skills/SKILL.md +133 -133
  22. package/src/agent/chat.js +207 -222
  23. package/src/agent/sub.js +29 -26
  24. package/src/agent/tool-loop.js +648 -0
  25. package/src/llm/provider.js +12 -28
  26. package/src/plugin/base.js +17 -14
  27. package/src/plugin/manager.js +19 -0
  28. package/src/tool/router.js +2 -2
  29. package/src/utils/message-validator.js +186 -0
  30. package/tests/core/chat-tool.test.js +187 -0
  31. package/tests/core/disable-thinking.test.js +64 -0
  32. package/tests/core/edit-file.test.js +194 -0
  33. package/tests/core/ext-call-empty-args.test.js +136 -0
  34. package/tests/core/reasoning-content.test.js +129 -0
  35. package/tests/core/sanitize-for-llm.test.js +152 -0
  36. package/tests/core/search.test.js +212 -0
  37. package/tests/core/skill-input-schema.test.js +150 -0
  38. package/tests/core/strip-stale-tool-calls.test.js +154 -0
  39. package/tests/core/sub-agent-parse.test.js +247 -0
  40. package/tests/core/sub-agent-skills.test.js +197 -0
  41. package/tests/core/tool-loop.test.js +208 -0
  42. 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 };