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.
- 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 +1 -1
- 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/skills/find-skills/SKILL.md +133 -133
- package/src/agent/chat.js +31 -9
- package/src/agent/sub.js +17 -3
- package/src/agent/tool-loop.js +119 -25
- package/src/common/atomic-write.js +33 -0
- package/src/context/compressor.js +22 -18
- package/src/framework/framework.js +19 -1
- package/src/plugin/manager.js +12 -2
- package/src/session/session.js +9 -4
- package/src/storage/manager.js +3 -1
- package/tests/core/chat-tool.test.js +187 -187
- package/tests/core/latest-message-not-filtered.test.js +143 -0
- package/tests/core/p0-fixes.test.js +129 -0
- package/tests/core/p1-fixes.test.js +76 -0
|
@@ -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
|
-
|
|
799
|
-
|
|
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
|
|
943
|
-
//
|
|
944
|
-
|
|
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
|
-
|
|
161
|
+
// ★ 不要再 push:ToolLoop.generate 就地 mutate 并返回同一个 messages 引用,
|
|
162
|
+
// messages.push(...result.response.messages) 等于 push(...messages),会让整段历史翻倍。
|
|
157
163
|
const full_text = cleanResponse(result.text);
|
|
158
|
-
|
|
159
|
-
|
|
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) {
|
package/src/agent/tool-loop.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|