@starlink-awaken/agentmesh 1.0.2 → 1.2.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +60 -41
  2. package/README.zh-CN.md +137 -167
  3. package/config/gateway.yaml +78 -0
  4. package/dist/src/adapters/base.d.ts +22 -0
  5. package/dist/src/adapters/base.js +10 -0
  6. package/dist/src/adapters/claude-code.d.ts +22 -0
  7. package/dist/src/adapters/claude-code.js +112 -0
  8. package/dist/src/adapters/openclaw.d.ts +22 -0
  9. package/dist/src/adapters/openclaw.js +110 -0
  10. package/dist/src/adapters/process.d.ts +28 -0
  11. package/dist/src/adapters/process.js +121 -0
  12. package/dist/src/cli/connect.d.ts +26 -0
  13. package/dist/src/cli/connect.js +544 -0
  14. package/dist/src/cli/setup.d.ts +2 -0
  15. package/dist/src/cli/setup.js +97 -0
  16. package/dist/src/cli.d.ts +2 -0
  17. package/dist/src/cli.js +410 -0
  18. package/dist/src/core/agent-registry.d.ts +48 -0
  19. package/dist/src/core/agent-registry.js +295 -0
  20. package/dist/src/core/config.d.ts +59 -0
  21. package/dist/src/core/config.js +101 -0
  22. package/dist/src/core/context-manager.d.ts +52 -0
  23. package/dist/src/core/context-manager.js +165 -0
  24. package/dist/src/core/event-bus.d.ts +35 -0
  25. package/dist/src/core/event-bus.js +62 -0
  26. package/dist/src/core/logger.d.ts +14 -0
  27. package/dist/src/core/logger.js +57 -0
  28. package/dist/src/core/metrics.d.ts +87 -0
  29. package/dist/src/core/metrics.js +167 -0
  30. package/dist/src/core/router.d.ts +46 -0
  31. package/dist/src/core/router.js +90 -0
  32. package/dist/src/core/task-manager.d.ts +41 -0
  33. package/dist/src/core/task-manager.js +197 -0
  34. package/dist/src/core/vector-store.d.ts +37 -0
  35. package/dist/src/core/vector-store.js +175 -0
  36. package/dist/src/index.d.ts +1 -0
  37. package/dist/src/index.js +105 -0
  38. package/dist/src/model-gateway/circuit-breaker.d.ts +21 -0
  39. package/dist/src/model-gateway/circuit-breaker.js +86 -0
  40. package/dist/src/model-gateway/health.d.ts +12 -0
  41. package/dist/src/model-gateway/health.js +80 -0
  42. package/dist/src/model-gateway/providers.d.ts +4 -0
  43. package/dist/src/model-gateway/providers.js +113 -0
  44. package/dist/src/model-gateway/quota.d.ts +4 -0
  45. package/dist/src/model-gateway/quota.js +107 -0
  46. package/dist/src/model-gateway/rate-limit.d.ts +12 -0
  47. package/dist/src/model-gateway/rate-limit.js +51 -0
  48. package/dist/src/model-gateway/retry.d.ts +14 -0
  49. package/dist/src/model-gateway/retry.js +48 -0
  50. package/dist/src/model-gateway/router.d.ts +4 -0
  51. package/dist/src/model-gateway/router.js +79 -0
  52. package/dist/src/model-gateway/routes.d.ts +2 -0
  53. package/dist/src/model-gateway/routes.js +172 -0
  54. package/dist/src/model-gateway/types.d.ts +47 -0
  55. package/dist/src/model-gateway/types.js +1 -0
  56. package/dist/src/routes/api.d.ts +2 -0
  57. package/dist/src/routes/api.js +128 -0
  58. package/dist/src/routes/websocket.d.ts +2 -0
  59. package/dist/src/routes/websocket.js +64 -0
  60. package/dist/src/types/index.d.ts +71 -0
  61. package/dist/src/types/index.js +1 -0
  62. package/dist/tests/core/context-manager.test.d.ts +1 -0
  63. package/dist/tests/core/context-manager.test.js +35 -0
  64. package/dist/tests/core/router.test.d.ts +1 -0
  65. package/dist/tests/core/router.test.js +79 -0
  66. package/dist/tests/model-gateway/circuit-breaker.test.d.ts +1 -0
  67. package/dist/tests/model-gateway/circuit-breaker.test.js +84 -0
  68. package/dist/tests/model-gateway/providers.test.d.ts +1 -0
  69. package/dist/tests/model-gateway/providers.test.js +80 -0
  70. package/dist/tests/model-gateway/quota.test.d.ts +1 -0
  71. package/dist/tests/model-gateway/quota.test.js +60 -0
  72. package/dist/tests/model-gateway/rate-limit.test.d.ts +1 -0
  73. package/dist/tests/model-gateway/rate-limit.test.js +42 -0
  74. package/dist/tests/model-gateway/retry.test.d.ts +1 -0
  75. package/dist/tests/model-gateway/retry.test.js +47 -0
  76. package/dist/tests/model-gateway/router.test.d.ts +1 -0
  77. package/dist/tests/model-gateway/router.test.js +108 -0
  78. package/dist/tests/model-gateway/routes.test.d.ts +1 -0
  79. package/dist/tests/model-gateway/routes.test.js +83 -0
  80. package/docs/api.md +187 -460
  81. package/docs/architecture.md +138 -0
  82. package/docs/configuration.md +188 -0
  83. package/package.json +3 -1
@@ -0,0 +1,172 @@
1
+ import { resolveProvider, getConfig } from './router.js';
2
+ import { callChatCompletions, callResponsesApi } from './providers.js';
3
+ import { getQuotaSummary, probeQuota } from './quota.js';
4
+ import { circuitBreakerRegistry } from './circuit-breaker.js';
5
+ import { checkAllProviders } from './health.js';
6
+ export async function modelGatewayRoutes(fastify) {
7
+ // 健康检查 + 配额总览
8
+ fastify.get('/model-gateway/health', async (_req, _reply) => {
9
+ const quota = await probeQuota();
10
+ return {
11
+ status: 'ok',
12
+ timestamp: Date.now(),
13
+ providers_available: Array.from(quota.entries())
14
+ .filter(([_, info]) => info.available)
15
+ .map(([name, info]) => ({ name, summary: info.summary })),
16
+ providers_unavailable: Array.from(quota.entries())
17
+ .filter(([_, info]) => !info.available)
18
+ .map(([name, info]) => ({ name, summary: info.summary })),
19
+ };
20
+ });
21
+ // 配额详情
22
+ fastify.get('/model-gateway/quota', async (_req, reply) => {
23
+ await probeQuota();
24
+ reply.send(getQuotaSummary());
25
+ });
26
+ // 可用模型列表
27
+ fastify.get('/v1/models', async (_req, reply) => {
28
+ const { getConfig } = await import('./router.js');
29
+ const cfg = getConfig();
30
+ const models = [];
31
+ if (cfg) {
32
+ for (const [providerName, providerCfg] of Object.entries(cfg.providers)) {
33
+ const providerModels = providerCfg.models || [providerName];
34
+ for (const model of providerModels) {
35
+ models.push({ id: model, object: 'model', owned_by: providerName });
36
+ }
37
+ }
38
+ }
39
+ reply.send({ object: 'list', data: models });
40
+ });
41
+ // POST /v1/chat/completions — 标准 OpenAI 兼容端点
42
+ fastify.post('/v1/chat/completions', async (request, reply) => {
43
+ const body = request.body;
44
+ if (!body || !body.messages) {
45
+ return reply.code(400).send({ error: { message: 'messages is required' } });
46
+ }
47
+ const model = body.model || 'deepseek-chat';
48
+ const provider = resolveProvider(model);
49
+ if (!provider) {
50
+ return reply.code(503).send({
51
+ error: { message: 'No available provider. Check API keys and quota.' },
52
+ });
53
+ }
54
+ console.log(`[ModelGW] ${model} → ${provider.name} (${body.stream ? 'stream' : 'sync'})`);
55
+ try {
56
+ const upstreamResp = await callChatCompletions(provider, {
57
+ model,
58
+ messages: body.messages,
59
+ stream: body.stream,
60
+ temperature: body.temperature,
61
+ max_tokens: body.max_tokens,
62
+ tools: body.tools,
63
+ tool_choice: body.tool_choice,
64
+ });
65
+ if (!upstreamResp.ok && upstreamResp.status !== 200) {
66
+ const errText = await upstreamResp.text();
67
+ console.error(`[ModelGW] ${provider.name} error ${upstreamResp.status}: ${errText.slice(0, 200)}`);
68
+ return reply.code(upstreamResp.status).send({
69
+ error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
70
+ });
71
+ }
72
+ if (body.stream) {
73
+ return reply.headers({
74
+ 'Content-Type': 'text/event-stream',
75
+ 'Cache-Control': 'no-cache',
76
+ Connection: 'keep-alive',
77
+ }).send(upstreamResp.body);
78
+ }
79
+ const data = await upstreamResp.json();
80
+ reply.send(data);
81
+ }
82
+ catch (err) {
83
+ console.error(`[ModelGW] Error calling ${provider.name}:`, err.message);
84
+ reply.code(502).send({
85
+ error: { message: `Provider error: ${err.message}` },
86
+ });
87
+ }
88
+ });
89
+ // POST /v1/responses — Codex Desktop Responses API 适配
90
+ fastify.post('/v1/responses', async (request, reply) => {
91
+ const body = request.body;
92
+ if (!body || !body.input) {
93
+ return reply.code(400).send({ error: { message: 'input is required' } });
94
+ }
95
+ const model = body.model || 'deepseek-chat';
96
+ const provider = resolveProvider(model);
97
+ if (!provider) {
98
+ return reply.code(503).send({
99
+ error: { message: 'No available provider. Check API keys and quota.' },
100
+ });
101
+ }
102
+ console.log(`[ModelGW:Responses] ${model} → ${provider.name}`);
103
+ try {
104
+ const upstreamResp = await callResponsesApi(provider, body);
105
+ if (!upstreamResp.ok) {
106
+ const errText = await upstreamResp.text();
107
+ return reply.code(upstreamResp.status).send({
108
+ error: { message: `${provider.name}: ${errText.slice(0, 500)}` },
109
+ });
110
+ }
111
+ if (body.stream) {
112
+ return reply.headers({
113
+ 'Content-Type': 'text/event-stream',
114
+ 'Cache-Control': 'no-cache',
115
+ }).send(upstreamResp.body);
116
+ }
117
+ // 将 Chat Completions 响应转回 Responses API 格式
118
+ const ccData = (await upstreamResp.json());
119
+ const choice = ccData.choices?.[0];
120
+ const responsesData = {
121
+ id: ccData.id,
122
+ object: 'response',
123
+ model: ccData.model,
124
+ output: [
125
+ {
126
+ type: 'message',
127
+ role: 'assistant',
128
+ content: [
129
+ {
130
+ type: 'output_text',
131
+ text: choice?.message?.content || '',
132
+ },
133
+ ],
134
+ },
135
+ ],
136
+ usage: ccData.usage,
137
+ };
138
+ reply.send(responsesData);
139
+ }
140
+ catch (err) {
141
+ console.error(`[ModelGW:Responses] Error:`, err.message);
142
+ reply.code(502).send({
143
+ error: { message: `Provider error: ${err.message}` },
144
+ });
145
+ }
146
+ });
147
+ // Provider 健康检查 + 熔断器状态
148
+ fastify.get('/model-gateway/health/:provider', async (request, reply) => {
149
+ const { provider } = request.params;
150
+ const cfg = getConfig();
151
+ if (!cfg || !cfg.providers[provider]) {
152
+ return reply.code(404).send({ error: { message: `Provider '${provider}' not found` } });
153
+ }
154
+ const results = await checkAllProviders(cfg);
155
+ const result = results.find(r => r.provider === provider);
156
+ if (!result) {
157
+ return reply.code(404).send({ error: { message: `Health check not available for '${provider}'` } });
158
+ }
159
+ reply.send({ ...result, circuit_breaker: circuitBreakerRegistry.getStatus() });
160
+ });
161
+ // 全部 Provider 健康检查
162
+ fastify.get('/model-gateway/health/all', async (_request, reply) => {
163
+ const cfg = getConfig();
164
+ if (!cfg)
165
+ return reply.code(503).send({ error: { message: 'Model gateway not configured' } });
166
+ const results = await checkAllProviders(cfg);
167
+ reply.send({
168
+ providers: results,
169
+ circuit_breaker: circuitBreakerRegistry.getStatus(),
170
+ });
171
+ });
172
+ }
@@ -0,0 +1,47 @@
1
+ export interface ModelProviderConfig {
2
+ base_url: string;
3
+ api_key_env?: string;
4
+ api_key?: string;
5
+ models?: string[];
6
+ }
7
+ export interface ModelGatewayConfig {
8
+ default_model: string;
9
+ providers: Record<string, ModelProviderConfig>;
10
+ fallback_chain: string[];
11
+ model_routing: Record<string, string[]>;
12
+ }
13
+ export interface ResolvedProvider {
14
+ name: string;
15
+ base_url: string;
16
+ api_key: string;
17
+ }
18
+ export interface QuotaInfo {
19
+ provider: string;
20
+ available: boolean;
21
+ usedPercent?: number;
22
+ balance?: number;
23
+ summary: string;
24
+ }
25
+ export interface ChatCompletionRequest {
26
+ model: string;
27
+ messages: Array<{
28
+ role: string;
29
+ content: string | any;
30
+ }>;
31
+ stream?: boolean;
32
+ temperature?: number;
33
+ max_tokens?: number;
34
+ tools?: any[];
35
+ tool_choice?: any;
36
+ }
37
+ export interface ResponsesRequest {
38
+ model: string;
39
+ input: Array<{
40
+ role?: string;
41
+ type?: string;
42
+ content?: string | any[];
43
+ }>;
44
+ stream?: boolean;
45
+ tools?: any[];
46
+ instructions?: string;
47
+ }
@@ -0,0 +1 @@
1
+ // 模型网关类型定义
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function apiRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,128 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { taskManager } from '../core/task-manager.js';
3
+ import { router } from '../core/router.js';
4
+ import { contextManager } from '../core/context-manager.js';
5
+ export async function apiRoutes(fastify) {
6
+ // 健康检查
7
+ fastify.get('/health', async (_request, _reply) => {
8
+ return {
9
+ status: 'ok',
10
+ timestamp: Date.now(),
11
+ agents: router.getAllAgents().map(a => ({
12
+ id: a.id,
13
+ name: a.name,
14
+ status: a.status
15
+ }))
16
+ };
17
+ });
18
+ // 提交任务
19
+ fastify.post('/tasks', async (request, reply) => {
20
+ const body = request.body || {};
21
+ const message = {
22
+ id: uuidv4(),
23
+ type: 'request',
24
+ source: body.source || 'api',
25
+ target: 'gateway',
26
+ correlation_id: body.correlation_id || uuidv4(),
27
+ timestamp: Date.now(),
28
+ payload: body.payload
29
+ };
30
+ try {
31
+ const task = await taskManager.processTask(message);
32
+ reply.code(202).send({
33
+ task_id: task.id,
34
+ status: task.status,
35
+ message: 'Task submitted successfully'
36
+ });
37
+ }
38
+ catch (error) {
39
+ const errorMessage = error instanceof Error ? error.message : String(error);
40
+ reply.code(500).send({
41
+ error: {
42
+ code: 'TASK_FAILED',
43
+ message: errorMessage
44
+ }
45
+ });
46
+ }
47
+ });
48
+ // 获取任务状态
49
+ fastify.get('/tasks/:taskId', async (request, reply) => {
50
+ const { taskId } = request.params;
51
+ const task = taskManager.getTask(taskId);
52
+ if (!task) {
53
+ reply.code(404).send({
54
+ error: {
55
+ code: 'TASK_NOT_FOUND',
56
+ message: `Task ${taskId} not found`
57
+ }
58
+ });
59
+ return;
60
+ }
61
+ reply.send({
62
+ id: task.id,
63
+ status: task.status,
64
+ assigned_agents: task.assignedAgents,
65
+ result: task.result,
66
+ error: task.error,
67
+ created_at: task.createdAt,
68
+ updated_at: task.updatedAt
69
+ });
70
+ });
71
+ // 获取所有任务
72
+ fastify.get('/tasks', async (_request, reply) => {
73
+ const tasks = taskManager.getAllTasks();
74
+ reply.send(tasks.map(t => ({
75
+ id: t.id,
76
+ status: t.status,
77
+ assigned_agents: t.assignedAgents,
78
+ created_at: t.createdAt
79
+ })));
80
+ });
81
+ // 创建共享空间
82
+ fastify.post('/spaces', async (request, reply) => {
83
+ const { metadata } = request.body || {};
84
+ const spaceId = await contextManager.createSharedSpace(metadata);
85
+ reply.code(201).send({ space_id: spaceId });
86
+ });
87
+ // 获取共享空间
88
+ fastify.get('/spaces/:spaceId', async (request, reply) => {
89
+ const { spaceId } = request.params;
90
+ const context = await contextManager.getSharedSpace(spaceId);
91
+ if (!context) {
92
+ reply.code(404).send({
93
+ error: {
94
+ code: 'SPACE_NOT_FOUND',
95
+ message: `Shared space ${spaceId} not found`
96
+ }
97
+ });
98
+ return;
99
+ }
100
+ reply.send({
101
+ shared_space_id: context.shared_space_id,
102
+ message_count: context.messages.length,
103
+ artifact_count: context.artifacts.size,
104
+ metadata: context.metadata,
105
+ created_at: context.createdAt,
106
+ updated_at: context.updatedAt
107
+ });
108
+ });
109
+ // 获取 Agent 列表
110
+ fastify.get('/agents', async (_request, reply) => {
111
+ const agents = router.getAllAgents();
112
+ reply.send(agents);
113
+ });
114
+ // 注册 Agent
115
+ fastify.post('/agents', async (request, reply) => {
116
+ const body = request.body || {};
117
+ router.registerAgent({
118
+ id: body.id || 'unknown',
119
+ name: body.name || 'Unknown',
120
+ type: body.type || 'process',
121
+ capabilities: body.capabilities || [],
122
+ status: 'online',
123
+ endpoint: body.endpoint,
124
+ lastSeen: Date.now()
125
+ });
126
+ reply.code(201).send({ id: body.id, status: 'registered' });
127
+ });
128
+ }
@@ -0,0 +1,2 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ export declare function websocketRoutes(fastify: FastifyInstance): Promise<void>;
@@ -0,0 +1,64 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { taskManager } from '../core/task-manager.js';
3
+ import { agentRegistry } from '../core/agent-registry.js';
4
+ export async function websocketRoutes(fastify) {
5
+ const clients = new Map();
6
+ // SSE 端点 - 用于实时任务更新
7
+ fastify.get('/events', async (request, reply) => {
8
+ const { taskId, spaceId } = request.query;
9
+ reply.raw.writeHead(200, {
10
+ 'Content-Type': 'text/event-stream',
11
+ 'Cache-Control': 'no-cache',
12
+ 'Connection': 'keep-alive'
13
+ });
14
+ const clientId = uuidv4();
15
+ clients.set(clientId, {
16
+ id: clientId,
17
+ reply
18
+ });
19
+ // 发送初始连接消息
20
+ reply.raw.write(`data: ${JSON.stringify({ type: 'connected', client_id: clientId })}\n\n`);
21
+ // 订阅任务事件
22
+ if (taskId) {
23
+ const task = taskManager.getTask(taskId);
24
+ if (task) {
25
+ reply.raw.write(`data: ${JSON.stringify({ type: 'task_status', task })}\n\n`);
26
+ }
27
+ }
28
+ // 发送欢迎消息
29
+ reply.raw.write(`data: ${JSON.stringify({ type: 'welcome', agents: agentRegistry.getAgents() })}\n\n`);
30
+ // 心跳
31
+ const heartbeat = setInterval(() => {
32
+ reply.raw.write(`data: ${JSON.stringify({ type: 'heartbeat', timestamp: Date.now() })}\n\n`);
33
+ }, 30000);
34
+ request.raw.on('close', () => {
35
+ clearInterval(heartbeat);
36
+ clients.delete(clientId);
37
+ });
38
+ });
39
+ // WebSocket 模拟端点 - 返回 SSE 连接信息
40
+ fastify.get('/ws-info', async (_request, reply) => {
41
+ reply.send({
42
+ message: 'Use /events for Server-Sent Events streaming',
43
+ endpoints: {
44
+ events: '/events?taskId=<task_id>&spaceId=<space_id>',
45
+ description: 'Subscribe to real-time task updates and agent responses'
46
+ },
47
+ example: 'curl -N http://localhost:3000/events?taskId=<task_id>'
48
+ });
49
+ });
50
+ // 广播消息到所有客户端
51
+ fastify.post('/broadcast', async (request, reply) => {
52
+ const { type, data } = request.body || {};
53
+ const message = JSON.stringify({ type, data, timestamp: Date.now() });
54
+ for (const client of clients.values()) {
55
+ try {
56
+ client.reply.raw.write(`data: ${message}\n\n`);
57
+ }
58
+ catch (e) {
59
+ // 客户端可能已断开
60
+ }
61
+ }
62
+ reply.send({ delivered: clients.size });
63
+ });
64
+ }
@@ -0,0 +1,71 @@
1
+ export type MessageType = 'request' | 'response' | 'event' | 'stream' | 'stream_end';
2
+ export type EventType = 'agent.registered' | 'agent.unregistered' | 'task.submitted' | 'task.assigned' | 'task.started' | 'task.progress' | 'task.completed' | 'task.failed' | 'context.updated';
3
+ export interface ContextRef {
4
+ shared_space_id: string;
5
+ history?: string[];
6
+ artifacts?: string[];
7
+ }
8
+ export interface Payload {
9
+ task?: string;
10
+ context?: ContextRef;
11
+ files?: string[];
12
+ options?: {
13
+ stream?: boolean;
14
+ timeout?: number;
15
+ };
16
+ }
17
+ export interface Error {
18
+ code: string;
19
+ message: string;
20
+ }
21
+ export interface AgentMessage {
22
+ id: string;
23
+ type: MessageType;
24
+ source: string;
25
+ target: string;
26
+ correlation_id: string;
27
+ timestamp: number;
28
+ payload?: Payload;
29
+ event_type?: EventType;
30
+ event_data?: Record<string, unknown>;
31
+ result?: unknown;
32
+ error?: Error;
33
+ }
34
+ export interface Agent {
35
+ id: string;
36
+ name: string;
37
+ type: 'claude-code' | 'openclaw' | 'process' | 'http';
38
+ capabilities: string[];
39
+ status: 'online' | 'offline' | 'busy';
40
+ endpoint?: string;
41
+ lastSeen: number;
42
+ }
43
+ export interface Task {
44
+ id: string;
45
+ status: 'pending' | 'assigned' | 'running' | 'completed' | 'failed';
46
+ request: AgentMessage;
47
+ assignedAgents: string[];
48
+ result?: unknown;
49
+ error?: Error;
50
+ createdAt: number;
51
+ updatedAt: number;
52
+ }
53
+ export interface RoutingRule {
54
+ name: string;
55
+ keywords: string[];
56
+ agent?: string;
57
+ strategy?: 'direct' | 'broadcast';
58
+ agents?: string[];
59
+ priority: number;
60
+ }
61
+ export interface GatewayConfig {
62
+ port: number;
63
+ wsPort: number;
64
+ host: string;
65
+ dataDir: string;
66
+ logDir: string;
67
+ routing: {
68
+ rules: RoutingRule[];
69
+ defaultAgent?: string;
70
+ };
71
+ }
@@ -0,0 +1 @@
1
+ // Agent Gateway 通信协议类型定义
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,35 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { ContextManager } from '../../src/core/context-manager.js';
3
+ describe('context manager', () => {
4
+ let cm;
5
+ beforeEach(() => {
6
+ cm = new ContextManager();
7
+ });
8
+ test('creates shared space with id', async () => {
9
+ const spaceId = await cm.createSharedSpace({ name: 'test' });
10
+ expect(spaceId).toBeDefined();
11
+ expect(typeof spaceId).toBe('string');
12
+ expect(spaceId.length).toBeGreaterThan(0);
13
+ });
14
+ test('retrieves created shared space', async () => {
15
+ const spaceId = await cm.createSharedSpace({ name: 'test' });
16
+ const space = await cm.getSharedSpace(spaceId);
17
+ expect(space).toBeDefined();
18
+ expect(space.shared_space_id).toBe(spaceId);
19
+ });
20
+ test('returns null for non-existent space', async () => {
21
+ const space = await cm.getSharedSpace('non-existent-id');
22
+ expect(space).toBeNull();
23
+ });
24
+ test('different calls create different space ids', async () => {
25
+ const id1 = await cm.createSharedSpace({});
26
+ const id2 = await cm.createSharedSpace({});
27
+ expect(id1).not.toBe(id2);
28
+ });
29
+ test('space stores metadata', async () => {
30
+ const metadata = { project: 'test', version: 1 };
31
+ const spaceId = await cm.createSharedSpace(metadata);
32
+ const space = await cm.getSharedSpace(spaceId);
33
+ expect(space.metadata).toEqual(metadata);
34
+ });
35
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,79 @@
1
+ import { describe, test, expect, beforeEach } from 'bun:test';
2
+ import { Router } from '../../src/core/router.js';
3
+ function makeAgent(id, caps, status = 'online') {
4
+ return { id, name: `Agent ${id}`, type: 'process', capabilities: caps, status, lastSeen: Date.now() };
5
+ }
6
+ describe('agent router', () => {
7
+ let router;
8
+ beforeEach(() => {
9
+ router = new Router();
10
+ });
11
+ test('registers and retrieves agents', () => {
12
+ router.registerAgent(makeAgent('test-agent', ['code-generation']));
13
+ expect(router.getAgent('test-agent').id).toBe('test-agent');
14
+ });
15
+ test('unregisters agents', () => {
16
+ router.registerAgent(makeAgent('test-agent', ['code-generation']));
17
+ router.unregisterAgent('test-agent');
18
+ expect(router.getAgent('test-agent')).toBeUndefined();
19
+ });
20
+ test('filters offline from online agents', () => {
21
+ router.registerAgent(makeAgent('a', ['x'], 'online'));
22
+ router.registerAgent(makeAgent('b', ['x'], 'offline'));
23
+ router.registerAgent(makeAgent('c', ['x'], 'online'));
24
+ expect(router.getOnlineAgents()).toHaveLength(2);
25
+ });
26
+ test('routes by keyword match', () => {
27
+ const rules = [
28
+ { name: 'review', keywords: ['review', 'code review'], agent: 'review-bot', priority: 15 },
29
+ { name: 'code', keywords: ['write code'], agent: 'code-bot', priority: 10 },
30
+ ];
31
+ router.configure(rules, 'fallback');
32
+ router.registerAgent(makeAgent('review-bot', ['review']));
33
+ router.registerAgent(makeAgent('code-bot', ['coding']));
34
+ router.registerAgent(makeAgent('fallback', ['general']));
35
+ const msg = {
36
+ id: '1', type: 'request', source: 'test', target: 'gateway',
37
+ correlation_id: '1', timestamp: Date.now(),
38
+ payload: { task: 'please review this code' },
39
+ };
40
+ const result = router.route(msg);
41
+ expect(result.agentIds).toContain('review-bot');
42
+ });
43
+ test('higher priority wins when keywords overlap', () => {
44
+ const rules = [
45
+ { name: 'low', keywords: ['code'], agent: 'low', priority: 5 },
46
+ { name: 'high', keywords: ['code'], agent: 'high', priority: 20 },
47
+ ];
48
+ router.configure(rules);
49
+ router.registerAgent(makeAgent('low', ['general']));
50
+ router.registerAgent(makeAgent('high', ['general']));
51
+ const msg = {
52
+ id: '1', type: 'request', source: 'test', target: 'gateway',
53
+ correlation_id: '1', timestamp: Date.now(),
54
+ payload: { task: 'help with code' },
55
+ };
56
+ expect(router.route(msg).agentIds).toContain('high');
57
+ });
58
+ test('falls back to default when no rule matches', () => {
59
+ router.configure([], 'default-bot');
60
+ router.registerAgent(makeAgent('default-bot', ['general']));
61
+ const msg = {
62
+ id: '1', type: 'request', source: 'test', target: 'gateway',
63
+ correlation_id: '1', timestamp: Date.now(),
64
+ payload: { task: 'something unrelated' },
65
+ };
66
+ expect(router.route(msg).agentIds).toContain('default-bot');
67
+ });
68
+ test('skips offline agents and uses fallback', () => {
69
+ router.configure([{ name: 'code', keywords: ['code'], agent: 'offline-bot', priority: 10 }], 'online-fallback');
70
+ router.registerAgent(makeAgent('offline-bot', ['coding'], 'offline'));
71
+ router.registerAgent(makeAgent('online-fallback', ['general'], 'online'));
72
+ const msg = {
73
+ id: '1', type: 'request', source: 'test', target: 'gateway',
74
+ correlation_id: '1', timestamp: Date.now(),
75
+ payload: { task: 'write code' },
76
+ };
77
+ expect(router.route(msg).agentIds).toContain('online-fallback');
78
+ });
79
+ });
@@ -0,0 +1 @@
1
+ export {};