foliko 2.0.4 → 2.0.6

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 (59) 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 +1180 -6
  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 +65 -19
  13. package/plugins/core/python-loader/index.js +43 -25
  14. package/plugins/core/skill-manager/PROMPT.md +6 -0
  15. package/plugins/core/skill-manager/index.js +538 -93
  16. package/plugins/core/sub-agent/PROMPT.md +10 -0
  17. package/plugins/core/sub-agent/index.js +36 -3
  18. package/plugins/core/think/index.js +1 -0
  19. package/plugins/core/workflow/index.js +106 -22
  20. package/plugins/executors/data-splitter/PROMPT.md +13 -0
  21. package/plugins/executors/data-splitter/index.js +5 -4
  22. package/plugins/executors/extension/extension-registry.js +145 -0
  23. package/plugins/executors/extension/index.js +405 -437
  24. package/plugins/executors/extension/prompt-builder.js +359 -0
  25. package/plugins/executors/extension/skill-helper.js +143 -0
  26. package/plugins/messaging/feishu/index.js +5 -3
  27. package/plugins/messaging/qq/index.js +6 -4
  28. package/plugins/messaging/telegram/index.js +6 -3
  29. package/plugins/messaging/weixin/index.js +5 -3
  30. package/plugins/tools/PROMPT.md +26 -0
  31. package/plugins/tools/index.js +6 -5
  32. package/skills/foliko/AGENTS.md +196 -43
  33. package/skills/foliko/SKILL.md +157 -28
  34. package/skills/mcp/SKILL.md +77 -118
  35. package/skills/plugins/SKILL.md +89 -3
  36. package/skills/python/SKILL.md +57 -39
  37. package/skills/skill-guide/SKILL.md +42 -34
  38. package/skills/workflows/SKILL.md +224 -9
  39. package/skills/workflows/workflow-troubleshooting/SKILL.md +221 -281
  40. package/src/agent/chat.js +48 -27
  41. package/src/agent/main.js +34 -13
  42. package/src/agent/prompt-registry.js +133 -87
  43. package/src/agent/prompts/PROMPT.md +3 -0
  44. package/src/agent/sub.js +1 -1
  45. package/src/cli/ui/chat-ui-old.js +5 -2
  46. package/src/cli/ui/chat-ui.js +5 -2
  47. package/src/common/constants.js +12 -0
  48. package/src/common/error-capture.js +91 -0
  49. package/src/common/logger.js +2 -2
  50. package/src/context/compressor.js +6 -2
  51. package/src/executors/mcp-executor.js +105 -125
  52. package/src/framework/framework.js +632 -6
  53. package/src/index.js +4 -0
  54. package/src/plugin/base.js +913 -10
  55. package/src/plugin/manager.js +29 -8
  56. package/src/tool/schema.js +32 -9
  57. package/tests/core/plugin-prompts.test.js +13 -13
  58. package/tests/core/prompt-registry.test.js +59 -51
  59. package/website/index.html +821 -0
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
 
@@ -924,8 +931,8 @@ class AgentChatHandler extends EventEmitter {
924
931
  name: toolName,
925
932
  description: toolDef.description || '',
926
933
  execute: async (args) => {
927
- // 清理参数
928
- const cleanedArgs = this._cleanToolArgs(args);
934
+ // 清理参数并验证类型
935
+ const cleanedArgs = this._cleanToolArgs(args, toolDef.inputSchema);
929
936
 
930
937
  // 执行工具
931
938
  this.emit('tool-call', { name: toolName, args: cleanedArgs });
@@ -954,7 +961,7 @@ class AgentChatHandler extends EventEmitter {
954
961
  } catch (err) {
955
962
  this.emit('tool-error', { name: toolName, args: cleanedArgs, error: err.message });
956
963
  // 返回错误信息字符串,而不是抛出异常
957
- return { success: false,error: err.message}
964
+ return { success: false, error: err.message }
958
965
  }
959
966
  },
960
967
  };
@@ -976,10 +983,13 @@ class AgentChatHandler extends EventEmitter {
976
983
  }
977
984
 
978
985
  /**
979
- * 清理工具参数
986
+ * 清理工具参数并根据 inputSchema 验证类型
980
987
  * @private
988
+ * @param {Object} args - 工具参数
989
+ * @param {Object} inputSchema - Zod schema (optional)
990
+ * @returns {Object} 清理后的参数
981
991
  */
982
- _cleanToolArgs(args) {
992
+ _cleanToolArgs(args, inputSchema) {
983
993
  if (!args || typeof args !== 'object') {
984
994
  return {};
985
995
  }
@@ -991,6 +1001,17 @@ class AgentChatHandler extends EventEmitter {
991
1001
  }
992
1002
  cleaned[key] = value;
993
1003
  }
1004
+
1005
+ // Validate against inputSchema if provided and is a Zod schema
1006
+ if (inputSchema && inputSchema._def && inputSchema.parse) {
1007
+ try {
1008
+ return inputSchema.parse(cleaned);
1009
+ } catch (err) {
1010
+ // If Zod validation fails, return cleaned args (backward compat)
1011
+ logger.warn(`[Tool] Args validation failed: ${err.message}`);
1012
+ }
1013
+ }
1014
+
994
1015
  return cleaned;
995
1016
  }
996
1017
 
@@ -1002,7 +1023,7 @@ class AgentChatHandler extends EventEmitter {
1002
1023
  return async ({ stepNumber, messages: inputMessages }) => {
1003
1024
  try {
1004
1025
  const tokenCount = this._countMessagesTokens(inputMessages);
1005
- const tokenLimit = this._maxContextTokens * 0.75;
1026
+ const tokenLimit = this._maxContextTokens * SMART_COMPRESS_TOKEN_RATIO;
1006
1027
 
1007
1028
  // 超过限制时,保留三分之一的消息
1008
1029
  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
  }
@@ -3,40 +3,29 @@
3
3
  /**
4
4
  * PromptRegistry — 全局系统提示词注册表
5
5
  *
6
- * 设计目标:
7
- * - 单一注册入口(替代散落在各 Plugin 的 registerPromptPart 调用)
8
- * - owner 命名空间隔离(避免 part 名冲突)
9
- * - 用语义槽位(PROMPT_SLOT)排序,替代魔法数字
10
- * - 可观测:list / preview / getPart / getSlots
11
- *
12
- * 与旧 API 的关系:
13
- * - 完全独立的新 API,不破坏 MainAgent._systemPromptBuilder
14
- * - 旧 registerPromptPart 调用继续工作(写入各 agent 自己的 builder)
15
- * - 新代码推荐使用 framework.prompts.register(owner, name, provider, { slot })
16
- *
17
- * 渐进迁移路径:
18
- * - P0:注册表可独立使用(plugins 主动注册,外部可预览)
19
- * - P2:Plugin 基类支持声明式 prompts 字段
20
- * - P5:MainAgent 完全切换到本注册表
6
+ * 简化设计:
7
+ * - priority: 数字越小越靠前(默认 100)
8
+ * - scope: 'global'(公用)| 'main'(主 agent)| agentId(特定子 agent)
9
+ * - 去掉槽位概念
21
10
  */
22
11
 
23
12
  const { EventEmitter } = require('../common/events');
24
- const { PROMPT_SLOT } = require('../common/constants');
13
+ const fs = require('fs');
14
+ const path = require('path');
25
15
 
26
16
  /**
27
17
  * 单个 prompt part
28
- * - provider 是 () => string|null,返回 null/空串时被 build 跳过
29
- * - 内部带缓存,失效后才重新计算
30
18
  */
31
19
  class PromptPart {
32
20
  constructor(owner, name, provider, options = {}) {
33
21
  this.owner = owner;
34
22
  this.name = name;
35
23
  this.provider = provider;
36
- this.slot = options.slot || 'CUSTOM';
37
- this.order = options.order ?? null; // 显式 order 覆盖 slot 默认
24
+ // scope: 'global' | 'main' | string (agentId)
25
+ this.scope = options.scope || 'global';
26
+ // priority: 数字越小越靠前,默认 100
27
+ this.priority = options.priority ?? 100;
38
28
  this.description = options.description || '';
39
- this.metadata = options.metadata || {};
40
29
  this._cached = null;
41
30
  this._dirty = true;
42
31
  this._error = null;
@@ -53,8 +42,6 @@ class PromptPart {
53
42
  this._error = e;
54
43
  this._cached = null;
55
44
  this._dirty = false;
56
- // 警告但不抛错 — 避免单个 part 失败导致整个 prompt 渲染失败
57
- // eslint-disable-next-line no-console
58
45
  console.warn(`[PromptRegistry:${this.owner}::${this.name}] provider failed:`, e.message);
59
46
  }
60
47
  }
@@ -70,61 +57,31 @@ class PromptPart {
70
57
  * PromptRegistry 主类
71
58
  */
72
59
  class PromptRegistry extends EventEmitter {
73
- constructor(options = {}) {
60
+ constructor(framework = null) {
74
61
  super();
75
-
76
62
  // key = "owner::name"
77
63
  this._parts = new Map();
78
-
79
- // slot 名称 → { order, label }
80
- this._slots = new Map();
81
-
82
- // 注入 slots:优先用参数,否则用全局 PROMPT_SLOT
83
- const slots = options.slots || PROMPT_SLOT;
84
- for (const [name, def] of Object.entries(slots)) {
85
- if (!def || typeof def.order !== 'number') continue;
86
- this._slots.set(name, {
87
- order: def.order,
88
- label: def.label || name,
89
- });
90
- }
91
-
92
- // 用于 render 时检测循环依赖
64
+ // 用于 build 时检测循环依赖
93
65
  this._building = false;
66
+ // 关联 framework(用于 build 时自动追加 main agent 的 originalPrompt)
67
+ this._framework = framework;
94
68
  }
95
69
 
96
- // ==================== 槽位管理 ====================
97
-
98
- /**
99
- * 定义/覆盖一个槽位
100
- */
101
- defineSlot(name, order, label) {
102
- if (typeof order !== 'number') throw new Error('slot order must be a number');
103
- this._slots.set(name, { order, label: label || name });
104
- this.emit('slot-defined', { name, order, label });
105
- return this;
106
- }
107
-
108
- /**
109
- * 获取所有槽位定义
110
- */
111
- getSlots() {
112
- return new Map(this._slots);
70
+ setFramework(framework) {
71
+ this._framework = framework;
113
72
  }
114
73
 
115
74
  // ==================== 注册/注销 ====================
116
75
 
117
76
  /**
118
77
  * 注册一个 prompt part
119
- * @param {string} owner 拥有者标识(通常是 plugin name 或 '_main')
78
+ * @param {string} owner 拥有者标识
120
79
  * @param {string} name part 名(在 owner 内唯一)
121
80
  * @param {Function} provider () => string|null
122
81
  * @param {Object} [options]
123
- * @param {string} [options.slot='CUSTOM'] 槽位名
124
- * @param {number} [options.order] 自定义 order(覆盖 slot 默认)
125
- * @param {string} [options.description] 描述(用于 list/debug)
126
- * @param {Object} [options.metadata] 额外元数据
127
- * @returns {PromptPart}
82
+ * @param {string} [options.scope='global'] 作用域: 'global'|'main'|agentId
83
+ * @param {number} [options.priority=100] 优先级(越小越靠前)
84
+ * @param {string} [options.description] 描述
128
85
  */
129
86
  register(owner, name, provider, options = {}) {
130
87
  if (!owner || typeof owner !== 'string') {
@@ -140,13 +97,59 @@ class PromptRegistry extends EventEmitter {
140
97
  const key = `${owner}::${name}`;
141
98
  const part = new PromptPart(owner, name, provider, options);
142
99
  this._parts.set(key, part);
143
- this.emit('register', { owner, name, slot: part.slot });
100
+ this.emit('register', { owner, name, scope: part.scope, priority: part.priority });
144
101
  return part;
145
102
  }
146
103
 
104
+ /**
105
+ * 注册一个 prompt part(从文件读取,支持变量插值)
106
+ * @param {string} owner 拥有者标识
107
+ * @param {string} filePath 文件路径
108
+ * @param {Object|Function} [vars] 变量对象或函数,模板中用 {{varName}} 引用
109
+ * - Object: 静态变量,注册时替换
110
+ * - Function: 动态变量,每次渲染时调用函数获取最新值
111
+ * @param {Object} [options] 同 register 选项
112
+ */
113
+ registerFile(owner, filePath, vars = null, options = {}) {
114
+ if (!owner || typeof owner !== 'string') {
115
+ throw new Error('PromptRegistry.registerFile: owner is required');
116
+ }
117
+ if (!filePath || typeof filePath !== 'string') {
118
+ throw new Error('PromptRegistry.registerFile: filePath is required');
119
+ }
120
+
121
+ // 读取文件内容
122
+ const fullPath = path.resolve(filePath);
123
+ if (!fs.existsSync(fullPath)) {
124
+ throw new Error(`PromptRegistry.registerFile: file not found: ${fullPath}`);
125
+ }
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
+ });
140
+ }
141
+ }
142
+ return content;
143
+ };
144
+
145
+ // 从文件名生成 name
146
+ const name = options.name || path.basename(filePath, path.extname(filePath));
147
+
148
+ return this.register(owner, name, provider, options);
149
+ }
150
+
147
151
  /**
148
152
  * 注销一个 part
149
- * @returns {boolean} 是否真的删除了
150
153
  */
151
154
  unregister(owner, name) {
152
155
  const key = `${owner}::${name}`;
@@ -157,7 +160,6 @@ class PromptRegistry extends EventEmitter {
157
160
 
158
161
  /**
159
162
  * 清空指定 owner 的所有 parts
160
- * @returns {number} 删除的 part 数
161
163
  */
162
164
  clearOwner(owner) {
163
165
  const prefix = `${owner}::`;
@@ -173,7 +175,7 @@ class PromptRegistry extends EventEmitter {
173
175
  }
174
176
 
175
177
  /**
176
- * 失效指定 part(下次 build 时重新计算)
178
+ * 失效指定 part
177
179
  */
178
180
  invalidate(owner, name) {
179
181
  const part = this._parts.get(`${owner}::${name}`);
@@ -210,10 +212,14 @@ class PromptRegistry extends EventEmitter {
210
212
  }
211
213
 
212
214
  /**
213
- * 列出所有 parts(按 order 排序)
215
+ * 列出所有 parts(按 priority 排序)
214
216
  */
215
217
  list() {
216
- return Array.from(this._parts.values()).sort((a, b) => this._compareOrder(a, b));
218
+ return Array.from(this._parts.values()).sort((a, b) => {
219
+ if (a.priority !== b.priority) return a.priority - b.priority;
220
+ if (a.owner !== b.owner) return a.owner.localeCompare(b.owner);
221
+ return a.name.localeCompare(b.name);
222
+ });
217
223
  }
218
224
 
219
225
  /**
@@ -224,19 +230,24 @@ class PromptRegistry extends EventEmitter {
224
230
  }
225
231
 
226
232
  /**
227
- * 按 slot 过滤
233
+ * 按 scope 过滤
228
234
  */
229
- listBySlot(slot) {
230
- return this.list().filter((p) => p.slot === slot);
235
+ listByScope(scope) {
236
+ return this.list().filter((p) => p.scope === scope);
231
237
  }
232
238
 
233
- _compareOrder(a, b) {
234
- const oa = a.order != null ? a.order : this._slots.get(a.slot)?.order ?? 999;
235
- const ob = b.order != null ? b.order : this._slots.get(b.slot)?.order ?? 999;
236
- if (oa !== ob) return oa - ob;
237
- // order 时按 owner → name 字母序,保证稳定
238
- if (a.owner !== b.owner) return a.owner.localeCompare(b.owner);
239
- return a.name.localeCompare(b.name);
239
+ /**
240
+ * 获取适用于指定 agent parts
241
+ * @param {string|null} agentId agent ID,null 表示主 agent
242
+ */
243
+ listForAgent(agentId = null) {
244
+ const isMain = agentId === null || agentId === 'main';
245
+ return this.list().filter((p) => {
246
+ if (p.scope === 'global') return true;
247
+ if (p.scope === 'main' && isMain) return true;
248
+ if (p.scope === agentId) return true;
249
+ return false;
250
+ });
240
251
  }
241
252
 
242
253
  count() {
@@ -246,16 +257,39 @@ class PromptRegistry extends EventEmitter {
246
257
  // ==================== 渲染 ====================
247
258
 
248
259
  /**
249
- * 渲染完整 prompt 文本(按 slot order 拼接)
260
+ * 渲染完整 prompt(按 priority 拼接)
261
+ * 自动包含 main agent 的 originalPrompt(作为首段)
250
262
  */
251
263
  build() {
264
+ return this.buildForAgent(null);
265
+ }
266
+
267
+ /**
268
+ * 渲染适用于指定 agent 的 prompt
269
+ * - main agent (agentId 为 null 或 'main'):自动在首段追加 `_originalPrompt`
270
+ * - 其他 agent:不追加(subagent 自己有 _customSystemPrompt)
271
+ *
272
+ * @param {string|null} agentId agent ID
273
+ */
274
+ buildForAgent(agentId = null) {
252
275
  if (this._building) {
253
276
  throw new Error('PromptRegistry.build() re-entered — check provider for circular calls');
254
277
  }
255
278
  this._building = true;
256
279
  try {
257
280
  const sections = [];
258
- for (const part of this.list()) {
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
292
+ for (const part of this.listForAgent(agentId)) {
259
293
  const content = part.get();
260
294
  if (content && content.trim()) sections.push(content.trim());
261
295
  }
@@ -266,15 +300,27 @@ class PromptRegistry extends EventEmitter {
266
300
  }
267
301
 
268
302
  /**
269
- * build() 的语义化别名,调试用
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
+
315
+ /**
316
+ * build() 的别名
270
317
  */
271
318
  preview() {
272
319
  return this.build();
273
320
  }
274
321
 
275
322
  /**
276
- * 渲染结构化清单(调试 / 文档生成)
277
- * 返回 [{ owner, name, slot, order, length, contentPreview }]
323
+ * 渲染结构化清单(调试用)
278
324
  */
279
325
  inspect() {
280
326
  return this.list().map((part) => {
@@ -282,8 +328,8 @@ class PromptRegistry extends EventEmitter {
282
328
  return {
283
329
  owner: part.owner,
284
330
  name: part.name,
285
- slot: part.slot,
286
- order: part.order != null ? part.order : this._slots.get(part.slot)?.order ?? 999,
331
+ scope: part.scope,
332
+ priority: part.priority,
287
333
  description: part.description,
288
334
  hasContent: Boolean(content && content.trim()),
289
335
  length: content ? content.length : 0,
@@ -293,4 +339,4 @@ class PromptRegistry extends EventEmitter {
293
339
  }
294
340
  }
295
341
 
296
- module.exports = { PromptRegistry, PromptPart };
342
+ module.exports = { PromptRegistry, PromptPart };
@@ -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) {
@@ -374,10 +374,13 @@ class ChatUI {
374
374
  if (trimmed.startsWith('/') && cmdName) {
375
375
  // 检查 skill 命令 (格式: skillname:cmdname)
376
376
  if (cmdName.includes(':')) {
377
+ const [skillName, ...rest] = cmdName.split(':');
378
+ const commandName = rest.join(':');
377
379
  try {
380
+ // 新格式:plugin = `skill:<name>`, tool = command name
378
381
  const result = await this.agent.framework.executeTool('ext_call', {
379
- plugin: 'skill',
380
- tool: cmdName,
382
+ plugin: `skill:${skillName}`,
383
+ tool: commandName,
381
384
  args: { command: cmdArgs }
382
385
  });
383
386
  if (result.success !== false) {
@@ -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
  };