@johpaz/hive-core 1.0.7 → 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,480 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.ts";
|
|
2
|
+
import { eventBus } from "../events/event-bus.ts";
|
|
3
|
+
import type { AgentRegistry, AgentDefinition, Task, TaskResult } from "./registry.ts";
|
|
4
|
+
import type { InterAgentBus } from "./inter-agent-bus.ts";
|
|
5
|
+
|
|
6
|
+
export type CoordinationMode = "normal" | "delegate" | "collaborative" | "hierarchical";
|
|
7
|
+
|
|
8
|
+
export interface TeamConfig {
|
|
9
|
+
name: string;
|
|
10
|
+
leadId: string;
|
|
11
|
+
memberIds: string[];
|
|
12
|
+
coordinationMode: CoordinationMode;
|
|
13
|
+
sharedContext: boolean;
|
|
14
|
+
maxParallelTasks?: number;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TeamState {
|
|
19
|
+
name: string;
|
|
20
|
+
status: "idle" | "working" | "waiting" | "error";
|
|
21
|
+
currentTaskId?: string;
|
|
22
|
+
activeMembers: string[];
|
|
23
|
+
completedSteps: number;
|
|
24
|
+
totalSteps: number;
|
|
25
|
+
startTime?: number;
|
|
26
|
+
sharedState: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Plan {
|
|
30
|
+
id: string;
|
|
31
|
+
taskId: string;
|
|
32
|
+
steps: PlanStep[];
|
|
33
|
+
createdAt: number;
|
|
34
|
+
approved: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PlanStep {
|
|
38
|
+
id: string;
|
|
39
|
+
assignedTo: string;
|
|
40
|
+
capability: string;
|
|
41
|
+
description: string;
|
|
42
|
+
dependencies: string[];
|
|
43
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
44
|
+
result?: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface TeamStats {
|
|
48
|
+
totalTeams: number;
|
|
49
|
+
activeTeams: number;
|
|
50
|
+
byMode: Record<CoordinationMode, number>;
|
|
51
|
+
tasksCompleted: number;
|
|
52
|
+
averageDuration: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class TeamCoordinator {
|
|
56
|
+
private teams: Map<string, TeamConfig> = new Map();
|
|
57
|
+
private teamStates: Map<string, TeamState> = new Map();
|
|
58
|
+
private plans: Map<string, Plan> = new Map();
|
|
59
|
+
private tasksCompleted = 0;
|
|
60
|
+
private totalDuration = 0;
|
|
61
|
+
private log = logger.child("team-coordinator");
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private registry: AgentRegistry,
|
|
65
|
+
private bus: InterAgentBus
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
createTeam(config: TeamConfig): void {
|
|
69
|
+
if (this.teams.has(config.name)) {
|
|
70
|
+
this.log.warn(`Team ${config.name} already exists, updating`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.teams.set(config.name, config);
|
|
74
|
+
this.teamStates.set(config.name, {
|
|
75
|
+
name: config.name,
|
|
76
|
+
status: "idle",
|
|
77
|
+
activeMembers: [],
|
|
78
|
+
completedSteps: 0,
|
|
79
|
+
totalSteps: 0,
|
|
80
|
+
sharedState: {},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.log.info(`Team created: ${config.name}`, {
|
|
84
|
+
lead: config.leadId,
|
|
85
|
+
members: config.memberIds,
|
|
86
|
+
mode: config.coordinationMode,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
removeTeam(name: string): void {
|
|
91
|
+
const state = this.teamStates.get(name);
|
|
92
|
+
if (state?.status === "working") {
|
|
93
|
+
this.log.warn(`Cannot remove team ${name} while working`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.teams.delete(name);
|
|
98
|
+
this.teamStates.delete(name);
|
|
99
|
+
this.log.info(`Team removed: ${name}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getTeam(name: string): TeamConfig | undefined {
|
|
103
|
+
return this.teams.get(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getTeamState(name: string): TeamState | undefined {
|
|
107
|
+
return this.teamStates.get(name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async coordinateTask(
|
|
111
|
+
teamName: string,
|
|
112
|
+
task: Task,
|
|
113
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
114
|
+
): Promise<TaskResult> {
|
|
115
|
+
const team = this.teams.get(teamName);
|
|
116
|
+
if (!team) {
|
|
117
|
+
throw new Error(`Team not found: ${teamName}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const state = this.teamStates.get(teamName)!;
|
|
121
|
+
state.status = "working";
|
|
122
|
+
state.currentTaskId = task.id;
|
|
123
|
+
state.startTime = Date.now();
|
|
124
|
+
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
|
|
127
|
+
this.log.info(`Coordinating task ${task.id} with team ${teamName}`, {
|
|
128
|
+
mode: team.coordinationMode,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
let result: TaskResult;
|
|
133
|
+
|
|
134
|
+
switch (team.coordinationMode) {
|
|
135
|
+
case "delegate":
|
|
136
|
+
result = await this.delegateExecute(team, task, executor);
|
|
137
|
+
break;
|
|
138
|
+
case "collaborative":
|
|
139
|
+
result = await this.collaborativeExecute(team, task, executor);
|
|
140
|
+
break;
|
|
141
|
+
case "hierarchical":
|
|
142
|
+
result = await this.hierarchicalExecute(team, task, executor);
|
|
143
|
+
break;
|
|
144
|
+
default:
|
|
145
|
+
result = await this.normalExecute(team, task, executor);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const duration = Date.now() - startTime;
|
|
149
|
+
this.tasksCompleted++;
|
|
150
|
+
this.totalDuration += duration;
|
|
151
|
+
|
|
152
|
+
state.status = "idle";
|
|
153
|
+
state.currentTaskId = undefined;
|
|
154
|
+
state.completedSteps = state.totalSteps;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
...result,
|
|
158
|
+
duration,
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
state.status = "error";
|
|
162
|
+
this.log.error(`Team ${teamName} failed on task ${task.id}`, {
|
|
163
|
+
error: (error as Error).message,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
taskId: task.id,
|
|
168
|
+
agentId: team.leadId,
|
|
169
|
+
status: "failed",
|
|
170
|
+
output: { error: (error as Error).message },
|
|
171
|
+
duration: Date.now() - startTime,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async normalExecute(
|
|
177
|
+
team: TeamConfig,
|
|
178
|
+
task: Task,
|
|
179
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
180
|
+
): Promise<TaskResult> {
|
|
181
|
+
const lead = this.registry.get(team.leadId);
|
|
182
|
+
if (!lead) throw new Error(`Lead agent not found: ${team.leadId}`);
|
|
183
|
+
|
|
184
|
+
this.registry.incrementLoad(lead.id);
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const output = await executor(lead, task);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
taskId: task.id,
|
|
191
|
+
agentId: lead.id,
|
|
192
|
+
status: "success",
|
|
193
|
+
output,
|
|
194
|
+
duration: 0,
|
|
195
|
+
};
|
|
196
|
+
} finally {
|
|
197
|
+
this.registry.decrementLoad(lead.id);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async delegateExecute(
|
|
202
|
+
team: TeamConfig,
|
|
203
|
+
task: Task,
|
|
204
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
205
|
+
): Promise<TaskResult> {
|
|
206
|
+
const lead = this.registry.get(team.leadId);
|
|
207
|
+
if (!lead) throw new Error(`Lead agent not found: ${team.leadId}`);
|
|
208
|
+
|
|
209
|
+
const plan = await this.createPlan(lead, task);
|
|
210
|
+
const approved = await this.approvePlan(team.name, plan);
|
|
211
|
+
|
|
212
|
+
if (!approved) {
|
|
213
|
+
throw new Error("Plan not approved");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return this.executePlan(team, plan, executor);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async collaborativeExecute(
|
|
220
|
+
team: TeamConfig,
|
|
221
|
+
task: Task,
|
|
222
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
223
|
+
): Promise<TaskResult> {
|
|
224
|
+
const members = [team.leadId, ...team.memberIds]
|
|
225
|
+
.map((id) => this.registry.get(id))
|
|
226
|
+
.filter((a): a is AgentDefinition => !!a);
|
|
227
|
+
|
|
228
|
+
const results: { agentId: string; result: unknown }[] = [];
|
|
229
|
+
|
|
230
|
+
for (const member of members) {
|
|
231
|
+
this.registry.incrementLoad(member.id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const maxParallel = team.maxParallelTasks ?? members.length;
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < members.length; i += maxParallel) {
|
|
238
|
+
const batch = members.slice(i, i + maxParallel);
|
|
239
|
+
|
|
240
|
+
const batchResults = await Promise.all(
|
|
241
|
+
batch.map(async (member) => {
|
|
242
|
+
const result = await executor(member, task);
|
|
243
|
+
return { agentId: member.id, result };
|
|
244
|
+
})
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
results.push(...batchResults);
|
|
248
|
+
|
|
249
|
+
if (team.sharedContext) {
|
|
250
|
+
this.updateSharedState(team.name, batchResults);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
taskId: task.id,
|
|
256
|
+
agentId: team.leadId,
|
|
257
|
+
status: "success",
|
|
258
|
+
output: {
|
|
259
|
+
collaborative: true,
|
|
260
|
+
contributions: results,
|
|
261
|
+
},
|
|
262
|
+
duration: 0,
|
|
263
|
+
};
|
|
264
|
+
} finally {
|
|
265
|
+
for (const member of members) {
|
|
266
|
+
this.registry.decrementLoad(member.id);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async hierarchicalExecute(
|
|
272
|
+
team: TeamConfig,
|
|
273
|
+
task: Task,
|
|
274
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
275
|
+
): Promise<TaskResult> {
|
|
276
|
+
const lead = this.registry.get(team.leadId);
|
|
277
|
+
if (!lead) throw new Error(`Lead agent not found: ${team.leadId}`);
|
|
278
|
+
|
|
279
|
+
const decomposition = await this.hierarchicalDecompose(lead, task);
|
|
280
|
+
|
|
281
|
+
const results: TaskResult[] = [];
|
|
282
|
+
|
|
283
|
+
for (const subtask of decomposition.subtasks) {
|
|
284
|
+
const agent = this.registry.findBestAgentForTask(subtask);
|
|
285
|
+
if (!agent) continue;
|
|
286
|
+
|
|
287
|
+
this.registry.incrementLoad(agent.id);
|
|
288
|
+
try {
|
|
289
|
+
const output = await executor(agent, subtask);
|
|
290
|
+
results.push({
|
|
291
|
+
taskId: subtask.id,
|
|
292
|
+
agentId: agent.id,
|
|
293
|
+
status: "success",
|
|
294
|
+
output,
|
|
295
|
+
duration: 0,
|
|
296
|
+
});
|
|
297
|
+
} finally {
|
|
298
|
+
this.registry.decrementLoad(agent.id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const synthesized = await this.synthesize(lead, results, executor);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
taskId: task.id,
|
|
306
|
+
agentId: lead.id,
|
|
307
|
+
status: results.every((r) => r.status === "success") ? "success" : "partial",
|
|
308
|
+
output: synthesized,
|
|
309
|
+
duration: 0,
|
|
310
|
+
subtasks: results,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async createPlan(lead: AgentDefinition, task: Task): Promise<Plan> {
|
|
315
|
+
const steps: PlanStep[] = task.requiredCapabilities.map((cap, i) => ({
|
|
316
|
+
id: `${task.id}-step-${i}`,
|
|
317
|
+
assignedTo: lead.id,
|
|
318
|
+
capability: cap,
|
|
319
|
+
description: `Execute ${cap} for task ${task.id}`,
|
|
320
|
+
dependencies: i > 0 ? [`${task.id}-step-${i - 1}`] : [],
|
|
321
|
+
status: "pending" as const,
|
|
322
|
+
}));
|
|
323
|
+
|
|
324
|
+
const plan: Plan = {
|
|
325
|
+
id: crypto.randomUUID(),
|
|
326
|
+
taskId: task.id,
|
|
327
|
+
steps,
|
|
328
|
+
createdAt: Date.now(),
|
|
329
|
+
approved: false,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
this.plans.set(plan.id, plan);
|
|
333
|
+
return plan;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private async approvePlan(teamName: string, plan: Plan): Promise<boolean> {
|
|
337
|
+
plan.approved = true;
|
|
338
|
+
this.log.info(`Plan ${plan.id} approved for team ${teamName}`);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async executePlan(
|
|
343
|
+
team: TeamConfig,
|
|
344
|
+
plan: Plan,
|
|
345
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
346
|
+
): Promise<TaskResult> {
|
|
347
|
+
const state = this.teamStates.get(team.name)!;
|
|
348
|
+
state.totalSteps = plan.steps.length;
|
|
349
|
+
state.completedSteps = 0;
|
|
350
|
+
|
|
351
|
+
const results: { stepId: string; result: unknown }[] = [];
|
|
352
|
+
|
|
353
|
+
for (const step of plan.steps) {
|
|
354
|
+
const agent = this.registry.get(step.assignedTo);
|
|
355
|
+
if (!agent) continue;
|
|
356
|
+
|
|
357
|
+
step.status = "running";
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const result = await executor(agent, {
|
|
361
|
+
id: step.id,
|
|
362
|
+
description: step.description,
|
|
363
|
+
requiredCapabilities: [step.capability],
|
|
364
|
+
priority: "normal",
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
step.status = "completed";
|
|
368
|
+
step.result = result;
|
|
369
|
+
results.push({ stepId: step.id, result });
|
|
370
|
+
state.completedSteps++;
|
|
371
|
+
|
|
372
|
+
if (team.sharedContext) {
|
|
373
|
+
this.updateSharedState(team.name, [{ agentId: agent.id, result }]);
|
|
374
|
+
}
|
|
375
|
+
} catch (error) {
|
|
376
|
+
step.status = "failed";
|
|
377
|
+
this.log.error(`Step ${step.id} failed`, { error: (error as Error).message });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
taskId: plan.taskId,
|
|
383
|
+
agentId: team.leadId,
|
|
384
|
+
status: plan.steps.every((s) => s.status === "completed") ? "success" : "partial",
|
|
385
|
+
output: { plan: plan.id, results },
|
|
386
|
+
duration: 0,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async hierarchicalDecompose(
|
|
391
|
+
lead: AgentDefinition,
|
|
392
|
+
task: Task
|
|
393
|
+
): Promise<{ subtasks: Task[] }> {
|
|
394
|
+
const subtasks: Task[] = [];
|
|
395
|
+
|
|
396
|
+
for (let i = 0; i < task.requiredCapabilities.length; i++) {
|
|
397
|
+
const capability = task.requiredCapabilities[i]!;
|
|
398
|
+
subtasks.push({
|
|
399
|
+
id: `${task.id}-h${i}`,
|
|
400
|
+
description: `Hierarchical subtask for ${capability}`,
|
|
401
|
+
requiredCapabilities: [capability],
|
|
402
|
+
priority: task.priority,
|
|
403
|
+
parentTaskId: task.id,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { subtasks };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async synthesize(
|
|
411
|
+
lead: AgentDefinition,
|
|
412
|
+
results: TaskResult[],
|
|
413
|
+
executor: (agent: AgentDefinition, task: Task) => Promise<unknown>
|
|
414
|
+
): Promise<unknown> {
|
|
415
|
+
if (results.length === 0) return null;
|
|
416
|
+
if (results.length === 1) return results[0]!.output;
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
synthesized: true,
|
|
420
|
+
byAgent: results.map((r) => ({ agentId: r.agentId, output: r.output })),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private updateSharedState(
|
|
425
|
+
teamName: string,
|
|
426
|
+
results: { agentId: string; result: unknown }[]
|
|
427
|
+
): void {
|
|
428
|
+
const state = this.teamStates.get(teamName);
|
|
429
|
+
if (!state) return;
|
|
430
|
+
|
|
431
|
+
for (const { agentId, result } of results) {
|
|
432
|
+
state.sharedState[agentId] = result;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getStats(): TeamStats {
|
|
437
|
+
const byMode: Record<CoordinationMode, number> = {
|
|
438
|
+
normal: 0,
|
|
439
|
+
delegate: 0,
|
|
440
|
+
collaborative: 0,
|
|
441
|
+
hierarchical: 0,
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
let activeTeams = 0;
|
|
445
|
+
|
|
446
|
+
for (const [name, config] of this.teams) {
|
|
447
|
+
byMode[config.coordinationMode]++;
|
|
448
|
+
const state = this.teamStates.get(name);
|
|
449
|
+
if (state?.status === "working") activeTeams++;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
totalTeams: this.teams.size,
|
|
454
|
+
activeTeams,
|
|
455
|
+
byMode,
|
|
456
|
+
tasksCompleted: this.tasksCompleted,
|
|
457
|
+
averageDuration: this.tasksCompleted > 0
|
|
458
|
+
? this.totalDuration / this.tasksCompleted
|
|
459
|
+
: 0,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
listTeams(): TeamConfig[] {
|
|
464
|
+
return Array.from(this.teams.values());
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
clear(): void {
|
|
468
|
+
this.teams.clear();
|
|
469
|
+
this.teamStates.clear();
|
|
470
|
+
this.plans.clear();
|
|
471
|
+
this.tasksCompleted = 0;
|
|
472
|
+
this.totalDuration = 0;
|
|
473
|
+
this.log.info("All teams cleared");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const teamCoordinator = (
|
|
478
|
+
registry: AgentRegistry,
|
|
479
|
+
bus: InterAgentBus
|
|
480
|
+
) => new TeamCoordinator(registry, bus);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "bun:test";
|
|
2
|
+
import { CanvasManager, WebSocketState, type WebSocketLike } from "./canvas-manager.ts";
|
|
3
|
+
|
|
4
|
+
describe("CanvasManager", () => {
|
|
5
|
+
let manager: CanvasManager;
|
|
6
|
+
|
|
7
|
+
const createMockWebSocket = (): WebSocketLike & { messages: string[] } => {
|
|
8
|
+
const messages: string[] = [];
|
|
9
|
+
const handlers: Map<string, (data: unknown) => void> = new Map();
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
readyState: WebSocketState.OPEN,
|
|
13
|
+
send: (data: string) => {
|
|
14
|
+
messages.push(data);
|
|
15
|
+
},
|
|
16
|
+
on: (event: string, callback: (data: unknown) => void) => {
|
|
17
|
+
handlers.set(event, callback);
|
|
18
|
+
},
|
|
19
|
+
messages,
|
|
20
|
+
} as WebSocketLike & { messages: string[] };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
manager = new CanvasManager();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("registerSession", () => {
|
|
28
|
+
it("should register a session", () => {
|
|
29
|
+
const ws = createMockWebSocket();
|
|
30
|
+
manager.registerSession("session-1", ws);
|
|
31
|
+
|
|
32
|
+
expect(manager.isSessionConnected("session-1")).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should track session stats", () => {
|
|
36
|
+
const ws = createMockWebSocket();
|
|
37
|
+
manager.registerSession("session-1", ws);
|
|
38
|
+
|
|
39
|
+
const stats = manager.getStats();
|
|
40
|
+
expect(stats.totalSessions).toBe(1);
|
|
41
|
+
expect(stats.activeSessions).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("render", () => {
|
|
46
|
+
it("should send render message to session", async () => {
|
|
47
|
+
const ws = createMockWebSocket();
|
|
48
|
+
manager.registerSession("session-1", ws);
|
|
49
|
+
|
|
50
|
+
const component = {
|
|
51
|
+
id: "btn-1",
|
|
52
|
+
type: "button" as const,
|
|
53
|
+
props: { label: "Click me" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await manager.render("session-1", component);
|
|
57
|
+
|
|
58
|
+
expect(ws.messages.length).toBe(1);
|
|
59
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
60
|
+
expect(msg.type).toBe("canvas:render");
|
|
61
|
+
expect(msg.payload.component.id).toBe("btn-1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should throw for disconnected session", async () => {
|
|
65
|
+
await expect(manager.render("unknown", { id: "x", type: "button", props: {} })).rejects.toThrow(
|
|
66
|
+
"Session not connected"
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("update", () => {
|
|
72
|
+
it("should send update message to session", async () => {
|
|
73
|
+
const ws = createMockWebSocket();
|
|
74
|
+
manager.registerSession("session-1", ws);
|
|
75
|
+
|
|
76
|
+
const component = {
|
|
77
|
+
id: "btn-1",
|
|
78
|
+
type: "button" as const,
|
|
79
|
+
props: { label: "Updated" },
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await manager.update("session-1", component);
|
|
83
|
+
|
|
84
|
+
expect(ws.messages.length).toBe(1);
|
|
85
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
86
|
+
expect(msg.type).toBe("canvas:update");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("clear", () => {
|
|
91
|
+
it("should send clear message to session", async () => {
|
|
92
|
+
const ws = createMockWebSocket();
|
|
93
|
+
manager.registerSession("session-1", ws);
|
|
94
|
+
|
|
95
|
+
await manager.clear("session-1");
|
|
96
|
+
|
|
97
|
+
expect(ws.messages.length).toBe(1);
|
|
98
|
+
const msg = JSON.parse(ws.messages[0]!);
|
|
99
|
+
expect(msg.type).toBe("canvas:clear");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("waitForInteraction", () => {
|
|
104
|
+
it("should resolve when interaction is received", async () => {
|
|
105
|
+
const ws = createMockWebSocket();
|
|
106
|
+
manager.registerSession("session-1", ws);
|
|
107
|
+
|
|
108
|
+
const component = { id: "form-1", type: "form" as const, props: {} };
|
|
109
|
+
await manager.render("session-1", component);
|
|
110
|
+
|
|
111
|
+
const interactionPromise = manager.waitForInteraction("session-1", "form-1", 5000);
|
|
112
|
+
|
|
113
|
+
manager.handleInteraction("session-1", "form-1", { name: "John" });
|
|
114
|
+
|
|
115
|
+
const result = await interactionPromise;
|
|
116
|
+
expect(result).toEqual({ name: "John" });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("getStats", () => {
|
|
121
|
+
it("should return correct stats", () => {
|
|
122
|
+
const ws1 = createMockWebSocket();
|
|
123
|
+
const ws2 = createMockWebSocket();
|
|
124
|
+
|
|
125
|
+
manager.registerSession("session-1", ws1);
|
|
126
|
+
manager.registerSession("session-2", ws2);
|
|
127
|
+
|
|
128
|
+
const stats = manager.getStats();
|
|
129
|
+
expect(stats.totalSessions).toBe(2);
|
|
130
|
+
expect(stats.activeSessions).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("getConnectedSessions", () => {
|
|
135
|
+
it("should list connected sessions", () => {
|
|
136
|
+
const ws1 = createMockWebSocket();
|
|
137
|
+
const ws2 = createMockWebSocket();
|
|
138
|
+
|
|
139
|
+
manager.registerSession("session-1", ws1);
|
|
140
|
+
manager.registerSession("session-2", ws2);
|
|
141
|
+
|
|
142
|
+
const sessions = manager.getConnectedSessions();
|
|
143
|
+
expect(sessions).toContain("session-1");
|
|
144
|
+
expect(sessions).toContain("session-2");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("clearAll", () => {
|
|
149
|
+
it("should clear all sessions and pending interactions", () => {
|
|
150
|
+
const ws = createMockWebSocket();
|
|
151
|
+
manager.registerSession("session-1", ws);
|
|
152
|
+
|
|
153
|
+
manager.clearAll();
|
|
154
|
+
|
|
155
|
+
const stats = manager.getStats();
|
|
156
|
+
expect(stats.totalSessions).toBe(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|