opc-agent 0.2.0 → 0.3.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/dist/analytics/index.d.ts +31 -0
- package/dist/analytics/index.js +52 -0
- package/dist/cli.js +66 -4
- package/dist/core/room.d.ts +24 -0
- package/dist/core/room.js +97 -0
- package/dist/core/sandbox.d.ts +28 -0
- package/dist/core/sandbox.js +118 -0
- package/dist/i18n/index.d.ts +13 -0
- package/dist/i18n/index.js +73 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +19 -1
- package/dist/plugins/index.d.ts +47 -0
- package/dist/plugins/index.js +59 -0
- package/dist/schema/oad.d.ts +131 -0
- package/dist/schema/oad.js +13 -1
- package/dist/templates/content-writer.d.ts +36 -0
- package/dist/templates/content-writer.js +52 -0
- package/dist/templates/hr-recruiter.d.ts +36 -0
- package/dist/templates/hr-recruiter.js +52 -0
- package/dist/templates/project-manager.d.ts +36 -0
- package/dist/templates/project-manager.js +52 -0
- package/dist/tools/mcp.d.ts +32 -0
- package/dist/tools/mcp.js +49 -0
- package/package.json +1 -1
- package/src/analytics/index.ts +66 -0
- package/src/cli.ts +76 -7
- package/src/core/room.ts +109 -0
- package/src/core/sandbox.ts +101 -0
- package/src/i18n/index.ts +79 -0
- package/src/index.ts +14 -0
- package/src/plugins/index.ts +87 -0
- package/src/schema/oad.ts +14 -0
- package/src/templates/content-writer.ts +58 -0
- package/src/templates/hr-recruiter.ts +58 -0
- package/src/templates/project-manager.ts +58 -0
- package/src/tools/mcp.ts +76 -0
- package/tests/analytics.test.ts +50 -0
- package/tests/i18n.test.ts +41 -0
- package/tests/mcp.test.ts +54 -0
- package/tests/plugin.test.ts +74 -0
- package/tests/room.test.ts +106 -0
- package/tests/sandbox.test.ts +46 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class ResumeScreeningSkill extends BaseSkill {
|
|
6
|
+
name = 'resume-screening';
|
|
7
|
+
description = 'Screen resumes against job requirements';
|
|
8
|
+
|
|
9
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
10
|
+
const lower = message.content.toLowerCase();
|
|
11
|
+
if (lower.includes('resume') || lower.includes('cv') || lower.includes('candidate')) {
|
|
12
|
+
return this.match(
|
|
13
|
+
'I can help screen resumes. Please share the candidate\'s resume and the job requirements, and I\'ll provide an analysis.',
|
|
14
|
+
0.8,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return this.noMatch();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class InterviewSchedulingSkill extends BaseSkill {
|
|
22
|
+
name = 'interview-scheduling';
|
|
23
|
+
description = 'Help schedule interviews with candidates';
|
|
24
|
+
|
|
25
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
const lower = message.content.toLowerCase();
|
|
27
|
+
if (lower.includes('schedule') || lower.includes('interview') || lower.includes('calendar')) {
|
|
28
|
+
return this.match(
|
|
29
|
+
'I can help schedule interviews. Please provide the candidate name, preferred dates, and interview format (phone/video/onsite).',
|
|
30
|
+
0.8,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return this.noMatch();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createHRRecruiterConfig() {
|
|
38
|
+
return {
|
|
39
|
+
apiVersion: 'opc/v1',
|
|
40
|
+
kind: 'Agent',
|
|
41
|
+
metadata: {
|
|
42
|
+
name: 'hr-recruiter',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
description: 'HR Recruiter — resume screening, interview scheduling, candidate Q&A',
|
|
45
|
+
author: 'Deepleaper',
|
|
46
|
+
license: 'Apache-2.0',
|
|
47
|
+
},
|
|
48
|
+
spec: {
|
|
49
|
+
model: 'deepseek-chat',
|
|
50
|
+
systemPrompt: 'You are an HR recruiter assistant. Help with resume screening, interview scheduling, and answering candidate questions. Be professional and friendly.',
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'resume-screening', description: 'Screen resumes' },
|
|
53
|
+
{ name: 'interview-scheduling', description: 'Schedule interviews' },
|
|
54
|
+
],
|
|
55
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { BaseSkill } from '../skills/base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class TaskTrackingSkill extends BaseSkill {
|
|
6
|
+
name = 'task-tracking';
|
|
7
|
+
description = 'Track project tasks and status';
|
|
8
|
+
|
|
9
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
10
|
+
const lower = message.content.toLowerCase();
|
|
11
|
+
if (lower.includes('task') || lower.includes('todo') || lower.includes('progress')) {
|
|
12
|
+
return this.match(
|
|
13
|
+
'I can help track tasks. Tell me the task name, assignee, and deadline, and I\'ll add it to the tracker.',
|
|
14
|
+
0.8,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
return this.noMatch();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MeetingNotesSkill extends BaseSkill {
|
|
22
|
+
name = 'meeting-notes';
|
|
23
|
+
description = 'Generate and manage meeting notes';
|
|
24
|
+
|
|
25
|
+
async execute(_context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
const lower = message.content.toLowerCase();
|
|
27
|
+
if (lower.includes('meeting') || lower.includes('notes') || lower.includes('minutes')) {
|
|
28
|
+
return this.match(
|
|
29
|
+
'I can help with meeting notes. Share the meeting details and I\'ll create structured notes with action items.',
|
|
30
|
+
0.8,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return this.noMatch();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createProjectManagerConfig() {
|
|
38
|
+
return {
|
|
39
|
+
apiVersion: 'opc/v1',
|
|
40
|
+
kind: 'Agent',
|
|
41
|
+
metadata: {
|
|
42
|
+
name: 'project-manager',
|
|
43
|
+
version: '1.0.0',
|
|
44
|
+
description: 'Project Manager — task tracking, status updates, meeting notes',
|
|
45
|
+
author: 'Deepleaper',
|
|
46
|
+
license: 'Apache-2.0',
|
|
47
|
+
},
|
|
48
|
+
spec: {
|
|
49
|
+
model: 'deepseek-chat',
|
|
50
|
+
systemPrompt: 'You are a project management assistant. Help track tasks, provide status updates, and manage meeting notes. Be organized and action-oriented.',
|
|
51
|
+
skills: [
|
|
52
|
+
{ name: 'task-tracking', description: 'Track tasks' },
|
|
53
|
+
{ name: 'meeting-notes', description: 'Manage meeting notes' },
|
|
54
|
+
],
|
|
55
|
+
channels: [{ type: 'web', port: 3000 }],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
package/src/tools/mcp.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AgentContext, Message } from '../core/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP (Model Context Protocol) compatible tool interface.
|
|
5
|
+
* Tools follow the MCP standard format with JSON Schema input validation.
|
|
6
|
+
*/
|
|
7
|
+
export interface MCPToolDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
inputSchema: Record<string, unknown>; // JSON Schema
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MCPToolResult {
|
|
14
|
+
content: string;
|
|
15
|
+
isError?: boolean;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MCPTool extends MCPToolDefinition {
|
|
20
|
+
execute(input: Record<string, unknown>, context?: AgentContext): Promise<MCPToolResult>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class MCPToolRegistry {
|
|
24
|
+
private tools: Map<string, MCPTool> = new Map();
|
|
25
|
+
|
|
26
|
+
register(tool: MCPTool): void {
|
|
27
|
+
this.tools.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
unregister(name: string): void {
|
|
31
|
+
this.tools.delete(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get(name: string): MCPTool | undefined {
|
|
35
|
+
return this.tools.get(name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list(): MCPToolDefinition[] {
|
|
39
|
+
return Array.from(this.tools.values()).map(({ name, description, inputSchema }) => ({
|
|
40
|
+
name,
|
|
41
|
+
description,
|
|
42
|
+
inputSchema,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
has(name: string): boolean {
|
|
47
|
+
return this.tools.has(name);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async execute(name: string, input: Record<string, unknown>, context?: AgentContext): Promise<MCPToolResult> {
|
|
51
|
+
const tool = this.tools.get(name);
|
|
52
|
+
if (!tool) {
|
|
53
|
+
return { content: `Tool '${name}' not found`, isError: true };
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return await tool.execute(input, context);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return {
|
|
59
|
+
content: `Tool execution error: ${err instanceof Error ? err.message : String(err)}`,
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create an MCP tool from a simple function.
|
|
68
|
+
*/
|
|
69
|
+
export function createMCPTool(
|
|
70
|
+
name: string,
|
|
71
|
+
description: string,
|
|
72
|
+
inputSchema: Record<string, unknown>,
|
|
73
|
+
executeFn: (input: Record<string, unknown>, context?: AgentContext) => Promise<MCPToolResult>,
|
|
74
|
+
): MCPTool {
|
|
75
|
+
return { name, description, inputSchema, execute: executeFn };
|
|
76
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Analytics } from '../src/analytics';
|
|
3
|
+
|
|
4
|
+
describe('Analytics', () => {
|
|
5
|
+
it('should track messages', () => {
|
|
6
|
+
const analytics = new Analytics();
|
|
7
|
+
analytics.recordMessage(100);
|
|
8
|
+
analytics.recordMessage(200);
|
|
9
|
+
const snap = analytics.getSnapshot();
|
|
10
|
+
expect(snap.messagesProcessed).toBe(2);
|
|
11
|
+
expect(snap.avgResponseTimeMs).toBe(150);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should track skill usage', () => {
|
|
15
|
+
const analytics = new Analytics();
|
|
16
|
+
analytics.recordSkillUsage('faq');
|
|
17
|
+
analytics.recordSkillUsage('faq');
|
|
18
|
+
analytics.recordSkillUsage('handoff');
|
|
19
|
+
const snap = analytics.getSnapshot();
|
|
20
|
+
expect(snap.skillUsage['faq']).toBe(2);
|
|
21
|
+
expect(snap.skillUsage['handoff']).toBe(1);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should track errors', () => {
|
|
25
|
+
const analytics = new Analytics();
|
|
26
|
+
analytics.recordError();
|
|
27
|
+
analytics.recordError();
|
|
28
|
+
expect(analytics.getSnapshot().errorCount).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should track token usage', () => {
|
|
32
|
+
const analytics = new Analytics();
|
|
33
|
+
analytics.recordTokens(100, 50);
|
|
34
|
+
analytics.recordTokens(200, 100);
|
|
35
|
+
const snap = analytics.getSnapshot();
|
|
36
|
+
expect(snap.tokenUsage.input).toBe(300);
|
|
37
|
+
expect(snap.tokenUsage.output).toBe(150);
|
|
38
|
+
expect(snap.tokenUsage.total).toBe(450);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should reset analytics', () => {
|
|
42
|
+
const analytics = new Analytics();
|
|
43
|
+
analytics.recordMessage(100);
|
|
44
|
+
analytics.recordError();
|
|
45
|
+
analytics.reset();
|
|
46
|
+
const snap = analytics.getSnapshot();
|
|
47
|
+
expect(snap.messagesProcessed).toBe(0);
|
|
48
|
+
expect(snap.errorCount).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { t, setLocale, getLocale, detectLocale, addMessages } from '../src/i18n';
|
|
3
|
+
|
|
4
|
+
describe('i18n', () => {
|
|
5
|
+
it('should return English messages by default', () => {
|
|
6
|
+
setLocale('en');
|
|
7
|
+
expect(t('agent.greeting')).toBe('Hello! How can I help you?');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should return Chinese messages', () => {
|
|
11
|
+
setLocale('zh-CN');
|
|
12
|
+
expect(t('agent.greeting')).toBe('您好!有什么可以帮您的?');
|
|
13
|
+
setLocale('en'); // reset
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should interpolate params', () => {
|
|
17
|
+
setLocale('en');
|
|
18
|
+
expect(t('agent.started', { name: 'TestBot' })).toBe('Agent "TestBot" started successfully');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fall back to key if not found', () => {
|
|
22
|
+
expect(t('nonexistent.key')).toBe('nonexistent.key');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should get and set locale', () => {
|
|
26
|
+
setLocale('zh-CN');
|
|
27
|
+
expect(getLocale()).toBe('zh-CN');
|
|
28
|
+
setLocale('en');
|
|
29
|
+
expect(getLocale()).toBe('en');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should add custom messages', () => {
|
|
33
|
+
addMessages('en', { 'custom.key': 'Custom value' });
|
|
34
|
+
expect(t('custom.key')).toBe('Custom value');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should detect locale from environment', () => {
|
|
38
|
+
const locale = detectLocale();
|
|
39
|
+
expect(['en', 'zh-CN']).toContain(locale);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { MCPToolRegistry, createMCPTool } from '../src/tools/mcp';
|
|
3
|
+
|
|
4
|
+
describe('MCP Tool System', () => {
|
|
5
|
+
it('should register and list tools', () => {
|
|
6
|
+
const registry = new MCPToolRegistry();
|
|
7
|
+
const tool = createMCPTool('calculator', 'Basic calculator', {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: { expression: { type: 'string' } },
|
|
10
|
+
}, async (input) => ({ content: `Result: ${input.expression}` }));
|
|
11
|
+
|
|
12
|
+
registry.register(tool);
|
|
13
|
+
expect(registry.has('calculator')).toBe(true);
|
|
14
|
+
expect(registry.list().length).toBe(1);
|
|
15
|
+
expect(registry.list()[0].name).toBe('calculator');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should execute a tool', async () => {
|
|
19
|
+
const registry = new MCPToolRegistry();
|
|
20
|
+
registry.register(createMCPTool('echo', 'Echo tool', {}, async (input) => ({
|
|
21
|
+
content: `Echo: ${input.text}`,
|
|
22
|
+
})));
|
|
23
|
+
|
|
24
|
+
const result = await registry.execute('echo', { text: 'hello' });
|
|
25
|
+
expect(result.content).toBe('Echo: hello');
|
|
26
|
+
expect(result.isError).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return error for missing tool', async () => {
|
|
30
|
+
const registry = new MCPToolRegistry();
|
|
31
|
+
const result = await registry.execute('nonexistent', {});
|
|
32
|
+
expect(result.isError).toBe(true);
|
|
33
|
+
expect(result.content).toContain('not found');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should handle tool execution errors', async () => {
|
|
37
|
+
const registry = new MCPToolRegistry();
|
|
38
|
+
registry.register(createMCPTool('failing', 'Fails', {}, async () => {
|
|
39
|
+
throw new Error('boom');
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
const result = await registry.execute('failing', {});
|
|
43
|
+
expect(result.isError).toBe(true);
|
|
44
|
+
expect(result.content).toContain('boom');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should unregister tools', () => {
|
|
48
|
+
const registry = new MCPToolRegistry();
|
|
49
|
+
registry.register(createMCPTool('temp', 'Temp', {}, async () => ({ content: '' })));
|
|
50
|
+
expect(registry.has('temp')).toBe(true);
|
|
51
|
+
registry.unregister('temp');
|
|
52
|
+
expect(registry.has('temp')).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { PluginManager } from '../src/plugins';
|
|
3
|
+
import type { IPlugin } from '../src/plugins';
|
|
4
|
+
|
|
5
|
+
describe('Plugin System', () => {
|
|
6
|
+
it('should register and list plugins', () => {
|
|
7
|
+
const pm = new PluginManager();
|
|
8
|
+
const plugin: IPlugin = { name: 'test-plugin', version: '1.0.0', description: 'Test' };
|
|
9
|
+
pm.register(plugin);
|
|
10
|
+
expect(pm.has('test-plugin')).toBe(true);
|
|
11
|
+
expect(pm.list().length).toBe(1);
|
|
12
|
+
expect(pm.list()[0].name).toBe('test-plugin');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should run lifecycle hooks', async () => {
|
|
16
|
+
const pm = new PluginManager();
|
|
17
|
+
const calls: string[] = [];
|
|
18
|
+
|
|
19
|
+
pm.register({
|
|
20
|
+
name: 'hook-plugin',
|
|
21
|
+
version: '1.0.0',
|
|
22
|
+
hooks: {
|
|
23
|
+
beforeInit: async () => { calls.push('beforeInit'); },
|
|
24
|
+
afterInit: async () => { calls.push('afterInit'); },
|
|
25
|
+
beforeShutdown: async () => { calls.push('beforeShutdown'); },
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await pm.runHook('beforeInit');
|
|
30
|
+
await pm.runHook('afterInit');
|
|
31
|
+
await pm.runHook('beforeShutdown');
|
|
32
|
+
expect(calls).toEqual(['beforeInit', 'afterInit', 'beforeShutdown']);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should collect skills from plugins', () => {
|
|
36
|
+
const pm = new PluginManager();
|
|
37
|
+
pm.register({
|
|
38
|
+
name: 'skill-plugin',
|
|
39
|
+
version: '1.0.0',
|
|
40
|
+
skills: [{
|
|
41
|
+
name: 'test-skill',
|
|
42
|
+
description: 'Test',
|
|
43
|
+
execute: async () => ({ handled: false, confidence: 0 }),
|
|
44
|
+
}],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(pm.getAllSkills().length).toBe(1);
|
|
48
|
+
expect(pm.getAllSkills()[0].name).toBe('test-skill');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should unregister plugins', () => {
|
|
52
|
+
const pm = new PluginManager();
|
|
53
|
+
pm.register({ name: 'temp', version: '1.0.0' });
|
|
54
|
+
pm.unregister('temp');
|
|
55
|
+
expect(pm.has('temp')).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should run hooks from multiple plugins in order', async () => {
|
|
59
|
+
const pm = new PluginManager();
|
|
60
|
+
const order: string[] = [];
|
|
61
|
+
|
|
62
|
+
pm.register({
|
|
63
|
+
name: 'p1', version: '1.0.0',
|
|
64
|
+
hooks: { beforeInit: async () => { order.push('p1'); } },
|
|
65
|
+
});
|
|
66
|
+
pm.register({
|
|
67
|
+
name: 'p2', version: '1.0.0',
|
|
68
|
+
hooks: { beforeInit: async () => { order.push('p2'); } },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await pm.runHook('beforeInit');
|
|
72
|
+
expect(order).toEqual(['p1', 'p2']);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Room } from '../src/core/room';
|
|
3
|
+
import { BaseAgent } from '../src/core/agent';
|
|
4
|
+
|
|
5
|
+
function makeAgent(name: string): BaseAgent {
|
|
6
|
+
const agent = new BaseAgent({ name });
|
|
7
|
+
// Synchronously set to ready by calling init
|
|
8
|
+
return agent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('Room System', () => {
|
|
12
|
+
it('should create a room with a name', () => {
|
|
13
|
+
const room = new Room('test-room');
|
|
14
|
+
expect(room.name).toBe('test-room');
|
|
15
|
+
expect(room.getAgents()).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should add and remove agents', () => {
|
|
19
|
+
const room = new Room('office');
|
|
20
|
+
const agent = makeAgent('agent-1');
|
|
21
|
+
room.addAgent(agent);
|
|
22
|
+
expect(room.getAgents()).toEqual(['agent-1']);
|
|
23
|
+
room.removeAgent('agent-1');
|
|
24
|
+
expect(room.getAgents()).toEqual([]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should emit events on join/leave', () => {
|
|
28
|
+
const room = new Room('office');
|
|
29
|
+
const events: string[] = [];
|
|
30
|
+
room.on('agent:join', (name) => events.push(`join:${name}`));
|
|
31
|
+
room.on('agent:leave', (name) => events.push(`leave:${name}`));
|
|
32
|
+
|
|
33
|
+
const agent = makeAgent('a1');
|
|
34
|
+
room.addAgent(agent);
|
|
35
|
+
room.removeAgent('a1');
|
|
36
|
+
expect(events).toEqual(['join:a1', 'leave:a1']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should support topic subscriptions', () => {
|
|
40
|
+
const room = new Room('office');
|
|
41
|
+
room.subscribe('agent-1', 'alerts');
|
|
42
|
+
room.subscribe('agent-2', 'alerts');
|
|
43
|
+
expect(room.getSubscribers('alerts')).toEqual(['agent-1', 'agent-2']);
|
|
44
|
+
room.unsubscribe('agent-1', 'alerts');
|
|
45
|
+
expect(room.getSubscribers('alerts')).toEqual(['agent-2']);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should broadcast to all agents except sender', async () => {
|
|
49
|
+
const room = new Room('office');
|
|
50
|
+
const a1 = makeAgent('a1');
|
|
51
|
+
const a2 = makeAgent('a2');
|
|
52
|
+
await a1.init();
|
|
53
|
+
await a2.init();
|
|
54
|
+
room.addAgent(a1);
|
|
55
|
+
room.addAgent(a2);
|
|
56
|
+
|
|
57
|
+
const responses = await room.broadcast('a1', 'Hello everyone');
|
|
58
|
+
expect(responses.length).toBe(1); // only a2 responds
|
|
59
|
+
expect(responses[0].role).toBe('assistant');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should send direct messages', async () => {
|
|
63
|
+
const room = new Room('office');
|
|
64
|
+
const a1 = makeAgent('a1');
|
|
65
|
+
const a2 = makeAgent('a2');
|
|
66
|
+
await a1.init();
|
|
67
|
+
await a2.init();
|
|
68
|
+
room.addAgent(a1);
|
|
69
|
+
room.addAgent(a2);
|
|
70
|
+
|
|
71
|
+
const responses = await room.send({
|
|
72
|
+
from: 'a1',
|
|
73
|
+
to: 'a2',
|
|
74
|
+
message: { id: 'm1', role: 'user', content: 'Hi a2', timestamp: Date.now() },
|
|
75
|
+
});
|
|
76
|
+
expect(responses.length).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should publish to topic subscribers only', async () => {
|
|
80
|
+
const room = new Room('office');
|
|
81
|
+
const a1 = makeAgent('a1');
|
|
82
|
+
const a2 = makeAgent('a2');
|
|
83
|
+
const a3 = makeAgent('a3');
|
|
84
|
+
await a1.init();
|
|
85
|
+
await a2.init();
|
|
86
|
+
await a3.init();
|
|
87
|
+
room.addAgent(a1);
|
|
88
|
+
room.addAgent(a2);
|
|
89
|
+
room.addAgent(a3);
|
|
90
|
+
|
|
91
|
+
room.subscribe('a2', 'alerts');
|
|
92
|
+
// a3 not subscribed
|
|
93
|
+
const responses = await room.publishToTopic('a1', 'alerts', 'Alert!');
|
|
94
|
+
expect(responses.length).toBe(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should remove agent from subscriptions on leave', () => {
|
|
98
|
+
const room = new Room('office');
|
|
99
|
+
room.subscribe('a1', 'topic1');
|
|
100
|
+
room.subscribe('a1', 'topic2');
|
|
101
|
+
expect(room.getSubscribers('topic1')).toContain('a1');
|
|
102
|
+
room.removeAgent('a1');
|
|
103
|
+
expect(room.getSubscribers('topic1')).not.toContain('a1');
|
|
104
|
+
expect(room.getSubscribers('topic2')).not.toContain('a1');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Sandbox } from '../src/core/sandbox';
|
|
3
|
+
|
|
4
|
+
describe('Security Sandbox', () => {
|
|
5
|
+
it('should create sandbox with trust level', () => {
|
|
6
|
+
const sb = new Sandbox({ trustLevel: 'sandbox', agentDir: '/tmp/agent' });
|
|
7
|
+
expect(sb.trustLevel).toBe('sandbox');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should restrict shell in sandbox mode', () => {
|
|
11
|
+
const sb = new Sandbox({ trustLevel: 'sandbox', agentDir: '/tmp/agent' });
|
|
12
|
+
expect(sb.checkShellAccess()).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should allow shell in certified mode', () => {
|
|
16
|
+
const sb = new Sandbox({ trustLevel: 'certified', agentDir: '/tmp/agent' });
|
|
17
|
+
expect(sb.checkShellAccess()).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should restrict network in sandbox mode', () => {
|
|
21
|
+
const sb = new Sandbox({ trustLevel: 'sandbox', agentDir: '/tmp/agent' });
|
|
22
|
+
expect(sb.checkNetworkAccess('https://api.openai.com')).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should allow network with allowlist', () => {
|
|
26
|
+
const sb = new Sandbox({
|
|
27
|
+
trustLevel: 'sandbox',
|
|
28
|
+
agentDir: '/tmp/agent',
|
|
29
|
+
networkAllowlist: ['api.openai.com'],
|
|
30
|
+
});
|
|
31
|
+
expect(sb.checkNetworkAccess('https://api.openai.com/v1/chat')).toBe(true);
|
|
32
|
+
expect(sb.checkNetworkAccess('https://evil.com')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should allow wildcard network in listed mode', () => {
|
|
36
|
+
const sb = new Sandbox({ trustLevel: 'listed', agentDir: '/tmp/agent' });
|
|
37
|
+
expect(sb.checkNetworkAccess('https://anything.com')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should return restrictions snapshot', () => {
|
|
41
|
+
const sb = new Sandbox({ trustLevel: 'verified', agentDir: '/tmp/agent' });
|
|
42
|
+
const r = sb.getRestrictions();
|
|
43
|
+
expect(r.shell).toBe(false);
|
|
44
|
+
expect(r.network.allowed.length).toBeGreaterThan(0);
|
|
45
|
+
});
|
|
46
|
+
});
|