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/icons.test.ts
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for agent icon system.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { AGENT_COLORS, AGENT_EMOJI, iconText, svgPath } from '../src/core/icons';
|
|
6
|
-
|
|
7
|
-
describe('AGENT_COLORS', () => {
|
|
8
|
-
it('has all 6 agents', () => {
|
|
9
|
-
expect(Object.keys(AGENT_COLORS).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('each agent has a non-empty color', () => {
|
|
13
|
-
for (const color of Object.values(AGENT_COLORS)) {
|
|
14
|
-
expect(color).toBeTruthy();
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('AGENT_EMOJI', () => {
|
|
20
|
-
it('has all 6 agents', () => {
|
|
21
|
-
expect(Object.keys(AGENT_EMOJI).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('unique emoji per agent', () => {
|
|
25
|
-
const values = Object.values(AGENT_EMOJI);
|
|
26
|
-
expect(new Set(values).size).toBe(values.length);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
describe('iconText', () => {
|
|
31
|
-
it('returns glyph for known agent', () => {
|
|
32
|
-
expect(iconText('fog')).toBe('≋');
|
|
33
|
-
expect(iconText('fair')).toBe('☼');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('returns name as fallback for unknown agent', () => {
|
|
37
|
-
expect(iconText('unknown')).toBe('unknown');
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe('svgPath', () => {
|
|
42
|
-
it('returns a path ending with .svg', () => {
|
|
43
|
-
expect(svgPath('fog')).toContain('icons');
|
|
44
|
-
});
|
|
45
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for agent icon system.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { AGENT_COLORS, AGENT_EMOJI, iconText, svgPath } from '../src/core/icons';
|
|
6
|
+
|
|
7
|
+
describe('AGENT_COLORS', () => {
|
|
8
|
+
it('has all 6 agents', () => {
|
|
9
|
+
expect(Object.keys(AGENT_COLORS).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('each agent has a non-empty color', () => {
|
|
13
|
+
for (const color of Object.values(AGENT_COLORS)) {
|
|
14
|
+
expect(color).toBeTruthy();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('AGENT_EMOJI', () => {
|
|
20
|
+
it('has all 6 agents', () => {
|
|
21
|
+
expect(Object.keys(AGENT_EMOJI).sort()).toEqual(['dew', 'fair', 'fog', 'frost', 'rain', 'snow']);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('unique emoji per agent', () => {
|
|
25
|
+
const values = Object.values(AGENT_EMOJI);
|
|
26
|
+
expect(new Set(values).size).toBe(values.length);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('iconText', () => {
|
|
31
|
+
it('returns glyph for known agent', () => {
|
|
32
|
+
expect(iconText('fog')).toBe('≋');
|
|
33
|
+
expect(iconText('fair')).toBe('☼');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns name as fallback for unknown agent', () => {
|
|
37
|
+
expect(iconText('unknown')).toBe('unknown');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('svgPath', () => {
|
|
42
|
+
it('returns a path ending with .svg', () => {
|
|
43
|
+
expect(svgPath('fog')).toContain('icons');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
import { Memory } from "../src/core/memory";
|
|
6
|
+
|
|
7
|
+
/** addMessage mutates shortTerm through an async mutex — let microtasks flush. */
|
|
8
|
+
const flush = () => new Promise((r) => setTimeout(r, 15));
|
|
9
|
+
|
|
10
|
+
let tmpDirs: string[] = [];
|
|
11
|
+
function tmpConfig(shortTermLimit = 100) {
|
|
12
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sky-mem-"));
|
|
13
|
+
tmpDirs.push(dir);
|
|
14
|
+
return { dbPath: path.join(dir, "memory.db"), shortTermLimit, maxPersistedMessages: 2000 };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } }
|
|
19
|
+
tmpDirs = [];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe("Memory · short-term (in-memory, no DB)", () => {
|
|
23
|
+
it("makes a message visible SYNCHRONOUSLY (regression: first-turn crash)", () => {
|
|
24
|
+
// Previously addMessage pushed inside an async mutex, so getMessages() in the
|
|
25
|
+
// same tick missed the message — crashing chatImpl/chatStreamImpl on a fresh
|
|
26
|
+
// session's first user message. The push must be synchronous.
|
|
27
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
28
|
+
mem.addMessage("user", "first message");
|
|
29
|
+
const msgs = mem.getMessages(); // no flush!
|
|
30
|
+
expect(msgs).toHaveLength(1);
|
|
31
|
+
expect(msgs[0]).toMatchObject({ role: "user", content: "first message" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("records and returns messages in order", async () => {
|
|
35
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
36
|
+
mem.addMessage("user", "hello");
|
|
37
|
+
mem.addMessage("assistant", "hi there");
|
|
38
|
+
await flush();
|
|
39
|
+
const msgs = mem.getMessages();
|
|
40
|
+
expect(msgs.map((m) => m.role)).toEqual(["user", "assistant"]);
|
|
41
|
+
expect(msgs[0].content).toBe("hello");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("preserves tool-call metadata in getMessages", async () => {
|
|
45
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
46
|
+
mem.addMessage("assistant", "", { toolCalls: [{ id: "t1", function: { name: "x" } }] });
|
|
47
|
+
mem.addMessage("tool", "result", { name: "x", toolCallId: "t1" });
|
|
48
|
+
await flush();
|
|
49
|
+
const msgs = mem.getMessages();
|
|
50
|
+
const toolMsg = msgs.find((m) => m.role === "tool");
|
|
51
|
+
expect(toolMsg?.tool_call_id).toBe("t1");
|
|
52
|
+
expect(toolMsg?.name).toBe("x");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("prunes past the short-term limit but keeps system messages", async () => {
|
|
56
|
+
const mem = new Memory(tmpConfig(3), "fog");
|
|
57
|
+
mem.addMessage("system", "persona");
|
|
58
|
+
for (let i = 0; i < 5; i++) mem.addMessage("user", `m${i}`);
|
|
59
|
+
await flush();
|
|
60
|
+
const msgs = mem.getMessages();
|
|
61
|
+
expect(msgs.length).toBeLessThanOrEqual(3);
|
|
62
|
+
expect(msgs.some((m) => m.role === "system" && m.content === "persona")).toBe(true);
|
|
63
|
+
// most recent user message survives
|
|
64
|
+
expect(msgs[msgs.length - 1].content).toBe("m4");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("clearShortTerm keeps system messages", async () => {
|
|
68
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
69
|
+
mem.addMessage("system", "persona");
|
|
70
|
+
mem.addMessage("user", "hello");
|
|
71
|
+
await flush();
|
|
72
|
+
await mem.clearShortTerm();
|
|
73
|
+
const msgs = mem.getMessages();
|
|
74
|
+
expect(msgs).toHaveLength(1);
|
|
75
|
+
expect(msgs[0].role).toBe("system");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Memory · context window estimation", () => {
|
|
80
|
+
it("counts CJK as heavier than ascii", async () => {
|
|
81
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
82
|
+
mem.addMessage("user", "你好世界"); // 4 CJK chars
|
|
83
|
+
await flush();
|
|
84
|
+
const usage = mem.getContextWindowUsage();
|
|
85
|
+
expect(usage.messageCount).toBe(1);
|
|
86
|
+
expect(usage.totalChars).toBe(4);
|
|
87
|
+
// CJK weight is 2/char => >= 8
|
|
88
|
+
expect(usage.estimatedTokens).toBeGreaterThanOrEqual(8);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("Memory · working memory", () => {
|
|
93
|
+
it("set/get/clear round-trips", () => {
|
|
94
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
95
|
+
mem.setWorking("plan", { step: 1 });
|
|
96
|
+
expect(mem.getWorking("plan")).toEqual({ step: 1 });
|
|
97
|
+
expect(mem.getWorking("missing", "fallback")).toBe("fallback");
|
|
98
|
+
mem.clearWorking();
|
|
99
|
+
expect(mem.getWorking("plan")).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("Memory · long-term (SQLite)", () => {
|
|
104
|
+
it("remember / recall / forget round-trips", async () => {
|
|
105
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
106
|
+
await mem.initDb();
|
|
107
|
+
try {
|
|
108
|
+
await mem.remember("favorite_lang", "typescript", "pref");
|
|
109
|
+
const hits = await mem.recall("favorite_lang");
|
|
110
|
+
expect(hits).toHaveLength(1);
|
|
111
|
+
expect(hits[0].value).toBe("typescript");
|
|
112
|
+
expect(hits[0].category).toBe("pref");
|
|
113
|
+
|
|
114
|
+
await mem.forget("favorite_lang");
|
|
115
|
+
expect(await mem.recall("favorite_lang")).toHaveLength(0);
|
|
116
|
+
} finally {
|
|
117
|
+
await mem.close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("recall filters by category", async () => {
|
|
122
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
123
|
+
await mem.initDb();
|
|
124
|
+
try {
|
|
125
|
+
await mem.remember("a", 1, "x");
|
|
126
|
+
await mem.remember("b", 2, "y");
|
|
127
|
+
const xs = await mem.recall(null, "x");
|
|
128
|
+
expect(xs).toHaveLength(1);
|
|
129
|
+
expect(xs[0].key).toBe("a");
|
|
130
|
+
} finally {
|
|
131
|
+
await mem.close();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("getMemoryStats returns a populated object", async () => {
|
|
136
|
+
const mem = new Memory(tmpConfig(), "fog");
|
|
137
|
+
await mem.initDb();
|
|
138
|
+
try {
|
|
139
|
+
await mem.remember("k", "v");
|
|
140
|
+
const stats = await mem.getMemoryStats();
|
|
141
|
+
expect(typeof stats).toBe("object");
|
|
142
|
+
expect(stats).not.toBeNull();
|
|
143
|
+
} finally {
|
|
144
|
+
await mem.close();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
package/tests/router.test.ts
CHANGED
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the complexity router.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { classify, pickAgentForGoal } from '../src/core/router';
|
|
6
|
-
|
|
7
|
-
describe('classify', () => {
|
|
8
|
-
it.each([
|
|
9
|
-
'你好',
|
|
10
|
-
'hi',
|
|
11
|
-
'在吗',
|
|
12
|
-
'谢谢',
|
|
13
|
-
'什么是 RAG?',
|
|
14
|
-
'为什么天空是蓝的?',
|
|
15
|
-
'1 + 1 = ?',
|
|
16
|
-
'解释一下闭包',
|
|
17
|
-
])('returns "direct" for simple questions: %s', (goal) => {
|
|
18
|
-
expect(classify(goal)).toBe('direct');
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it.each([
|
|
22
|
-
'帮我写一个二分查找函数',
|
|
23
|
-
'搜一下今天的天气',
|
|
24
|
-
'审查 src/foo.py 的安全问题',
|
|
25
|
-
'把这段中文翻译成英文:我喜欢猫',
|
|
26
|
-
])('returns "single" for focused tasks: %s', (goal) => {
|
|
27
|
-
expect(classify(goal)).toBe('single');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it.each([
|
|
31
|
-
'先帮我分析这段代码,然后重构它,最后写测试',
|
|
32
|
-
'首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
|
|
33
|
-
'1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
|
|
34
|
-
])('returns "orchestrate" for multi-step: %s', (goal) => {
|
|
35
|
-
expect(classify(goal)).toBe('orchestrate');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('empty goal returns direct', () => {
|
|
39
|
-
expect(classify('')).toBe('direct');
|
|
40
|
-
expect(classify(' ')).toBe('direct');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('inline enumerated list is orchestrate', () => {
|
|
44
|
-
expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
|
|
45
|
-
expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('two inline items is not orchestrate', () => {
|
|
49
|
-
expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('pickAgentForGoal', () => {
|
|
54
|
-
const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
|
|
55
|
-
|
|
56
|
-
it('security keyword picks frost', () => {
|
|
57
|
-
expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('research keyword picks fog', () => {
|
|
61
|
-
expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('greeting picks fair', () => {
|
|
65
|
-
expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('falls back to rain', () => {
|
|
69
|
-
expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('binary search picks rain not fog', () => {
|
|
73
|
-
expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
|
|
74
|
-
expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('skips missing agents', () => {
|
|
78
|
-
const available = new Set(['rain', 'snow']);
|
|
79
|
-
const result = pickAgentForGoal('做安全审查', available);
|
|
80
|
-
expect(available.has(result)).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('single agent available', () => {
|
|
84
|
-
expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the complexity router.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { classify, pickAgentForGoal } from '../src/core/router';
|
|
6
|
+
|
|
7
|
+
describe('classify', () => {
|
|
8
|
+
it.each([
|
|
9
|
+
'你好',
|
|
10
|
+
'hi',
|
|
11
|
+
'在吗',
|
|
12
|
+
'谢谢',
|
|
13
|
+
'什么是 RAG?',
|
|
14
|
+
'为什么天空是蓝的?',
|
|
15
|
+
'1 + 1 = ?',
|
|
16
|
+
'解释一下闭包',
|
|
17
|
+
])('returns "direct" for simple questions: %s', (goal) => {
|
|
18
|
+
expect(classify(goal)).toBe('direct');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
'帮我写一个二分查找函数',
|
|
23
|
+
'搜一下今天的天气',
|
|
24
|
+
'审查 src/foo.py 的安全问题',
|
|
25
|
+
'把这段中文翻译成英文:我喜欢猫',
|
|
26
|
+
])('returns "single" for focused tasks: %s', (goal) => {
|
|
27
|
+
expect(classify(goal)).toBe('single');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it.each([
|
|
31
|
+
'先帮我分析这段代码,然后重构它,最后写测试',
|
|
32
|
+
'首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
|
|
33
|
+
'1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
|
|
34
|
+
])('returns "orchestrate" for multi-step: %s', (goal) => {
|
|
35
|
+
expect(classify(goal)).toBe('orchestrate');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('empty goal returns direct', () => {
|
|
39
|
+
expect(classify('')).toBe('direct');
|
|
40
|
+
expect(classify(' ')).toBe('direct');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('inline enumerated list is orchestrate', () => {
|
|
44
|
+
expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
|
|
45
|
+
expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('two inline items is not orchestrate', () => {
|
|
49
|
+
expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('pickAgentForGoal', () => {
|
|
54
|
+
const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
|
|
55
|
+
|
|
56
|
+
it('security keyword picks frost', () => {
|
|
57
|
+
expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('research keyword picks fog', () => {
|
|
61
|
+
expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('greeting picks fair', () => {
|
|
65
|
+
expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('falls back to rain', () => {
|
|
69
|
+
expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('binary search picks rain not fog', () => {
|
|
73
|
+
expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
|
|
74
|
+
expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('skips missing agents', () => {
|
|
78
|
+
const available = new Set(['rain', 'snow']);
|
|
79
|
+
const result = pickAgentForGoal('做安全审查', available);
|
|
80
|
+
expect(available.has(result)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('single agent available', () => {
|
|
84
|
+
expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
|
|
85
|
+
});
|
|
86
|
+
});
|
package/tests/schemas.test.ts
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for structured output schema validation.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect } from 'vitest';
|
|
5
|
-
import { validateTaskPlan, TaskPlanSchema, parseSchema, SchemaValidationError } from '../src/core/schemas';
|
|
6
|
-
|
|
7
|
-
describe('validateTaskPlan', () => {
|
|
8
|
-
it('parses valid JSON plan', () => {
|
|
9
|
-
const data = JSON.parse('{"goal": "build app", "steps": [{"id": "1", "description": "design", "agent": "fog"}]}');
|
|
10
|
-
const plan = validateTaskPlan(data);
|
|
11
|
-
expect(plan.goal).toBe('build app');
|
|
12
|
-
expect(plan.steps).toHaveLength(1);
|
|
13
|
-
expect(plan.steps[0].agent).toBe('fog');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('passes through agent name', () => {
|
|
17
|
-
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "agent": "unknown"}]}');
|
|
18
|
-
const plan = validateTaskPlan(data);
|
|
19
|
-
// Schema-level validation just passes through; agent fallback is in SnowAgent
|
|
20
|
-
expect(plan.steps[0].agent).toBe('unknown');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('empty steps', () => {
|
|
24
|
-
const data = JSON.parse('{"goal": "x", "steps": []}');
|
|
25
|
-
const plan = validateTaskPlan(data);
|
|
26
|
-
expect(plan.steps).toHaveLength(0);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('parses depends_on', () => {
|
|
30
|
-
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "depends_on": ["0"]}]}');
|
|
31
|
-
const plan = validateTaskPlan(data);
|
|
32
|
-
expect(plan.steps[0].depends_on).toEqual(['0']);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('default fields', () => {
|
|
36
|
-
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a"}]}');
|
|
37
|
-
const plan = validateTaskPlan(data);
|
|
38
|
-
expect(plan.steps[0].agent).toBe('rain');
|
|
39
|
-
expect(plan.steps[0].depends_on).toEqual([]);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
describe('SchemaValidationError', () => {
|
|
44
|
-
it('raises on empty', () => {
|
|
45
|
-
expect(() => parseSchema('', TaskPlanSchema)).toThrow(SchemaValidationError);
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('raises on garbage', () => {
|
|
49
|
-
expect(() => parseSchema('<html>garbage</html>', TaskPlanSchema)).toThrow(SchemaValidationError);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for structured output schema validation.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { validateTaskPlan, TaskPlanSchema, parseSchema, SchemaValidationError } from '../src/core/schemas';
|
|
6
|
+
|
|
7
|
+
describe('validateTaskPlan', () => {
|
|
8
|
+
it('parses valid JSON plan', () => {
|
|
9
|
+
const data = JSON.parse('{"goal": "build app", "steps": [{"id": "1", "description": "design", "agent": "fog"}]}');
|
|
10
|
+
const plan = validateTaskPlan(data);
|
|
11
|
+
expect(plan.goal).toBe('build app');
|
|
12
|
+
expect(plan.steps).toHaveLength(1);
|
|
13
|
+
expect(plan.steps[0].agent).toBe('fog');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('passes through agent name', () => {
|
|
17
|
+
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "agent": "unknown"}]}');
|
|
18
|
+
const plan = validateTaskPlan(data);
|
|
19
|
+
// Schema-level validation just passes through; agent fallback is in SnowAgent
|
|
20
|
+
expect(plan.steps[0].agent).toBe('unknown');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('empty steps', () => {
|
|
24
|
+
const data = JSON.parse('{"goal": "x", "steps": []}');
|
|
25
|
+
const plan = validateTaskPlan(data);
|
|
26
|
+
expect(plan.steps).toHaveLength(0);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('parses depends_on', () => {
|
|
30
|
+
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a", "depends_on": ["0"]}]}');
|
|
31
|
+
const plan = validateTaskPlan(data);
|
|
32
|
+
expect(plan.steps[0].depends_on).toEqual(['0']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('default fields', () => {
|
|
36
|
+
const data = JSON.parse('{"goal": "x", "steps": [{"id": "1", "description": "a"}]}');
|
|
37
|
+
const plan = validateTaskPlan(data);
|
|
38
|
+
expect(plan.steps[0].agent).toBe('rain');
|
|
39
|
+
expect(plan.steps[0].depends_on).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('SchemaValidationError', () => {
|
|
44
|
+
it('raises on empty', () => {
|
|
45
|
+
expect(() => parseSchema('', TaskPlanSchema)).toThrow(SchemaValidationError);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('raises on garbage', () => {
|
|
49
|
+
expect(() => parseSchema('<html>garbage</html>', TaskPlanSchema)).toThrow(SchemaValidationError);
|
|
50
|
+
});
|
|
51
|
+
});
|