skyloom 1.12.0 → 1.13.0
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 +142 -46
- package/config/default.yaml +43 -47
- package/config/models.yaml +155 -155
- 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/main.js +103 -51
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +8 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent/task.d.ts +58 -0
- package/dist/core/agent/task.d.ts.map +1 -0
- package/dist/core/agent/task.js +83 -0
- package/dist/core/agent/task.js.map +1 -0
- package/dist/core/agent.d.ts +2 -45
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -145
- package/dist/core/agent.js.map +1 -1
- package/dist/core/agent_helpers.d.ts +10 -0
- package/dist/core/agent_helpers.d.ts.map +1 -1
- package/dist/core/agent_helpers.js +39 -0
- package/dist/core/agent_helpers.js.map +1 -1
- package/dist/core/catalog.d.ts +71 -0
- package/dist/core/catalog.d.ts.map +1 -0
- package/dist/core/catalog.js +176 -0
- package/dist/core/catalog.js.map +1 -0
- package/dist/core/config.d.ts +8 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/factory.js +16 -16
- package/dist/core/llm.d.ts +7 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +139 -7
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/memory.d.ts.map +1 -1
- package/dist/core/memory.js +69 -62
- package/dist/core/memory.js.map +1 -1
- package/dist/core/theme.d.ts +46 -0
- package/dist/core/theme.d.ts.map +1 -0
- package/dist/core/theme.js +42 -0
- package/dist/core/theme.js.map +1 -0
- package/dist/web/server.js +542 -519
- package/dist/web/server.js.map +1 -1
- package/docs/AESTHETIC_DESIGN.md +144 -0
- package/docs/OPTIMIZATION_PLAN.md +178 -0
- package/package.json +60 -60
- 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/main.ts +425 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +272 -269
- package/src/core/agent/task.ts +100 -0
- package/src/core/agent.ts +1446 -1549
- package/src/core/agent_helpers.ts +496 -461
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -0
- package/src/core/checkpoint.ts +94 -94
- package/src/core/config.ts +20 -4
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +627 -627
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- 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 +108 -5
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp_server.ts +176 -176
- package/src/core/memory.ts +1178 -1171
- 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 +342 -342
- package/src/core/theme.ts +65 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +46 -46
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +380 -380
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/web/server.ts +660 -634
- package/src/web/tts.ts +93 -93
- package/tests/agent_helpers.test.ts +48 -0
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -0
- package/tests/config.test.ts +41 -0
- package/tests/icons.test.ts +45 -45
- package/tests/memory.test.ts +147 -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/task.test.ts +60 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/vitest.config.ts +17 -17
package/tests/semantic.test.ts
CHANGED
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for lightweight semantic retrieval.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { SemanticScorer, getScorer } from '../src/core/semantic';
|
|
6
|
-
|
|
7
|
-
describe('SemanticScorer', () => {
|
|
8
|
-
const scorer = new SemanticScorer();
|
|
9
|
-
|
|
10
|
-
it('identical strings score 1.0', () => {
|
|
11
|
-
expect(scorer.similarity('hello world', 'hello world')).toBe(1.0);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('completely different strings score 0.0', () => {
|
|
15
|
-
expect(scorer.similarity('aaaaa', 'bbbbb')).toBe(0.0);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('partial overlap scores between 0 and 1', () => {
|
|
19
|
-
const score = scorer.similarity('deploy to server', 'deployment script');
|
|
20
|
-
expect(score).toBeGreaterThan(0.0);
|
|
21
|
-
expect(score).toBeLessThan(1.0);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('CJK similarity works', () => {
|
|
25
|
-
const score = scorer.similarity('部署', '部署命令');
|
|
26
|
-
expect(score).toBeGreaterThan(0.0);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('empty strings return 0', () => {
|
|
30
|
-
expect(scorer.similarity('', 'hello')).toBe(0.0);
|
|
31
|
-
expect(scorer.similarity('hello', '')).toBe(0.0);
|
|
32
|
-
expect(scorer.similarity('', '')).toBe(0.0);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('case insensitive', () => {
|
|
36
|
-
expect(scorer.similarity('Hello World', 'hello world')).toBe(1.0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('mixed language overlap', () => {
|
|
40
|
-
const score = scorer.similarity('pnpm install', 'pnpm 安装');
|
|
41
|
-
expect(score).toBeGreaterThan(0.0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('code identifiers match partially', () => {
|
|
45
|
-
const score = scorer.similarity('snake_case_var', 'snakeCaseVar');
|
|
46
|
-
expect(score).toBeGreaterThan(0.0);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('rank returns ordered results', () => {
|
|
50
|
-
const candidates = [
|
|
51
|
-
{ key: 'k1', value: 'deploy to production' },
|
|
52
|
-
{ key: 'k2', value: 'install dependencies' },
|
|
53
|
-
{ key: 'k3', value: 'rollback version' },
|
|
54
|
-
];
|
|
55
|
-
const ranked = scorer.rank('deploy', candidates, 'value', 2);
|
|
56
|
-
expect(ranked.length).toBeLessThanOrEqual(2);
|
|
57
|
-
expect(ranked[0][1].key).toBe('k1');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('rank filters below minScore', () => {
|
|
61
|
-
const candidates = [
|
|
62
|
-
{ key: 'k1', value: 'completely unrelated text here' },
|
|
63
|
-
];
|
|
64
|
-
const ranked = scorer.rank('zzzzz', candidates, 'value', 1, 0.5);
|
|
65
|
-
expect(ranked).toHaveLength(0);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('rank uses key field boost', () => {
|
|
69
|
-
const candidates = [
|
|
70
|
-
{ key: 'deploy_command', value: 'npm run build' },
|
|
71
|
-
];
|
|
72
|
-
const ranked = scorer.rank('deploy', candidates, 'value', 1);
|
|
73
|
-
expect(ranked).toHaveLength(1);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('getScorer singleton', () => {
|
|
78
|
-
it('returns the same instance', () => {
|
|
79
|
-
const s1 = getScorer();
|
|
80
|
-
const s2 = getScorer();
|
|
81
|
-
expect(s1).toBe(s2);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for lightweight semantic retrieval.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { SemanticScorer, getScorer } from '../src/core/semantic';
|
|
6
|
+
|
|
7
|
+
describe('SemanticScorer', () => {
|
|
8
|
+
const scorer = new SemanticScorer();
|
|
9
|
+
|
|
10
|
+
it('identical strings score 1.0', () => {
|
|
11
|
+
expect(scorer.similarity('hello world', 'hello world')).toBe(1.0);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('completely different strings score 0.0', () => {
|
|
15
|
+
expect(scorer.similarity('aaaaa', 'bbbbb')).toBe(0.0);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('partial overlap scores between 0 and 1', () => {
|
|
19
|
+
const score = scorer.similarity('deploy to server', 'deployment script');
|
|
20
|
+
expect(score).toBeGreaterThan(0.0);
|
|
21
|
+
expect(score).toBeLessThan(1.0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('CJK similarity works', () => {
|
|
25
|
+
const score = scorer.similarity('部署', '部署命令');
|
|
26
|
+
expect(score).toBeGreaterThan(0.0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('empty strings return 0', () => {
|
|
30
|
+
expect(scorer.similarity('', 'hello')).toBe(0.0);
|
|
31
|
+
expect(scorer.similarity('hello', '')).toBe(0.0);
|
|
32
|
+
expect(scorer.similarity('', '')).toBe(0.0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('case insensitive', () => {
|
|
36
|
+
expect(scorer.similarity('Hello World', 'hello world')).toBe(1.0);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('mixed language overlap', () => {
|
|
40
|
+
const score = scorer.similarity('pnpm install', 'pnpm 安装');
|
|
41
|
+
expect(score).toBeGreaterThan(0.0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('code identifiers match partially', () => {
|
|
45
|
+
const score = scorer.similarity('snake_case_var', 'snakeCaseVar');
|
|
46
|
+
expect(score).toBeGreaterThan(0.0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('rank returns ordered results', () => {
|
|
50
|
+
const candidates = [
|
|
51
|
+
{ key: 'k1', value: 'deploy to production' },
|
|
52
|
+
{ key: 'k2', value: 'install dependencies' },
|
|
53
|
+
{ key: 'k3', value: 'rollback version' },
|
|
54
|
+
];
|
|
55
|
+
const ranked = scorer.rank('deploy', candidates, 'value', 2);
|
|
56
|
+
expect(ranked.length).toBeLessThanOrEqual(2);
|
|
57
|
+
expect(ranked[0][1].key).toBe('k1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rank filters below minScore', () => {
|
|
61
|
+
const candidates = [
|
|
62
|
+
{ key: 'k1', value: 'completely unrelated text here' },
|
|
63
|
+
];
|
|
64
|
+
const ranked = scorer.rank('zzzzz', candidates, 'value', 1, 0.5);
|
|
65
|
+
expect(ranked).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('rank uses key field boost', () => {
|
|
69
|
+
const candidates = [
|
|
70
|
+
{ key: 'deploy_command', value: 'npm run build' },
|
|
71
|
+
];
|
|
72
|
+
const ranked = scorer.rank('deploy', candidates, 'value', 1);
|
|
73
|
+
expect(ranked).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('getScorer singleton', () => {
|
|
78
|
+
it('returns the same instance', () => {
|
|
79
|
+
const s1 = getScorer();
|
|
80
|
+
const s2 = getScorer();
|
|
81
|
+
expect(s1).toBe(s2);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/tests/setup.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test setup — global fixtures and mocks for Skyloom tests.
|
|
3
|
-
*/
|
|
4
|
-
import { vi, beforeEach } from 'vitest';
|
|
5
|
-
import { MessageBus } from '../src/core/bus';
|
|
6
|
-
|
|
7
|
-
// Clear tool state before each test
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
// No-op — individual tests handle their own setup
|
|
10
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Test setup — global fixtures and mocks for Skyloom tests.
|
|
3
|
+
*/
|
|
4
|
+
import { vi, beforeEach } from 'vitest';
|
|
5
|
+
import { MessageBus } from '../src/core/bus';
|
|
6
|
+
|
|
7
|
+
// Clear tool state before each test
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// No-op — individual tests handle their own setup
|
|
10
|
+
});
|
package/tests/skill.test.ts
CHANGED
|
@@ -1,172 +1,172 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for skill system.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
|
|
8
|
-
describe('Skill', () => {
|
|
9
|
-
it('creates a skill with model override', async () => {
|
|
10
|
-
const { Skill } = await import('../src/core/skill');
|
|
11
|
-
const skill = new Skill({
|
|
12
|
-
name: 'test_skill',
|
|
13
|
-
description: 'A test skill',
|
|
14
|
-
model: 'claude-opus-4-7',
|
|
15
|
-
});
|
|
16
|
-
expect(skill.model).toBe('claude-opus-4-7');
|
|
17
|
-
expect(skill.temperature).toBeNull();
|
|
18
|
-
expect(skill.maxTokens).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('creates a skill with temperature override', async () => {
|
|
22
|
-
const { Skill } = await import('../src/core/skill');
|
|
23
|
-
const skill = new Skill({ name: 'test_skill', description: 'A test skill', temperature: 0.3 });
|
|
24
|
-
expect(skill.temperature).toBe(0.3);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('creates a skill with max_tokens override', async () => {
|
|
28
|
-
const { Skill } = await import('../src/core/skill');
|
|
29
|
-
const skill = new Skill({ name: 'test_skill', description: 'A test skill', maxTokens: 32000 });
|
|
30
|
-
expect(skill.maxTokens).toBe(32000);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('default has no overrides', async () => {
|
|
34
|
-
const { Skill } = await import('../src/core/skill');
|
|
35
|
-
const skill = new Skill({ name: 'test', description: 'test' });
|
|
36
|
-
expect(skill.model).toBeNull();
|
|
37
|
-
expect(skill.temperature).toBeNull();
|
|
38
|
-
expect(skill.maxTokens).toBeNull();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('Skill.fromMarkdown', () => {
|
|
43
|
-
it('loads skill with YAML frontmatter', async () => {
|
|
44
|
-
const { Skill } = await import('../src/core/skill');
|
|
45
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
46
|
-
const mdPath = path.join(tmpDir, 'test.md');
|
|
47
|
-
fs.writeFileSync(mdPath, `---
|
|
48
|
-
name: test_skill
|
|
49
|
-
description: A test
|
|
50
|
-
model: claude-sonnet-4-6
|
|
51
|
-
temperature: 0.5
|
|
52
|
-
max_tokens: 8192
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## Test Skill
|
|
56
|
-
This is a test skill.
|
|
57
|
-
`, 'utf-8');
|
|
58
|
-
|
|
59
|
-
const skill = Skill.fromMarkdown(mdPath);
|
|
60
|
-
expect(skill).not.toBeNull();
|
|
61
|
-
if (skill) {
|
|
62
|
-
expect(skill.model).toBe('claude-sonnet-4-6');
|
|
63
|
-
expect(skill.temperature).toBe(0.5);
|
|
64
|
-
expect(skill.maxTokens).toBe(8192);
|
|
65
|
-
expect(skill.systemPrompt).toContain('This is a test skill');
|
|
66
|
-
}
|
|
67
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('supports string path', async () => {
|
|
71
|
-
const { Skill } = await import('../src/core/skill');
|
|
72
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
73
|
-
const mdPath = path.join(tmpDir, 'x.md');
|
|
74
|
-
fs.writeFileSync(mdPath, '---\nname: x\ndescription: x\n---\n\nBody.', 'utf-8');
|
|
75
|
-
const s = Skill.fromMarkdown(mdPath);
|
|
76
|
-
expect(s).not.toBeNull();
|
|
77
|
-
if (s) {
|
|
78
|
-
expect(s.name).toBe('x');
|
|
79
|
-
expect(typeof s.sourcePath).toBe('string');
|
|
80
|
-
}
|
|
81
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('derives triggers from quoted descriptions', async () => {
|
|
85
|
-
const { Skill } = await import('../src/core/skill');
|
|
86
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
87
|
-
const mdPath = path.join(tmpDir, 'pptx.md');
|
|
88
|
-
fs.writeFileSync(mdPath, `---
|
|
89
|
-
name: pptx
|
|
90
|
-
description: |-
|
|
91
|
-
Use this skill any time a .pptx file is involved.
|
|
92
|
-
Trigger whenever the user mentions "deck," "slides," or "presentation."
|
|
93
|
-
If a .pptx file needs to be opened, use it.
|
|
94
|
-
---
|
|
95
|
-
Body.`, 'utf-8');
|
|
96
|
-
|
|
97
|
-
const s = Skill.fromMarkdown(mdPath);
|
|
98
|
-
expect(s).not.toBeNull();
|
|
99
|
-
if (s) {
|
|
100
|
-
const triggersLower = s.triggers.map(t => t.toLowerCase());
|
|
101
|
-
expect(triggersLower).toContain('deck');
|
|
102
|
-
expect(triggersLower).toContain('slides');
|
|
103
|
-
expect(triggersLower).toContain('presentation');
|
|
104
|
-
expect(triggersLower).toContain('.pptx');
|
|
105
|
-
}
|
|
106
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('small body loaded in full', async () => {
|
|
110
|
-
const { Skill } = await import('../src/core/skill');
|
|
111
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
112
|
-
const mdPath = path.join(tmpDir, 'small.md');
|
|
113
|
-
fs.writeFileSync(mdPath, '---\nname: small\ndescription: x\n---\n\n# Small Skill\n\nThis fits inline easily.', 'utf-8');
|
|
114
|
-
const s = Skill.fromMarkdown(mdPath);
|
|
115
|
-
expect(s).not.toBeNull();
|
|
116
|
-
if (s) {
|
|
117
|
-
expect(s.bodyTruncated).toBe(false);
|
|
118
|
-
expect(s.systemPrompt).toContain('This fits inline easily');
|
|
119
|
-
}
|
|
120
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('large body truncated to head', async () => {
|
|
124
|
-
const { Skill } = await import('../src/core/skill');
|
|
125
|
-
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
126
|
-
const mdPath = path.join(tmpDir, 'big.md');
|
|
127
|
-
|
|
128
|
-
let body = '# Big Skill\n\n## Quick Reference\nFirst section content.\n\n';
|
|
129
|
-
body += '## Detailed Guide\n';
|
|
130
|
-
body += 'X'.repeat(5000);
|
|
131
|
-
|
|
132
|
-
fs.writeFileSync(mdPath, `---\nname: big\ndescription: x\n---\n\n${body}`, 'utf-8');
|
|
133
|
-
const s = Skill.fromMarkdown(mdPath);
|
|
134
|
-
expect(s).not.toBeNull();
|
|
135
|
-
if (s) {
|
|
136
|
-
expect(s.bodyTruncated).toBe(true);
|
|
137
|
-
expect(s.systemPrompt).toContain('Big Skill');
|
|
138
|
-
expect(s.systemPrompt).toContain('Quick Reference');
|
|
139
|
-
expect(s.systemPrompt).not.toContain('Detailed Guide');
|
|
140
|
-
}
|
|
141
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('SkillRegistry', () => {
|
|
146
|
-
it('registers and retrieves skills', async () => {
|
|
147
|
-
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
148
|
-
const reg = new SkillRegistry();
|
|
149
|
-
const skill = new Skill({ name: 'test', description: 'Test' });
|
|
150
|
-
reg.register(skill);
|
|
151
|
-
expect(reg.get('test')).toBe(skill);
|
|
152
|
-
expect(reg.get('missing')).toBeUndefined();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('lists names', async () => {
|
|
156
|
-
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
157
|
-
const reg = new SkillRegistry();
|
|
158
|
-
reg.register(new Skill({ name: 'a', description: 'A' }));
|
|
159
|
-
reg.register(new Skill({ name: 'b', description: 'B' }));
|
|
160
|
-
expect(reg.listNames()).toEqual(['a', 'b']);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('merges registries', async () => {
|
|
164
|
-
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
165
|
-
const r1 = new SkillRegistry();
|
|
166
|
-
r1.register(new Skill({ name: 'a', description: 'A' }));
|
|
167
|
-
const r2 = new SkillRegistry();
|
|
168
|
-
r2.register(new Skill({ name: 'b', description: 'B' }));
|
|
169
|
-
r1.merge(r2);
|
|
170
|
-
expect(r1.get('b')).toBeDefined();
|
|
171
|
-
});
|
|
172
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for skill system.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
describe('Skill', () => {
|
|
9
|
+
it('creates a skill with model override', async () => {
|
|
10
|
+
const { Skill } = await import('../src/core/skill');
|
|
11
|
+
const skill = new Skill({
|
|
12
|
+
name: 'test_skill',
|
|
13
|
+
description: 'A test skill',
|
|
14
|
+
model: 'claude-opus-4-7',
|
|
15
|
+
});
|
|
16
|
+
expect(skill.model).toBe('claude-opus-4-7');
|
|
17
|
+
expect(skill.temperature).toBeNull();
|
|
18
|
+
expect(skill.maxTokens).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('creates a skill with temperature override', async () => {
|
|
22
|
+
const { Skill } = await import('../src/core/skill');
|
|
23
|
+
const skill = new Skill({ name: 'test_skill', description: 'A test skill', temperature: 0.3 });
|
|
24
|
+
expect(skill.temperature).toBe(0.3);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('creates a skill with max_tokens override', async () => {
|
|
28
|
+
const { Skill } = await import('../src/core/skill');
|
|
29
|
+
const skill = new Skill({ name: 'test_skill', description: 'A test skill', maxTokens: 32000 });
|
|
30
|
+
expect(skill.maxTokens).toBe(32000);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('default has no overrides', async () => {
|
|
34
|
+
const { Skill } = await import('../src/core/skill');
|
|
35
|
+
const skill = new Skill({ name: 'test', description: 'test' });
|
|
36
|
+
expect(skill.model).toBeNull();
|
|
37
|
+
expect(skill.temperature).toBeNull();
|
|
38
|
+
expect(skill.maxTokens).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('Skill.fromMarkdown', () => {
|
|
43
|
+
it('loads skill with YAML frontmatter', async () => {
|
|
44
|
+
const { Skill } = await import('../src/core/skill');
|
|
45
|
+
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
46
|
+
const mdPath = path.join(tmpDir, 'test.md');
|
|
47
|
+
fs.writeFileSync(mdPath, `---
|
|
48
|
+
name: test_skill
|
|
49
|
+
description: A test
|
|
50
|
+
model: claude-sonnet-4-6
|
|
51
|
+
temperature: 0.5
|
|
52
|
+
max_tokens: 8192
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Test Skill
|
|
56
|
+
This is a test skill.
|
|
57
|
+
`, 'utf-8');
|
|
58
|
+
|
|
59
|
+
const skill = Skill.fromMarkdown(mdPath);
|
|
60
|
+
expect(skill).not.toBeNull();
|
|
61
|
+
if (skill) {
|
|
62
|
+
expect(skill.model).toBe('claude-sonnet-4-6');
|
|
63
|
+
expect(skill.temperature).toBe(0.5);
|
|
64
|
+
expect(skill.maxTokens).toBe(8192);
|
|
65
|
+
expect(skill.systemPrompt).toContain('This is a test skill');
|
|
66
|
+
}
|
|
67
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('supports string path', async () => {
|
|
71
|
+
const { Skill } = await import('../src/core/skill');
|
|
72
|
+
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
73
|
+
const mdPath = path.join(tmpDir, 'x.md');
|
|
74
|
+
fs.writeFileSync(mdPath, '---\nname: x\ndescription: x\n---\n\nBody.', 'utf-8');
|
|
75
|
+
const s = Skill.fromMarkdown(mdPath);
|
|
76
|
+
expect(s).not.toBeNull();
|
|
77
|
+
if (s) {
|
|
78
|
+
expect(s.name).toBe('x');
|
|
79
|
+
expect(typeof s.sourcePath).toBe('string');
|
|
80
|
+
}
|
|
81
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('derives triggers from quoted descriptions', async () => {
|
|
85
|
+
const { Skill } = await import('../src/core/skill');
|
|
86
|
+
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
87
|
+
const mdPath = path.join(tmpDir, 'pptx.md');
|
|
88
|
+
fs.writeFileSync(mdPath, `---
|
|
89
|
+
name: pptx
|
|
90
|
+
description: |-
|
|
91
|
+
Use this skill any time a .pptx file is involved.
|
|
92
|
+
Trigger whenever the user mentions "deck," "slides," or "presentation."
|
|
93
|
+
If a .pptx file needs to be opened, use it.
|
|
94
|
+
---
|
|
95
|
+
Body.`, 'utf-8');
|
|
96
|
+
|
|
97
|
+
const s = Skill.fromMarkdown(mdPath);
|
|
98
|
+
expect(s).not.toBeNull();
|
|
99
|
+
if (s) {
|
|
100
|
+
const triggersLower = s.triggers.map(t => t.toLowerCase());
|
|
101
|
+
expect(triggersLower).toContain('deck');
|
|
102
|
+
expect(triggersLower).toContain('slides');
|
|
103
|
+
expect(triggersLower).toContain('presentation');
|
|
104
|
+
expect(triggersLower).toContain('.pptx');
|
|
105
|
+
}
|
|
106
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('small body loaded in full', async () => {
|
|
110
|
+
const { Skill } = await import('../src/core/skill');
|
|
111
|
+
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
112
|
+
const mdPath = path.join(tmpDir, 'small.md');
|
|
113
|
+
fs.writeFileSync(mdPath, '---\nname: small\ndescription: x\n---\n\n# Small Skill\n\nThis fits inline easily.', 'utf-8');
|
|
114
|
+
const s = Skill.fromMarkdown(mdPath);
|
|
115
|
+
expect(s).not.toBeNull();
|
|
116
|
+
if (s) {
|
|
117
|
+
expect(s.bodyTruncated).toBe(false);
|
|
118
|
+
expect(s.systemPrompt).toContain('This fits inline easily');
|
|
119
|
+
}
|
|
120
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('large body truncated to head', async () => {
|
|
124
|
+
const { Skill } = await import('../src/core/skill');
|
|
125
|
+
const tmpDir = fs.mkdtempSync('skill-test-');
|
|
126
|
+
const mdPath = path.join(tmpDir, 'big.md');
|
|
127
|
+
|
|
128
|
+
let body = '# Big Skill\n\n## Quick Reference\nFirst section content.\n\n';
|
|
129
|
+
body += '## Detailed Guide\n';
|
|
130
|
+
body += 'X'.repeat(5000);
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(mdPath, `---\nname: big\ndescription: x\n---\n\n${body}`, 'utf-8');
|
|
133
|
+
const s = Skill.fromMarkdown(mdPath);
|
|
134
|
+
expect(s).not.toBeNull();
|
|
135
|
+
if (s) {
|
|
136
|
+
expect(s.bodyTruncated).toBe(true);
|
|
137
|
+
expect(s.systemPrompt).toContain('Big Skill');
|
|
138
|
+
expect(s.systemPrompt).toContain('Quick Reference');
|
|
139
|
+
expect(s.systemPrompt).not.toContain('Detailed Guide');
|
|
140
|
+
}
|
|
141
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('SkillRegistry', () => {
|
|
146
|
+
it('registers and retrieves skills', async () => {
|
|
147
|
+
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
148
|
+
const reg = new SkillRegistry();
|
|
149
|
+
const skill = new Skill({ name: 'test', description: 'Test' });
|
|
150
|
+
reg.register(skill);
|
|
151
|
+
expect(reg.get('test')).toBe(skill);
|
|
152
|
+
expect(reg.get('missing')).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('lists names', async () => {
|
|
156
|
+
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
157
|
+
const reg = new SkillRegistry();
|
|
158
|
+
reg.register(new Skill({ name: 'a', description: 'A' }));
|
|
159
|
+
reg.register(new Skill({ name: 'b', description: 'B' }));
|
|
160
|
+
expect(reg.listNames()).toEqual(['a', 'b']);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('merges registries', async () => {
|
|
164
|
+
const { Skill, SkillRegistry } = await import('../src/core/skill');
|
|
165
|
+
const r1 = new SkillRegistry();
|
|
166
|
+
r1.register(new Skill({ name: 'a', description: 'A' }));
|
|
167
|
+
const r2 = new SkillRegistry();
|
|
168
|
+
r2.register(new Skill({ name: 'b', description: 'B' }));
|
|
169
|
+
r1.merge(r2);
|
|
170
|
+
expect(r1.get('b')).toBeDefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { Task, TaskState, TaskResult, VALID_TRANSITIONS } from "../src/core/agent/task";
|
|
3
|
+
// Also assert the re-export path stays stable for external importers.
|
|
4
|
+
import { Task as TaskViaAgent, TaskState as TaskStateViaAgent } from "../src/core/agent";
|
|
5
|
+
|
|
6
|
+
describe("Task domain model", () => {
|
|
7
|
+
it("applies sensible defaults", () => {
|
|
8
|
+
const t = new Task({ id: "1", description: "do a thing" });
|
|
9
|
+
expect(t.status).toBe(TaskState.PENDING);
|
|
10
|
+
expect(t.assignedTo).toBeNull();
|
|
11
|
+
expect(t.dependsOn).toEqual([]);
|
|
12
|
+
expect(t.priority).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("allows PENDING -> RUNNING -> COMPLETED", () => {
|
|
16
|
+
const t = new Task({ id: "1", description: "x" });
|
|
17
|
+
t.transitionTo(TaskState.RUNNING);
|
|
18
|
+
expect(t.status).toBe(TaskState.RUNNING);
|
|
19
|
+
t.transitionTo(TaskState.COMPLETED);
|
|
20
|
+
expect(t.status).toBe(TaskState.COMPLETED);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("rejects illegal transitions", () => {
|
|
24
|
+
const t = new Task({ id: "1", description: "x" });
|
|
25
|
+
// PENDING -> COMPLETED is not allowed (must go through RUNNING)
|
|
26
|
+
expect(() => t.transitionTo(TaskState.COMPLETED)).toThrow(/Invalid task state transition/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("treats COMPLETED as terminal", () => {
|
|
30
|
+
expect(VALID_TRANSITIONS[TaskState.COMPLETED].size).toBe(0);
|
|
31
|
+
expect(VALID_TRANSITIONS[TaskState.SKIPPED].size).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("allows FAILED -> RUNNING (retry) and FAILED -> SKIPPED", () => {
|
|
35
|
+
const t = new Task({ id: "1", description: "x", status: TaskState.FAILED });
|
|
36
|
+
expect(() => t.transitionTo(TaskState.RUNNING)).not.toThrow();
|
|
37
|
+
const t2 = new Task({ id: "2", description: "y", status: TaskState.FAILED });
|
|
38
|
+
expect(() => t2.transitionTo(TaskState.SKIPPED)).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("allDeps merges parentId with dependsOn without duplicates", () => {
|
|
42
|
+
const t = new Task({ id: "3", description: "z", parentId: "1", dependsOn: ["1", "2"] });
|
|
43
|
+
expect(t.allDeps.sort()).toEqual(["1", "2"]);
|
|
44
|
+
const t2 = new Task({ id: "4", description: "z", parentId: "9", dependsOn: ["2"] });
|
|
45
|
+
expect(t2.allDeps.sort()).toEqual(["2", "9"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("TaskResult carries success/content/data", () => {
|
|
49
|
+
const ok = new TaskResult(true, "done", { x: 1 });
|
|
50
|
+
expect(ok.success).toBe(true);
|
|
51
|
+
expect(ok.content).toBe("done");
|
|
52
|
+
expect(ok.data).toEqual({ x: 1 });
|
|
53
|
+
expect(new TaskResult(false, "oops").data).toEqual({});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("re-export from ../core/agent stays identical", () => {
|
|
57
|
+
expect(TaskViaAgent).toBe(Task);
|
|
58
|
+
expect(TaskStateViaAgent).toBe(TaskState);
|
|
59
|
+
});
|
|
60
|
+
});
|