foliko 2.0.6 → 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 (54) hide show
  1. package/.editorconfig +56 -56
  2. package/.lintstagedrc +7 -7
  3. package/.prettierignore +29 -29
  4. package/.prettierrc +11 -11
  5. package/Dockerfile +63 -63
  6. package/install.ps1 +129 -129
  7. package/install.sh +121 -121
  8. package/package.json +1 -1
  9. package/plugins/core/scheduler/index.js +1 -0
  10. package/plugins/core/workflow/context.js +941 -0
  11. package/plugins/core/workflow/engine.js +66 -0
  12. package/plugins/core/workflow/examples/01-basic.js +42 -0
  13. package/plugins/core/workflow/examples/01-basic.json +30 -0
  14. package/plugins/core/workflow/examples/02-choice.js +75 -0
  15. package/plugins/core/workflow/examples/02-choice.json +59 -0
  16. package/plugins/core/workflow/examples/03-chain-style.js +114 -0
  17. package/plugins/core/workflow/examples/03-each.json +41 -0
  18. package/plugins/core/workflow/examples/04-parallel.js +52 -0
  19. package/plugins/core/workflow/examples/04-parallel.json +51 -0
  20. package/plugins/core/workflow/examples/05-chain-style.json +68 -0
  21. package/plugins/core/workflow/examples/05-each.js +65 -0
  22. package/plugins/core/workflow/examples/06-script.js +82 -0
  23. package/plugins/core/workflow/examples/07-module-export.js +43 -0
  24. package/plugins/core/workflow/examples/08-default-export.js +29 -0
  25. package/plugins/core/workflow/examples/09-next-with-args.js +50 -0
  26. package/plugins/core/workflow/examples/10-logger.js +34 -0
  27. package/plugins/core/workflow/examples/11-storage.js +68 -0
  28. package/plugins/core/workflow/examples/simple.js +77 -0
  29. package/plugins/core/workflow/examples/simple.json +75 -0
  30. package/plugins/core/workflow/index.js +122 -56
  31. package/plugins/core/workflow/js-runner.js +318 -0
  32. package/plugins/core/workflow/json-runner.js +323 -0
  33. package/plugins/core/workflow/stages/action.js +211 -0
  34. package/plugins/core/workflow/stages/choice.js +74 -0
  35. package/plugins/core/workflow/stages/delay.js +73 -0
  36. package/plugins/core/workflow/stages/each.js +123 -0
  37. package/plugins/core/workflow/stages/parallel.js +69 -0
  38. package/plugins/core/workflow/stages/try.js +142 -0
  39. package/plugins/executors/data-splitter/index.js +3 -2
  40. package/sandbox/check-context.js +5 -0
  41. package/sandbox/test-context.js +27 -0
  42. package/sandbox/test-fixes.js +40 -0
  43. package/sandbox/test-hello-js.js +46 -0
  44. package/skills/find-skills/SKILL.md +133 -133
  45. package/skills/workflows/SKILL.md +715 -613
  46. package/src/agent/chat.js +8 -1
  47. package/src/agent/main.js +5 -1
  48. package/src/cli/ui/chat-ui.js +4 -3
  49. package/src/common/json-safe.js +20 -0
  50. package/src/framework/framework.js +55 -1
  51. package/src/plugin/manager.js +95 -1
  52. package/src/utils/sandbox.js +1 -1
  53. package/skills/workflows/workflow-troubleshooting/DEBUGGING.md +0 -197
  54. package/skills/workflows/workflow-troubleshooting/SKILL.md +0 -331
package/src/agent/chat.js CHANGED
@@ -907,7 +907,14 @@ class AgentChatHandler extends EventEmitter {
907
907
  */
908
908
  _computeToolsCacheKey() {
909
909
  const allTools = this.agent.framework.getTools();
910
- 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);
911
918
  }
912
919
 
913
920
  /**
package/src/agent/main.js CHANGED
@@ -215,6 +215,10 @@ class MainAgent extends BaseAgent {
215
215
 
216
216
  _initChatHandler() {
217
217
  const { AgentChatHandler } = require('./chat');
218
+ // Remove listeners from previous chatHandler to prevent accumulation
219
+ if (this._chatHandler) {
220
+ this._chatHandler.removeAllListeners();
221
+ }
218
222
  let aiClient = null;
219
223
  const aiPlugin = this.framework.pluginManager?.get('ai');
220
224
  if (aiPlugin) aiClient = aiPlugin.getAIClient();
@@ -410,7 +414,7 @@ class MainAgent extends BaseAgent {
410
414
  const { generateEntryId, EntryTypes } = require('../session/entry');
411
415
  sessionManager._appendEntry({
412
416
  id: generateEntryId(sessionManager.byId), type: EntryTypes.COMPACTION,
413
- 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 || '',
414
418
  firstKeptEntryId: null, tokensBefore: before, timestamp: Date.now(),
415
419
  });
416
420
  }
@@ -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');
@@ -596,11 +597,11 @@ class ChatUI {
596
597
  this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
597
598
  }
598
599
  } else if (chunk.type === 'tool-call') {
599
- const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 30) : '';
600
+ const args = chunk.input ? safeStringify(chunk.input).slice(0, 30) : '';
600
601
  this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(args+'...')}`)
601
602
  } else if(chunk.type==='tool-result'){
602
603
  const result = chunk.result;
603
- 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);
604
605
  this.statusBar.tooler.show(`${folikoGold(chunk.toolName.toLocaleUpperCase())} ${chalk.gray(shortResult+'...')}`)
605
606
 
606
607
  // 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
@@ -613,7 +614,7 @@ class ChatUI {
613
614
  if (resultObj && resultObj.diff) {
614
615
  const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
615
616
  const bgColor = chalk.bgHex('#1a1a2e');
616
- this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
617
+ this._currentBotMessage&&this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
617
618
  // this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
618
619
  this.statusBar.tooler.show(`${chalk.green('[差异]')} ${folikoGold(resultObj.filePath)}`);
619
620
  }
@@ -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 };
@@ -206,16 +206,70 @@ class Framework extends EventEmitter {
206
206
  async disablePlugin(name) { await this.pluginManager.disable(name); return this; }
207
207
  updatePluginConfig(name, config) { return this.pluginManager.updatePluginConfig(name, config); }
208
208
 
209
- registerTool(tool) { this.toolRegistry.register(tool); return this; }
209
+ registerTool(tool) {
210
+ this.toolRegistry.register(tool);
211
+ // 通知所有 Agent 使 ChatHandler._aiToolsCache 失效。
212
+ // 🔴 修复:热重载插件时,插件调 framework.registerTool 注册新工具,
213
+ // 但 ChatHandler 的 _aiToolsCache 只在 agent.registerTool 被调用时清,
214
+ // 所以 reload 后 LLM 仍使用旧的 execute 闭包。
215
+ if (Array.isArray(this._agents)) {
216
+ for (const agent of this._agents) {
217
+ const ch = agent && agent._chatHandler;
218
+ if (ch) {
219
+ ch._aiToolsCache = null;
220
+ ch._aiToolsCacheKey = '';
221
+ ch._toolsTokensCacheVersion = 0;
222
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
223
+ ch._toolExecutor._tools.set(tool.name, tool);
224
+ }
225
+ }
226
+ }
227
+ }
228
+ this.emit('tool:registered-changed', tool);
229
+ return this;
230
+ }
210
231
  tool(toolDef) {
211
232
  const tool = { ...toolDef };
212
233
  if (tool.input && !tool.inputSchema) tool.inputSchema = tool.input;
213
234
  delete tool.input;
214
235
  this.toolRegistry.register(tool);
236
+ // 同步使所有 Agent 的 ChatHandler 缓存失效并同步工具。
237
+ if (Array.isArray(this._agents)) {
238
+ for (const agent of this._agents) {
239
+ const ch = agent && agent._chatHandler;
240
+ if (ch) {
241
+ ch._aiToolsCache = null;
242
+ ch._aiToolsCacheKey = '';
243
+ ch._toolsTokensCacheVersion = 0;
244
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
245
+ ch._toolExecutor._tools.set(tool.name, tool);
246
+ }
247
+ }
248
+ }
249
+ }
215
250
  return this;
216
251
  }
217
252
  getTools() { return this.toolRegistry.getAll(); }
218
253
 
254
+ unregisterTool(name) {
255
+ if (this.toolRegistry.unregister) this.toolRegistry.unregister(name);
256
+ if (Array.isArray(this._agents)) {
257
+ for (const agent of this._agents) {
258
+ const ch = agent && agent._chatHandler;
259
+ if (ch) {
260
+ ch._aiToolsCache = null;
261
+ ch._aiToolsCacheKey = '';
262
+ ch._toolsTokensCacheVersion = 0;
263
+ if (ch._toolExecutor && ch._toolExecutor._tools) {
264
+ ch._toolExecutor._tools.delete(name);
265
+ }
266
+ }
267
+ }
268
+ }
269
+ this.emit('tool:unregistered-changed', { name });
270
+ return this;
271
+ }
272
+
219
273
  // tools 对象别名
220
274
  get tools() {
221
275
  const self = this;
@@ -78,7 +78,7 @@ class PluginManager {
78
78
  instance: plugin,
79
79
  status: 'registered',
80
80
  enabled,
81
- sourcePath: plugin.sourcePath || null,
81
+ sourcePath: this._detectPluginSourcePath(plugin, plugin.sourcePath),
82
82
  });
83
83
  this._knownPlugins.add(plugin.name);
84
84
  this._log.debug(`Plugin registered: ${plugin.name} (enabled: ${enabled})`);
@@ -89,6 +89,71 @@ class PluginManager {
89
89
  return this._plugins.has(name);
90
90
  }
91
91
 
92
+ /**
93
+ * Find the source file path for a plugin instance by scanning plugin
94
+ * class names in candidate directories (used as a fallback when the
95
+ * plugin was created directly via `new SomePlugin(...)` instead of going
96
+ * through `loadPluginFromPath`).
97
+ */
98
+ _detectPluginSourcePath(plugin, declared) {
99
+ if (declared) return declared;
100
+
101
+ // For system plugins registered in plugins/core/*, the class name often
102
+ // matches the on-disk filename (e.g. `WorkflowPlugin` => `index.js` of
103
+ // `plugins/core/workflow`, `SubAgentManagerPlugin` => `.../sub-agent/index.js`).
104
+ const candidates = [];
105
+ const fs = require('fs');
106
+ const path = require('path');
107
+ try {
108
+ const projectRoot = path.resolve(__dirname, '..', '..');
109
+ candidates.push(path.resolve(projectRoot, 'plugins'));
110
+ } catch (_) {}
111
+
112
+ const className =
113
+ (plugin.constructor && plugin.constructor.name) || plugin.name;
114
+ if (!className) return null;
115
+
116
+ for (const base of candidates) {
117
+ // exact-match: <base>/<dir>/index.js with class declared inside
118
+ // heuristic: walk top-level plugin dirs and look for index.js exports
119
+ // a class named `className`.
120
+ let dirs = [];
121
+ try {
122
+ dirs = fs.readdirSync(base);
123
+ } catch (_) {
124
+ continue;
125
+ }
126
+ for (const sub of dirs) {
127
+ const subDir = path.join(base, sub);
128
+ let files = [];
129
+ try {
130
+ files = fs.statSync(subDir).isDirectory()
131
+ ? fs.readdirSync(subDir)
132
+ : [path.basename(subDir)];
133
+ subDir = fs.statSync(subDir).isDirectory()
134
+ ? subDir
135
+ : path.dirname(subDir);
136
+ } catch (_) {
137
+ continue;
138
+ }
139
+ for (const file of files) {
140
+ if (!file.endsWith('.js')) continue;
141
+ const full = path.join(subDir, file);
142
+ try {
143
+ const src = fs.readFileSync(full, 'utf-8');
144
+ // Heuristic class declaration
145
+ if (
146
+ new RegExp(`class\\s+${className}\\s+extends\\s+Plugin`).test(src)
147
+ ) {
148
+ return full;
149
+ }
150
+ } catch (_) {}
151
+ }
152
+ }
153
+ }
154
+ return null;
155
+ }
156
+
92
157
  get(name) {
93
158
  const entry = this._plugins.get(name);
94
159
  return entry ? entry.instance : null;
@@ -264,6 +329,35 @@ class PluginManager {
264
329
  this._log.info(`Reloading plugin: ${name}`);
265
330
  const plugin = entry.instance;
266
331
 
332
+ // Refresh the plugin's prototype with the latest source code BEFORE invoking
333
+ // its reload hook. Without this, plugin.reload() still runs against the
334
+ // prototype cached at construction time (and Node.js' require cache means
335
+ // the same source file is returned on disk), so changes to plugin code
336
+ // (e.g., workflow/_loadWorkflows) won't take effect until the process
337
+ // restarts.
338
+ if (entry.sourcePath) {
339
+ try {
340
+ delete require.cache[require.resolve(entry.sourcePath)];
341
+ const mod = require(entry.sourcePath);
342
+ // The module may export a single class or an object map.
343
+ const PluginClass =
344
+ (mod && (mod.default || mod.WorkflowPlugin || mod)) ||
345
+ null;
346
+ if (
347
+ PluginClass &&
348
+ PluginClass.prototype &&
349
+ Object.getPrototypeOf(plugin) !== PluginClass.prototype
350
+ ) {
351
+ Object.setPrototypeOf(plugin, PluginClass.prototype);
352
+ }
353
+ } catch (err) {
354
+ this._log.warn(
355
+ `Failed to refresh prototype for plugin '${name}' from ${entry.sourcePath}:`,
356
+ err.message
357
+ );
358
+ }
359
+ }
360
+
267
361
  if (typeof plugin.reload === 'function') {
268
362
  await plugin.reload(this.framework);
269
363
  } else {
@@ -29,7 +29,7 @@ function createSandboxContext(customContext = {}, options = {}) {
29
29
  Number: { isNaN: Number.isNaN, isFinite: Number.isFinite, parseInt: Number.parseInt, parseFloat: Number.parseFloat },
30
30
  Boolean,
31
31
  Object: { keys: Object.keys, values: Object.values, entries: Object.entries, assign: Object.assign, create: Object.create, freeze: Object.freeze },
32
- Date: { now: Date.now },
32
+ Date,
33
33
  RegExp,
34
34
  Error,
35
35
  Promise,
@@ -1,197 +0,0 @@
1
- # 工作流开发调试指南
2
-
3
- ## 快速测试工作流
4
-
5
- ### 1. 使用 execute_workflow 直接测试
6
-
7
- ```javascript
8
- execute_workflow({
9
- workflow: {
10
- name: 'test-workflow',
11
- description: '测试工作流',
12
- steps: [
13
- {
14
- id: 'step1',
15
- type: 'tool',
16
- tool: 'py_execute',
17
- args: {
18
- code: "print('Hello from Python!')",
19
- },
20
- outputVariable: 'result',
21
- },
22
- ],
23
- },
24
- });
25
- ```
26
-
27
- ### 2. JSON 格式注意事项
28
-
29
- - ✅ 使用双引号包裹字符串
30
- - ✅ 确保 JSON 语法正确
31
- - ❌ 避免在 JSON 中使用未转义的特殊字符
32
- - ❌ 不要在 code 中混用单引号和双引号
33
-
34
- ### 3. 常见错误
35
-
36
- ```
37
- Unexpected identifier 'xxx'
38
- ```
39
-
40
- - 原因:JSON 格式错误,可能是引号或逗号问题
41
- - 解决:检查 JSON 语法
42
-
43
- ```
44
- Unexpected end of JSON input
45
- ```
46
-
47
- - 原因:模板字符串 `${}` 在 JSON 中导致解析错误
48
- - 解决:不要在 JSON 中使用模板变量,改用纯 Python 代码
49
-
50
- ## 工作流重载问题
51
-
52
- ### 问题
53
-
54
- 添加新工作流后,`workflow_reload` 不会重新加载同名工作流。
55
-
56
- ### 解决
57
-
58
- ```bash
59
- # 方法1:使用不同的文件名
60
- news-dashboard-v3.json # 不会被 v2 覆盖
61
-
62
- # 方法2:重启插件
63
- reload_plugins() # 重载所有插件
64
- ```
65
-
66
- ## Python 执行最佳实践
67
-
68
- ### 1. 错误处理
69
-
70
- ```python
71
- try:
72
- req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
73
- with urllib.request.urlopen(req, timeout=10) as resp:
74
- data = resp.read().decode('utf-8')
75
- print('SUCCESS')
76
- except Exception as e:
77
- print(f'ERROR: {e}')
78
- # fallback 逻辑
79
- ```
80
-
81
- ### 2. 输出格式
82
-
83
- ```python
84
- # 简单输出
85
- print('Status: OK')
86
- print(f'Count: {len(items)}')
87
-
88
- # JSON 输出
89
- import json
90
- print('---JSON_START---')
91
- print(json.dumps(data, ensure_ascii=False))
92
- print('---JSON_END---')
93
- ```
94
-
95
- ### 3. 超时设置
96
-
97
- ```python
98
- with urllib.request.urlopen(req, timeout=30) as resp: # 30秒超时
99
- ...
100
- ```
101
-
102
- ## 工作流步骤类型
103
-
104
- | 类型 | 用途 | 示例 |
105
- | ----------- | --------------- | ------------------------------------- |
106
- | `tool` | 调用工具 | `py_execute`, `shell_exec`, `get_time` |
107
- | `script` | 执行 JavaScript | 数据处理、变量操作 |
108
- | `condition` | 条件分支 | if/else 逻辑 |
109
- | `loop` | 循环执行 | 遍历列表 |
110
- | `delay` | 延时等待 | 等待指定时间 |
111
- | `message` | 消息通知 | 发送通知 |
112
- | `think` | LLM 思考 | 反思/分析 |
113
-
114
- ## 工具输出变量
115
-
116
- ### 正确用法
117
-
118
- ```json
119
- {
120
- "id": "step1",
121
- "type": "tool",
122
- "tool": "get_time",
123
- "outputVariable": "current_time"
124
- }
125
- ```
126
-
127
- ### 访问输出
128
-
129
- ```javascript
130
- // 在 script 步骤中
131
- context.input.current_time
132
-
133
- // 在下一个 tool 步骤中
134
- ${step1.current_time}
135
- ```
136
-
137
- ## 调试技巧
138
-
139
- ### 1. 打印中间变量
140
-
141
- ```python
142
- print('Debug - data:', data)
143
- print('Debug - length:', len(data))
144
- ```
145
-
146
- ### 2. 保存到文件
147
-
148
- ```python
149
- with open('debug.log', 'w') as f:
150
- f.write(str(data))
151
- ```
152
-
153
- ### 3. 逐步执行
154
-
155
- ```javascript
156
- {
157
- "steps": [
158
- { "id": "step1", ... }, // 先测试 step1
159
- { "id": "step2", ... } // 确认 step1 成功后添加
160
- ]
161
- }
162
- ```
163
-
164
- ## 性能优化
165
-
166
- ### 1. 合并步骤
167
-
168
- ```javascript
169
- // ❌ 多个小步骤
170
- { "id": "step1", "tool": "py_execute", "code": "a = 1" }
171
- { "id": "step2", "tool": "py_execute", "code": "b = 2" }
172
-
173
- // ✅ 单个大步骤
174
- { "id": "step1", "tool": "py_execute", "code": "a = 1; b = 2; print(a + b)" }
175
- ```
176
-
177
- ### 2. 减少变量传递
178
-
179
- ```javascript
180
- // ❌ 多步骤传递
181
- { "outputVariable": "data1" }
182
- { "outputVariable": "data2" }
183
-
184
- // ✅ 单步骤完成
185
- { "outputVariable": "final_result" }
186
- ```
187
-
188
- ### 3. 超时设置
189
-
190
- ```json
191
- {
192
- "args": {
193
- "code": "...",
194
- "timeout": 60000 // 60秒超时
195
- }
196
- }
197
- ```