foliko 2.0.5 → 2.0.7
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/.claude/settings.local.json +4 -1
- package/.cli_default_systemPrompt.md +291 -0
- package/CLAUDE.md +3 -0
- package/README.md +20 -3
- package/docs/architecture.md +34 -2
- package/docs/extensions.md +199 -0
- package/docs/migration.md +100 -0
- package/docs/public-api.md +280 -24
- package/docs/usage.md +122 -30
- package/package.json +1 -1
- package/plugins/core/audit/index.js +1 -1
- package/plugins/core/default/bootstrap.js +44 -19
- package/plugins/core/python-loader/index.js +43 -25
- package/plugins/core/scheduler/index.js +1 -0
- package/plugins/core/skill-manager/PROMPT.md +6 -0
- package/plugins/core/skill-manager/index.js +402 -115
- package/plugins/core/sub-agent/PROMPT.md +10 -0
- package/plugins/core/sub-agent/index.js +36 -3
- package/plugins/core/think/index.js +1 -0
- package/plugins/core/workflow/context.js +941 -0
- package/plugins/core/workflow/engine.js +66 -0
- package/plugins/core/workflow/examples/01-basic.js +42 -0
- package/plugins/core/workflow/examples/01-basic.json +30 -0
- package/plugins/core/workflow/examples/02-choice.js +75 -0
- package/plugins/core/workflow/examples/02-choice.json +59 -0
- package/plugins/core/workflow/examples/03-chain-style.js +114 -0
- package/plugins/core/workflow/examples/03-each.json +41 -0
- package/plugins/core/workflow/examples/04-parallel.js +52 -0
- package/plugins/core/workflow/examples/04-parallel.json +51 -0
- package/plugins/core/workflow/examples/05-chain-style.json +68 -0
- package/plugins/core/workflow/examples/05-each.js +65 -0
- package/plugins/core/workflow/examples/06-script.js +82 -0
- package/plugins/core/workflow/examples/07-module-export.js +43 -0
- package/plugins/core/workflow/examples/08-default-export.js +29 -0
- package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
- package/plugins/core/workflow/examples/10-logger.js +34 -0
- package/plugins/core/workflow/examples/11-storage.js +68 -0
- package/plugins/core/workflow/examples/simple.js +77 -0
- package/plugins/core/workflow/examples/simple.json +75 -0
- package/plugins/core/workflow/index.js +204 -78
- package/plugins/core/workflow/js-runner.js +318 -0
- package/plugins/core/workflow/json-runner.js +323 -0
- package/plugins/core/workflow/stages/action.js +211 -0
- package/plugins/core/workflow/stages/choice.js +74 -0
- package/plugins/core/workflow/stages/delay.js +73 -0
- package/plugins/core/workflow/stages/each.js +123 -0
- package/plugins/core/workflow/stages/parallel.js +69 -0
- package/plugins/core/workflow/stages/try.js +142 -0
- package/plugins/executors/data-splitter/PROMPT.md +13 -0
- package/plugins/executors/data-splitter/index.js +8 -6
- package/plugins/executors/extension/extension-registry.js +145 -0
- package/plugins/executors/extension/index.js +405 -437
- package/plugins/executors/extension/prompt-builder.js +359 -0
- package/plugins/executors/extension/skill-helper.js +143 -0
- package/plugins/messaging/feishu/index.js +5 -3
- package/plugins/messaging/qq/index.js +6 -4
- package/plugins/messaging/telegram/index.js +6 -3
- package/plugins/messaging/weixin/index.js +5 -3
- package/plugins/tools/PROMPT.md +26 -0
- package/plugins/tools/index.js +6 -5
- package/sandbox/check-context.js +5 -0
- package/sandbox/test-context.js +27 -0
- package/sandbox/test-fixes.js +40 -0
- package/sandbox/test-hello-js.js +46 -0
- package/skills/foliko/AGENTS.md +196 -43
- package/skills/foliko/SKILL.md +157 -28
- package/skills/mcp/SKILL.md +77 -118
- package/skills/plugins/SKILL.md +89 -3
- package/skills/python/SKILL.md +57 -39
- package/skills/skill-guide/SKILL.md +42 -34
- package/skills/workflows/SKILL.md +753 -436
- package/src/agent/chat.js +56 -28
- package/src/agent/main.js +39 -14
- package/src/agent/prompt-registry.js +56 -16
- package/src/agent/prompts/PROMPT.md +3 -0
- package/src/agent/sub.js +1 -1
- package/src/cli/ui/chat-ui-old.js +5 -2
- package/src/cli/ui/chat-ui.js +9 -5
- package/src/common/constants.js +12 -0
- package/src/common/error-capture.js +91 -0
- package/src/common/json-safe.js +20 -0
- package/src/common/logger.js +2 -2
- package/src/context/compressor.js +6 -2
- package/src/executors/mcp-executor.js +105 -125
- package/src/framework/framework.js +78 -12
- package/src/index.js +4 -0
- package/src/plugin/base.js +908 -5
- package/src/plugin/manager.js +124 -9
- package/src/tool/schema.js +32 -9
- package/src/utils/sandbox.js +1 -1
- package/website/index.html +821 -0
- package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
- package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -391
package/src/agent/chat.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
ToolLoopAgent,
|
|
17
17
|
isLoopFinished,
|
|
18
18
|
} = require('ai');
|
|
19
|
+
const { z } = require('zod');
|
|
19
20
|
const { autoSplitToolResult } = require('../../plugins/executors/data-splitter');
|
|
20
21
|
const { cleanResponse } = require('../utils');
|
|
21
22
|
const { ChatQueueManager } = require('../common/queue');
|
|
@@ -32,6 +33,8 @@ const {
|
|
|
32
33
|
DEFAULT_MAX_CONCURRENT,
|
|
33
34
|
DEFAULT_RETRY_ATTEMPTS,
|
|
34
35
|
DEFAULT_RETRY_DELAY_MS,
|
|
36
|
+
SIMPLE_COMPRESS_TOKEN_RATIO,
|
|
37
|
+
SMART_COMPRESS_TOKEN_RATIO,
|
|
35
38
|
} = require('../common/constants');
|
|
36
39
|
|
|
37
40
|
class AgentChatHandler extends EventEmitter {
|
|
@@ -135,26 +138,22 @@ class AgentChatHandler extends EventEmitter {
|
|
|
135
138
|
this._setupEventForwarding();
|
|
136
139
|
|
|
137
140
|
// 转发 queueManager 事件到 ChatSession(让 SessionScope 能收到 stream:chunk 和 queue:completed 等)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.queueManager.on('queue:failed',
|
|
145
|
-
this._chatSession.emit('queue:failed', data);
|
|
146
|
-
});
|
|
141
|
+
// 保存引用以便 destroy 时能正确移除
|
|
142
|
+
this._onStreamChunk = (data) => this._chatSession.emit('stream:chunk', data);
|
|
143
|
+
this._onQueueCompleted = (data) => this._chatSession.emit('queue:completed', data);
|
|
144
|
+
this._onQueueFailed = (data) => this._chatSession.emit('queue:failed', data);
|
|
145
|
+
this.queueManager.on('stream:chunk', this._onStreamChunk);
|
|
146
|
+
this.queueManager.on('queue:completed', this._onQueueCompleted);
|
|
147
|
+
this.queueManager.on('queue:failed', this._onQueueFailed);
|
|
147
148
|
|
|
148
149
|
// 转发 message:complete 和 message:error 到 ChatSession(让 SessionScope 能收到)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.on('message:error',
|
|
156
|
-
this._chatSession.emit('message:error', data);
|
|
157
|
-
});
|
|
150
|
+
// 保存引用以便 destroy 时能正确移除
|
|
151
|
+
this._onMessageStart = (data) => this._chatSession.emit('message:start', data);
|
|
152
|
+
this._onMessageComplete = (data) => this._chatSession.emit('message:complete', data);
|
|
153
|
+
this._onMessageError = (data) => this._chatSession.emit('message:error', data);
|
|
154
|
+
this.on('message:start', this._onMessageStart);
|
|
155
|
+
this.on('message:complete', this._onMessageComplete);
|
|
156
|
+
this.on('message:error', this._onMessageError);
|
|
158
157
|
|
|
159
158
|
// 设置消息处理器(使用 bind 保持 this 引用)
|
|
160
159
|
this._chatSession.setMessageProcessor(this._processMessage.bind(this));
|
|
@@ -476,7 +475,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
476
475
|
throw new Error('AI 客户端未配置');
|
|
477
476
|
}
|
|
478
477
|
this._validateToolCalls(messages);
|
|
479
|
-
const systemPrompt = framework.
|
|
478
|
+
const systemPrompt = framework.prompts.build();
|
|
480
479
|
//await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
|
|
481
480
|
const tools = this._getAITools(aiTool);
|
|
482
481
|
const agent = new ToolLoopAgent({
|
|
@@ -628,6 +627,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
628
627
|
* 销毁
|
|
629
628
|
*/
|
|
630
629
|
destroy() {
|
|
630
|
+
// Remove listeners from queueManager
|
|
631
|
+
this.queueManager.off('stream:chunk', this._onStreamChunk);
|
|
632
|
+
this.queueManager.off('queue:completed', this._onQueueCompleted);
|
|
633
|
+
this.queueManager.off('queue:failed', this._onQueueFailed);
|
|
634
|
+
// Remove listeners from this (message forwarding)
|
|
635
|
+
this.off('message:start', this._onMessageStart);
|
|
636
|
+
this.off('message:complete', this._onMessageComplete);
|
|
637
|
+
this.off('message:error', this._onMessageError);
|
|
631
638
|
if (this._chatSession) {
|
|
632
639
|
this._chatSession.removeAllListeners();
|
|
633
640
|
}
|
|
@@ -659,7 +666,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
659
666
|
throw new Error('AI 客户端未配置');
|
|
660
667
|
}
|
|
661
668
|
this._validateToolCalls(messages);
|
|
662
|
-
const systemPrompt = framework.
|
|
669
|
+
const systemPrompt = framework.prompts.build();
|
|
663
670
|
const tools = this._getAITools(aiTool);
|
|
664
671
|
|
|
665
672
|
// DeepSeek thinking mode: 处理参数
|
|
@@ -763,7 +770,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
763
770
|
const toolsTokens = this._getCachedToolsTokens();
|
|
764
771
|
const systemPromptTokens = this._getCachedSystemPromptTokens();
|
|
765
772
|
const totalTokens = cachedMsgTokens + toolsTokens + systemPromptTokens;
|
|
766
|
-
const limit = this._maxContextTokens *
|
|
773
|
+
const limit = this._maxContextTokens * SIMPLE_COMPRESS_TOKEN_RATIO;
|
|
767
774
|
|
|
768
775
|
// logger.info(`BEFORE: messages=${messages.length}, tokens=${totalTokens}/${limit}`);
|
|
769
776
|
|
|
@@ -900,7 +907,14 @@ class AgentChatHandler extends EventEmitter {
|
|
|
900
907
|
*/
|
|
901
908
|
_computeToolsCacheKey() {
|
|
902
909
|
const allTools = this.agent.framework.getTools();
|
|
903
|
-
|
|
910
|
+
// Use name + inputSchema hash for stable cache key
|
|
911
|
+
// description.length is unstable across calls (changes affect cache incorrectly)
|
|
912
|
+
const { createHash } = require('crypto');
|
|
913
|
+
const toolDescriptors = allTools
|
|
914
|
+
.map((t) => `${t.name}::${t.inputSchema ? JSON.stringify(t.inputSchema) : ''}`)
|
|
915
|
+
.sort()
|
|
916
|
+
.join('|');
|
|
917
|
+
return createHash('sha256').update(toolDescriptors).digest('hex').slice(0, 32);
|
|
904
918
|
}
|
|
905
919
|
|
|
906
920
|
/**
|
|
@@ -924,8 +938,8 @@ class AgentChatHandler extends EventEmitter {
|
|
|
924
938
|
name: toolName,
|
|
925
939
|
description: toolDef.description || '',
|
|
926
940
|
execute: async (args) => {
|
|
927
|
-
//
|
|
928
|
-
const cleanedArgs = this._cleanToolArgs(args);
|
|
941
|
+
// 清理参数并验证类型
|
|
942
|
+
const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
|
|
929
943
|
|
|
930
944
|
// 执行工具
|
|
931
945
|
this.emit('tool-call', { name: toolName, args: cleanedArgs });
|
|
@@ -954,7 +968,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
954
968
|
} catch (err) {
|
|
955
969
|
this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
|
|
956
970
|
// 返回错误信息字符串,而不是抛出异常
|
|
957
|
-
return { success: false,error: err.message}
|
|
971
|
+
return { success: false, error: err.message }
|
|
958
972
|
}
|
|
959
973
|
},
|
|
960
974
|
};
|
|
@@ -976,10 +990,13 @@ class AgentChatHandler extends EventEmitter {
|
|
|
976
990
|
}
|
|
977
991
|
|
|
978
992
|
/**
|
|
979
|
-
*
|
|
993
|
+
* 清理工具参数并根据 inputSchema 验证类型
|
|
980
994
|
* @private
|
|
995
|
+
* @param {Object} args - 工具参数
|
|
996
|
+
* @param {Object} inputSchema - Zod schema (optional)
|
|
997
|
+
* @returns {Object} 清理后的参数
|
|
981
998
|
*/
|
|
982
|
-
_cleanToolArgs(args) {
|
|
999
|
+
_cleanToolArgs(args, inputSchema) {
|
|
983
1000
|
if (!args || typeof args !== 'object') {
|
|
984
1001
|
return {};
|
|
985
1002
|
}
|
|
@@ -991,6 +1008,17 @@ class AgentChatHandler extends EventEmitter {
|
|
|
991
1008
|
}
|
|
992
1009
|
cleaned[key] = value;
|
|
993
1010
|
}
|
|
1011
|
+
|
|
1012
|
+
// Validate against inputSchema if provided and is a Zod schema
|
|
1013
|
+
if (inputSchema && inputSchema._def && inputSchema.parse) {
|
|
1014
|
+
try {
|
|
1015
|
+
return inputSchema.parse(cleaned);
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
// If Zod validation fails, return cleaned args (backward compat)
|
|
1018
|
+
logger.warn(`[Tool] Args validation failed: ${err.message}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
994
1022
|
return cleaned;
|
|
995
1023
|
}
|
|
996
1024
|
|
|
@@ -1002,7 +1030,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
1002
1030
|
return async ({ stepNumber, messages: inputMessages }) => {
|
|
1003
1031
|
try {
|
|
1004
1032
|
const tokenCount = this._countMessagesTokens(inputMessages);
|
|
1005
|
-
const tokenLimit = this._maxContextTokens *
|
|
1033
|
+
const tokenLimit = this._maxContextTokens * SMART_COMPRESS_TOKEN_RATIO;
|
|
1006
1034
|
|
|
1007
1035
|
// 超过限制时,保留三分之一的消息
|
|
1008
1036
|
if (tokenCount > tokenLimit) {
|
package/src/agent/main.js
CHANGED
|
@@ -99,7 +99,29 @@ class MainAgent extends BaseAgent {
|
|
|
99
99
|
}
|
|
100
100
|
return metaParts.join('\n');
|
|
101
101
|
}, 'USER');
|
|
102
|
-
|
|
102
|
+
|
|
103
|
+
// capabilities 从文件加载(动态内容通过 interpolate 注入)
|
|
104
|
+
const capsProvider = () => {
|
|
105
|
+
const list = this._buildCapabilitiesList();
|
|
106
|
+
return list ? `## 系统能力\n\n${list}` : '';
|
|
107
|
+
};
|
|
108
|
+
registerBoth('capabilities', PROMPT_PRIORITY.CAPABILITIES, capsProvider, 'CAPABILITY');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_buildCapabilitiesList() {
|
|
112
|
+
const plugins = this.framework.pluginManager.getAll();
|
|
113
|
+
if (!plugins || plugins.length === 0) return '';
|
|
114
|
+
const keyPlugins = plugins.filter(p => p.name !== 'defaults' && p.name !== 'agent');
|
|
115
|
+
if (keyPlugins.length === 0) return '';
|
|
116
|
+
|
|
117
|
+
const lines = [];
|
|
118
|
+
keyPlugins.forEach((plugin) => {
|
|
119
|
+
const name = plugin.instance?.name || plugin.name || 'unknown';
|
|
120
|
+
const description = plugin.instance?.description || '无描述';
|
|
121
|
+
lines.push(`- **${name}**:${description}`);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return lines.join('\n');
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
_getMetadataValue(key) {
|
|
@@ -156,27 +178,26 @@ class MainAgent extends BaseAgent {
|
|
|
156
178
|
_buildSystemPrompt() {
|
|
157
179
|
if (!this._systemPromptDirty && this._systemPromptCache !== null) return this._systemPromptCache;
|
|
158
180
|
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
// 2. 全局注册表(skills, tools, extensions 等共享部分,跳过 _main 的 original-prompt)
|
|
163
|
-
const reg = this.framework?.promptRegistry;
|
|
164
|
-
if (reg && reg.count() > 0) {
|
|
181
|
+
// 统一走 framework.prompts.build():自动包含 _originalPrompt + 注册表所有 parts
|
|
182
|
+
// (PromptRegistry.build() 已处理两段拼接,PromptPart 自带 _dirty 缓存)
|
|
183
|
+
if (this.framework?.prompts?.build) {
|
|
165
184
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
const text = this.framework.prompts.build();
|
|
186
|
+
if (text) {
|
|
187
|
+
this._systemPromptCache = text;
|
|
188
|
+
this._systemPromptDirty = false;
|
|
189
|
+
return this._systemPromptCache;
|
|
170
190
|
}
|
|
171
191
|
} catch (err) {
|
|
172
|
-
//
|
|
192
|
+
// 降级到旧 builder
|
|
173
193
|
this._systemPromptCache = this._systemPromptBuilder.build();
|
|
174
194
|
this._systemPromptDirty = false;
|
|
175
195
|
return this._systemPromptCache;
|
|
176
196
|
}
|
|
177
197
|
}
|
|
178
198
|
|
|
179
|
-
|
|
199
|
+
// 没有 framework 时:只返回 _originalPrompt
|
|
200
|
+
this._systemPromptCache = this._originalPrompt || '';
|
|
180
201
|
this._systemPromptDirty = false;
|
|
181
202
|
return this._systemPromptCache;
|
|
182
203
|
}
|
|
@@ -194,6 +215,10 @@ class MainAgent extends BaseAgent {
|
|
|
194
215
|
|
|
195
216
|
_initChatHandler() {
|
|
196
217
|
const { AgentChatHandler } = require('./chat');
|
|
218
|
+
// Remove listeners from previous chatHandler to prevent accumulation
|
|
219
|
+
if (this._chatHandler) {
|
|
220
|
+
this._chatHandler.removeAllListeners();
|
|
221
|
+
}
|
|
197
222
|
let aiClient = null;
|
|
198
223
|
const aiPlugin = this.framework.pluginManager?.get('ai');
|
|
199
224
|
if (aiPlugin) aiClient = aiPlugin.getAIClient();
|
|
@@ -389,7 +414,7 @@ class MainAgent extends BaseAgent {
|
|
|
389
414
|
const { generateEntryId, EntryTypes } = require('../session/entry');
|
|
390
415
|
sessionManager._appendEntry({
|
|
391
416
|
id: generateEntryId(sessionManager.byId), type: EntryTypes.COMPACTION,
|
|
392
|
-
summary: messages.find(m => m.role === 'assistant' && m.content
|
|
417
|
+
summary: messages.find(m => m.role === 'assistant' && typeof m.content === 'string' && m.content.startsWith('['))?.content || '',
|
|
393
418
|
firstKeptEntryId: null, tokensBefore: before, timestamp: Date.now(),
|
|
394
419
|
});
|
|
395
420
|
}
|
|
@@ -57,12 +57,18 @@ class PromptPart {
|
|
|
57
57
|
* PromptRegistry 主类
|
|
58
58
|
*/
|
|
59
59
|
class PromptRegistry extends EventEmitter {
|
|
60
|
-
constructor() {
|
|
60
|
+
constructor(framework = null) {
|
|
61
61
|
super();
|
|
62
62
|
// key = "owner::name"
|
|
63
63
|
this._parts = new Map();
|
|
64
64
|
// 用于 build 时检测循环依赖
|
|
65
65
|
this._building = false;
|
|
66
|
+
// 关联 framework(用于 build 时自动追加 main agent 的 originalPrompt)
|
|
67
|
+
this._framework = framework;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setFramework(framework) {
|
|
71
|
+
this._framework = framework;
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// ==================== 注册/注销 ====================
|
|
@@ -99,10 +105,12 @@ class PromptRegistry extends EventEmitter {
|
|
|
99
105
|
* 注册一个 prompt part(从文件读取,支持变量插值)
|
|
100
106
|
* @param {string} owner 拥有者标识
|
|
101
107
|
* @param {string} filePath 文件路径
|
|
102
|
-
* @param {Object} [vars]
|
|
108
|
+
* @param {Object|Function} [vars] 变量对象或函数,模板中用 {{varName}} 引用
|
|
109
|
+
* - Object: 静态变量,注册时替换
|
|
110
|
+
* - Function: 动态变量,每次渲染时调用函数获取最新值
|
|
103
111
|
* @param {Object} [options] 同 register 选项
|
|
104
112
|
*/
|
|
105
|
-
registerFile(owner, filePath, vars =
|
|
113
|
+
registerFile(owner, filePath, vars = null, options = {}) {
|
|
106
114
|
if (!owner || typeof owner !== 'string') {
|
|
107
115
|
throw new Error('PromptRegistry.registerFile: owner is required');
|
|
108
116
|
}
|
|
@@ -115,20 +123,24 @@ class PromptRegistry extends EventEmitter {
|
|
|
115
123
|
if (!fs.existsSync(fullPath)) {
|
|
116
124
|
throw new Error(`PromptRegistry.registerFile: file not found: ${fullPath}`);
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
content =
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
const rawContent = fs.readFileSync(fullPath, 'utf-8');
|
|
127
|
+
|
|
128
|
+
// 转为 provider,渲染时动态插值
|
|
129
|
+
const provider = () => {
|
|
130
|
+
let content = rawContent;
|
|
131
|
+
if (vars) {
|
|
132
|
+
const varsObj = typeof vars === 'function' ? vars() : vars;
|
|
133
|
+
if (varsObj && typeof varsObj === 'object') {
|
|
134
|
+
content = content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
|
|
135
|
+
if (Object.prototype.hasOwnProperty.call(varsObj, varName)) {
|
|
136
|
+
return String(varsObj[varName]);
|
|
137
|
+
}
|
|
138
|
+
return match; // 未找到的变量保留原样
|
|
139
|
+
});
|
|
125
140
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// 转为 provider
|
|
131
|
-
const provider = () => content;
|
|
141
|
+
}
|
|
142
|
+
return content;
|
|
143
|
+
};
|
|
132
144
|
|
|
133
145
|
// 从文件名生成 name
|
|
134
146
|
const name = options.name || path.basename(filePath, path.extname(filePath));
|
|
@@ -246,6 +258,7 @@ class PromptRegistry extends EventEmitter {
|
|
|
246
258
|
|
|
247
259
|
/**
|
|
248
260
|
* 渲染完整 prompt(按 priority 拼接)
|
|
261
|
+
* 自动包含 main agent 的 originalPrompt(作为首段)
|
|
249
262
|
*/
|
|
250
263
|
build() {
|
|
251
264
|
return this.buildForAgent(null);
|
|
@@ -253,6 +266,9 @@ class PromptRegistry extends EventEmitter {
|
|
|
253
266
|
|
|
254
267
|
/**
|
|
255
268
|
* 渲染适用于指定 agent 的 prompt
|
|
269
|
+
* - main agent (agentId 为 null 或 'main'):自动在首段追加 `_originalPrompt`
|
|
270
|
+
* - 其他 agent:不追加(subagent 自己有 _customSystemPrompt)
|
|
271
|
+
*
|
|
256
272
|
* @param {string|null} agentId agent ID
|
|
257
273
|
*/
|
|
258
274
|
buildForAgent(agentId = null) {
|
|
@@ -262,6 +278,17 @@ class PromptRegistry extends EventEmitter {
|
|
|
262
278
|
this._building = true;
|
|
263
279
|
try {
|
|
264
280
|
const sections = [];
|
|
281
|
+
|
|
282
|
+
// 1. main agent 的 originalPrompt 作为首段(per-Agent,identity)
|
|
283
|
+
const isMain = agentId === null || agentId === 'main';
|
|
284
|
+
if (isMain) {
|
|
285
|
+
const originalPrompt = this._getMainAgentOriginalPrompt();
|
|
286
|
+
if (originalPrompt && originalPrompt.trim()) {
|
|
287
|
+
sections.push(originalPrompt.trim());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 2. 全局注册表的所有 parts
|
|
265
292
|
for (const part of this.listForAgent(agentId)) {
|
|
266
293
|
const content = part.get();
|
|
267
294
|
if (content && content.trim()) sections.push(content.trim());
|
|
@@ -272,6 +299,19 @@ class PromptRegistry extends EventEmitter {
|
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
301
|
|
|
302
|
+
/**
|
|
303
|
+
* 获取 main agent 的 originalPrompt
|
|
304
|
+
*/
|
|
305
|
+
_getMainAgentOriginalPrompt() {
|
|
306
|
+
const main = this._framework?.getMainAgent?.();
|
|
307
|
+
if (!main) return '';
|
|
308
|
+
// 优先用 _originalPrompt(per-Agent 私有),回退到 getOriginalPrompt()
|
|
309
|
+
if (typeof main.getOriginalPrompt === 'function') {
|
|
310
|
+
return main.getOriginalPrompt() || main._originalPrompt || '';
|
|
311
|
+
}
|
|
312
|
+
return main._originalPrompt || '';
|
|
313
|
+
}
|
|
314
|
+
|
|
275
315
|
/**
|
|
276
316
|
* build() 的别名
|
|
277
317
|
*/
|
package/src/agent/sub.js
CHANGED
|
@@ -27,7 +27,7 @@ class SubAgent extends BaseAgent {
|
|
|
27
27
|
this.baseURL = config.baseURL;
|
|
28
28
|
this.providerOptions = config.providerOptions || {};
|
|
29
29
|
|
|
30
|
-
this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call'
|
|
30
|
+
this.defaulTools = ['ext_skill', 'skill_load', 'ext_call', 'subagent_call'];
|
|
31
31
|
this.bindTools = {
|
|
32
32
|
read: 'read_file', write: 'write_file', edit: 'modify_file',
|
|
33
33
|
glob: 'read_directory', grep: 'search_file', bash: 'shell_exec',
|
|
@@ -129,10 +129,13 @@ class ChatUIOld {
|
|
|
129
129
|
if (trimmed.startsWith('/') && trimmed.includes(':')) {
|
|
130
130
|
const cmdName = trimmed.slice(1).split(' ')[0];
|
|
131
131
|
const cmdArgs = trimmed.slice(cmdName.length + 2);
|
|
132
|
+
const [skillName, ...rest] = cmdName.split(':');
|
|
133
|
+
const commandName = rest.join(':');
|
|
132
134
|
try {
|
|
135
|
+
// 新格式:plugin = `skill:<name>`, tool = command name
|
|
133
136
|
const result = await this.agent.framework.executeTool('ext_call', {
|
|
134
|
-
plugin:
|
|
135
|
-
tool:
|
|
137
|
+
plugin: `skill:${skillName}`,
|
|
138
|
+
tool: commandName,
|
|
136
139
|
args: { command: cmdArgs }
|
|
137
140
|
});
|
|
138
141
|
if (result.success !== false) {
|
package/src/cli/ui/chat-ui.js
CHANGED
|
@@ -8,6 +8,7 @@ const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils
|
|
|
8
8
|
const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matchesKey, Container } = require('@earendil-works/pi-tui');
|
|
9
9
|
const { cleanResponse, logger } = require('../../../src/utils');
|
|
10
10
|
const { logEmitter } = require('../../../src/common/logger');
|
|
11
|
+
const { safeStringify } = require('../../../src/common/json-safe');
|
|
11
12
|
const { renderLine } = require('../utils/markdown');
|
|
12
13
|
const { MessageBubble } = require('./components/message-bubble');
|
|
13
14
|
const Queue=require('js-queue');
|
|
@@ -374,10 +375,13 @@ class ChatUI {
|
|
|
374
375
|
if (trimmed.startsWith('/') && cmdName) {
|
|
375
376
|
// 检查 skill 命令 (格式: skillname:cmdname)
|
|
376
377
|
if (cmdName.includes(':')) {
|
|
378
|
+
const [skillName, ...rest] = cmdName.split(':');
|
|
379
|
+
const commandName = rest.join(':');
|
|
377
380
|
try {
|
|
381
|
+
// 新格式:plugin = `skill:<name>`, tool = command name
|
|
378
382
|
const result = await this.agent.framework.executeTool('ext_call', {
|
|
379
|
-
plugin:
|
|
380
|
-
tool:
|
|
383
|
+
plugin: `skill:${skillName}`,
|
|
384
|
+
tool: commandName,
|
|
381
385
|
args: { command: cmdArgs }
|
|
382
386
|
});
|
|
383
387
|
if (result.success !== false) {
|
|
@@ -593,11 +597,11 @@ class ChatUI {
|
|
|
593
597
|
this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
|
|
594
598
|
}
|
|
595
599
|
} else if (chunk.type === 'tool-call') {
|
|
596
|
-
const args = chunk.input ?
|
|
600
|
+
const args = chunk.input ? safeStringify(chunk.input).slice(0, 30) : '';
|
|
597
601
|
this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(args+'...')}`)
|
|
598
602
|
} else if(chunk.type==='tool-result'){
|
|
599
603
|
const result = chunk.result;
|
|
600
|
-
const shortResult = typeof result === 'string' ? result.slice(0, 30) :
|
|
604
|
+
const shortResult = typeof result === 'string' ? result.slice(0, 30) : safeStringify(result).slice(0, 30);
|
|
601
605
|
this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(shortResult+'...')}`)
|
|
602
606
|
|
|
603
607
|
// 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
|
|
@@ -610,7 +614,7 @@ class ChatUI {
|
|
|
610
614
|
if (resultObj && resultObj.diff) {
|
|
611
615
|
const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
|
|
612
616
|
const bgColor = chalk.bgHex('#1a1a2e');
|
|
613
|
-
this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
|
|
617
|
+
this._currentBotMessage&&this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
|
|
614
618
|
// this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
|
|
615
619
|
this.statusBar.tooler.show(`${chalk.green('[差异]')} ${folikoGold(resultObj.filePath)}`);
|
|
616
620
|
}
|
package/src/common/constants.js
CHANGED
|
@@ -53,6 +53,9 @@ const DEFAULT_MAX_MESSAGES_PER_SESSION = 1000;
|
|
|
53
53
|
/** 保留最近的 N 条消息 */
|
|
54
54
|
const KEEP_RECENT_MESSAGES = 20;
|
|
55
55
|
|
|
56
|
+
/** 压缩消息数量阈值 */
|
|
57
|
+
const COMPRESSION_MESSAGE_THRESHOLD = 200;
|
|
58
|
+
|
|
56
59
|
/** 智能压缩启用 */
|
|
57
60
|
const ENABLE_SMART_COMPRESS = true;
|
|
58
61
|
|
|
@@ -191,6 +194,12 @@ const TOKENS_PER_TOOL = 500;
|
|
|
191
194
|
/** 每消息固定开销 token */
|
|
192
195
|
const TOKENS_PER_MESSAGE_OVERHEAD = 4;
|
|
193
196
|
|
|
197
|
+
/** 简单压缩模式 token 限制比例(50%) */
|
|
198
|
+
const SIMPLE_COMPRESS_TOKEN_RATIO = 0.5;
|
|
199
|
+
|
|
200
|
+
/** 智能压缩模式 token 限制比例(75%) */
|
|
201
|
+
const SMART_COMPRESS_TOKEN_RATIO = 0.75;
|
|
202
|
+
|
|
194
203
|
// ============================================================================
|
|
195
204
|
// 导出
|
|
196
205
|
// ============================================================================
|
|
@@ -211,6 +220,7 @@ module.exports = {
|
|
|
211
220
|
|
|
212
221
|
// Compression
|
|
213
222
|
KEEP_RECENT_MESSAGES,
|
|
223
|
+
COMPRESSION_MESSAGE_THRESHOLD,
|
|
214
224
|
ENABLE_SMART_COMPRESS,
|
|
215
225
|
COMPRESSION_TIMEOUT_MS,
|
|
216
226
|
MAX_TOOL_RESULT_SIZE,
|
|
@@ -244,4 +254,6 @@ module.exports = {
|
|
|
244
254
|
TOKENS_PER_CHAR,
|
|
245
255
|
TOKENS_PER_TOOL,
|
|
246
256
|
TOKENS_PER_MESSAGE_OVERHEAD,
|
|
257
|
+
SIMPLE_COMPRESS_TOKEN_RATIO,
|
|
258
|
+
SMART_COMPRESS_TOKEN_RATIO,
|
|
247
259
|
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 全局错误捕获 — 把 uncaughtException 和 unhandledRejection 的完整堆栈落盘
|
|
5
|
+
* 下次出现 Cannot read properties of undefined (reading 'delete') 这类异步错误,
|
|
6
|
+
* 用户直接看 .foliko/error.log 即可拿到带文件名/行号的完整堆栈,无需复现。
|
|
7
|
+
*
|
|
8
|
+
* 设计要点:
|
|
9
|
+
* - 静默写文件,不污染 console(避免和 TUI / CLI 抢输出)
|
|
10
|
+
* - 同时保留原始 process 监听器(若用户已注册),不会覆盖
|
|
11
|
+
* - 一次安装,重复 require 不会重复绑定
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const INSTALLED = Symbol.for('foliko.error-capture.installed');
|
|
18
|
+
const PROJECT_LOG = path.join(process.cwd(), '.foliko', 'error.log');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 获取当前活跃的 sessionId (兼容 AsyncLocalStorage)
|
|
22
|
+
*/
|
|
23
|
+
function getActiveSessionId() {
|
|
24
|
+
try {
|
|
25
|
+
const { requestContext, sessionContext } = require('./context-helpers');
|
|
26
|
+
if (typeof sessionContext?.getStore === 'function') {
|
|
27
|
+
const store = sessionContext.getStore();
|
|
28
|
+
if (store?.sessionId) return store.sessionId;
|
|
29
|
+
}
|
|
30
|
+
} catch (_) { /* 未安装 context 系统时跳过 */ }
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 把一个事件载荷写为单条记录到 .foliko/error.log
|
|
36
|
+
*/
|
|
37
|
+
function dumpEvent(kind, error) {
|
|
38
|
+
let record;
|
|
39
|
+
try {
|
|
40
|
+
const ts = new Date().toISOString();
|
|
41
|
+
const sid = getActiveSessionId();
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
const node = process.version;
|
|
44
|
+
const pid = process.pid;
|
|
45
|
+
|
|
46
|
+
const errObj = error instanceof Error ? error : new Error(String(error));
|
|
47
|
+
const stack = errObj.stack || `(no stack; value=${String(error)})`;
|
|
48
|
+
|
|
49
|
+
record =
|
|
50
|
+
`\n${'='.repeat(72)}\n` +
|
|
51
|
+
`[${ts}] [pid=${pid}] [node=${node}] [cwd=${cwd}] [sessionId=${sid || '(none)'}] kind=${kind}\n` +
|
|
52
|
+
`error.name=${errObj.name || 'Error'} error.message=${errObj.message || '(no message)'}\n` +
|
|
53
|
+
`stack:\n${stack}\n`;
|
|
54
|
+
} catch (innerErr) {
|
|
55
|
+
record = `\n[${new Date().toISOString()}] dump failed: ${innerErr?.message || innerErr}\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(path.dirname(PROJECT_LOG), { recursive: true });
|
|
60
|
+
fs.appendFileSync(PROJECT_LOG, record, 'utf8');
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// 连日志都写不了就别折腾了
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 安装全局错误捕获。重复调用安全(no-op)。
|
|
68
|
+
*/
|
|
69
|
+
function install(options = {}) {
|
|
70
|
+
const logFile = options.logFile || PROJECT_LOG;
|
|
71
|
+
|
|
72
|
+
// 用 global symbol 标记,避免重复 require 时再次 process.on
|
|
73
|
+
if (process[INSTALLED]) return process[INSTALLED];
|
|
74
|
+
if (!process[INSTALLED]) process[INSTALLED] = new Set();
|
|
75
|
+
if (process[INSTALLED].has('core')) return 'core';
|
|
76
|
+
process[INSTALLED].add('core');
|
|
77
|
+
|
|
78
|
+
// 捕获 unhandledRejection —— 这正是用户报的 Promise 拒绝
|
|
79
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
80
|
+
dumpEvent('unhandledRejection', reason);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 捕获 uncaughtException —— 同步抛错兜底
|
|
84
|
+
process.on('uncaughtException', (err, origin) => {
|
|
85
|
+
dumpEvent(`uncaughtException(${origin})`, err);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return 'core';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { install, dumpEvent, PROJECT_LOG };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely stringify objects that may contain circular references.
|
|
5
|
+
* Replaces circular values with '[Circular]' string.
|
|
6
|
+
*/
|
|
7
|
+
function safeStringify(obj, space) {
|
|
8
|
+
const seen = new WeakSet();
|
|
9
|
+
return JSON.stringify(obj, (key, value) => {
|
|
10
|
+
if (typeof value === 'object' && value !== null) {
|
|
11
|
+
if (seen.has(value)) {
|
|
12
|
+
return '[Circular]';
|
|
13
|
+
}
|
|
14
|
+
seen.add(value);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}, space);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { safeStringify };
|
package/src/common/logger.js
CHANGED
|
@@ -23,7 +23,7 @@ function ensureLogDir(logDir) {
|
|
|
23
23
|
function getLogFilePath(logDir) {
|
|
24
24
|
ensureLogDir(logDir);
|
|
25
25
|
const date = new Date().toISOString().split('T')[0];
|
|
26
|
-
return path.join(logDir, `
|
|
26
|
+
return path.join(logDir, `foliko-${date}.log`);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// 当前日志文件路径
|
|
@@ -62,7 +62,7 @@ function initLogger(options = {}) {
|
|
|
62
62
|
const {
|
|
63
63
|
level = process.env.LOG_LEVEL || 'info',
|
|
64
64
|
logDir = DEFAULT_LOG_DIR,
|
|
65
|
-
name = '
|
|
65
|
+
name = 'foliko',
|
|
66
66
|
silent = false, // 是否静默(不输出到控制台)
|
|
67
67
|
} = options;
|
|
68
68
|
|
|
@@ -19,6 +19,10 @@ const {
|
|
|
19
19
|
TURN_PREFIX_SUMMARIZATION_PROMPT, BRANCH_SUMMARY_PREAMBLE, BRANCH_SUMMARY_PROMPT,
|
|
20
20
|
DEFAULT_COMPACTION_SETTINGS, MODEL_CONTEXT_LIMITS, COMPRESSION_TIMEOUT_MS,
|
|
21
21
|
} = require('./compaction-prompts');
|
|
22
|
+
const {
|
|
23
|
+
KEEP_RECENT_MESSAGES,
|
|
24
|
+
COMPRESSION_MESSAGE_THRESHOLD,
|
|
25
|
+
} = require('../common/constants');
|
|
22
26
|
|
|
23
27
|
// ─── Helper classes ───────────────────────────────────────────────────
|
|
24
28
|
|
|
@@ -184,8 +188,8 @@ class ContextCompressor {
|
|
|
184
188
|
|
|
185
189
|
this.model = config.model || 'deepseek-chat';
|
|
186
190
|
this._maxContextTokens = config.maxContextTokens || this._getDefaultContextLimit();
|
|
187
|
-
this._keepRecentMessages = config.keepRecentMessages ||
|
|
188
|
-
this._compressionMessageThreshold = config.compressionMessageThreshold ||
|
|
191
|
+
this._keepRecentMessages = config.keepRecentMessages || KEEP_RECENT_MESSAGES;
|
|
192
|
+
this._compressionMessageThreshold = config.compressionMessageThreshold || COMPRESSION_MESSAGE_THRESHOLD;
|
|
189
193
|
this._enableSmartCompress = config.enableSmartCompress !== false;
|
|
190
194
|
this._compactionSettings = config.compactionSettings || DEFAULT_COMPACTION_SETTINGS;
|
|
191
195
|
|