skyloom 1.4.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 -0
- package/CONVERSION_PLAN.md +191 -0
- package/README.md +67 -0
- package/dist/agents/dew.d.ts +15 -0
- package/dist/agents/dew.d.ts.map +1 -0
- package/dist/agents/dew.js +74 -0
- package/dist/agents/dew.js.map +1 -0
- package/dist/agents/fair.d.ts +15 -0
- package/dist/agents/fair.d.ts.map +1 -0
- package/dist/agents/fair.js +106 -0
- package/dist/agents/fair.js.map +1 -0
- package/dist/agents/fog.d.ts +15 -0
- package/dist/agents/fog.d.ts.map +1 -0
- package/dist/agents/fog.js +52 -0
- package/dist/agents/fog.js.map +1 -0
- package/dist/agents/frost.d.ts +15 -0
- package/dist/agents/frost.d.ts.map +1 -0
- package/dist/agents/frost.js +54 -0
- package/dist/agents/frost.js.map +1 -0
- package/dist/agents/rain.d.ts +15 -0
- package/dist/agents/rain.d.ts.map +1 -0
- package/dist/agents/rain.js +54 -0
- package/dist/agents/rain.js.map +1 -0
- package/dist/agents/snow.d.ts +27 -0
- package/dist/agents/snow.d.ts.map +1 -0
- package/dist/agents/snow.js +226 -0
- package/dist/agents/snow.js.map +1 -0
- package/dist/cli/main.d.ts +7 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +402 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/mode.d.ts +17 -0
- package/dist/cli/mode.d.ts.map +1 -0
- package/dist/cli/mode.js +56 -0
- package/dist/cli/mode.js.map +1 -0
- package/dist/core/agent.d.ts +174 -0
- package/dist/core/agent.d.ts.map +1 -0
- package/dist/core/agent.js +1332 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/agent_helpers.d.ts +51 -0
- package/dist/core/agent_helpers.d.ts.map +1 -0
- package/dist/core/agent_helpers.js +477 -0
- package/dist/core/agent_helpers.js.map +1 -0
- package/dist/core/bus.d.ts +99 -0
- package/dist/core/bus.d.ts.map +1 -0
- package/dist/core/bus.js +191 -0
- package/dist/core/bus.js.map +1 -0
- package/dist/core/cache.d.ts +63 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +121 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/checkpoint.d.ts +19 -0
- package/dist/core/checkpoint.d.ts.map +1 -0
- package/dist/core/checkpoint.js +120 -0
- package/dist/core/checkpoint.js.map +1 -0
- package/dist/core/circuit_breaker.d.ts +46 -0
- package/dist/core/circuit_breaker.d.ts.map +1 -0
- package/dist/core/circuit_breaker.js +99 -0
- package/dist/core/circuit_breaker.js.map +1 -0
- package/dist/core/config.d.ts +97 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +281 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/constants.d.ts +78 -0
- package/dist/core/constants.d.ts.map +1 -0
- package/dist/core/constants.js +84 -0
- package/dist/core/constants.js.map +1 -0
- package/dist/core/factory.d.ts +63 -0
- package/dist/core/factory.d.ts.map +1 -0
- package/dist/core/factory.js +537 -0
- package/dist/core/factory.js.map +1 -0
- package/dist/core/icons.d.ts +28 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +86 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/index.d.ts +29 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +54 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/llm.d.ts +121 -0
- package/dist/core/llm.d.ts.map +1 -0
- package/dist/core/llm.js +532 -0
- package/dist/core/llm.js.map +1 -0
- package/dist/core/logger.d.ts +57 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +122 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mcp.d.ts +190 -0
- package/dist/core/mcp.d.ts.map +1 -0
- package/dist/core/mcp.js +822 -0
- package/dist/core/mcp.js.map +1 -0
- package/dist/core/mcp_server.d.ts +26 -0
- package/dist/core/mcp_server.d.ts.map +1 -0
- package/dist/core/mcp_server.js +211 -0
- package/dist/core/mcp_server.js.map +1 -0
- package/dist/core/memory.d.ts +190 -0
- package/dist/core/memory.d.ts.map +1 -0
- package/dist/core/memory.js +988 -0
- package/dist/core/memory.js.map +1 -0
- package/dist/core/middleware.d.ts +114 -0
- package/dist/core/middleware.d.ts.map +1 -0
- package/dist/core/middleware.js +248 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/pipelines.d.ts +87 -0
- package/dist/core/pipelines.d.ts.map +1 -0
- package/dist/core/pipelines.js +301 -0
- package/dist/core/pipelines.js.map +1 -0
- package/dist/core/profile.d.ts +23 -0
- package/dist/core/profile.d.ts.map +1 -0
- package/dist/core/profile.js +289 -0
- package/dist/core/profile.js.map +1 -0
- package/dist/core/router.d.ts +24 -0
- package/dist/core/router.d.ts.map +1 -0
- package/dist/core/router.js +111 -0
- package/dist/core/router.js.map +1 -0
- package/dist/core/schemas.d.ts +82 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +200 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/semantic.d.ts +92 -0
- package/dist/core/semantic.d.ts.map +1 -0
- package/dist/core/semantic.js +175 -0
- package/dist/core/semantic.js.map +1 -0
- package/dist/core/skill.d.ts +68 -0
- package/dist/core/skill.d.ts.map +1 -0
- package/dist/core/skill.js +350 -0
- package/dist/core/skill.js.map +1 -0
- package/dist/core/tool.d.ts +99 -0
- package/dist/core/tool.d.ts.map +1 -0
- package/dist/core/tool.js +341 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/core/tool_router.d.ts +29 -0
- package/dist/core/tool_router.d.ts.map +1 -0
- package/dist/core/tool_router.js +172 -0
- package/dist/core/tool_router.js.map +1 -0
- package/dist/core/workspace.d.ts +48 -0
- package/dist/core/workspace.d.ts.map +1 -0
- package/dist/core/workspace.js +179 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/plugins/loader.d.ts +17 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +96 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/skills/loader.d.ts +9 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +78 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/tools/builtin.d.ts +10 -0
- package/dist/tools/builtin.d.ts.map +1 -0
- package/dist/tools/builtin.js +414 -0
- package/dist/tools/builtin.js.map +1 -0
- package/dist/tools/computer.d.ts +12 -0
- package/dist/tools/computer.d.ts.map +1 -0
- package/dist/tools/computer.js +326 -0
- package/dist/tools/computer.js.map +1 -0
- package/dist/tools/delegate.d.ts +10 -0
- package/dist/tools/delegate.d.ts.map +1 -0
- package/dist/tools/delegate.js +45 -0
- package/dist/tools/delegate.js.map +1 -0
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.d.ts.map +1 -0
- package/dist/web/server.js +647 -0
- package/dist/web/server.js.map +1 -0
- package/dist/web/tts.d.ts +33 -0
- package/dist/web/tts.d.ts.map +1 -0
- package/dist/web/tts.js +69 -0
- package/dist/web/tts.js.map +1 -0
- package/package.json +60 -0
- package/scripts/install.js +48 -0
- package/scripts/link.js +10 -0
- package/setup.bat +79 -0
- package/skill-test-ty2fOA/test.md +10 -0
- package/src/agents/dew.ts +70 -0
- package/src/agents/fair.ts +102 -0
- package/src/agents/fog.ts +48 -0
- package/src/agents/frost.ts +50 -0
- package/src/agents/rain.ts +50 -0
- package/src/agents/snow.ts +239 -0
- package/src/cli/main.ts +405 -0
- package/src/cli/mode.ts +58 -0
- package/src/core/agent.ts +1506 -0
- package/src/core/agent_helpers.ts +461 -0
- package/src/core/bus.ts +221 -0
- package/src/core/cache.ts +153 -0
- package/src/core/checkpoint.ts +94 -0
- package/src/core/circuit_breaker.ts +119 -0
- package/src/core/config.ts +341 -0
- package/src/core/constants.ts +95 -0
- package/src/core/factory.ts +627 -0
- package/src/core/icons.ts +53 -0
- package/src/core/index.ts +31 -0
- package/src/core/llm.ts +724 -0
- package/src/core/logger.ts +144 -0
- package/src/core/mcp.ts +953 -0
- package/src/core/mcp_server.ts +176 -0
- package/src/core/memory.ts +1169 -0
- package/src/core/middleware.ts +350 -0
- package/src/core/pipelines.ts +424 -0
- package/src/core/profile.ts +255 -0
- package/src/core/router.ts +124 -0
- package/src/core/schemas.ts +282 -0
- package/src/core/semantic.ts +211 -0
- package/src/core/skill.ts +342 -0
- package/src/core/tool.ts +427 -0
- package/src/core/tool_router.ts +193 -0
- package/src/core/workspace.ts +150 -0
- package/src/plugins/loader.ts +66 -0
- package/src/skills/loader.ts +46 -0
- package/src/sql.js.d.ts +29 -0
- package/src/tools/builtin.ts +382 -0
- package/src/tools/computer.ts +269 -0
- package/src/tools/delegate.ts +49 -0
- package/src/web/server.ts +634 -0
- package/src/web/tts.ts +93 -0
- package/tests/bus.test.ts +121 -0
- package/tests/icons.test.ts +45 -0
- package/tests/router.test.ts +86 -0
- package/tests/schemas.test.ts +51 -0
- package/tests/semantic.test.ts +83 -0
- package/tests/setup.ts +10 -0
- package/tests/skill.test.ts +172 -0
- package/tests/tool.test.ts +108 -0
- package/tests/tool_router.test.ts +71 -0
- package/tsconfig.json +37 -0
- package/vitest.config.ts +17 -0
package/src/web/tts.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|
package/tests/setup.ts
ADDED
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
});
|