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.
- package/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +20 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +199 -16
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -134
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +195 -16
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -75
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
- package/=12 +0 -0
- 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;
|