skyloom 1.13.6 → 1.13.8

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 (193) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent.d.ts +20 -0
  49. package/dist/core/agent.d.ts.map +1 -1
  50. package/dist/core/agent.js +199 -16
  51. package/dist/core/agent.js.map +1 -1
  52. package/dist/core/factory.d.ts.map +1 -1
  53. package/dist/core/factory.js +34 -2
  54. package/dist/core/factory.js.map +1 -1
  55. package/dist/core/file_checkpoint.d.ts +57 -0
  56. package/dist/core/file_checkpoint.d.ts.map +1 -0
  57. package/dist/core/file_checkpoint.js +162 -0
  58. package/dist/core/file_checkpoint.js.map +1 -0
  59. package/dist/core/hooks.d.ts +43 -0
  60. package/dist/core/hooks.d.ts.map +1 -0
  61. package/dist/core/hooks.js +110 -0
  62. package/dist/core/hooks.js.map +1 -0
  63. package/dist/core/llm.d.ts.map +1 -1
  64. package/dist/core/llm.js +15 -9
  65. package/dist/core/llm.js.map +1 -1
  66. package/dist/core/longdoc.js +5 -5
  67. package/dist/core/mcp.d.ts +16 -0
  68. package/dist/core/mcp.d.ts.map +1 -1
  69. package/dist/core/mcp.js +55 -0
  70. package/dist/core/mcp.js.map +1 -1
  71. package/dist/core/model_config.d.ts +40 -0
  72. package/dist/core/model_config.d.ts.map +1 -0
  73. package/dist/core/model_config.js +191 -0
  74. package/dist/core/model_config.js.map +1 -0
  75. package/dist/core/skill.d.ts +7 -0
  76. package/dist/core/skill.d.ts.map +1 -1
  77. package/dist/core/skill.js +47 -0
  78. package/dist/core/skill.js.map +1 -1
  79. package/dist/core/skymd.d.ts +39 -0
  80. package/dist/core/skymd.d.ts.map +1 -0
  81. package/dist/core/skymd.js +177 -0
  82. package/dist/core/skymd.js.map +1 -0
  83. package/dist/core/tool.d.ts +12 -0
  84. package/dist/core/tool.d.ts.map +1 -1
  85. package/dist/core/tool.js +30 -0
  86. package/dist/core/tool.js.map +1 -1
  87. package/dist/core/verify.d.ts +27 -0
  88. package/dist/core/verify.d.ts.map +1 -0
  89. package/dist/core/verify.js +62 -0
  90. package/dist/core/verify.js.map +1 -0
  91. package/dist/skills/loader.d.ts +22 -2
  92. package/dist/skills/loader.d.ts.map +1 -1
  93. package/dist/skills/loader.js +45 -15
  94. package/dist/skills/loader.js.map +1 -1
  95. package/dist/tools/builtin.d.ts.map +1 -1
  96. package/dist/tools/builtin.js +13 -3
  97. package/dist/tools/builtin.js.map +1 -1
  98. package/dist/tools/model_tool.d.ts +11 -0
  99. package/dist/tools/model_tool.d.ts.map +1 -0
  100. package/dist/tools/model_tool.js +71 -0
  101. package/dist/tools/model_tool.js.map +1 -0
  102. package/dist/tools/todo.d.ts +30 -0
  103. package/dist/tools/todo.d.ts.map +1 -0
  104. package/dist/tools/todo.js +78 -0
  105. package/dist/tools/todo.js.map +1 -0
  106. package/docs/AESTHETIC_DESIGN.md +152 -144
  107. package/docs/OPTIMIZATION_PLAN.md +178 -178
  108. package/package.json +68 -68
  109. package/scripts/install.js +48 -48
  110. package/scripts/link.js +10 -10
  111. package/setup.bat +79 -79
  112. package/skill-test-ty2fOA/test.md +10 -10
  113. package/src/agents/dew.ts +70 -70
  114. package/src/agents/fair.ts +102 -102
  115. package/src/agents/fog.ts +48 -48
  116. package/src/agents/frost.ts +50 -50
  117. package/src/agents/rain.ts +50 -50
  118. package/src/agents/snow.ts +239 -239
  119. package/src/cli/commands_md.ts +112 -0
  120. package/src/cli/input_macros.ts +83 -0
  121. package/src/cli/loom.ts +982 -0
  122. package/src/cli/loom_chat.ts +598 -0
  123. package/src/cli/main.ts +255 -9
  124. package/src/cli/mode.ts +58 -58
  125. package/src/cli/tui.ts +228 -222
  126. package/src/core/agent/guard.ts +134 -134
  127. package/src/core/agent/task.ts +100 -100
  128. package/src/core/agent.ts +195 -16
  129. package/src/core/arbitrate.ts +162 -162
  130. package/src/core/catalog.ts +178 -178
  131. package/src/core/checkpoint.ts +94 -94
  132. package/src/core/estimate.ts +104 -104
  133. package/src/core/evolve.ts +191 -191
  134. package/src/core/factory.ts +31 -2
  135. package/src/core/file_checkpoint.ts +136 -0
  136. package/src/core/filter.ts +103 -103
  137. package/src/core/graph.ts +156 -156
  138. package/src/core/hooks.ts +126 -0
  139. package/src/core/icons.ts +53 -53
  140. package/src/core/index.ts +37 -37
  141. package/src/core/learn.ts +146 -146
  142. package/src/core/llm.ts +15 -9
  143. package/src/core/longdoc.ts +155 -155
  144. package/src/core/mcp.ts +48 -0
  145. package/src/core/mcp_server.ts +176 -176
  146. package/src/core/model_config.ts +157 -0
  147. package/src/core/profile.ts +255 -255
  148. package/src/core/router.ts +124 -124
  149. package/src/core/sandbox.ts +142 -142
  150. package/src/core/security.ts +243 -243
  151. package/src/core/skill.ts +42 -0
  152. package/src/core/skymd.ts +143 -0
  153. package/src/core/theme.ts +65 -65
  154. package/src/core/tool.ts +30 -0
  155. package/src/core/tool_router.ts +193 -193
  156. package/src/core/vector.ts +152 -152
  157. package/src/core/verify.ts +71 -0
  158. package/src/core/workspace.ts +150 -150
  159. package/src/plugins/loader.ts +66 -66
  160. package/src/skills/loader.ts +45 -16
  161. package/src/sql.js.d.ts +29 -29
  162. package/src/tools/builtin.ts +13 -3
  163. package/src/tools/computer.ts +269 -269
  164. package/src/tools/delegate.ts +49 -49
  165. package/src/tools/model_tool.ts +74 -0
  166. package/src/tools/todo.ts +76 -0
  167. package/src/web/tts.ts +93 -93
  168. package/tests/agent.test.ts +159 -159
  169. package/tests/agent_helpers.test.ts +48 -48
  170. package/tests/bus.test.ts +121 -121
  171. package/tests/catalog.test.ts +86 -86
  172. package/tests/checkpoint_commands.test.ts +124 -0
  173. package/tests/claude_compat.test.ts +110 -0
  174. package/tests/config.test.ts +41 -41
  175. package/tests/guard.test.ts +75 -75
  176. package/tests/icons.test.ts +45 -45
  177. package/tests/loom.test.ts +248 -0
  178. package/tests/memory.test.ts +170 -170
  179. package/tests/model_config.test.ts +109 -0
  180. package/tests/router.test.ts +86 -86
  181. package/tests/schemas.test.ts +51 -51
  182. package/tests/semantic.test.ts +83 -83
  183. package/tests/setup.ts +10 -10
  184. package/tests/skill.test.ts +172 -172
  185. package/tests/skymd.test.ts +146 -0
  186. package/tests/task.test.ts +60 -60
  187. package/tests/todo_toolstats.test.ts +94 -0
  188. package/tests/tool.test.ts +108 -108
  189. package/tests/tool_router.test.ts +71 -71
  190. package/tests/tui.test.ts +67 -67
  191. package/vitest.config.ts +17 -17
  192. package/=12 +0 -0
  193. package/=8 +0 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Hooks — user-configured shell commands at tool-execution lifecycle points.
3
+ *
4
+ * Unlike prompts or memory, hooks are *enforced*: they run regardless of what
5
+ * the model decides. A pre_tool hook exiting non-zero blocks the call.
6
+ *
7
+ * config.yaml:
8
+ * hooks:
9
+ * session_start:
10
+ * - "echo session up"
11
+ * pre_tool:
12
+ * - matcher: "run_bash|delete_file" # regex on tool name
13
+ * command: "./scripts/guard.sh" # non-zero exit blocks the tool
14
+ * post_tool:
15
+ * - matcher: "write_file|edit_file"
16
+ * command: "npx prettier --write \"$SKY_FILE\""
17
+ *
18
+ * Hook env: SKY_TOOL (tool name), SKY_ARGS (args JSON), SKY_FILE (path arg
19
+ * if present), SKY_AGENT (agent name).
20
+ */
21
+
22
+ import { spawnSync } from 'child_process';
23
+ import { getLogger } from './logger';
24
+
25
+ const log = getLogger('hooks');
26
+
27
+ export interface HookSpec {
28
+ matcher?: string;
29
+ command: string;
30
+ }
31
+
32
+ export interface Hooks {
33
+ sessionStart: string[];
34
+ preTool: HookSpec[];
35
+ postTool: HookSpec[];
36
+ }
37
+
38
+ const HOOK_TIMEOUT_MS = 30_000;
39
+
40
+ function normalizeSpecs(raw: any): HookSpec[] {
41
+ if (!Array.isArray(raw)) return [];
42
+ const out: HookSpec[] = [];
43
+ for (const item of raw) {
44
+ if (typeof item === 'string' && item.trim()) out.push({ command: item });
45
+ else if (item && typeof item.command === 'string' && item.command.trim()) {
46
+ out.push({ matcher: typeof item.matcher === 'string' ? item.matcher : undefined, command: item.command });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+
52
+ export function loadHooks(config: any): Hooks {
53
+ const h: any = config?.hooks || {};
54
+ return {
55
+ sessionStart: normalizeSpecs(h.session_start).map(s => s.command),
56
+ preTool: normalizeSpecs(h.pre_tool),
57
+ postTool: normalizeSpecs(h.post_tool),
58
+ };
59
+ }
60
+
61
+ export function matches(spec: HookSpec, toolName: string): boolean {
62
+ if (!spec.matcher) return true;
63
+ try {
64
+ return new RegExp(spec.matcher).test(toolName);
65
+ } catch {
66
+ return spec.matcher === toolName;
67
+ }
68
+ }
69
+
70
+ function hookEnv(toolName: string, args: Record<string, any>, agent: string): NodeJS.ProcessEnv {
71
+ return {
72
+ ...process.env,
73
+ SKY_TOOL: toolName,
74
+ SKY_ARGS: JSON.stringify(args ?? {}).slice(0, 8000),
75
+ SKY_FILE: String(args?.path ?? args?.file ?? args?.file_path ?? ''),
76
+ SKY_AGENT: agent,
77
+ };
78
+ }
79
+
80
+ function runHook(command: string, env: NodeJS.ProcessEnv): { code: number; output: string } {
81
+ const r = spawnSync(command, { shell: true, encoding: 'utf-8', timeout: HOOK_TIMEOUT_MS, env });
82
+ const output = `${r.stdout || ''}${r.stderr || ''}`.trim().slice(0, 1000);
83
+ return { code: r.error ? 1 : (r.status ?? 1), output };
84
+ }
85
+
86
+ /**
87
+ * Run matching pre_tool hooks. The first non-zero exit blocks the call.
88
+ */
89
+ export function runPreToolHooks(
90
+ hooks: Hooks,
91
+ toolName: string,
92
+ args: Record<string, any>,
93
+ agent: string
94
+ ): { allowed: boolean; reason: string } {
95
+ for (const spec of hooks.preTool) {
96
+ if (!matches(spec, toolName)) continue;
97
+ const { code, output } = runHook(spec.command, hookEnv(toolName, args, agent));
98
+ if (code !== 0) {
99
+ log.warn('pre_tool_hook_blocked', { tool: toolName, hook: spec.command, code });
100
+ return { allowed: false, reason: output || `hook exited ${code}` };
101
+ }
102
+ }
103
+ return { allowed: true, reason: '' };
104
+ }
105
+
106
+ /** Run matching post_tool hooks (best-effort; failures only logged). */
107
+ export function runPostToolHooks(
108
+ hooks: Hooks,
109
+ toolName: string,
110
+ args: Record<string, any>,
111
+ agent: string
112
+ ): void {
113
+ for (const spec of hooks.postTool) {
114
+ if (!matches(spec, toolName)) continue;
115
+ const { code, output } = runHook(spec.command, hookEnv(toolName, args, agent));
116
+ if (code !== 0) log.warn('post_tool_hook_failed', { tool: toolName, hook: spec.command, code, output });
117
+ }
118
+ }
119
+
120
+ /** Run session_start hooks once at system construction. */
121
+ export function runSessionStartHooks(hooks: Hooks): void {
122
+ for (const command of hooks.sessionStart) {
123
+ const { code, output } = runHook(command, process.env);
124
+ if (code !== 0) log.warn('session_start_hook_failed', { hook: command, code, output });
125
+ }
126
+ }
package/src/core/icons.ts CHANGED
@@ -1,53 +1,53 @@
1
- /**
2
- * Agent icon system — dynamic status indicators.
3
- *
4
- * Each agent is identified by its display name with color styling.
5
- * During processing, agent spinners provide dynamic status.
6
- */
7
-
8
- import * as path from 'path';
9
-
10
- const ICONS_DIR = path.resolve(__dirname, '..', 'assets', 'icons');
11
-
12
- export const AGENT_COLORS: Record<string, string> = {
13
- fog: 'bright_white',
14
- rain: 'blue',
15
- frost: 'cyan',
16
- snow: 'bright_white',
17
- dew: 'green',
18
- fair: '#FFD700',
19
- };
20
-
21
- export const AGENT_EMOJI: Record<string, string> = {
22
- fog: '≋',
23
- rain: '⸽',
24
- frost: '✱',
25
- snow: '❉',
26
- dew: '∘',
27
- fair: '☼',
28
- };
29
-
30
- /**
31
- * Return the filesystem path to an agent's SVG icon file.
32
- */
33
- export function svgPath(name: string): string {
34
- return path.join(ICONS_DIR, `${name}.svg`);
35
- }
36
-
37
- /**
38
- * Return the plain-text icon for an agent.
39
- *
40
- * Used in dashboards, prompts, logs, and any UI where SVG can't render.
41
- * The glyphs are deliberately chosen from Unicode blocks that render
42
- * as monochrome text on virtually every terminal.
43
- *
44
- * fog ≋ three wavy lines — drifting mist
45
- * rain ⸽ six vertical dots — falling rain streaks
46
- * frost ✱ pointed asterisk — frost crystal
47
- * snow ❉ balloon-spoked star — snowflake
48
- * dew ∘ ring — dewdrop
49
- * fair ☼ sun with rays — clear sky
50
- */
51
- export function iconText(name: string): string {
52
- return AGENT_EMOJI[name] ?? name;
53
- }
1
+ /**
2
+ * Agent icon system — dynamic status indicators.
3
+ *
4
+ * Each agent is identified by its display name with color styling.
5
+ * During processing, agent spinners provide dynamic status.
6
+ */
7
+
8
+ import * as path from 'path';
9
+
10
+ const ICONS_DIR = path.resolve(__dirname, '..', 'assets', 'icons');
11
+
12
+ export const AGENT_COLORS: Record<string, string> = {
13
+ fog: 'bright_white',
14
+ rain: 'blue',
15
+ frost: 'cyan',
16
+ snow: 'bright_white',
17
+ dew: 'green',
18
+ fair: '#FFD700',
19
+ };
20
+
21
+ export const AGENT_EMOJI: Record<string, string> = {
22
+ fog: '≋',
23
+ rain: '⸽',
24
+ frost: '✱',
25
+ snow: '❉',
26
+ dew: '∘',
27
+ fair: '☼',
28
+ };
29
+
30
+ /**
31
+ * Return the filesystem path to an agent's SVG icon file.
32
+ */
33
+ export function svgPath(name: string): string {
34
+ return path.join(ICONS_DIR, `${name}.svg`);
35
+ }
36
+
37
+ /**
38
+ * Return the plain-text icon for an agent.
39
+ *
40
+ * Used in dashboards, prompts, logs, and any UI where SVG can't render.
41
+ * The glyphs are deliberately chosen from Unicode blocks that render
42
+ * as monochrome text on virtually every terminal.
43
+ *
44
+ * fog ≋ three wavy lines — drifting mist
45
+ * rain ⸽ six vertical dots — falling rain streaks
46
+ * frost ✱ pointed asterisk — frost crystal
47
+ * snow ❉ balloon-spoked star — snowflake
48
+ * dew ∘ ring — dewdrop
49
+ * fair ☼ sun with rays — clear sky
50
+ */
51
+ export function iconText(name: string): string {
52
+ return AGENT_EMOJI[name] ?? name;
53
+ }
package/src/core/index.ts CHANGED
@@ -1,37 +1,37 @@
1
- /**
2
- * Skyloom Core Module Exports
3
- */
4
-
5
- export * from './constants';
6
- export * from './schemas';
7
- export * from './logger';
8
- export * from './config';
9
- export * from './tool';
10
- export * from './circuit_breaker';
11
- export * from './bus';
12
- export * from './cache';
13
- export * from './memory';
14
- export * from './middleware';
15
- export * from './llm';
16
- export * from './mcp';
17
- export { matchPipeline, buildTasksFromPipeline, listPipelines, getPipelineByName, matchAllPipelines, validateDAG, topologicalSort, type Pipeline, type PipelineStep } from './pipelines';
18
- export * from './semantic';
19
- export * from './icons';
20
- export * from './checkpoint';
21
- export * from './workspace';
22
- export * from './profile';
23
- export * from './tool_router';
24
- export * from './agent_helpers';
25
- export * from './skill';
26
- export * from './router';
27
- export * from './agent';
28
- export * from './factory';
29
- export * from './security';
30
- export * from './learn';
31
- export * from './longdoc';
32
- export * from './filter';
33
- export * from './estimate';
34
- export * from './arbitrate';
35
-
36
- // Version — read from package.json
37
- export const VERSION = (() => { try { return require('../../package.json').version; } catch { return '1.6.0'; } })();
1
+ /**
2
+ * Skyloom Core Module Exports
3
+ */
4
+
5
+ export * from './constants';
6
+ export * from './schemas';
7
+ export * from './logger';
8
+ export * from './config';
9
+ export * from './tool';
10
+ export * from './circuit_breaker';
11
+ export * from './bus';
12
+ export * from './cache';
13
+ export * from './memory';
14
+ export * from './middleware';
15
+ export * from './llm';
16
+ export * from './mcp';
17
+ export { matchPipeline, buildTasksFromPipeline, listPipelines, getPipelineByName, matchAllPipelines, validateDAG, topologicalSort, type Pipeline, type PipelineStep } from './pipelines';
18
+ export * from './semantic';
19
+ export * from './icons';
20
+ export * from './checkpoint';
21
+ export * from './workspace';
22
+ export * from './profile';
23
+ export * from './tool_router';
24
+ export * from './agent_helpers';
25
+ export * from './skill';
26
+ export * from './router';
27
+ export * from './agent';
28
+ export * from './factory';
29
+ export * from './security';
30
+ export * from './learn';
31
+ export * from './longdoc';
32
+ export * from './filter';
33
+ export * from './estimate';
34
+ export * from './arbitrate';
35
+
36
+ // Version — read from package.json
37
+ export const VERSION = (() => { try { return require('../../package.json').version; } catch { return '1.6.0'; } })();
package/src/core/learn.ts CHANGED
@@ -1,146 +1,146 @@
1
- /**
2
- * 持续学习模块 — post-task review + experience recording.
3
- *
4
- * After each task, the agent writes a structured review.
5
- * Failed attempts are indexed for similarity search to avoid repetition.
6
- */
7
-
8
- import * as fs from "fs";
9
- import * as path from "path";
10
- import { USER_CONFIG_DIR } from "./config";
11
- import { getLogger } from "./logger";
12
-
13
- const log = getLogger("learn");
14
-
15
- /* ── Data types ── */
16
- export interface TaskReview {
17
- ts: string;
18
- agent: string;
19
- goal: string;
20
- success: boolean;
21
- durationMs: number;
22
- toolCalls: string[];
23
- errorMsg?: string;
24
- rootCause?: string;
25
- improvement?: string;
26
- }
27
-
28
- export interface ExperienceEntry {
29
- id: string;
30
- pattern: string; // What went wrong (key for similarity search)
31
- solution: string; // What fixed it
32
- frequency: number; // How often this pattern repeats
33
- lastSeen: string;
34
- }
35
-
36
- /* ── Persistence ── */
37
- const reviewDir = path.join(USER_CONFIG_DIR, "reviews");
38
- const expFile = path.join(USER_CONFIG_DIR, "experiences.json");
39
- const reviewDir_ = reviewDir; // for closure
40
-
41
- function ensureDir() { if (!fs.existsSync(reviewDir_)) fs.mkdirSync(reviewDir_, { recursive: true }); }
42
-
43
- /* ═══════════════════════════════════════
44
- Task Review Recording
45
- ═══════════════════════════════════════ */
46
- export function recordReview(review: TaskReview): void {
47
- ensureDir();
48
- const file = path.join(reviewDir_, `${review.ts.slice(0, 10)}_${review.agent}.jsonl`);
49
- const line = JSON.stringify(review);
50
- fs.appendFileSync(file, line + "\n");
51
- log.debug("review_recorded", { agent: review.agent, success: review.success });
52
-
53
- // If failed, also record as experience
54
- if (!review.success && review.errorMsg) {
55
- recordExperience(review.errorMsg, review.rootCause || "unknown", review.improvement || "no improvement noted");
56
- }
57
- }
58
-
59
- /* ═══════════════════════════════════════
60
- Experience Recording (for failure patterns)
61
- ═══════════════════════════════════════ */
62
- function loadExperiences(): ExperienceEntry[] {
63
- try {
64
- if (fs.existsSync(expFile)) return JSON.parse(fs.readFileSync(expFile, "utf-8"));
65
- } catch { /* ignore */ }
66
- return [];
67
- }
68
-
69
- function saveExperiences(entries: ExperienceEntry[]): void {
70
- ensureDir();
71
- fs.writeFileSync(expFile, JSON.stringify(entries, null, 2), "utf-8");
72
- }
73
-
74
- export function recordExperience(errorPattern: string, rootCause: string, solution: string): void {
75
- const entries = loadExperiences();
76
- const normalized = errorPattern.toLowerCase().slice(0, 200);
77
-
78
- // Check for existing similar pattern (simple substring match)
79
- const existing = entries.find(e => e.pattern.toLowerCase().includes(normalized.slice(0, 50)) || normalized.includes(e.pattern.toLowerCase().slice(0, 50)));
80
- if (existing) {
81
- existing.frequency++;
82
- existing.lastSeen = new Date().toISOString();
83
- if (solution && solution !== "no improvement noted") existing.solution = solution;
84
- } else {
85
- entries.push({
86
- id: Math.random().toString(36).slice(2, 10),
87
- pattern: errorPattern.slice(0, 200),
88
- solution,
89
- frequency: 1,
90
- lastSeen: new Date().toISOString(),
91
- });
92
- }
93
-
94
- // Keep top 100 experiences, sorted by frequency
95
- entries.sort((a, b) => b.frequency - a.frequency);
96
- if (entries.length > 100) entries.splice(100);
97
- saveExperiences(entries);
98
- }
99
-
100
- /* ═══════════════════════════════════════
101
- Query experiences
102
- ═══════════════════════════════════════ */
103
- export function queryExperiences(problem: string, limit: number = 3): ExperienceEntry[] {
104
- const entries = loadExperiences();
105
- const lower = problem.toLowerCase();
106
- return entries
107
- .filter(e => {
108
- const plow = e.pattern.toLowerCase();
109
- // Simple token overlap scoring
110
- const tokens = lower.split(/\s+/).filter(t => t.length > 2);
111
- const matches = tokens.filter(t => plow.includes(t));
112
- return matches.length >= 2;
113
- })
114
- .sort((a, b) => b.frequency - a.frequency)
115
- .slice(0, limit);
116
- }
117
-
118
- /* ═══════════════════════════════════════
119
- Format experiences for system prompt injection
120
- ═══════════════════════════════════════ */
121
- export function formatExperiencesForPrompt(problem: string): string {
122
- const exps = queryExperiences(problem);
123
- if (!exps.length) return "";
124
- const lines = ["## 历史教训(从经验库检索)", "以下是与当前任务相关的过往失败案例,请避免重复:"];
125
- for (const e of exps) {
126
- lines.push(`- **模式**: ${e.pattern.slice(0, 120)}`);
127
- lines.push(` **解决**: ${e.solution.slice(0, 200)} (出现 ${e.frequency} 次)`);
128
- }
129
- return lines.join("\n");
130
- }
131
-
132
- /* ═══════════════════════════════════════
133
- Generate a structured review after task completion
134
- ═══════════════════════════════════════ */
135
- export function generateReview(
136
- agent: string, goal: string, success: boolean, durationMs: number,
137
- toolCalls: string[], errorMsg?: string
138
- ): TaskReview {
139
- return {
140
- ts: new Date().toISOString(),
141
- agent, goal, success, durationMs, toolCalls,
142
- errorMsg,
143
- rootCause: errorMsg ? "auto-detected failure" : undefined,
144
- improvement: errorMsg ? "review error and adjust approach" : undefined,
145
- };
146
- }
1
+ /**
2
+ * 持续学习模块 — post-task review + experience recording.
3
+ *
4
+ * After each task, the agent writes a structured review.
5
+ * Failed attempts are indexed for similarity search to avoid repetition.
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import { USER_CONFIG_DIR } from "./config";
11
+ import { getLogger } from "./logger";
12
+
13
+ const log = getLogger("learn");
14
+
15
+ /* ── Data types ── */
16
+ export interface TaskReview {
17
+ ts: string;
18
+ agent: string;
19
+ goal: string;
20
+ success: boolean;
21
+ durationMs: number;
22
+ toolCalls: string[];
23
+ errorMsg?: string;
24
+ rootCause?: string;
25
+ improvement?: string;
26
+ }
27
+
28
+ export interface ExperienceEntry {
29
+ id: string;
30
+ pattern: string; // What went wrong (key for similarity search)
31
+ solution: string; // What fixed it
32
+ frequency: number; // How often this pattern repeats
33
+ lastSeen: string;
34
+ }
35
+
36
+ /* ── Persistence ── */
37
+ const reviewDir = path.join(USER_CONFIG_DIR, "reviews");
38
+ const expFile = path.join(USER_CONFIG_DIR, "experiences.json");
39
+ const reviewDir_ = reviewDir; // for closure
40
+
41
+ function ensureDir() { if (!fs.existsSync(reviewDir_)) fs.mkdirSync(reviewDir_, { recursive: true }); }
42
+
43
+ /* ═══════════════════════════════════════
44
+ Task Review Recording
45
+ ═══════════════════════════════════════ */
46
+ export function recordReview(review: TaskReview): void {
47
+ ensureDir();
48
+ const file = path.join(reviewDir_, `${review.ts.slice(0, 10)}_${review.agent}.jsonl`);
49
+ const line = JSON.stringify(review);
50
+ fs.appendFileSync(file, line + "\n");
51
+ log.debug("review_recorded", { agent: review.agent, success: review.success });
52
+
53
+ // If failed, also record as experience
54
+ if (!review.success && review.errorMsg) {
55
+ recordExperience(review.errorMsg, review.rootCause || "unknown", review.improvement || "no improvement noted");
56
+ }
57
+ }
58
+
59
+ /* ═══════════════════════════════════════
60
+ Experience Recording (for failure patterns)
61
+ ═══════════════════════════════════════ */
62
+ function loadExperiences(): ExperienceEntry[] {
63
+ try {
64
+ if (fs.existsSync(expFile)) return JSON.parse(fs.readFileSync(expFile, "utf-8"));
65
+ } catch { /* ignore */ }
66
+ return [];
67
+ }
68
+
69
+ function saveExperiences(entries: ExperienceEntry[]): void {
70
+ ensureDir();
71
+ fs.writeFileSync(expFile, JSON.stringify(entries, null, 2), "utf-8");
72
+ }
73
+
74
+ export function recordExperience(errorPattern: string, rootCause: string, solution: string): void {
75
+ const entries = loadExperiences();
76
+ const normalized = errorPattern.toLowerCase().slice(0, 200);
77
+
78
+ // Check for existing similar pattern (simple substring match)
79
+ const existing = entries.find(e => e.pattern.toLowerCase().includes(normalized.slice(0, 50)) || normalized.includes(e.pattern.toLowerCase().slice(0, 50)));
80
+ if (existing) {
81
+ existing.frequency++;
82
+ existing.lastSeen = new Date().toISOString();
83
+ if (solution && solution !== "no improvement noted") existing.solution = solution;
84
+ } else {
85
+ entries.push({
86
+ id: Math.random().toString(36).slice(2, 10),
87
+ pattern: errorPattern.slice(0, 200),
88
+ solution,
89
+ frequency: 1,
90
+ lastSeen: new Date().toISOString(),
91
+ });
92
+ }
93
+
94
+ // Keep top 100 experiences, sorted by frequency
95
+ entries.sort((a, b) => b.frequency - a.frequency);
96
+ if (entries.length > 100) entries.splice(100);
97
+ saveExperiences(entries);
98
+ }
99
+
100
+ /* ═══════════════════════════════════════
101
+ Query experiences
102
+ ═══════════════════════════════════════ */
103
+ export function queryExperiences(problem: string, limit: number = 3): ExperienceEntry[] {
104
+ const entries = loadExperiences();
105
+ const lower = problem.toLowerCase();
106
+ return entries
107
+ .filter(e => {
108
+ const plow = e.pattern.toLowerCase();
109
+ // Simple token overlap scoring
110
+ const tokens = lower.split(/\s+/).filter(t => t.length > 2);
111
+ const matches = tokens.filter(t => plow.includes(t));
112
+ return matches.length >= 2;
113
+ })
114
+ .sort((a, b) => b.frequency - a.frequency)
115
+ .slice(0, limit);
116
+ }
117
+
118
+ /* ═══════════════════════════════════════
119
+ Format experiences for system prompt injection
120
+ ═══════════════════════════════════════ */
121
+ export function formatExperiencesForPrompt(problem: string): string {
122
+ const exps = queryExperiences(problem);
123
+ if (!exps.length) return "";
124
+ const lines = ["## 历史教训(从经验库检索)", "以下是与当前任务相关的过往失败案例,请避免重复:"];
125
+ for (const e of exps) {
126
+ lines.push(`- **模式**: ${e.pattern.slice(0, 120)}`);
127
+ lines.push(` **解决**: ${e.solution.slice(0, 200)} (出现 ${e.frequency} 次)`);
128
+ }
129
+ return lines.join("\n");
130
+ }
131
+
132
+ /* ═══════════════════════════════════════
133
+ Generate a structured review after task completion
134
+ ═══════════════════════════════════════ */
135
+ export function generateReview(
136
+ agent: string, goal: string, success: boolean, durationMs: number,
137
+ toolCalls: string[], errorMsg?: string
138
+ ): TaskReview {
139
+ return {
140
+ ts: new Date().toISOString(),
141
+ agent, goal, success, durationMs, toolCalls,
142
+ errorMsg,
143
+ rootCause: errorMsg ? "auto-detected failure" : undefined,
144
+ improvement: errorMsg ? "review error and adjust approach" : undefined,
145
+ };
146
+ }
package/src/core/llm.ts CHANGED
@@ -690,10 +690,10 @@ export class LLMClient {
690
690
  let usage: UsageStats = { promptTokens: 0, completionTokens: 0 };
691
691
 
692
692
  if (isAnthropic) {
693
- const r = await this.callAnthropic(model, messages, tools, temperature, maxTokens);
693
+ const r = await this.callAnthropic(model, messages, tools, temperature, maxTokens, agentName);
694
694
  content = r.content; toolCalls = r.toolCalls; usage = r.usage;
695
695
  } else {
696
- const r = await this.callOpenAI(model, messages, tools, temperature, maxTokens);
696
+ const r = await this.callOpenAI(model, messages, tools, temperature, maxTokens, agentName);
697
697
  content = r.content; toolCalls = r.toolCalls; usage = r.usage;
698
698
  }
699
699
 
@@ -714,9 +714,9 @@ export class LLMClient {
714
714
  }
715
715
 
716
716
  private async callOpenAI(
717
- m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
717
+ m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number, agentName?: string
718
718
  ): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
719
- const apiKey = this.getApiKey(m);
719
+ const apiKey = this.getApiKey(m, agentName);
720
720
  const baseUrl = this.getBaseUrl(m);
721
721
  const body: Record<string, unknown> = { model: m, messages, temperature: temp ?? 0.7, max_tokens: maxTok ?? 4096 };
722
722
  if (tools?.length) {
@@ -731,9 +731,9 @@ export class LLMClient {
731
731
  }
732
732
 
733
733
  private async callAnthropic(
734
- m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number
734
+ m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number, agentName?: string
735
735
  ): Promise<{ content: string; toolCalls: ToolCall[]; usage: UsageStats }> {
736
- const apiKey = this.getApiKey("anthropic");
736
+ const apiKey = this.getApiKey("anthropic", agentName);
737
737
  const body: Record<string, unknown> = { model: m, max_tokens: maxTok ?? 4096, messages: messages.filter(msg => msg.role !== "system"), temperature: temp ?? 0.7 };
738
738
  const sys = messages.find(msg => msg.role === "system"); if (sys) body.system = sys.content;
739
739
  if (tools?.length) {
@@ -754,7 +754,13 @@ export class LLMClient {
754
754
  return { type: "object", properties: props, ...(required.length > 0 ? { required } : {}) };
755
755
  }
756
756
 
757
- private getApiKey(model: string): string {
757
+ private getApiKey(model: string, agentName?: string): string {
758
+ // 0. Per-agent override (agents.<name>.api_key) beats everything
759
+ if (agentName) {
760
+ const agentKey = (this.config.agents as any)?.[agentName]?.api_key;
761
+ if (agentKey) return String(agentKey);
762
+ }
763
+
758
764
  let provider = "openai"; const [pr] = splitProvider(model); if (pr) provider = pr;
759
765
  else { const l = model.toLowerCase(); if (l.includes("claude")) provider = "anthropic"; else if (l.includes("deepseek")) provider = "deepseek"; else if (l.includes("groq")) provider = "groq"; else if (l.includes("openrouter")) provider = "openrouter"; else if (l.includes("gemini")) provider = "gemini"; }
760
766
  const envMap = getProviderEnvMap();
@@ -802,7 +808,7 @@ export class LLMClient {
802
808
  * emitted once complete. Usage comes from the final `stream_options` chunk.
803
809
  */
804
810
  private async *callOpenAIStream(
805
- m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number, signal?: AbortSignal
811
+ m: string, messages: Record<string, unknown>[], tools?: string[], temp?: number, maxTok?: number, signal?: AbortSignal, agentName?: string
806
812
  ): AsyncGenerator<StreamEvent> {
807
813
  const apiKey = this.getApiKey(m);
808
814
  const baseUrl = this.getBaseUrl(m);
@@ -884,7 +890,7 @@ export class LLMClient {
884
890
  let started = false;
885
891
  let usage: UsageStats = { promptTokens: 0, completionTokens: 0 };
886
892
  try {
887
- for await (const ev of this.callOpenAIStream(model, messages, tools, temperature, maxTokens, signal)) {
893
+ for await (const ev of this.callOpenAIStream(model, messages, tools, temperature, maxTokens, signal, agentName)) {
888
894
  if (ev.type === "content" || ev.type === "tool_call") started = true;
889
895
  if (ev.type === "done" && ev.usage) usage = ev.usage;
890
896
  yield ev;