@johpaz/hive-core 1.0.8 → 1.0.10
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/package.json +10 -9
- package/src/agent/ethics.ts +70 -68
- package/src/agent/index.ts +48 -17
- package/src/agent/providers/index.ts +11 -5
- package/src/agent/soul.ts +19 -15
- package/src/agent/user.ts +19 -15
- package/src/agent/workspace.ts +6 -6
- package/src/agents/index.ts +4 -0
- package/src/agents/inter-agent-bus.test.ts +264 -0
- package/src/agents/inter-agent-bus.ts +279 -0
- package/src/agents/registry.test.ts +275 -0
- package/src/agents/registry.ts +273 -0
- package/src/agents/router.test.ts +229 -0
- package/src/agents/router.ts +251 -0
- package/src/agents/team-coordinator.test.ts +401 -0
- package/src/agents/team-coordinator.ts +480 -0
- package/src/canvas/canvas-manager.test.ts +159 -0
- package/src/canvas/canvas-manager.ts +219 -0
- package/src/canvas/canvas-tools.ts +189 -0
- package/src/canvas/index.ts +2 -0
- package/src/channels/whatsapp.ts +12 -12
- package/src/config/loader.ts +12 -9
- package/src/events/event-bus.test.ts +98 -0
- package/src/events/event-bus.ts +171 -0
- package/src/gateway/server.ts +131 -35
- package/src/index.ts +9 -1
- package/src/multi-agent/manager.ts +12 -12
- package/src/plugins/api.ts +129 -0
- package/src/plugins/index.ts +2 -0
- package/src/plugins/loader.test.ts +285 -0
- package/src/plugins/loader.ts +363 -0
- package/src/resilience/circuit-breaker.test.ts +129 -0
- package/src/resilience/circuit-breaker.ts +223 -0
- package/src/security/google-chat.test.ts +219 -0
- package/src/security/google-chat.ts +269 -0
- package/src/security/index.ts +5 -0
- package/src/security/pairing.test.ts +302 -0
- package/src/security/pairing.ts +250 -0
- package/src/security/rate-limit.test.ts +239 -0
- package/src/security/rate-limit.ts +270 -0
- package/src/security/signal.test.ts +92 -0
- package/src/security/signal.ts +321 -0
- package/src/state/store.test.ts +190 -0
- package/src/state/store.ts +310 -0
- package/src/storage/sqlite.ts +3 -3
- package/src/tools/cron.ts +42 -2
- package/src/tools/dynamic-registry.test.ts +226 -0
- package/src/tools/dynamic-registry.ts +258 -0
- package/src/tools/fs.test.ts +127 -0
- package/src/tools/fs.ts +364 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/read.ts +23 -19
- package/src/utils/logger.ts +112 -33
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { AgentRegistry, type AgentDefinition, type Task } from "./registry";
|
|
3
|
+
|
|
4
|
+
describe("AgentRegistry", () => {
|
|
5
|
+
let registry: AgentRegistry;
|
|
6
|
+
|
|
7
|
+
const createAgent = (id: string, capabilities: string[], maxTasks = 3): AgentDefinition => ({
|
|
8
|
+
id,
|
|
9
|
+
name: `Agent ${id}`,
|
|
10
|
+
description: `Test agent ${id}`,
|
|
11
|
+
capabilities,
|
|
12
|
+
modelConfig: {
|
|
13
|
+
provider: "openai",
|
|
14
|
+
model: "gpt-4",
|
|
15
|
+
},
|
|
16
|
+
systemPrompt: "You are a helpful assistant",
|
|
17
|
+
maxConcurrentTasks: maxTasks,
|
|
18
|
+
currentLoad: 0,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const createTask = (id: string, capabilities: string[], priority: Task["priority"] = "normal"): Task => ({
|
|
22
|
+
id,
|
|
23
|
+
description: `Task ${id}`,
|
|
24
|
+
requiredCapabilities: capabilities,
|
|
25
|
+
priority,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
registry = new AgentRegistry();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("register", () => {
|
|
33
|
+
it("should register an agent", () => {
|
|
34
|
+
const agent = createAgent("agent-1", ["code", "test"]);
|
|
35
|
+
registry.register(agent);
|
|
36
|
+
|
|
37
|
+
expect(registry.get("agent-1")).toBeDefined();
|
|
38
|
+
expect(registry.get("agent-1")?.name).toBe("Agent agent-1");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should update existing agent", () => {
|
|
42
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
43
|
+
registry.register(createAgent("agent-1", ["code", "test", "deploy"]));
|
|
44
|
+
|
|
45
|
+
const agent = registry.get("agent-1");
|
|
46
|
+
expect(agent?.capabilities.length).toBe(3);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should index capabilities", () => {
|
|
50
|
+
registry.register(createAgent("agent-1", ["code", "test"]));
|
|
51
|
+
registry.register(createAgent("agent-2", ["test", "deploy"]));
|
|
52
|
+
|
|
53
|
+
const codeAgents = registry.getByCapability("code");
|
|
54
|
+
const testAgents = registry.getByCapability("test");
|
|
55
|
+
|
|
56
|
+
expect(codeAgents.length).toBe(1);
|
|
57
|
+
expect(testAgents.length).toBe(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("unregister", () => {
|
|
62
|
+
it("should unregister an agent", () => {
|
|
63
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
64
|
+
registry.unregister("agent-1");
|
|
65
|
+
|
|
66
|
+
expect(registry.get("agent-1")).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should clean up capability index", () => {
|
|
70
|
+
registry.register(createAgent("agent-1", ["code", "test"]));
|
|
71
|
+
registry.register(createAgent("agent-2", ["test"]));
|
|
72
|
+
registry.unregister("agent-1");
|
|
73
|
+
|
|
74
|
+
const testAgents = registry.getByCapability("test");
|
|
75
|
+
expect(testAgents.length).toBe(1);
|
|
76
|
+
expect(testAgents[0]?.id).toBe("agent-2");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should handle non-existent agent", () => {
|
|
80
|
+
expect(() => registry.unregister("non-existent")).not.toThrow();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getByCapability", () => {
|
|
85
|
+
it("should return agents with capability", () => {
|
|
86
|
+
registry.register(createAgent("agent-1", ["code", "test"]));
|
|
87
|
+
registry.register(createAgent("agent-2", ["test", "deploy"]));
|
|
88
|
+
registry.register(createAgent("agent-3", ["code"]));
|
|
89
|
+
|
|
90
|
+
const testAgents = registry.getByCapability("test");
|
|
91
|
+
expect(testAgents.length).toBe(2);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return empty array for unknown capability", () => {
|
|
95
|
+
expect(registry.getByCapability("unknown")).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("findBestAgentForTask", () => {
|
|
100
|
+
it("should find agent with matching capabilities", () => {
|
|
101
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
102
|
+
registry.register(createAgent("agent-2", ["test", "deploy"]));
|
|
103
|
+
|
|
104
|
+
const task = createTask("task-1", ["test"]);
|
|
105
|
+
const agent = registry.findBestAgentForTask(task);
|
|
106
|
+
|
|
107
|
+
expect(agent?.id).toBe("agent-2");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should prefer agent with all capabilities", () => {
|
|
111
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
112
|
+
registry.register(createAgent("agent-2", ["code", "test", "deploy"]));
|
|
113
|
+
|
|
114
|
+
const task = createTask("task-1", ["code", "test"]);
|
|
115
|
+
const agent = registry.findBestAgentForTask(task);
|
|
116
|
+
|
|
117
|
+
expect(agent?.id).toBe("agent-2");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should consider load when selecting", () => {
|
|
121
|
+
registry.register(createAgent("agent-1", ["code"], 3));
|
|
122
|
+
registry.register(createAgent("agent-2", ["code"], 3));
|
|
123
|
+
|
|
124
|
+
registry.setLoad("agent-1", 2);
|
|
125
|
+
registry.setLoad("agent-2", 0);
|
|
126
|
+
|
|
127
|
+
const task = createTask("task-1", ["code"]);
|
|
128
|
+
const agent = registry.findBestAgentForTask(task);
|
|
129
|
+
|
|
130
|
+
expect(agent?.id).toBe("agent-2");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should return least loaded when all at capacity", () => {
|
|
134
|
+
registry.register(createAgent("agent-1", ["code"], 3));
|
|
135
|
+
registry.register(createAgent("agent-2", ["code"], 3));
|
|
136
|
+
|
|
137
|
+
registry.setLoad("agent-1", 3);
|
|
138
|
+
registry.setLoad("agent-2", 3);
|
|
139
|
+
|
|
140
|
+
const task = createTask("task-1", ["code"]);
|
|
141
|
+
const agent = registry.findBestAgentForTask(task);
|
|
142
|
+
|
|
143
|
+
expect(agent).toBeDefined();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should return undefined when no agents available", () => {
|
|
147
|
+
const task = createTask("task-1", ["unknown"]);
|
|
148
|
+
expect(registry.findBestAgentForTask(task)).toBeUndefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should handle empty capabilities", () => {
|
|
152
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
153
|
+
const task = createTask("task-1", []);
|
|
154
|
+
|
|
155
|
+
const agent = registry.findBestAgentForTask(task);
|
|
156
|
+
expect(agent).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("findTeamForTask", () => {
|
|
161
|
+
it("should find team for multiple capabilities", () => {
|
|
162
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
163
|
+
registry.register(createAgent("agent-2", ["test"]));
|
|
164
|
+
registry.register(createAgent("agent-3", ["deploy"]));
|
|
165
|
+
|
|
166
|
+
const task = createTask("task-1", ["code", "test", "deploy"]);
|
|
167
|
+
const team = registry.findTeamForTask(task);
|
|
168
|
+
|
|
169
|
+
expect(team.length).toBe(3);
|
|
170
|
+
expect(team.map(a => a.id)).toContain("agent-1");
|
|
171
|
+
expect(team.map(a => a.id)).toContain("agent-2");
|
|
172
|
+
expect(team.map(a => a.id)).toContain("agent-3");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should not duplicate agents", () => {
|
|
176
|
+
registry.register(createAgent("agent-1", ["code", "test", "deploy"]));
|
|
177
|
+
|
|
178
|
+
const task = createTask("task-1", ["code", "test"]);
|
|
179
|
+
const team = registry.findTeamForTask(task);
|
|
180
|
+
|
|
181
|
+
expect(team.length).toBe(1);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should prefer available agents", () => {
|
|
185
|
+
registry.register(createAgent("agent-1", ["code"], 1));
|
|
186
|
+
registry.register(createAgent("agent-2", ["code"], 3));
|
|
187
|
+
|
|
188
|
+
registry.setLoad("agent-1", 1);
|
|
189
|
+
|
|
190
|
+
const task = createTask("task-1", ["code"]);
|
|
191
|
+
const team = registry.findTeamForTask(task);
|
|
192
|
+
|
|
193
|
+
expect(team[0]?.id).toBe("agent-2");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should skip capabilities with no agents", () => {
|
|
197
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
198
|
+
|
|
199
|
+
const task = createTask("task-1", ["code", "nonexistent"]);
|
|
200
|
+
const team = registry.findTeamForTask(task);
|
|
201
|
+
|
|
202
|
+
expect(team.length).toBe(1);
|
|
203
|
+
expect(team[0]?.id).toBe("agent-1");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("load management", () => {
|
|
208
|
+
it("should increment load", () => {
|
|
209
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
210
|
+
registry.incrementLoad("agent-1");
|
|
211
|
+
|
|
212
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(1);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should decrement load", () => {
|
|
216
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
217
|
+
registry.setLoad("agent-1", 2);
|
|
218
|
+
registry.decrementLoad("agent-1");
|
|
219
|
+
|
|
220
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("should not go below zero", () => {
|
|
224
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
225
|
+
registry.decrementLoad("agent-1");
|
|
226
|
+
|
|
227
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("should set load", () => {
|
|
231
|
+
registry.register(createAgent("agent-1", ["code"], 5));
|
|
232
|
+
registry.setLoad("agent-1", 3);
|
|
233
|
+
|
|
234
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(3);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("should clamp load to valid range", () => {
|
|
238
|
+
registry.register(createAgent("agent-1", ["code"], 5));
|
|
239
|
+
registry.setLoad("agent-1", 10);
|
|
240
|
+
|
|
241
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(5);
|
|
242
|
+
|
|
243
|
+
registry.setLoad("agent-1", -1);
|
|
244
|
+
expect(registry.get("agent-1")?.currentLoad).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe("getStats", () => {
|
|
249
|
+
it("should return correct stats", () => {
|
|
250
|
+
registry.register(createAgent("agent-1", ["code", "test"], 3));
|
|
251
|
+
registry.register(createAgent("agent-2", ["test"], 2));
|
|
252
|
+
registry.setLoad("agent-1", 1);
|
|
253
|
+
|
|
254
|
+
const stats = registry.getStats();
|
|
255
|
+
|
|
256
|
+
expect(stats.totalAgents).toBe(2);
|
|
257
|
+
expect(stats.byCapability["code"]).toBe(1);
|
|
258
|
+
expect(stats.byCapability["test"]).toBe(2);
|
|
259
|
+
expect(stats.totalCapacity).toBe(5);
|
|
260
|
+
expect(stats.currentLoad).toBe(1);
|
|
261
|
+
expect(stats.availableSlots).toBe(4);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("clear", () => {
|
|
266
|
+
it("should clear all agents", () => {
|
|
267
|
+
registry.register(createAgent("agent-1", ["code"]));
|
|
268
|
+
registry.register(createAgent("agent-2", ["test"]));
|
|
269
|
+
registry.clear();
|
|
270
|
+
|
|
271
|
+
expect(registry.getAll().length).toBe(0);
|
|
272
|
+
expect(registry.getByCapability("code").length).toBe(0);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.ts";
|
|
2
|
+
|
|
3
|
+
export interface ModelConfig {
|
|
4
|
+
provider: string;
|
|
5
|
+
model: string;
|
|
6
|
+
temperature?: number;
|
|
7
|
+
maxTokens?: number;
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AgentDefinition {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
capabilities: string[];
|
|
16
|
+
modelConfig: ModelConfig;
|
|
17
|
+
systemPrompt: string;
|
|
18
|
+
maxConcurrentTasks: number;
|
|
19
|
+
currentLoad: number;
|
|
20
|
+
priority?: number;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
metadata?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface Task {
|
|
26
|
+
id: string;
|
|
27
|
+
description: string;
|
|
28
|
+
requiredCapabilities: string[];
|
|
29
|
+
priority: "low" | "normal" | "high" | "critical";
|
|
30
|
+
payload?: Record<string, unknown>;
|
|
31
|
+
requiresTeam?: boolean;
|
|
32
|
+
maxAgents?: number;
|
|
33
|
+
deadline?: number;
|
|
34
|
+
parentTaskId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TaskResult {
|
|
38
|
+
taskId: string;
|
|
39
|
+
agentId: string;
|
|
40
|
+
status: "success" | "partial" | "failed";
|
|
41
|
+
output: unknown;
|
|
42
|
+
duration: number;
|
|
43
|
+
subtasks?: TaskResult[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface AgentStats {
|
|
47
|
+
totalAgents: number;
|
|
48
|
+
byCapability: Record<string, number>;
|
|
49
|
+
totalCapacity: number;
|
|
50
|
+
currentLoad: number;
|
|
51
|
+
availableSlots: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class AgentRegistry {
|
|
55
|
+
private agents: Map<string, AgentDefinition> = new Map();
|
|
56
|
+
private capabilities: Map<string, Set<string>> = new Map();
|
|
57
|
+
private log = logger.child("agent-registry");
|
|
58
|
+
|
|
59
|
+
register(agent: AgentDefinition): void {
|
|
60
|
+
if (this.agents.has(agent.id)) {
|
|
61
|
+
this.log.warn(`Agent ${agent.id} already registered, updating`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.agents.set(agent.id, { ...agent, currentLoad: agent.currentLoad ?? 0 });
|
|
65
|
+
|
|
66
|
+
for (const cap of agent.capabilities) {
|
|
67
|
+
if (!this.capabilities.has(cap)) {
|
|
68
|
+
this.capabilities.set(cap, new Set());
|
|
69
|
+
}
|
|
70
|
+
this.capabilities.get(cap)!.add(agent.id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.log.info(`Agent registered: ${agent.id}`, {
|
|
74
|
+
name: agent.name,
|
|
75
|
+
capabilities: agent.capabilities,
|
|
76
|
+
maxTasks: agent.maxConcurrentTasks,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unregister(agentId: string): void {
|
|
81
|
+
const agent = this.agents.get(agentId);
|
|
82
|
+
if (!agent) {
|
|
83
|
+
this.log.warn(`Agent ${agentId} not found`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const cap of agent.capabilities) {
|
|
88
|
+
this.capabilities.get(cap)?.delete(agentId);
|
|
89
|
+
if (this.capabilities.get(cap)?.size === 0) {
|
|
90
|
+
this.capabilities.delete(cap);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.agents.delete(agentId);
|
|
95
|
+
this.log.info(`Agent unregistered: ${agentId}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get(agentId: string): AgentDefinition | undefined {
|
|
99
|
+
return this.agents.get(agentId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getAll(): AgentDefinition[] {
|
|
103
|
+
return Array.from(this.agents.values());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getByCapability(capability: string): AgentDefinition[] {
|
|
107
|
+
const agentIds = this.capabilities.get(capability);
|
|
108
|
+
if (!agentIds) return [];
|
|
109
|
+
|
|
110
|
+
return Array.from(agentIds)
|
|
111
|
+
.map((id) => this.agents.get(id))
|
|
112
|
+
.filter((a): a is AgentDefinition => !!a);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
findBestAgentForTask(task: Task): AgentDefinition | undefined {
|
|
116
|
+
const candidates = this.findCandidates(task.requiredCapabilities);
|
|
117
|
+
|
|
118
|
+
if (candidates.length === 0) {
|
|
119
|
+
this.log.warn(`No agents found for capabilities: ${task.requiredCapabilities.join(", ")}`);
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const available = candidates.filter(
|
|
124
|
+
(a) => a.currentLoad < a.maxConcurrentTasks
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (available.length === 0) {
|
|
128
|
+
this.log.warn(`All agents at capacity for task ${task.id}`);
|
|
129
|
+
return candidates.reduce((best, curr) =>
|
|
130
|
+
curr.currentLoad < best.currentLoad ? curr : best
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scored = available
|
|
135
|
+
.map((agent) => ({
|
|
136
|
+
agent,
|
|
137
|
+
score: this.calculateScore(agent, task),
|
|
138
|
+
}))
|
|
139
|
+
.sort((a, b) => {
|
|
140
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
141
|
+
return a.agent.currentLoad - b.agent.currentLoad;
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return scored[0]?.agent;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
findTeamForTask(task: Task): AgentDefinition[] {
|
|
148
|
+
const uniqueCapabilities = [...new Set(task.requiredCapabilities)];
|
|
149
|
+
const team: AgentDefinition[] = [];
|
|
150
|
+
const usedAgents = new Set<string>();
|
|
151
|
+
|
|
152
|
+
for (const capability of uniqueCapabilities) {
|
|
153
|
+
const candidates = this.getByCapability(capability)
|
|
154
|
+
.filter((a) => !usedAgents.has(a.id) && a.currentLoad < a.maxConcurrentTasks);
|
|
155
|
+
|
|
156
|
+
if (candidates.length === 0) {
|
|
157
|
+
const anyAvailable = this.getByCapability(capability)
|
|
158
|
+
.filter((a) => !usedAgents.has(a.id));
|
|
159
|
+
if (anyAvailable.length === 0) {
|
|
160
|
+
this.log.warn(`No agent available for capability: ${capability}`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const best = anyAvailable.reduce((best, curr) =>
|
|
164
|
+
curr.currentLoad < best.currentLoad ? curr : best
|
|
165
|
+
);
|
|
166
|
+
team.push(best);
|
|
167
|
+
usedAgents.add(best.id);
|
|
168
|
+
} else {
|
|
169
|
+
const best = candidates.reduce((best, curr) =>
|
|
170
|
+
curr.currentLoad < best.currentLoad ? curr : best
|
|
171
|
+
);
|
|
172
|
+
team.push(best);
|
|
173
|
+
usedAgents.add(best.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return team;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
incrementLoad(agentId: string): void {
|
|
181
|
+
const agent = this.agents.get(agentId);
|
|
182
|
+
if (agent) {
|
|
183
|
+
agent.currentLoad++;
|
|
184
|
+
this.log.debug(`Agent ${agentId} load increased to ${agent.currentLoad}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
decrementLoad(agentId: string): void {
|
|
189
|
+
const agent = this.agents.get(agentId);
|
|
190
|
+
if (agent && agent.currentLoad > 0) {
|
|
191
|
+
agent.currentLoad--;
|
|
192
|
+
this.log.debug(`Agent ${agentId} load decreased to ${agent.currentLoad}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setLoad(agentId: string, load: number): void {
|
|
197
|
+
const agent = this.agents.get(agentId);
|
|
198
|
+
if (agent) {
|
|
199
|
+
agent.currentLoad = Math.max(0, Math.min(load, agent.maxConcurrentTasks));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
getStats(): AgentStats {
|
|
204
|
+
const byCapability: Record<string, number> = {};
|
|
205
|
+
|
|
206
|
+
for (const [cap, agents] of this.capabilities) {
|
|
207
|
+
byCapability[cap] = agents.size;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const agents = Array.from(this.agents.values());
|
|
211
|
+
const totalCapacity = agents.reduce((sum, a) => sum + a.maxConcurrentTasks, 0);
|
|
212
|
+
const currentLoad = agents.reduce((sum, a) => sum + a.currentLoad, 0);
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
totalAgents: this.agents.size,
|
|
216
|
+
byCapability,
|
|
217
|
+
totalCapacity,
|
|
218
|
+
currentLoad,
|
|
219
|
+
availableSlots: totalCapacity - currentLoad,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
clear(): void {
|
|
224
|
+
this.agents.clear();
|
|
225
|
+
this.capabilities.clear();
|
|
226
|
+
this.log.info("All agents cleared");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private findCandidates(capabilities: string[]): AgentDefinition[] {
|
|
230
|
+
if (capabilities.length === 0) {
|
|
231
|
+
return this.getAll();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const agentMatchCount = new Map<string, number>();
|
|
235
|
+
|
|
236
|
+
for (const cap of capabilities) {
|
|
237
|
+
const agentIds = this.capabilities.get(cap);
|
|
238
|
+
if (agentIds) {
|
|
239
|
+
for (const id of agentIds) {
|
|
240
|
+
agentMatchCount.set(id, (agentMatchCount.get(id) ?? 0) + 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const sortedIds = Array.from(agentMatchCount.entries())
|
|
246
|
+
.sort((a, b) => b[1] - a[1])
|
|
247
|
+
.map(([id]) => id);
|
|
248
|
+
|
|
249
|
+
return sortedIds
|
|
250
|
+
.map((id) => this.agents.get(id))
|
|
251
|
+
.filter((a): a is AgentDefinition => !!a);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private calculateScore(agent: AgentDefinition, task: Task): number {
|
|
255
|
+
const matchedCapabilities = task.requiredCapabilities.filter((c) =>
|
|
256
|
+
agent.capabilities.includes(c)
|
|
257
|
+
).length;
|
|
258
|
+
|
|
259
|
+
const capabilityScore = (matchedCapabilities / task.requiredCapabilities.length) * 100;
|
|
260
|
+
|
|
261
|
+
const loadScore = (1 - agent.currentLoad / agent.maxConcurrentTasks) * 50;
|
|
262
|
+
|
|
263
|
+
const priorityScore = task.priority === "critical" ? 20 :
|
|
264
|
+
task.priority === "high" ? 10 :
|
|
265
|
+
task.priority === "normal" ? 5 : 0;
|
|
266
|
+
|
|
267
|
+
const agentPriorityScore = (agent.priority ?? 5) * 2;
|
|
268
|
+
|
|
269
|
+
return capabilityScore + loadScore + priorityScore + agentPriorityScore;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export const agentRegistry = new AgentRegistry();
|