foliko 2.0.31 → 2.0.32

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.
@@ -1,133 +1,133 @@
1
- ---
2
- name: find-skills
3
- description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
4
- ---
5
-
6
- # Find Skills
7
-
8
- This skill helps you discover and install skills from the open agent skills ecosystem.
9
-
10
- ## When to Use This Skill
11
-
12
- Use this skill when the user:
13
-
14
- - Asks "how do I do X" where X might be a common task with an existing skill
15
- - Says "find a skill for X" or "is there a skill for X"
16
- - Asks "can you do X" where X is a specialized capability
17
- - Expresses interest in extending agent capabilities
18
- - Wants to search for tools, templates, or workflows
19
- - Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
20
-
21
- ## What is the Skills CLI?
22
-
23
- The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
24
-
25
- **Key commands:**
26
-
27
- - `npx skills find [query]` - Search for skills interactively or by keyword
28
- - `npx skills add <package>` - Install a skill from GitHub or other sources
29
- - `npx skills check` - Check for skill updates
30
- - `npx skills update` - Update all installed skills
31
-
32
- **Browse skills at:** https://skills.sh/
33
-
34
- ## How to Help Users Find Skills
35
-
36
- ### Step 1: Understand What They Need
37
-
38
- When a user asks for help with something, identify:
39
-
40
- 1. The domain (e.g., React, testing, design, deployment)
41
- 2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
42
- 3. Whether this is a common enough task that a skill likely exists
43
-
44
- ### Step 2: Search for Skills
45
-
46
- Run the find command with a relevant query:
47
-
48
- ```bash
49
- npx skills find [query]
50
- ```
51
-
52
- For example:
53
-
54
- - User asks "how do I make my React app faster?" → `npx skills find react performance`
55
- - User asks "can you help me with PR reviews?" → `npx skills find pr review`
56
- - User asks "I need to create a changelog" → `npx skills find changelog`
57
-
58
- The command will return results like:
59
-
60
- ```
61
- Install with npx skills add <owner/repo@skill>
62
-
63
- vercel-labs/agent-skills@vercel-react-best-practices
64
- └ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
65
- ```
66
-
67
- ### Step 3: Present Options to the User
68
-
69
- When you find relevant skills, present them to the user with:
70
-
71
- 1. The skill name and what it does
72
- 2. The install command they can run
73
- 3. A link to learn more at skills.sh
74
-
75
- Example response:
76
-
77
- ```
78
- I found a skill that might help! The "vercel-react-best-practices" skill provides
79
- React and Next.js performance optimization guidelines from Vercel Engineering.
80
-
81
- To install it:
82
- npx skills add vercel-labs/agent-skills@vercel-react-best-practices
83
-
84
- Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
85
- ```
86
-
87
- ### Step 4: Offer to Install
88
-
89
- If the user wants to proceed, you can install the skill for them:
90
-
91
- ```bash
92
- npx skills add <owner/repo@skill> -g -y
93
- ```
94
-
95
- The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
96
-
97
- ## Common Skill Categories
98
-
99
- When searching, consider these common categories:
100
-
101
- | Category | Example Queries |
102
- | --------------- | ---------------------------------------- |
103
- | Web Development | react, nextjs, typescript, css, tailwind |
104
- | Testing | testing, jest, playwright, e2e |
105
- | DevOps | deploy, docker, kubernetes, ci-cd |
106
- | Documentation | docs, readme, changelog, api-docs |
107
- | Code Quality | review, lint, refactor, best-practices |
108
- | Design | ui, ux, design-system, accessibility |
109
- | Productivity | workflow, automation, git |
110
-
111
- ## Tips for Effective Searches
112
-
113
- 1. **Use specific keywords**: "react testing" is better than just "testing"
114
- 2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
115
- 3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
116
-
117
- ## When No Skills Are Found
118
-
119
- If no relevant skills exist:
120
-
121
- 1. Acknowledge that no existing skill was found
122
- 2. Offer to help with the task directly using your general capabilities
123
- 3. Suggest the user could create their own skill with `npx skills init`
124
-
125
- Example:
126
-
127
- ```
128
- I searched for skills related to "xyz" but didn't find any matches.
129
- I can still help you with this task directly! Would you like me to proceed?
130
-
131
- If this is something you do often, you could create your own skill:
132
- npx skills init my-xyz-skill
133
- ```
1
+ ---
2
+ name: find-skills
3
+ description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
4
+ ---
5
+
6
+ # Find Skills
7
+
8
+ This skill helps you discover and install skills from the open agent skills ecosystem.
9
+
10
+ ## When to Use This Skill
11
+
12
+ Use this skill when the user:
13
+
14
+ - Asks "how do I do X" where X might be a common task with an existing skill
15
+ - Says "find a skill for X" or "is there a skill for X"
16
+ - Asks "can you do X" where X is a specialized capability
17
+ - Expresses interest in extending agent capabilities
18
+ - Wants to search for tools, templates, or workflows
19
+ - Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
20
+
21
+ ## What is the Skills CLI?
22
+
23
+ The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
24
+
25
+ **Key commands:**
26
+
27
+ - `npx skills find [query]` - Search for skills interactively or by keyword
28
+ - `npx skills add <package>` - Install a skill from GitHub or other sources
29
+ - `npx skills check` - Check for skill updates
30
+ - `npx skills update` - Update all installed skills
31
+
32
+ **Browse skills at:** https://skills.sh/
33
+
34
+ ## How to Help Users Find Skills
35
+
36
+ ### Step 1: Understand What They Need
37
+
38
+ When a user asks for help with something, identify:
39
+
40
+ 1. The domain (e.g., React, testing, design, deployment)
41
+ 2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
42
+ 3. Whether this is a common enough task that a skill likely exists
43
+
44
+ ### Step 2: Search for Skills
45
+
46
+ Run the find command with a relevant query:
47
+
48
+ ```bash
49
+ npx skills find [query]
50
+ ```
51
+
52
+ For example:
53
+
54
+ - User asks "how do I make my React app faster?" → `npx skills find react performance`
55
+ - User asks "can you help me with PR reviews?" → `npx skills find pr review`
56
+ - User asks "I need to create a changelog" → `npx skills find changelog`
57
+
58
+ The command will return results like:
59
+
60
+ ```
61
+ Install with npx skills add <owner/repo@skill>
62
+
63
+ vercel-labs/agent-skills@vercel-react-best-practices
64
+ └ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
65
+ ```
66
+
67
+ ### Step 3: Present Options to the User
68
+
69
+ When you find relevant skills, present them to the user with:
70
+
71
+ 1. The skill name and what it does
72
+ 2. The install command they can run
73
+ 3. A link to learn more at skills.sh
74
+
75
+ Example response:
76
+
77
+ ```
78
+ I found a skill that might help! The "vercel-react-best-practices" skill provides
79
+ React and Next.js performance optimization guidelines from Vercel Engineering.
80
+
81
+ To install it:
82
+ npx skills add vercel-labs/agent-skills@vercel-react-best-practices
83
+
84
+ Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
85
+ ```
86
+
87
+ ### Step 4: Offer to Install
88
+
89
+ If the user wants to proceed, you can install the skill for them:
90
+
91
+ ```bash
92
+ npx skills add <owner/repo@skill> -g -y
93
+ ```
94
+
95
+ The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
96
+
97
+ ## Common Skill Categories
98
+
99
+ When searching, consider these common categories:
100
+
101
+ | Category | Example Queries |
102
+ | --------------- | ---------------------------------------- |
103
+ | Web Development | react, nextjs, typescript, css, tailwind |
104
+ | Testing | testing, jest, playwright, e2e |
105
+ | DevOps | deploy, docker, kubernetes, ci-cd |
106
+ | Documentation | docs, readme, changelog, api-docs |
107
+ | Code Quality | review, lint, refactor, best-practices |
108
+ | Design | ui, ux, design-system, accessibility |
109
+ | Productivity | workflow, automation, git |
110
+
111
+ ## Tips for Effective Searches
112
+
113
+ 1. **Use specific keywords**: "react testing" is better than just "testing"
114
+ 2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
115
+ 3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
116
+
117
+ ## When No Skills Are Found
118
+
119
+ If no relevant skills exist:
120
+
121
+ 1. Acknowledge that no existing skill was found
122
+ 2. Offer to help with the task directly using your general capabilities
123
+ 3. Suggest the user could create their own skill with `npx skills init`
124
+
125
+ Example:
126
+
127
+ ```
128
+ I searched for skills related to "xyz" but didn't find any matches.
129
+ I can still help you with this task directly! Would you like me to proceed?
130
+
131
+ If this is something you do often, you could create your own skill:
132
+ npx skills init my-xyz-skill
133
+ ```
package/src/agent/chat.js CHANGED
@@ -116,6 +116,11 @@ class AgentChatHandler extends EventEmitter {
116
116
  this._messageTokenCache = new Map();
117
117
  this._messageTokenCacheMaxSize = 200;
118
118
 
119
+ // ★ 每 session 串行锁:非流式 chat() 不走 queueManager,
120
+ // 同一 session 并发调用会并发 mutate/save 消息数组 → 交错/丢失。
121
+ // 用 promise 链把同 session 的调用串行化;不同 session 仍并行。
122
+ this._sessionLocks = new Map();
123
+
119
124
  // ChatQueueManager: 队列管理
120
125
  this.queueManager = new ChatQueueManager({
121
126
  maxConcurrent: config.maxConcurrent || DEFAULT_MAX_CONCURRENT,
@@ -711,10 +716,29 @@ class AgentChatHandler extends EventEmitter {
711
716
  */
712
717
  async chat(message, options = {}) {
713
718
  const sessionId = options.sessionId || this.getDefaultSessionId();
714
- const result = await this._processMessage(sessionId, message, options);
719
+ const result = await this._withSessionLock(sessionId, () => this._processMessage(sessionId, message, options));
715
720
  return { message: result.text, ...result };
716
721
  }
717
722
 
723
+ /**
724
+ * 按 sessionId 串行执行 fn:同一 session 的调用排队,不同 session 并行。
725
+ * 无论前一个成功或失败都继续(不因一次失败卡死队列)。
726
+ * @private
727
+ */
728
+ _withSessionLock(sessionId, fn) {
729
+ const key = sessionId || '__default__';
730
+ const prev = this._sessionLocks.get(key) || Promise.resolve();
731
+ const run = prev.then(fn, fn); // 前一个 settle(成功/失败)后都执行本次
732
+ // gate 只用于排队,吞掉 run 的结果/异常,避免影响下一个
733
+ const gate = run.then(() => {}, () => {});
734
+ this._sessionLocks.set(key, gate);
735
+ gate.finally(() => {
736
+ // 队尾清理,避免 Map 无限增长
737
+ if (this._sessionLocks.get(key) === gate) this._sessionLocks.delete(key);
738
+ });
739
+ return run;
740
+ }
741
+
718
742
  /**
719
743
  * 流式聊天
720
744
  */
@@ -795,8 +819,9 @@ class AgentChatHandler extends EventEmitter {
795
819
  // yield usage 数据,让 UI 层能直接获取
796
820
  // (usage 已在 stream 循环中 yield,无需重复)
797
821
 
798
- const userMsg = messages.length > 0 ? messages[messages.length - 1] : null;
799
- this.emit('message', { content: fullText, sessionId: sessionId, userMessage: userMsg });
822
+ // _prepareSession 返回的 userMessage,而非 messages 末尾——
823
+ // 末尾现在是刚追加的 assistant 最终答复,不再是 user 消息。
824
+ this.emit('message', { content: fullText, sessionId: sessionId, userMessage });
800
825
 
801
826
  // ★ 修复:成功路径才持久化。AI 失败时若 finally 也 save,
802
827
  // 会把刚 push 的 userMessage 写盘 → 下次请求 loadHistory 再次
@@ -939,12 +964,9 @@ class AgentChatHandler extends EventEmitter {
939
964
  messageStore.usage = { promptTokens: inputTokens, completionTokens: outputTokens };
940
965
  }
941
966
 
942
- // ToolLoop 已经直接在 messages 末尾添加了新消息,不需要再 push
943
- // 计算 original user message
944
- const userMsg = result.text
945
- ? messages.slice(-1)[0]
946
- : messages[messages.length - 1];
947
- this.emit('message', { content: result.text, sessionId: sessionId, userMessage: userMsg });
967
+ // ToolLoop 已经直接在 messages 末尾添加了新消息(含最终 assistant 答复)
968
+ // userMessage _prepareSession 返回值,不能再取 messages 末尾(已是 assistant)
969
+ this.emit('message', { content: result.text, sessionId: sessionId, userMessage });
948
970
 
949
971
  // ★ 修复:AI 调用成功后才持久化(之前 finally 总是 save,
950
972
  // 会把刚 push 的 userMessage 写盘;下次请求 loadHistory 再次 load 到,
package/src/agent/sub.js CHANGED
@@ -9,6 +9,7 @@
9
9
  const { BaseAgent } = require('./base');
10
10
  const { cleanResponse } = require('../utils');
11
11
  const { ToolLoop } = require('./tool-loop');
12
+ const { isThinkingModel } = require('../llm/provider');
12
13
  const { z } = require('zod');
13
14
  const { logger } = require('../common/logger');
14
15
  const { validateAll } = require('../utils/message-validator');
@@ -150,13 +151,26 @@ class SubAgent extends BaseAgent {
150
151
  tools,
151
152
  maxSteps,
152
153
  prepareStep: this._prepareStep ? this._prepareStep.bind(this) : undefined,
154
+ // ★ 子 Agent 同样要感知 thinking 模式,否则缺 reasoning_content 时会 400。
155
+ // 白名单命中或 providerOptions 显式开启即视为思考模式;
156
+ // 即便都没命中,tool-loop 内部还有 _forceReasoning 自愈兜底。
157
+ thinkingMode: isThinkingModel(this.model) || !!this.providerOptions?.deepseek?.thinking,
153
158
  });
154
159
 
155
160
  const result = await loop.generate({ messages, systemPrompt, model: this.model, client, ...this.providerOptions });
156
- messages.push(...result.response.messages);
161
+ // ★ 不要再 push:ToolLoop.generate 就地 mutate 并返回同一个 messages 引用,
162
+ // messages.push(...result.response.messages) 等于 push(...messages),会让整段历史翻倍。
157
163
  const full_text = cleanResponse(result.text);
158
- this.emit('complete', { message: full_text, steps: result.steps?.length || 0 });
159
- return { success: true, message: full_text, steps: result.steps?.length || 0, messages };
164
+ // maxSteps 耗尽(最后一步仍是工具调用、无最终答复)不能当成功,
165
+ // 否则调用方拿到空/半截结果却以为完成。
166
+ if (result.stopReason === 'max_steps') {
167
+ const msg = `子Agent「${this.name}」达到最大步数(${maxSteps})仍未给出最终答复`;
168
+ logger.warn(`[SubAgent:${this.name}] ${msg}`);
169
+ this.emit('error', { error: msg });
170
+ return { success: false, message: full_text, error: msg, stopReason: 'max_steps', steps: result.steps || 0, messages };
171
+ }
172
+ this.emit('complete', { message: full_text, steps: result.steps || 0 });
173
+ return { success: true, message: full_text, steps: result.steps || 0, messages };
160
174
  } catch (err) {
161
175
  lastError = err;
162
176
  if (isNetworkError(err) && attempt < maxRetries) {
@@ -24,6 +24,20 @@ class ToolLoop extends EventEmitter {
24
24
  this.abortSignal = config.abortSignal || null;
25
25
  this._repairFn = config.repairToolCall || null;
26
26
  this._thinkingMode = config.thinkingMode === true;
27
+ // ★ 自愈开关:一旦服务端报"reasoning_content must be passed back",
28
+ // 置 true 后本 loop 的所有请求都强制给每条 assistant 补 reasoning_content。
29
+ // 彻底兜底掉主/子 Agent、白名单漏判、跨轮持久化丢失等一切场景。
30
+ this._forceReasoning = false;
31
+ }
32
+
33
+ /**
34
+ * 判断是否是"thinking 模式必须回传 reasoning_content"这一类服务端 400。
35
+ * 兼容不同网关的措辞。
36
+ */
37
+ _isReasoningPassbackError(err) {
38
+ const msg = (err && (err.message || err.error?.message || '')) || '';
39
+ const s = String(msg).toLowerCase();
40
+ return s.includes('reasoning_content') && (s.includes('thinking') || s.includes('passed back') || s.includes('must be'));
27
41
  }
28
42
 
29
43
  _checkAbort() {
@@ -183,6 +197,20 @@ class ToolLoop extends EventEmitter {
183
197
  const api = [];
184
198
  if (systemPrompt) api.push({ role: 'system', content: systemPrompt });
185
199
 
200
+ // ★ 动态判定是否处于 thinking 模式。
201
+ // isThinkingModel 是硬编码白名单,覆盖不到"服务端强制思考但模型名不在名单"的场景,
202
+ // 导致 this._thinkingMode=false → 不补 reasoning_content → 报
203
+ // "reasoning_content in the thinking mode must be passed back"。
204
+ // 只要本批消息里任意一条 assistant 已带 reasoning(服务端回过),就认定思考已激活,
205
+ // 给缺失的 assistant 消息(压缩摘要、上一轮纯文本答复等)补占位 reasoning_content。
206
+ const msgHasReasoning = (m) => {
207
+ if (!m || m.role !== 'assistant') return false;
208
+ if (m.reasoning_content || m.reasoning) return true;
209
+ if (Array.isArray(m.content)) return m.content.some(c => c && c.type === 'reasoning' && c.text);
210
+ return false;
211
+ };
212
+ const thinkingActive = this._thinkingMode || this._forceReasoning || messages.some(msgHasReasoning);
213
+
186
214
  for (const msg of messages) {
187
215
  if (msg.role === 'system') {
188
216
  api.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' });
@@ -194,8 +222,9 @@ class ToolLoop extends EventEmitter {
194
222
  api.push({ role: 'user', content: text || '' });
195
223
  }
196
224
  } else if (msg.role === 'assistant') {
197
- // ★ 提取 text + tool_calls(兼容 content=string、content=array、顶层 tool_calls 三种格式)
225
+ // ★ 提取 text + reasoning + tool_calls(兼容 content=string、content=array、顶层字段)
198
226
  let text = '';
227
+ let reasoningText = '';
199
228
  const toolCalls = [];
200
229
 
201
230
  if (typeof msg.content === 'string') {
@@ -204,6 +233,7 @@ class ToolLoop extends EventEmitter {
204
233
  for (const part of msg.content) {
205
234
  if (!part) continue;
206
235
  if (part.type === 'text') text += part.text;
236
+ else if (part.type === 'reasoning') reasoningText += part.text || '';
207
237
  else if (part.type === 'tool-call' || part.type === 'tool-use') {
208
238
  toolCalls.push({
209
239
  id: part.toolCallId,
@@ -217,6 +247,9 @@ class ToolLoop extends EventEmitter {
217
247
  }
218
248
  }
219
249
 
250
+ // 顶层 reasoning_content / reasoning 兜底(content 数组里没提取到时)
251
+ if (!reasoningText) reasoningText = msg.reasoning_content || msg.reasoning || '';
252
+
220
253
  // ★ 合并顶层 tool_calls(OpenAI 格式,来自 session 文件)
221
254
  const existingIds = new Set(toolCalls.map(tc => tc.id));
222
255
  if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
@@ -226,13 +259,14 @@ class ToolLoop extends EventEmitter {
226
259
  }
227
260
  }
228
261
 
262
+ // thinking 激活时,缺失 reasoning 一律补 '...' 占位,确保被服务端接受
263
+ const reasonVal = reasoningText || (thinkingActive ? '...' : '');
264
+ const reasonField = reasonVal ? { reasoning_content: reasonVal } : {};
265
+
229
266
  if (toolCalls.length > 0) {
230
- // 核心:thinking mode 下,所有带 tool_calls 的消息都必须有 reasoning_content
231
- // 无条件注入,不依赖 hasReasoningTopLevel 检测
232
- const reasonVal = msg.reasoning_content || msg.reasoning || (this._thinkingMode ? '...' : '');
233
- api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...(reasonVal ? { reasoning_content: reasonVal } : {}) });
267
+ api.push({ role: 'assistant', content: text || null, tool_calls: toolCalls, ...reasonField });
234
268
  } else {
235
- api.push({ role: 'assistant', content: text || '' });
269
+ api.push({ role: 'assistant', content: text || '', ...reasonField });
236
270
  }
237
271
  } else if (msg.role === 'tool') {
238
272
  if (Array.isArray(msg.content)) {
@@ -433,6 +467,7 @@ class ToolLoop extends EventEmitter {
433
467
  let fullText = '';
434
468
  let fullReasoning = '';
435
469
  let totalUsage = null;
470
+ let finishedNaturally = false; // 是否自然结束(模型给出最终答复),用于区分 maxSteps 耗尽
436
471
 
437
472
  while (step < this.maxSteps) {
438
473
  this._checkAbort();
@@ -443,7 +478,7 @@ class ToolLoop extends EventEmitter {
443
478
  }
444
479
 
445
480
  // 2. 构建 API 请求
446
- const apiMessages = this._buildApiMessages(messages, systemPrompt);
481
+ let apiMessages = this._buildApiMessages(messages, systemPrompt);
447
482
  const apiTools = this._buildApiTools(this.tools);
448
483
  const apiOptions = this._prepareApiOptions(options);
449
484
 
@@ -457,12 +492,22 @@ class ToolLoop extends EventEmitter {
457
492
  stream: false,
458
493
  });
459
494
  } catch (apiErr) {
460
- log.error(`[ToolLoop] API call failed: ${apiErr.message}`);
461
- const recent = apiMessages.slice(-3).map(m => ({
462
- role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
463
- }));
464
- log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
465
- throw apiErr;
495
+ // 自愈:服务端要求回传 reasoning_content → 置强制开关、重建消息、重试一次
496
+ if (!this._forceReasoning && this._isReasoningPassbackError(apiErr)) {
497
+ this._forceReasoning = true;
498
+ log.warn('[ToolLoop] reasoning_content required by server → force-inject & retry');
499
+ apiMessages = this._buildApiMessages(messages, systemPrompt);
500
+ response = await client.chat.completions.create({
501
+ model, messages: apiMessages, tools: apiTools, ...apiOptions, stream: false,
502
+ });
503
+ } else {
504
+ log.error(`[ToolLoop] API call failed: ${apiErr.message}`);
505
+ const recent = apiMessages.slice(-3).map(m => ({
506
+ role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
507
+ }));
508
+ log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
509
+ throw apiErr;
510
+ }
466
511
  }
467
512
 
468
513
  // 3. 解析响应
@@ -483,7 +528,10 @@ class ToolLoop extends EventEmitter {
483
528
  if (reasoningContent) fullReasoning += reasoningContent;
484
529
 
485
530
  // 4. 处理工具调用
486
- if (choice.finish_reason === 'tool_calls' && message.tool_calls && message.tool_calls.length > 0) {
531
+ // 只要模型产出了 tool_calls 就执行,不依赖 finish_reason === 'tool_calls'。
532
+ // 部分 OpenAI 兼容 / reasoning 供应商即使有 tool_calls 也会返回
533
+ // finish_reason: 'stop',若严格判断会把工具调用丢弃 → 循环提前退出、任务未完成。
534
+ if (message.tool_calls && message.tool_calls.length > 0) {
487
535
  // 插入 assistant 消息(保留原格式,后面的 _processMessage 会存盘)
488
536
  const assistantMsg = {
489
537
  role: 'assistant',
@@ -506,6 +554,14 @@ class ToolLoop extends EventEmitter {
506
554
  } else {
507
555
  // 没有工具调用 → 完成
508
556
  fullText = message.content || '';
557
+ // ★ 把最终回复也写进 messages,否则持久化历史缺失助手每轮答复、
558
+ // 下一轮模型看不到自己上次说了什么(多轮记忆丢失)。
559
+ if (fullText || reasoningContent) {
560
+ const finalMsg = { role: 'assistant', content: fullText };
561
+ if (reasoningContent) finalMsg.reasoning_content = reasoningContent;
562
+ messages.push(finalMsg);
563
+ }
564
+ finishedNaturally = true;
509
565
  break;
510
566
  }
511
567
  }
@@ -516,6 +572,9 @@ class ToolLoop extends EventEmitter {
516
572
  steps: step,
517
573
  usage: totalUsage,
518
574
  reasoningText: fullReasoning || undefined,
575
+ // ★ 区分正常完成与 maxSteps 耗尽:耗尽时最后一步是工具调用、无最终答复,
576
+ // 调用方(SubAgent 等)不应把它当成功。
577
+ stopReason: finishedNaturally ? 'stop' : 'max_steps',
519
578
  };
520
579
  }
521
580
 
@@ -530,6 +589,7 @@ class ToolLoop extends EventEmitter {
530
589
  let fullText = '';
531
590
  let fullReasoning = '';
532
591
  let totalUsage = null;
592
+ let finishedNaturally = false;
533
593
 
534
594
  while (step < this.maxSteps) {
535
595
  this._checkAbort();
@@ -540,7 +600,7 @@ class ToolLoop extends EventEmitter {
540
600
  }
541
601
 
542
602
  // 2. 构建请求
543
- const apiMessages = this._buildApiMessages(messages, systemPrompt);
603
+ let apiMessages = this._buildApiMessages(messages, systemPrompt);
544
604
  const apiTools = this._buildApiTools(this.tools);
545
605
  const apiOptions = this._prepareApiOptions(options);
546
606
 
@@ -555,13 +615,24 @@ class ToolLoop extends EventEmitter {
555
615
  stream_options: { include_usage: true },
556
616
  });
557
617
  } catch (apiErr) {
558
- log.error(`[ToolLoop] stream call failed: ${apiErr.message}`);
559
- log.error(`[ToolLoop] model=${model} msgs=${apiMessages.length}`);
560
- const recent = apiMessages.slice(-3).map(m => ({
561
- role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
562
- }));
563
- log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
564
- throw apiErr;
618
+ // 自愈:服务端要求回传 reasoning_content → 置强制开关、重建消息、重试一次
619
+ if (!this._forceReasoning && this._isReasoningPassbackError(apiErr)) {
620
+ this._forceReasoning = true;
621
+ log.warn('[ToolLoop] reasoning_content required by server → force-inject & retry (stream)');
622
+ apiMessages = this._buildApiMessages(messages, systemPrompt);
623
+ stream = await client.chat.completions.create({
624
+ model, messages: apiMessages, tools: apiTools, ...apiOptions,
625
+ stream: true, stream_options: { include_usage: true },
626
+ });
627
+ } else {
628
+ log.error(`[ToolLoop] stream call failed: ${apiErr.message}`);
629
+ log.error(`[ToolLoop] model=${model} msgs=${apiMessages.length}`);
630
+ const recent = apiMessages.slice(-3).map(m => ({
631
+ role: m.role, hasTc: !!(m.tool_calls?.length), hasReason: !!m.reasoning_content,
632
+ }));
633
+ log.error(`[ToolLoop] last msgs: ${JSON.stringify(recent)}`);
634
+ throw apiErr;
635
+ }
565
636
  }
566
637
 
567
638
  // 3. 处理流式响应(累计 tool_calls delta)
@@ -585,8 +656,14 @@ class ToolLoop extends EventEmitter {
585
656
  }
586
657
 
587
658
  // Reasoning content
659
+ // ★ 与 text-delta 对称:用 fullReasoning 作为"已发射长度"游标,只发新增部分。
660
+ // 否则每个 chunk 都 slice(旧游标) → 把本 step 从头到现在的 reasoning 重复发出。
588
661
  if (result.hasReasoning) {
589
- yield { type: 'thinking', text: state.fullReasoning.slice(fullReasoning.length) };
662
+ const delta = state.fullReasoning.slice(fullReasoning.length);
663
+ if (delta) {
664
+ fullReasoning += delta;
665
+ yield { type: 'thinking', text: delta };
666
+ }
590
667
  }
591
668
 
592
669
  // Text delta
@@ -606,9 +683,12 @@ class ToolLoop extends EventEmitter {
606
683
  totalUsage = state.totalUsage;
607
684
  fullReasoning = state.fullReasoning;
608
685
 
609
- const toolCalls = Object.values(toolCallAccum).filter(tc => tc.id);
686
+ const toolCalls = Object.values(toolCallAccum).filter(tc => tc.id && tc.function?.name);
610
687
 
611
- if (finishReason === 'tool_calls' && toolCalls.length > 0) {
688
+ // ★ 只要累积到有效 tool_calls 就执行,不依赖 finishReason === 'tool_calls'
689
+ // 部分供应商(尤其内联 <think> 的 reasoning 模型)即使调用工具也返回
690
+ // finish_reason: 'stop',严格判断会丢弃工具调用 → 提前退出、任务未完成。
691
+ if (toolCalls.length > 0) {
612
692
  // 插入 assistant 消息(含 tool_calls)
613
693
  const assistantMsg = { role: 'assistant', content: fullText || '', tool_calls: toolCalls };
614
694
  // ★ 保存 reasoning_content(下次 API 调用时需要传回)
@@ -625,6 +705,14 @@ class ToolLoop extends EventEmitter {
625
705
  } else {
626
706
  // 无工具调用 → 完成(与 generate() 保持一致:确保 fullText 有值)
627
707
  fullText = state.fullText || fullText;
708
+ // ★ 把最终回复也写进 messages,否则持久化历史缺失助手每轮答复、
709
+ // 下一轮模型看不到自己上次说了什么(多轮记忆丢失)。
710
+ if (fullText || fullReasoning) {
711
+ const finalMsg = { role: 'assistant', content: fullText };
712
+ if (fullReasoning) finalMsg.reasoning_content = fullReasoning;
713
+ messages.push(finalMsg);
714
+ }
715
+ finishedNaturally = true;
628
716
  break;
629
717
  }
630
718
  }
@@ -636,12 +724,18 @@ class ToolLoop extends EventEmitter {
636
724
  outputTokens: totalUsage?.completionTokens || 0,
637
725
  };
638
726
 
727
+ // ★ maxSteps 耗尽时提示调用方(区分正常完成)
728
+ if (!finishedNaturally) {
729
+ yield { type: 'max-steps', steps: step };
730
+ }
731
+
639
732
  return {
640
733
  text: fullText,
641
734
  response: { messages },
642
735
  steps: step,
643
736
  usage: totalUsage,
644
737
  reasoningText: fullReasoning || undefined,
738
+ stopReason: finishedNaturally ? 'stop' : 'max_steps',
645
739
  };
646
740
  }
647
741
  }