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.
Files changed (93) hide show
  1. package/.claude/settings.local.json +4 -1
  2. package/.cli_default_systemPrompt.md +291 -0
  3. package/CLAUDE.md +3 -0
  4. package/README.md +20 -3
  5. package/docs/architecture.md +34 -2
  6. package/docs/extensions.md +199 -0
  7. package/docs/migration.md +100 -0
  8. package/docs/public-api.md +280 -24
  9. package/docs/usage.md +122 -30
  10. package/package.json +1 -1
  11. package/plugins/core/audit/index.js +1 -1
  12. package/plugins/core/default/bootstrap.js +44 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/scheduler/index.js +1 -0
  15. package/plugins/core/skill-manager/PROMPT.md +6 -0
  16. package/plugins/core/skill-manager/index.js +402 -115
  17. package/plugins/core/sub-agent/PROMPT.md +10 -0
  18. package/plugins/core/sub-agent/index.js +36 -3
  19. package/plugins/core/think/index.js +1 -0
  20. package/plugins/core/workflow/context.js +941 -0
  21. package/plugins/core/workflow/engine.js +66 -0
  22. package/plugins/core/workflow/examples/01-basic.js +42 -0
  23. package/plugins/core/workflow/examples/01-basic.json +30 -0
  24. package/plugins/core/workflow/examples/02-choice.js +75 -0
  25. package/plugins/core/workflow/examples/02-choice.json +59 -0
  26. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  27. package/plugins/core/workflow/examples/03-each.json +41 -0
  28. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  29. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  30. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  31. package/plugins/core/workflow/examples/05-each.js +65 -0
  32. package/plugins/core/workflow/examples/06-script.js +82 -0
  33. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  34. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  35. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  36. package/plugins/core/workflow/examples/10-logger.js +34 -0
  37. package/plugins/core/workflow/examples/11-storage.js +68 -0
  38. package/plugins/core/workflow/examples/simple.js +77 -0
  39. package/plugins/core/workflow/examples/simple.json +75 -0
  40. package/plugins/core/workflow/index.js +204 -78
  41. package/plugins/core/workflow/js-runner.js +318 -0
  42. package/plugins/core/workflow/json-runner.js +323 -0
  43. package/plugins/core/workflow/stages/action.js +211 -0
  44. package/plugins/core/workflow/stages/choice.js +74 -0
  45. package/plugins/core/workflow/stages/delay.js +73 -0
  46. package/plugins/core/workflow/stages/each.js +123 -0
  47. package/plugins/core/workflow/stages/parallel.js +69 -0
  48. package/plugins/core/workflow/stages/try.js +142 -0
  49. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  50. package/plugins/executors/data-splitter/index.js +8 -6
  51. package/plugins/executors/extension/extension-registry.js +145 -0
  52. package/plugins/executors/extension/index.js +405 -437
  53. package/plugins/executors/extension/prompt-builder.js +359 -0
  54. package/plugins/executors/extension/skill-helper.js +143 -0
  55. package/plugins/messaging/feishu/index.js +5 -3
  56. package/plugins/messaging/qq/index.js +6 -4
  57. package/plugins/messaging/telegram/index.js +6 -3
  58. package/plugins/messaging/weixin/index.js +5 -3
  59. package/plugins/tools/PROMPT.md +26 -0
  60. package/plugins/tools/index.js +6 -5
  61. package/sandbox/check-context.js +5 -0
  62. package/sandbox/test-context.js +27 -0
  63. package/sandbox/test-fixes.js +40 -0
  64. package/sandbox/test-hello-js.js +46 -0
  65. package/skills/foliko/AGENTS.md +196 -43
  66. package/skills/foliko/SKILL.md +157 -28
  67. package/skills/mcp/SKILL.md +77 -118
  68. package/skills/plugins/SKILL.md +89 -3
  69. package/skills/python/SKILL.md +57 -39
  70. package/skills/skill-guide/SKILL.md +42 -34
  71. package/skills/workflows/SKILL.md +753 -436
  72. package/src/agent/chat.js +56 -28
  73. package/src/agent/main.js +39 -14
  74. package/src/agent/prompt-registry.js +56 -16
  75. package/src/agent/prompts/PROMPT.md +3 -0
  76. package/src/agent/sub.js +1 -1
  77. package/src/cli/ui/chat-ui-old.js +5 -2
  78. package/src/cli/ui/chat-ui.js +9 -5
  79. package/src/common/constants.js +12 -0
  80. package/src/common/error-capture.js +91 -0
  81. package/src/common/json-safe.js +20 -0
  82. package/src/common/logger.js +2 -2
  83. package/src/context/compressor.js +6 -2
  84. package/src/executors/mcp-executor.js +105 -125
  85. package/src/framework/framework.js +78 -12
  86. package/src/index.js +4 -0
  87. package/src/plugin/base.js +908 -5
  88. package/src/plugin/manager.js +124 -9
  89. package/src/tool/schema.js +32 -9
  90. package/src/utils/sandbox.js +1 -1
  91. package/website/index.html +821 -0
  92. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  93. 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
- this.queueManager.on('stream:chunk', (data) => {
139
- this._chatSession.emit('stream:chunk', data);
140
- });
141
- this.queueManager.on('queue:completed', (data) => {
142
- this._chatSession.emit('queue:completed', data);
143
- });
144
- this.queueManager.on('queue:failed', (data) => {
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
- this.on('message:start', (data) => {
150
- this._chatSession.emit('message:start', data);
151
- });
152
- this.on('message:complete', (data) => {
153
- this._chatSession.emit('message:complete', data);
154
- });
155
- this.on('message:error', (data) => {
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.getSystemPrompt();
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.getSystemPrompt();
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 * 0.5;
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
- return allTools.map((t) => `${t.name}:${t.description ? t.description.length : 0}`).join('|');
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 * 0.75;
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
- registerBoth('capabilities', PROMPT_PRIORITY.CAPABILITIES, () => this._buildCapabilitiesDescription(), 'CAPABILITY');
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
- // 1. Agent 自己的 originalPrompt(per-Agent)
160
- const parts = [this._originalPrompt];
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
- for (const part of reg.list()) {
167
- if (part.owner === '_main' && part.name === 'original-prompt') continue;
168
- const content = part.get();
169
- if (content && content.trim()) parts.push(content.trim());
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
- // 注册表 build 失败时降级到旧 builder
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
- this._systemPromptCache = parts.join('\n\n');
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?.startsWith('['))?.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] 变量对象,模板中用 {{varName}} 引用
108
+ * @param {Object|Function} [vars] 变量对象或函数,模板中用 {{varName}} 引用
109
+ * - Object: 静态变量,注册时替换
110
+ * - Function: 动态变量,每次渲染时调用函数获取最新值
103
111
  * @param {Object} [options] 同 register 选项
104
112
  */
105
- registerFile(owner, filePath, vars = {}, options = {}) {
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
- let content = fs.readFileSync(fullPath, 'utf-8');
119
-
120
- // 变量插值
121
- if (vars && typeof vars === 'object') {
122
- content = content.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
123
- if (Object.prototype.hasOwnProperty.call(vars, varName)) {
124
- return String(vars[varName]);
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
- return match; // 未找到的变量保留原样
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
  */
@@ -0,0 +1,3 @@
1
+ ## 系统能力
2
+
3
+ {{capabilities}}
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', 'mcp_call', 'mcp_tool_schema'];
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: 'skill',
135
- tool: cmdName,
137
+ plugin: `skill:${skillName}`,
138
+ tool: commandName,
136
139
  args: { command: cmdArgs }
137
140
  });
138
141
  if (result.success !== false) {
@@ -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: 'skill',
380
- tool: cmdName,
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 ? JSON.stringify(chunk.input).slice(0, 30) : '';
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) : JSON.stringify(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
  }
@@ -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 };
@@ -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, `vb-agent-${date}.log`);
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 = 'vb-agent',
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 || 20;
188
- this._compressionMessageThreshold = config.compressionMessageThreshold || 200;
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