opc-agent 1.3.2 → 2.0.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/ISSUE_TEMPLATE/bug_report.md +20 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/ci.yml +24 -0
- package/CHANGELOG.md +48 -63
- package/CONTRIBUTING.md +21 -60
- package/README.md +284 -348
- package/README.zh-CN.md +415 -415
- package/dist/channels/slack.js +93 -10
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/channels/web.d.ts +10 -0
- package/dist/channels/web.js +33 -2
- package/dist/cli.js +667 -65
- package/dist/core/agent.d.ts +23 -0
- package/dist/core/agent.js +120 -3
- package/dist/core/runtime.d.ts +5 -0
- package/dist/core/runtime.js +71 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/deploy/hermes.js +22 -22
- package/dist/deploy/openclaw.js +31 -40
- package/dist/index.d.ts +10 -10
- package/dist/index.js +22 -15
- package/dist/providers/index.d.ts +6 -2
- package/dist/providers/index.js +22 -9
- package/dist/schema/oad.d.ts +180 -6
- package/dist/schema/oad.js +12 -1
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/templates/code-reviewer.d.ts +0 -8
- package/dist/templates/code-reviewer.js +5 -9
- package/dist/templates/customer-service.d.ts +0 -8
- package/dist/templates/customer-service.js +2 -6
- package/dist/templates/data-analyst.d.ts +0 -8
- package/dist/templates/data-analyst.js +5 -9
- package/dist/templates/knowledge-base.d.ts +0 -8
- package/dist/templates/knowledge-base.js +2 -6
- package/dist/templates/sales-assistant.d.ts +0 -8
- package/dist/templates/sales-assistant.js +4 -8
- package/dist/templates/teacher.d.ts +0 -8
- package/dist/templates/teacher.js +6 -10
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/dist/traces/index.d.ts +49 -0
- package/dist/traces/index.js +102 -0
- package/docs/.vitepress/config.ts +103 -103
- package/docs/api/cli.md +48 -48
- package/docs/api/oad-schema.md +64 -64
- package/docs/api/sdk.md +80 -80
- package/docs/guide/concepts.md +51 -51
- package/docs/guide/configuration.md +79 -79
- package/docs/guide/deployment.md +42 -42
- package/docs/guide/getting-started.md +44 -44
- package/docs/guide/templates.md +28 -28
- package/docs/guide/testing.md +84 -84
- package/docs/index.md +27 -27
- package/docs/zh/api/cli.md +54 -54
- package/docs/zh/api/oad-schema.md +87 -87
- package/docs/zh/api/sdk.md +102 -102
- package/docs/zh/guide/concepts.md +104 -104
- package/docs/zh/guide/configuration.md +135 -135
- package/docs/zh/guide/deployment.md +81 -81
- package/docs/zh/guide/getting-started.md +82 -82
- package/docs/zh/guide/templates.md +84 -84
- package/docs/zh/guide/testing.md +88 -88
- package/docs/zh/index.md +27 -27
- package/examples/README.md +22 -0
- package/examples/basic-agent.ts +90 -0
- package/examples/brain-integration.ts +71 -0
- package/examples/customer-service-demo/README.md +90 -90
- package/examples/customer-service-demo/oad.yaml +107 -107
- package/examples/multi-channel.ts +74 -0
- package/package.json +1 -1
- package/src/analytics/index.ts +66 -66
- package/src/channels/discord.ts +192 -192
- package/src/channels/email.ts +177 -177
- package/src/channels/feishu.ts +236 -236
- package/src/channels/index.ts +15 -15
- package/src/channels/slack.ts +217 -160
- package/src/channels/telegram.ts +155 -33
- package/src/channels/voice.ts +106 -106
- package/src/channels/web.ts +38 -2
- package/src/channels/webhook.ts +199 -199
- package/src/channels/websocket.ts +87 -87
- package/src/channels/wechat.ts +149 -149
- package/src/cli.ts +697 -63
- package/src/core/a2a.ts +143 -143
- package/src/core/agent.ts +146 -3
- package/src/core/analytics-engine.ts +186 -186
- package/src/core/auth.ts +57 -57
- package/src/core/cache.ts +141 -141
- package/src/core/compose.ts +77 -77
- package/src/core/config.ts +14 -14
- package/src/core/errors.ts +148 -148
- package/src/core/hitl.ts +138 -138
- package/src/core/logger.ts +57 -57
- package/src/core/orchestrator.ts +215 -215
- package/src/core/performance.ts +187 -187
- package/src/core/rate-limiter.ts +128 -128
- package/src/core/room.ts +109 -109
- package/src/core/runtime.ts +230 -152
- package/src/core/sandbox.ts +101 -101
- package/src/core/scheduler.ts +187 -0
- package/src/core/security.ts +171 -171
- package/src/core/subagent.ts +98 -0
- package/src/core/types.ts +68 -68
- package/src/core/versioning.ts +106 -106
- package/src/core/watch.ts +178 -178
- package/src/core/workflow.ts +235 -235
- package/src/daemon.ts +96 -0
- package/src/deploy/hermes.ts +156 -156
- package/src/deploy/openclaw.ts +190 -200
- package/src/i18n/index.ts +216 -216
- package/src/index.ts +14 -10
- package/src/memory/deepbrain.ts +108 -108
- package/src/memory/index.ts +34 -34
- package/src/plugins/index.ts +208 -208
- package/src/providers/index.ts +354 -331
- package/src/schema/oad.ts +14 -2
- package/src/skills/auto-learn.ts +262 -0
- package/src/skills/base.ts +16 -16
- package/src/skills/document.ts +100 -100
- package/src/skills/http.ts +35 -35
- package/src/skills/index.ts +27 -27
- package/src/skills/scheduler.ts +80 -80
- package/src/skills/webhook-trigger.ts +59 -59
- package/src/templates/code-reviewer.ts +30 -34
- package/src/templates/customer-service.ts +76 -80
- package/src/templates/data-analyst.ts +66 -70
- package/src/templates/executive-assistant.ts +71 -71
- package/src/templates/financial-advisor.ts +60 -60
- package/src/templates/knowledge-base.ts +27 -31
- package/src/templates/legal-assistant.ts +71 -71
- package/src/templates/sales-assistant.ts +75 -79
- package/src/templates/teacher.ts +75 -79
- package/src/testing/index.ts +181 -181
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/calculator.ts +73 -73
- package/src/tools/datetime.ts +149 -149
- package/src/tools/json-transform.ts +187 -187
- package/src/tools/mcp-client.ts +131 -0
- package/src/tools/mcp.ts +76 -76
- package/src/tools/text-analysis.ts +116 -116
- package/src/traces/index.ts +132 -0
- package/templates/Dockerfile +15 -15
- package/templates/code-reviewer/README.md +27 -27
- package/templates/code-reviewer/oad.yaml +41 -41
- package/templates/customer-service/README.md +22 -22
- package/templates/customer-service/oad.yaml +36 -36
- package/templates/docker-compose.yml +21 -21
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/knowledge-base/README.md +28 -28
- package/templates/knowledge-base/oad.yaml +38 -38
- package/templates/sales-assistant/README.md +26 -26
- package/templates/sales-assistant/oad.yaml +43 -43
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -0
- package/test-agent/README.md +50 -0
- package/test-agent/agent.yaml +23 -0
- package/test-agent/docker-compose.yml +11 -0
- package/test-agent/oad.yaml +31 -0
- package/test-agent/package-lock.json +1492 -0
- package/test-agent/package.json +18 -0
- package/test-agent/src/index.ts +24 -0
- package/test-agent/src/skills/echo.ts +15 -0
- package/test-agent/tsconfig.json +25 -0
- package/tests/a2a.test.ts +66 -66
- package/tests/agent.test.ts +72 -72
- package/tests/analytics.test.ts +50 -50
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/channel.test.ts +39 -39
- package/tests/cli.test.ts +46 -0
- package/tests/e2e.test.ts +134 -134
- package/tests/errors.test.ts +83 -83
- package/tests/hitl.test.ts +71 -71
- package/tests/i18n.test.ts +41 -41
- package/tests/mcp.test.ts +54 -54
- package/tests/oad.test.ts +68 -68
- package/tests/performance.test.ts +115 -115
- package/tests/plugin.test.ts +74 -74
- package/tests/room.test.ts +106 -106
- package/tests/runtime.test.ts +42 -42
- package/tests/sandbox.test.ts +46 -46
- package/tests/security.test.ts +60 -60
- package/tests/subagent.test.ts +130 -0
- package/tests/telegram-discord.test.ts +60 -0
- package/tests/templates.test.ts +77 -77
- package/tests/v070.test.ts +76 -76
- package/tests/versioning.test.ts +75 -75
- package/tests/voice.test.ts +61 -61
- package/tests/webhook.test.ts +29 -29
- package/tests/workflow.test.ts +143 -143
- package/tsconfig.json +19 -19
- package/vitest.config.ts +9 -9
- package/dist/core/dashboard.d.ts +0 -35
- package/dist/core/dashboard.js +0 -157
- package/dist/core/priority.d.ts +0 -52
- package/dist/core/priority.js +0 -102
- package/src/core/dashboard.ts +0 -219
- package/src/core/priority.ts +0 -140
- package/src/dtv/data.ts +0 -29
- package/src/dtv/trust.ts +0 -43
- package/src/dtv/value.ts +0 -47
- package/src/marketplace/index.ts +0 -223
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "test-agent",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "opc run",
|
|
7
|
+
"dev": "opc dev",
|
|
8
|
+
"chat": "opc chat",
|
|
9
|
+
"build": "tsc"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"opc-agent": "^1.3.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"typescript": "^5.5.0",
|
|
16
|
+
"tsx": "^4.0.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AgentRuntime } from 'opc-agent';
|
|
2
|
+
import { EchoSkill } from './skills/echo';
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const runtime = new AgentRuntime();
|
|
6
|
+
|
|
7
|
+
// Load OAD config
|
|
8
|
+
await runtime.loadConfig('./agent.yaml');
|
|
9
|
+
|
|
10
|
+
// Initialize agent with channels, memory, etc.
|
|
11
|
+
const agent = await runtime.initialize();
|
|
12
|
+
|
|
13
|
+
// Register custom skills
|
|
14
|
+
runtime.registerSkill(new EchoSkill());
|
|
15
|
+
|
|
16
|
+
// Start serving
|
|
17
|
+
await runtime.start();
|
|
18
|
+
|
|
19
|
+
console.log('🤖 Agent is running!');
|
|
20
|
+
console.log(' Web UI: http://localhost:3000');
|
|
21
|
+
console.log(' Press Ctrl+C to stop');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BaseSkill } from 'opc-agent';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from 'opc-agent';
|
|
3
|
+
|
|
4
|
+
export class EchoSkill extends BaseSkill {
|
|
5
|
+
name = 'echo';
|
|
6
|
+
description = 'Echo back the message (test skill)';
|
|
7
|
+
|
|
8
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
9
|
+
if (message.content.toLowerCase().startsWith('/echo ')) {
|
|
10
|
+
const text = message.content.slice(6);
|
|
11
|
+
return this.match(`🔊 Echo: ${text}`);
|
|
12
|
+
}
|
|
13
|
+
return this.noMatch();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": [
|
|
6
|
+
"ES2022"
|
|
7
|
+
],
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"rootDir": "src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"declaration": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": [
|
|
19
|
+
"src/**/*"
|
|
20
|
+
],
|
|
21
|
+
"exclude": [
|
|
22
|
+
"node_modules",
|
|
23
|
+
"dist"
|
|
24
|
+
]
|
|
25
|
+
}
|
package/tests/a2a.test.ts
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { AgentRegistry } from '../src/core/a2a';
|
|
3
|
-
import { BaseAgent } from '../src/core/agent';
|
|
4
|
-
import { InMemoryStore } from '../src/memory';
|
|
5
|
-
|
|
6
|
-
function createTestAgent(name: string): BaseAgent {
|
|
7
|
-
const agent = new BaseAgent({ name, model: 'test', systemPrompt: `I am ${name}` }, new InMemoryStore());
|
|
8
|
-
return agent;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('AgentRegistry (A2A)', () => {
|
|
12
|
-
let registry: AgentRegistry;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
registry = new AgentRegistry();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should register and discover agents', () => {
|
|
19
|
-
const agent = createTestAgent('agent-1');
|
|
20
|
-
registry.register(agent, [{ name: 'summarize', description: 'Summarize text' }]);
|
|
21
|
-
|
|
22
|
-
const all = registry.discover();
|
|
23
|
-
expect(all).toHaveLength(1);
|
|
24
|
-
expect(all[0].agentName).toBe('agent-1');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('should discover by capability', () => {
|
|
28
|
-
const a1 = createTestAgent('a1');
|
|
29
|
-
const a2 = createTestAgent('a2');
|
|
30
|
-
registry.register(a1, [{ name: 'summarize', description: 'Summarize' }]);
|
|
31
|
-
registry.register(a2, [{ name: 'translate', description: 'Translate' }]);
|
|
32
|
-
|
|
33
|
-
const found = registry.discover('translate');
|
|
34
|
-
expect(found).toHaveLength(1);
|
|
35
|
-
expect(found[0].agentName).toBe('a2');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should unregister agents', () => {
|
|
39
|
-
const agent = createTestAgent('temp');
|
|
40
|
-
registry.register(agent, []);
|
|
41
|
-
registry.unregister('temp');
|
|
42
|
-
expect(registry.discover()).toHaveLength(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should handle request to unknown agent', async () => {
|
|
46
|
-
const response = await registry.call('a1', 'unknown', 'test', 'hello');
|
|
47
|
-
expect(response.status).toBe('error');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should send A2A request and get response', async () => {
|
|
51
|
-
const agent = createTestAgent('responder');
|
|
52
|
-
await agent.init();
|
|
53
|
-
registry.register(agent, [{ name: 'chat', description: 'Chat' }]);
|
|
54
|
-
|
|
55
|
-
const response = await registry.call('caller', 'responder', 'chat', 'hello');
|
|
56
|
-
expect(response.status).toBe('success');
|
|
57
|
-
expect(response.payload).toBeDefined();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should get agent by name', () => {
|
|
61
|
-
const agent = createTestAgent('finder');
|
|
62
|
-
registry.register(agent, []);
|
|
63
|
-
expect(registry.getAgent('finder')).toBeDefined();
|
|
64
|
-
expect(registry.getAgent('none')).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
});
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AgentRegistry } from '../src/core/a2a';
|
|
3
|
+
import { BaseAgent } from '../src/core/agent';
|
|
4
|
+
import { InMemoryStore } from '../src/memory';
|
|
5
|
+
|
|
6
|
+
function createTestAgent(name: string): BaseAgent {
|
|
7
|
+
const agent = new BaseAgent({ name, model: 'test', systemPrompt: `I am ${name}` }, new InMemoryStore());
|
|
8
|
+
return agent;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('AgentRegistry (A2A)', () => {
|
|
12
|
+
let registry: AgentRegistry;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
registry = new AgentRegistry();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should register and discover agents', () => {
|
|
19
|
+
const agent = createTestAgent('agent-1');
|
|
20
|
+
registry.register(agent, [{ name: 'summarize', description: 'Summarize text' }]);
|
|
21
|
+
|
|
22
|
+
const all = registry.discover();
|
|
23
|
+
expect(all).toHaveLength(1);
|
|
24
|
+
expect(all[0].agentName).toBe('agent-1');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should discover by capability', () => {
|
|
28
|
+
const a1 = createTestAgent('a1');
|
|
29
|
+
const a2 = createTestAgent('a2');
|
|
30
|
+
registry.register(a1, [{ name: 'summarize', description: 'Summarize' }]);
|
|
31
|
+
registry.register(a2, [{ name: 'translate', description: 'Translate' }]);
|
|
32
|
+
|
|
33
|
+
const found = registry.discover('translate');
|
|
34
|
+
expect(found).toHaveLength(1);
|
|
35
|
+
expect(found[0].agentName).toBe('a2');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should unregister agents', () => {
|
|
39
|
+
const agent = createTestAgent('temp');
|
|
40
|
+
registry.register(agent, []);
|
|
41
|
+
registry.unregister('temp');
|
|
42
|
+
expect(registry.discover()).toHaveLength(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle request to unknown agent', async () => {
|
|
46
|
+
const response = await registry.call('a1', 'unknown', 'test', 'hello');
|
|
47
|
+
expect(response.status).toBe('error');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should send A2A request and get response', async () => {
|
|
51
|
+
const agent = createTestAgent('responder');
|
|
52
|
+
await agent.init();
|
|
53
|
+
registry.register(agent, [{ name: 'chat', description: 'Chat' }]);
|
|
54
|
+
|
|
55
|
+
const response = await registry.call('caller', 'responder', 'chat', 'hello');
|
|
56
|
+
expect(response.status).toBe('success');
|
|
57
|
+
expect(response.payload).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should get agent by name', () => {
|
|
61
|
+
const agent = createTestAgent('finder');
|
|
62
|
+
registry.register(agent, []);
|
|
63
|
+
expect(registry.getAgent('finder')).toBeDefined();
|
|
64
|
+
expect(registry.getAgent('none')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
});
|
package/tests/agent.test.ts
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { BaseAgent } from '../src/core/agent';
|
|
3
|
-
|
|
4
|
-
describe('BaseAgent', () => {
|
|
5
|
-
it('should start in init state', () => {
|
|
6
|
-
const agent = new BaseAgent({ name: 'test' });
|
|
7
|
-
expect(agent.state).toBe('init');
|
|
8
|
-
expect(agent.name).toBe('test');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should transition through lifecycle', async () => {
|
|
12
|
-
const agent = new BaseAgent({ name: 'test' });
|
|
13
|
-
const states: string[] = [];
|
|
14
|
-
agent.on('state:change', (_from, to) => states.push(to));
|
|
15
|
-
|
|
16
|
-
await agent.init();
|
|
17
|
-
expect(agent.state).toBe('ready');
|
|
18
|
-
|
|
19
|
-
await agent.start();
|
|
20
|
-
expect(agent.state).toBe('running');
|
|
21
|
-
|
|
22
|
-
await agent.stop();
|
|
23
|
-
expect(agent.state).toBe('stopped');
|
|
24
|
-
|
|
25
|
-
expect(states).toEqual(['ready', 'running', 'stopped']);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should not start if not ready', async () => {
|
|
29
|
-
const agent = new BaseAgent({ name: 'test' });
|
|
30
|
-
await expect(agent.start()).rejects.toThrow('Cannot start agent in state: init');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should handle messages with stub provider', async () => {
|
|
34
|
-
const agent = new BaseAgent({ name: 'test', systemPrompt: 'Be helpful' });
|
|
35
|
-
await agent.init();
|
|
36
|
-
|
|
37
|
-
const response = await agent.handleMessage({
|
|
38
|
-
id: 'msg_1',
|
|
39
|
-
role: 'user',
|
|
40
|
-
content: 'Hello',
|
|
41
|
-
timestamp: Date.now(),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
expect(response.role).toBe('assistant');
|
|
45
|
-
expect(response.content).toContain('Hello');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should use skills before LLM fallback', async () => {
|
|
49
|
-
const agent = new BaseAgent({ name: 'test' });
|
|
50
|
-
await agent.init();
|
|
51
|
-
|
|
52
|
-
agent.registerSkill({
|
|
53
|
-
name: 'greeter',
|
|
54
|
-
description: 'Greets users',
|
|
55
|
-
execute: async (_ctx, msg) => {
|
|
56
|
-
if (msg.content.toLowerCase().includes('hello')) {
|
|
57
|
-
return { handled: true, response: 'Hi there!', confidence: 1.0 };
|
|
58
|
-
}
|
|
59
|
-
return { handled: false, confidence: 0 };
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const response = await agent.handleMessage({
|
|
64
|
-
id: 'msg_1',
|
|
65
|
-
role: 'user',
|
|
66
|
-
content: 'Hello!',
|
|
67
|
-
timestamp: Date.now(),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
expect(response.content).toBe('Hi there!');
|
|
71
|
-
});
|
|
72
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { BaseAgent } from '../src/core/agent';
|
|
3
|
+
|
|
4
|
+
describe('BaseAgent', () => {
|
|
5
|
+
it('should start in init state', () => {
|
|
6
|
+
const agent = new BaseAgent({ name: 'test' });
|
|
7
|
+
expect(agent.state).toBe('init');
|
|
8
|
+
expect(agent.name).toBe('test');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should transition through lifecycle', async () => {
|
|
12
|
+
const agent = new BaseAgent({ name: 'test' });
|
|
13
|
+
const states: string[] = [];
|
|
14
|
+
agent.on('state:change', (_from, to) => states.push(to));
|
|
15
|
+
|
|
16
|
+
await agent.init();
|
|
17
|
+
expect(agent.state).toBe('ready');
|
|
18
|
+
|
|
19
|
+
await agent.start();
|
|
20
|
+
expect(agent.state).toBe('running');
|
|
21
|
+
|
|
22
|
+
await agent.stop();
|
|
23
|
+
expect(agent.state).toBe('stopped');
|
|
24
|
+
|
|
25
|
+
expect(states).toEqual(['ready', 'running', 'stopped']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not start if not ready', async () => {
|
|
29
|
+
const agent = new BaseAgent({ name: 'test' });
|
|
30
|
+
await expect(agent.start()).rejects.toThrow('Cannot start agent in state: init');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle messages with stub provider', async () => {
|
|
34
|
+
const agent = new BaseAgent({ name: 'test', systemPrompt: 'Be helpful' });
|
|
35
|
+
await agent.init();
|
|
36
|
+
|
|
37
|
+
const response = await agent.handleMessage({
|
|
38
|
+
id: 'msg_1',
|
|
39
|
+
role: 'user',
|
|
40
|
+
content: 'Hello',
|
|
41
|
+
timestamp: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(response.role).toBe('assistant');
|
|
45
|
+
expect(response.content).toContain('Hello');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should use skills before LLM fallback', async () => {
|
|
49
|
+
const agent = new BaseAgent({ name: 'test' });
|
|
50
|
+
await agent.init();
|
|
51
|
+
|
|
52
|
+
agent.registerSkill({
|
|
53
|
+
name: 'greeter',
|
|
54
|
+
description: 'Greets users',
|
|
55
|
+
execute: async (_ctx, msg) => {
|
|
56
|
+
if (msg.content.toLowerCase().includes('hello')) {
|
|
57
|
+
return { handled: true, response: 'Hi there!', confidence: 1.0 };
|
|
58
|
+
}
|
|
59
|
+
return { handled: false, confidence: 0 };
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const response = await agent.handleMessage({
|
|
64
|
+
id: 'msg_1',
|
|
65
|
+
role: 'user',
|
|
66
|
+
content: 'Hello!',
|
|
67
|
+
timestamp: Date.now(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(response.content).toBe('Hi there!');
|
|
71
|
+
});
|
|
72
|
+
});
|
package/tests/analytics.test.ts
CHANGED
|
@@ -1,50 +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
|
-
});
|
|
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,105 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import { SkillLearner, skillToMarkdown, parseSkillMarkdown, type LearnedSkill } from '../src/skills/auto-learn';
|
|
6
|
+
|
|
7
|
+
function makeSkill(overrides: Partial<LearnedSkill> = {}): LearnedSkill {
|
|
8
|
+
return {
|
|
9
|
+
name: 'test-skill',
|
|
10
|
+
description: 'A test skill',
|
|
11
|
+
trigger: 'deploy|deployment',
|
|
12
|
+
instructions: '1. Check env\n2. Run deploy\n3. Verify',
|
|
13
|
+
examples: ['deploy to production', 'run deployment'],
|
|
14
|
+
createdAt: new Date('2026-01-01T00:00:00Z'),
|
|
15
|
+
usageCount: 0,
|
|
16
|
+
version: 1,
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('SkillLearner', () => {
|
|
22
|
+
let tmpDir: string;
|
|
23
|
+
let learner: SkillLearner;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opc-skills-'));
|
|
27
|
+
learner = new SkillLearner(tmpDir);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('saveSkill / loadLearnedSkills', () => {
|
|
35
|
+
it('should save and load a skill', async () => {
|
|
36
|
+
const skill = makeSkill();
|
|
37
|
+
await learner.saveSkill(skill);
|
|
38
|
+
|
|
39
|
+
const loaded = await learner.loadLearnedSkills();
|
|
40
|
+
expect(loaded).toHaveLength(1);
|
|
41
|
+
expect(loaded[0].name).toBe('test-skill');
|
|
42
|
+
expect(loaded[0].description).toBe('A test skill');
|
|
43
|
+
expect(loaded[0].trigger).toBe('deploy|deployment');
|
|
44
|
+
expect(loaded[0].examples).toEqual(['deploy to production', 'run deployment']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should create directory if not exists', async () => {
|
|
48
|
+
const nested = path.join(tmpDir, 'deep', 'nested');
|
|
49
|
+
const l = new SkillLearner(nested);
|
|
50
|
+
await l.saveSkill(makeSkill());
|
|
51
|
+
expect(fs.existsSync(path.join(nested, 'test-skill.md'))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should return empty array for nonexistent dir', async () => {
|
|
55
|
+
const l = new SkillLearner(path.join(tmpDir, 'nope'));
|
|
56
|
+
const skills = await l.loadLearnedSkills();
|
|
57
|
+
expect(skills).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('matchSkill', () => {
|
|
62
|
+
it('should match by regex pattern', async () => {
|
|
63
|
+
await learner.saveSkill(makeSkill({ trigger: 'deploy|deployment' }));
|
|
64
|
+
await learner.loadLearnedSkills();
|
|
65
|
+
|
|
66
|
+
expect(learner.matchSkill('please deploy to production')).not.toBeNull();
|
|
67
|
+
expect(learner.matchSkill('run deployment now')).not.toBeNull();
|
|
68
|
+
expect(learner.matchSkill('hello world')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should match by keyword fallback for invalid regex', async () => {
|
|
72
|
+
// Use an actually invalid regex so the catch branch is triggered
|
|
73
|
+
await learner.saveSkill(makeSkill({ trigger: '(deploy[broken, kubernetes' }));
|
|
74
|
+
await learner.loadLearnedSkills();
|
|
75
|
+
|
|
76
|
+
expect(learner.matchSkill('kubernetes cluster')).not.toBeNull();
|
|
77
|
+
expect(learner.matchSkill('random text')).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return null if not loaded', () => {
|
|
81
|
+
expect(learner.matchSkill('deploy')).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('skillToMarkdown / parseSkillMarkdown', () => {
|
|
86
|
+
it('should round-trip a skill through markdown', () => {
|
|
87
|
+
const skill = makeSkill();
|
|
88
|
+
const md = skillToMarkdown(skill);
|
|
89
|
+
const parsed = parseSkillMarkdown(md);
|
|
90
|
+
|
|
91
|
+
expect(parsed).not.toBeNull();
|
|
92
|
+
expect(parsed!.name).toBe(skill.name);
|
|
93
|
+
expect(parsed!.description).toBe(skill.description);
|
|
94
|
+
expect(parsed!.trigger).toBe(skill.trigger);
|
|
95
|
+
expect(parsed!.instructions).toBe(skill.instructions);
|
|
96
|
+
expect(parsed!.examples).toEqual(skill.examples);
|
|
97
|
+
expect(parsed!.version).toBe(1);
|
|
98
|
+
expect(parsed!.usageCount).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should return null for invalid markdown', () => {
|
|
102
|
+
expect(parseSkillMarkdown('just some text')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { getBuiltinTools, getBuiltinToolsByName } from '../src/tools/builtin';
|
|
6
|
+
import { fileTool, shellTool, datetimeTool } from '../src/tools/builtin';
|
|
7
|
+
|
|
8
|
+
describe('getBuiltinTools', () => {
|
|
9
|
+
it('returns 4 tools', () => {
|
|
10
|
+
const tools = getBuiltinTools();
|
|
11
|
+
expect(tools).toHaveLength(4);
|
|
12
|
+
const names = tools.map(t => t.name);
|
|
13
|
+
expect(names).toContain('file_operations');
|
|
14
|
+
expect(names).toContain('web_fetch');
|
|
15
|
+
expect(names).toContain('shell_exec');
|
|
16
|
+
expect(names).toContain('datetime');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('getBuiltinToolsByName filters correctly', () => {
|
|
20
|
+
const tools = getBuiltinToolsByName(['datetime', 'file_operations']);
|
|
21
|
+
expect(tools).toHaveLength(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('getBuiltinToolsByName with no args returns all', () => {
|
|
25
|
+
expect(getBuiltinToolsByName()).toHaveLength(4);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('file_operations tool', () => {
|
|
30
|
+
// file tool resolves paths relative to cwd, so use relative paths from a temp dir
|
|
31
|
+
// Actually, it uses process.cwd() as workspace. Let's just test with paths relative to cwd.
|
|
32
|
+
const testFile = `tmp-test-${Date.now()}.txt`;
|
|
33
|
+
|
|
34
|
+
afterAll(() => {
|
|
35
|
+
try { require('fs').unlinkSync(testFile); } catch {}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('write and read a file', async () => {
|
|
39
|
+
const writeRes = await fileTool.execute({ action: 'write', path: testFile, content: 'hello' });
|
|
40
|
+
expect(writeRes.isError).toBe(false);
|
|
41
|
+
|
|
42
|
+
const readRes = await fileTool.execute({ action: 'read', path: testFile });
|
|
43
|
+
expect(readRes.isError).toBe(false);
|
|
44
|
+
expect(readRes.content).toBe('hello');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('list files', async () => {
|
|
48
|
+
const res = await fileTool.execute({ action: 'list', path: '.' });
|
|
49
|
+
expect(res.isError).toBe(false);
|
|
50
|
+
expect(res.content).toContain('package.json');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('exists check', async () => {
|
|
54
|
+
const res = await fileTool.execute({ action: 'exists', path: testFile });
|
|
55
|
+
expect(res.content).toBe('true');
|
|
56
|
+
|
|
57
|
+
const res2 = await fileTool.execute({ action: 'exists', path: 'nope-does-not-exist.txt' });
|
|
58
|
+
expect(res2.content).toBe('false');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('rejects path outside workspace', async () => {
|
|
62
|
+
const res = await fileTool.execute({ action: 'read', path: '../../etc/passwd' });
|
|
63
|
+
expect(res.isError).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('datetime tool', () => {
|
|
68
|
+
it('returns valid JSON with iso field', async () => {
|
|
69
|
+
const res = await datetimeTool.execute({});
|
|
70
|
+
expect(res.isError).toBe(false);
|
|
71
|
+
const parsed = JSON.parse(res.content);
|
|
72
|
+
expect(parsed.iso).toBeDefined();
|
|
73
|
+
expect(new Date(parsed.iso).toISOString()).toBe(parsed.iso);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('shell_exec tool', () => {
|
|
78
|
+
it('runs a command', async () => {
|
|
79
|
+
const res = await shellTool.execute({ command: 'echo hello' });
|
|
80
|
+
expect(res.isError).toBe(false);
|
|
81
|
+
expect(res.content).toContain('hello');
|
|
82
|
+
});
|
|
83
|
+
});
|