skyloom 1.12.0 → 1.13.1
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 +137 -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 +127 -74
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts +52 -19
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +198 -265
- 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 +417 -372
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +174 -223
- 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/tests/tui.test.ts +67 -0
- package/vitest.config.ts +17 -17
package/src/web/tts.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
|
|
3
|
-
* Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
|
7
|
-
|
|
8
|
-
export interface VoiceOption {
|
|
9
|
-
key: string;
|
|
10
|
-
name: string;
|
|
11
|
-
desc: string;
|
|
12
|
-
voiceType: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const VOICE_CATALOG: VoiceOption[] = [
|
|
16
|
-
{ key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
|
|
17
|
-
{ key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
|
|
18
|
-
{ key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
|
|
19
|
-
{ key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
|
|
20
|
-
{ key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
|
|
21
|
-
{ key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
export interface TTSOptions {
|
|
25
|
-
text: string;
|
|
26
|
-
voice?: string;
|
|
27
|
-
speed?: number;
|
|
28
|
-
pitch?: number;
|
|
29
|
-
apiKey?: string;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface TTSResult {
|
|
33
|
-
success: boolean;
|
|
34
|
-
audioBase64?: string;
|
|
35
|
-
format?: string;
|
|
36
|
-
error?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Convert text to speech using Volcano Engine TTS API.
|
|
41
|
-
*/
|
|
42
|
-
export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
|
|
43
|
-
const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
|
|
44
|
-
|
|
45
|
-
if (!text || !text.trim()) {
|
|
46
|
-
return { success: false, error: 'Text is required' };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
|
|
50
|
-
if (!apiKey) {
|
|
51
|
-
return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const payload = {
|
|
55
|
-
app: { appid: process.env.VOLC_APP_ID || '' },
|
|
56
|
-
user: { uid: 'skyloom' },
|
|
57
|
-
request: {
|
|
58
|
-
reqid: Math.random().toString(36).slice(2, 14),
|
|
59
|
-
text,
|
|
60
|
-
text_type: 'plain',
|
|
61
|
-
operation: 'query',
|
|
62
|
-
frontend_type: 'unitTson',
|
|
63
|
-
voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
|
|
64
|
-
audio: { audio_type: 'mp3' },
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const response = await fetch(HTTP_ENDPOINT, {
|
|
70
|
-
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
'Content-Type': 'application/json',
|
|
73
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify(payload),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const data: any = await response.json();
|
|
79
|
-
if (data.code !== 3000) {
|
|
80
|
-
return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
|
|
81
|
-
}
|
|
82
|
-
return { success: true, audioBase64: data.data, format: 'mp3' };
|
|
83
|
-
} catch (e: any) {
|
|
84
|
-
return { success: false, error: `TTS request failed: ${e.message || e}` };
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* List available TTS voices.
|
|
90
|
-
*/
|
|
91
|
-
export function listVoices(): VoiceOption[] {
|
|
92
|
-
return [...VOICE_CATALOG];
|
|
93
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
|
|
3
|
+
* Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
|
|
7
|
+
|
|
8
|
+
export interface VoiceOption {
|
|
9
|
+
key: string;
|
|
10
|
+
name: string;
|
|
11
|
+
desc: string;
|
|
12
|
+
voiceType: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const VOICE_CATALOG: VoiceOption[] = [
|
|
16
|
+
{ key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
|
|
17
|
+
{ key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
|
|
18
|
+
{ key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
|
|
19
|
+
{ key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
|
|
20
|
+
{ key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
|
|
21
|
+
{ key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface TTSOptions {
|
|
25
|
+
text: string;
|
|
26
|
+
voice?: string;
|
|
27
|
+
speed?: number;
|
|
28
|
+
pitch?: number;
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TTSResult {
|
|
33
|
+
success: boolean;
|
|
34
|
+
audioBase64?: string;
|
|
35
|
+
format?: string;
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert text to speech using Volcano Engine TTS API.
|
|
41
|
+
*/
|
|
42
|
+
export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
|
|
43
|
+
const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
|
|
44
|
+
|
|
45
|
+
if (!text || !text.trim()) {
|
|
46
|
+
return { success: false, error: 'Text is required' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
|
|
50
|
+
if (!apiKey) {
|
|
51
|
+
return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const payload = {
|
|
55
|
+
app: { appid: process.env.VOLC_APP_ID || '' },
|
|
56
|
+
user: { uid: 'skyloom' },
|
|
57
|
+
request: {
|
|
58
|
+
reqid: Math.random().toString(36).slice(2, 14),
|
|
59
|
+
text,
|
|
60
|
+
text_type: 'plain',
|
|
61
|
+
operation: 'query',
|
|
62
|
+
frontend_type: 'unitTson',
|
|
63
|
+
voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
|
|
64
|
+
audio: { audio_type: 'mp3' },
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(HTTP_ENDPOINT, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify(payload),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data: any = await response.json();
|
|
79
|
+
if (data.code !== 3000) {
|
|
80
|
+
return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
|
|
81
|
+
}
|
|
82
|
+
return { success: true, audioBase64: data.data, format: 'mp3' };
|
|
83
|
+
} catch (e: any) {
|
|
84
|
+
return { success: false, error: `TTS request failed: ${e.message || e}` };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List available TTS voices.
|
|
90
|
+
*/
|
|
91
|
+
export function listVoices(): VoiceOption[] {
|
|
92
|
+
return [...VOICE_CATALOG];
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseExtractedFacts, synthesizeDelegationSummary } from "../src/core/agent_helpers";
|
|
3
|
+
|
|
4
|
+
describe("parseExtractedFacts", () => {
|
|
5
|
+
it("parses a raw JSON array", () => {
|
|
6
|
+
const out = parseExtractedFacts('[{"key":"lang","value":"ts"}]');
|
|
7
|
+
expect(out).toEqual([{ key: "lang", value: "ts" }]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("parses JSON inside a markdown fence", () => {
|
|
11
|
+
const out = parseExtractedFacts('```json\n[{"key":"a","value":1}]\n```');
|
|
12
|
+
expect(out).toEqual([{ key: "a", value: 1 }]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("extracts a JSON array embedded in prose", () => {
|
|
16
|
+
const out = parseExtractedFacts('Sure! Here are the facts: [{"key":"x","value":true}] done.');
|
|
17
|
+
expect(out).toEqual([{ key: "x", value: true }]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns [] for empty or non-JSON input", () => {
|
|
21
|
+
expect(parseExtractedFacts("")).toEqual([]);
|
|
22
|
+
expect(parseExtractedFacts("no json here")).toEqual([]);
|
|
23
|
+
expect(parseExtractedFacts(" ")).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns [] when JSON is an object, not an array", () => {
|
|
27
|
+
expect(parseExtractedFacts('{"key":"x"}')).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("filters out non-object array members", () => {
|
|
31
|
+
const out = parseExtractedFacts('[{"key":"a","value":1}, "junk", 42, null]');
|
|
32
|
+
// null is typeof 'object' in JS, so it survives the filter — assert the real members
|
|
33
|
+
expect(out).toContainEqual({ key: "a", value: 1 });
|
|
34
|
+
expect(out).not.toContain("junk");
|
|
35
|
+
expect(out).not.toContain(42);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("synthesizeDelegationSummary", () => {
|
|
40
|
+
it("summarizes successes and failures", () => {
|
|
41
|
+
expect(synthesizeDelegationSummary([["fog", true], ["rain", false]])).toBe(
|
|
42
|
+
"[Delegated: fog | Failed: rain]"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
it("returns empty string when no delegations", () => {
|
|
46
|
+
expect(synthesizeDelegationSummary([])).toBe("");
|
|
47
|
+
});
|
|
48
|
+
});
|
package/tests/bus.test.ts
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the event bus.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
-
import { MessageBus, Event, EventType } from '../src/core/bus';
|
|
6
|
-
|
|
7
|
-
describe('MessageBus', () => {
|
|
8
|
-
it('subscribe and publish broadcasts to subscribers', async () => {
|
|
9
|
-
const bus = new MessageBus();
|
|
10
|
-
const received: Event[] = [];
|
|
11
|
-
|
|
12
|
-
async function handler(event: Event) {
|
|
13
|
-
received.push(event);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
bus.subscribe('test_agent', handler);
|
|
17
|
-
const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
|
|
18
|
-
await bus.publish(event);
|
|
19
|
-
|
|
20
|
-
expect(received).toHaveLength(1);
|
|
21
|
-
expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
|
|
22
|
-
expect(received[0].data).toEqual({ msg: 'hello' });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('direct message goes to target agent only', async () => {
|
|
26
|
-
const bus = new MessageBus();
|
|
27
|
-
const received: Event[] = [];
|
|
28
|
-
|
|
29
|
-
async function handler(event: Event) {
|
|
30
|
-
received.push(event);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
bus.subscribe('target_agent', handler);
|
|
34
|
-
const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
|
|
35
|
-
await bus.publish(event);
|
|
36
|
-
|
|
37
|
-
expect(received).toHaveLength(1);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('unsubscribe removes handler', async () => {
|
|
41
|
-
const bus = new MessageBus();
|
|
42
|
-
const received: Event[] = [];
|
|
43
|
-
|
|
44
|
-
async function handler(event: Event) {
|
|
45
|
-
received.push(event);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
bus.subscribe('agent', handler);
|
|
49
|
-
bus.unsubscribe('agent');
|
|
50
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
51
|
-
expect(received).toHaveLength(0);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('state listener receives state change events', async () => {
|
|
55
|
-
const bus = new MessageBus();
|
|
56
|
-
const received: Event[] = [];
|
|
57
|
-
|
|
58
|
-
async function listener(event: Event) {
|
|
59
|
-
received.push(event);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
bus.onStateChange(listener);
|
|
63
|
-
await bus.notifyStateChange(
|
|
64
|
-
new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
expect(received).toHaveLength(1);
|
|
68
|
-
expect(received[0].type).toBe(EventType.STATE_CHANGE);
|
|
69
|
-
expect(received[0].source).toBe('fog');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('state listener ignores non-state events', async () => {
|
|
73
|
-
const bus = new MessageBus();
|
|
74
|
-
const received: Event[] = [];
|
|
75
|
-
|
|
76
|
-
async function listener(event: Event) {
|
|
77
|
-
received.push(event);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
bus.onStateChange(listener);
|
|
81
|
-
await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
|
|
82
|
-
expect(received).toHaveLength(0);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('history tracks published events', async () => {
|
|
86
|
-
const bus = new MessageBus();
|
|
87
|
-
for (let i = 0; i < 5; i++) {
|
|
88
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
89
|
-
}
|
|
90
|
-
expect(bus.getHistory()).toHaveLength(5);
|
|
91
|
-
expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('broadcast skips source agent', async () => {
|
|
95
|
-
const bus = new MessageBus();
|
|
96
|
-
const called: string[] = [];
|
|
97
|
-
|
|
98
|
-
async function fogHandler(_event: Event) { called.push('fog'); }
|
|
99
|
-
async function rainHandler(_event: Event) { called.push('rain'); }
|
|
100
|
-
|
|
101
|
-
bus.subscribe('fog', fogHandler);
|
|
102
|
-
bus.subscribe('rain', rainHandler);
|
|
103
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
104
|
-
|
|
105
|
-
expect(called).not.toContain('fog');
|
|
106
|
-
expect(called).toContain('rain');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('handler exception does not block other handlers', async () => {
|
|
110
|
-
const bus = new MessageBus();
|
|
111
|
-
const good: Event[] = [];
|
|
112
|
-
|
|
113
|
-
async function failing(_event: Event) { throw new Error('failed'); }
|
|
114
|
-
async function ok(event: Event) { good.push(event); }
|
|
115
|
-
|
|
116
|
-
bus.subscribe('rain', failing);
|
|
117
|
-
bus.subscribe('dew', ok);
|
|
118
|
-
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
119
|
-
expect(good).toHaveLength(1);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the event bus.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
5
|
+
import { MessageBus, Event, EventType } from '../src/core/bus';
|
|
6
|
+
|
|
7
|
+
describe('MessageBus', () => {
|
|
8
|
+
it('subscribe and publish broadcasts to subscribers', async () => {
|
|
9
|
+
const bus = new MessageBus();
|
|
10
|
+
const received: Event[] = [];
|
|
11
|
+
|
|
12
|
+
async function handler(event: Event) {
|
|
13
|
+
received.push(event);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
bus.subscribe('test_agent', handler);
|
|
17
|
+
const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
|
|
18
|
+
await bus.publish(event);
|
|
19
|
+
|
|
20
|
+
expect(received).toHaveLength(1);
|
|
21
|
+
expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
|
|
22
|
+
expect(received[0].data).toEqual({ msg: 'hello' });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('direct message goes to target agent only', async () => {
|
|
26
|
+
const bus = new MessageBus();
|
|
27
|
+
const received: Event[] = [];
|
|
28
|
+
|
|
29
|
+
async function handler(event: Event) {
|
|
30
|
+
received.push(event);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
bus.subscribe('target_agent', handler);
|
|
34
|
+
const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
|
|
35
|
+
await bus.publish(event);
|
|
36
|
+
|
|
37
|
+
expect(received).toHaveLength(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('unsubscribe removes handler', async () => {
|
|
41
|
+
const bus = new MessageBus();
|
|
42
|
+
const received: Event[] = [];
|
|
43
|
+
|
|
44
|
+
async function handler(event: Event) {
|
|
45
|
+
received.push(event);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
bus.subscribe('agent', handler);
|
|
49
|
+
bus.unsubscribe('agent');
|
|
50
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
51
|
+
expect(received).toHaveLength(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('state listener receives state change events', async () => {
|
|
55
|
+
const bus = new MessageBus();
|
|
56
|
+
const received: Event[] = [];
|
|
57
|
+
|
|
58
|
+
async function listener(event: Event) {
|
|
59
|
+
received.push(event);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bus.onStateChange(listener);
|
|
63
|
+
await bus.notifyStateChange(
|
|
64
|
+
new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(received).toHaveLength(1);
|
|
68
|
+
expect(received[0].type).toBe(EventType.STATE_CHANGE);
|
|
69
|
+
expect(received[0].source).toBe('fog');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('state listener ignores non-state events', async () => {
|
|
73
|
+
const bus = new MessageBus();
|
|
74
|
+
const received: Event[] = [];
|
|
75
|
+
|
|
76
|
+
async function listener(event: Event) {
|
|
77
|
+
received.push(event);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
bus.onStateChange(listener);
|
|
81
|
+
await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
|
|
82
|
+
expect(received).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('history tracks published events', async () => {
|
|
86
|
+
const bus = new MessageBus();
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
|
|
89
|
+
}
|
|
90
|
+
expect(bus.getHistory()).toHaveLength(5);
|
|
91
|
+
expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('broadcast skips source agent', async () => {
|
|
95
|
+
const bus = new MessageBus();
|
|
96
|
+
const called: string[] = [];
|
|
97
|
+
|
|
98
|
+
async function fogHandler(_event: Event) { called.push('fog'); }
|
|
99
|
+
async function rainHandler(_event: Event) { called.push('rain'); }
|
|
100
|
+
|
|
101
|
+
bus.subscribe('fog', fogHandler);
|
|
102
|
+
bus.subscribe('rain', rainHandler);
|
|
103
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
104
|
+
|
|
105
|
+
expect(called).not.toContain('fog');
|
|
106
|
+
expect(called).toContain('rain');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handler exception does not block other handlers', async () => {
|
|
110
|
+
const bus = new MessageBus();
|
|
111
|
+
const good: Event[] = [];
|
|
112
|
+
|
|
113
|
+
async function failing(_event: Event) { throw new Error('failed'); }
|
|
114
|
+
async function ok(event: Event) { good.push(event); }
|
|
115
|
+
|
|
116
|
+
bus.subscribe('rain', failing);
|
|
117
|
+
bus.subscribe('dew', ok);
|
|
118
|
+
await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
|
|
119
|
+
expect(good).toHaveLength(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
loadCatalog,
|
|
4
|
+
listProviders,
|
|
5
|
+
modelsFor,
|
|
6
|
+
allModels,
|
|
7
|
+
getModelInfo,
|
|
8
|
+
isKnownModel,
|
|
9
|
+
providerLabel,
|
|
10
|
+
validateModel,
|
|
11
|
+
resetCatalogCache,
|
|
12
|
+
PROVIDER_META,
|
|
13
|
+
} from "../src/core/catalog";
|
|
14
|
+
|
|
15
|
+
describe("catalog", () => {
|
|
16
|
+
beforeEach(() => resetCatalogCache());
|
|
17
|
+
|
|
18
|
+
it("loads providers from models.yaml", () => {
|
|
19
|
+
const providers = listProviders();
|
|
20
|
+
expect(providers.length).toBeGreaterThan(0);
|
|
21
|
+
expect(providers).toContain("openai");
|
|
22
|
+
expect(providers).toContain("deepseek");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("orders providers by wizard order", () => {
|
|
26
|
+
const providers = listProviders();
|
|
27
|
+
const orders = providers.map((p) => PROVIDER_META[p]?.order ?? 99);
|
|
28
|
+
const sorted = [...orders].sort((a, b) => a - b);
|
|
29
|
+
expect(orders).toEqual(sorted);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("parses real model fields (context + cost)", () => {
|
|
33
|
+
const gpt4o = getModelInfo("gpt-4o");
|
|
34
|
+
expect(gpt4o).not.toBeNull();
|
|
35
|
+
expect(gpt4o!.provider).toBe("openai");
|
|
36
|
+
expect(gpt4o!.context).toBeGreaterThan(0);
|
|
37
|
+
expect(gpt4o!.costIn).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("contains all real, API-verified deepseek models", () => {
|
|
41
|
+
// Verified live against https://api.deepseek.com/v1/models
|
|
42
|
+
expect(isKnownModel("deepseek-chat")).toBe(true);
|
|
43
|
+
expect(isKnownModel("deepseek-reasoner")).toBe(true);
|
|
44
|
+
expect(isKnownModel("deepseek-v4-flash")).toBe(true);
|
|
45
|
+
expect(isKnownModel("deepseek-v4-pro")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("still rejects genuinely unknown model ids", () => {
|
|
49
|
+
expect(isKnownModel("deepseek-v9-ultra")).toBe(false);
|
|
50
|
+
expect(isKnownModel("gpt-5-imaginary")).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("marks ollama / zero-cost models as local", () => {
|
|
54
|
+
const local = allModels().filter((m) => m.local);
|
|
55
|
+
expect(local.every((m) => m.costIn === 0 && m.costOut === 0)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("tolerates provider/ prefix when resolving", () => {
|
|
59
|
+
// openrouter lists "openai/gpt-4.1"; bare "gpt-4.1" should also resolve
|
|
60
|
+
const direct = getModelInfo("gpt-4.1");
|
|
61
|
+
expect(direct).not.toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("validateModel passes for a real model and fails with suggestions otherwise", () => {
|
|
65
|
+
expect(validateModel("gpt-4o").ok).toBe(true);
|
|
66
|
+
const bad = validateModel("totally-made-up-model");
|
|
67
|
+
expect(bad.ok).toBe(false);
|
|
68
|
+
expect(bad.suggestions.length).toBeGreaterThan(0);
|
|
69
|
+
expect(bad.suggestions.every((s) => isKnownModel(s))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("provides display labels", () => {
|
|
73
|
+
expect(providerLabel("openai")).toBe("OpenAI");
|
|
74
|
+
expect(providerLabel("unknown-xyz")).toBe("unknown-xyz");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("modelsFor returns empty for unknown provider", () => {
|
|
78
|
+
expect(modelsFor("nope")).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("caches the catalog across calls", () => {
|
|
82
|
+
const a = loadCatalog();
|
|
83
|
+
const b = loadCatalog();
|
|
84
|
+
expect(a).toBe(b);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mergeConfigs } from "../src/core/config";
|
|
3
|
+
|
|
4
|
+
describe("mergeConfigs", () => {
|
|
5
|
+
it("preserves top-level default_model / default_provider (regression)", () => {
|
|
6
|
+
// Previously these were dropped, so the wizard's chosen model never applied.
|
|
7
|
+
const def: any = { agents: { fog: { temperature: 0.7 } }, llm: { default_model: "gpt-4o" } };
|
|
8
|
+
const user: any = { default_model: "deepseek-v4-flash", default_provider: "deepseek" };
|
|
9
|
+
const merged: any = mergeConfigs(def, user);
|
|
10
|
+
expect(merged.default_model).toBe("deepseek-v4-flash");
|
|
11
|
+
expect(merged.default_provider).toBe("deepseek");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("deep-merges the llm block with the user winning", () => {
|
|
15
|
+
const def: any = { agents: {}, llm: { default_model: "gpt-4o", temperature: 0.7 } };
|
|
16
|
+
const user: any = { llm: { default_model: "deepseek-chat" } };
|
|
17
|
+
const merged: any = mergeConfigs(def, user);
|
|
18
|
+
expect(merged.llm.default_model).toBe("deepseek-chat");
|
|
19
|
+
expect(merged.llm.temperature).toBe(0.7); // preserved from default
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("merges agents (user overrides, defaults retained)", () => {
|
|
23
|
+
const def: any = { agents: { fog: { temperature: 0.7 }, rain: { temperature: 0.5 } } };
|
|
24
|
+
const user: any = { agents: { fog: { model: "deepseek-v4-pro", temperature: 0.2 } } };
|
|
25
|
+
const merged: any = mergeConfigs(def, user);
|
|
26
|
+
expect(merged.agents.fog.model).toBe("deepseek-v4-pro");
|
|
27
|
+
expect(merged.agents.rain.temperature).toBe(0.5);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns the default config unchanged when no user config", () => {
|
|
31
|
+
const def: any = { agents: { fog: {} }, default_model: "gpt-4o" };
|
|
32
|
+
expect(mergeConfigs(def, null)).toBe(def);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("preserves passthrough top-level blocks (memory, workspace)", () => {
|
|
36
|
+
const def: any = { agents: {}, memory: { short_term_limit: 100 }, workspace: { path: "auto" } };
|
|
37
|
+
const merged: any = mergeConfigs(def, { agents: {} } as any);
|
|
38
|
+
expect(merged.memory.short_term_limit).toBe(100);
|
|
39
|
+
expect(merged.workspace.path).toBe("auto");
|
|
40
|
+
});
|
|
41
|
+
});
|