@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.
Files changed (53) hide show
  1. package/package.json +10 -9
  2. package/src/agent/ethics.ts +70 -68
  3. package/src/agent/index.ts +48 -17
  4. package/src/agent/providers/index.ts +11 -5
  5. package/src/agent/soul.ts +19 -15
  6. package/src/agent/user.ts +19 -15
  7. package/src/agent/workspace.ts +6 -6
  8. package/src/agents/index.ts +4 -0
  9. package/src/agents/inter-agent-bus.test.ts +264 -0
  10. package/src/agents/inter-agent-bus.ts +279 -0
  11. package/src/agents/registry.test.ts +275 -0
  12. package/src/agents/registry.ts +273 -0
  13. package/src/agents/router.test.ts +229 -0
  14. package/src/agents/router.ts +251 -0
  15. package/src/agents/team-coordinator.test.ts +401 -0
  16. package/src/agents/team-coordinator.ts +480 -0
  17. package/src/canvas/canvas-manager.test.ts +159 -0
  18. package/src/canvas/canvas-manager.ts +219 -0
  19. package/src/canvas/canvas-tools.ts +189 -0
  20. package/src/canvas/index.ts +2 -0
  21. package/src/channels/whatsapp.ts +12 -12
  22. package/src/config/loader.ts +12 -9
  23. package/src/events/event-bus.test.ts +98 -0
  24. package/src/events/event-bus.ts +171 -0
  25. package/src/gateway/server.ts +131 -35
  26. package/src/index.ts +9 -1
  27. package/src/multi-agent/manager.ts +12 -12
  28. package/src/plugins/api.ts +129 -0
  29. package/src/plugins/index.ts +2 -0
  30. package/src/plugins/loader.test.ts +285 -0
  31. package/src/plugins/loader.ts +363 -0
  32. package/src/resilience/circuit-breaker.test.ts +129 -0
  33. package/src/resilience/circuit-breaker.ts +223 -0
  34. package/src/security/google-chat.test.ts +219 -0
  35. package/src/security/google-chat.ts +269 -0
  36. package/src/security/index.ts +5 -0
  37. package/src/security/pairing.test.ts +302 -0
  38. package/src/security/pairing.ts +250 -0
  39. package/src/security/rate-limit.test.ts +239 -0
  40. package/src/security/rate-limit.ts +270 -0
  41. package/src/security/signal.test.ts +92 -0
  42. package/src/security/signal.ts +321 -0
  43. package/src/state/store.test.ts +190 -0
  44. package/src/state/store.ts +310 -0
  45. package/src/storage/sqlite.ts +3 -3
  46. package/src/tools/cron.ts +42 -2
  47. package/src/tools/dynamic-registry.test.ts +226 -0
  48. package/src/tools/dynamic-registry.ts +258 -0
  49. package/src/tools/fs.test.ts +127 -0
  50. package/src/tools/fs.ts +364 -0
  51. package/src/tools/index.ts +1 -0
  52. package/src/tools/read.ts +23 -19
  53. package/src/utils/logger.ts +112 -33
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect, beforeEach, vi } from "bun:test";
2
+ import { InterAgentBus, type AgentMessage } from "./inter-agent-bus";
3
+ import { eventBus } from "../events/event-bus";
4
+
5
+ describe("InterAgentBus", () => {
6
+ let bus: InterAgentBus;
7
+
8
+ beforeEach(() => {
9
+ bus = new InterAgentBus();
10
+ eventBus.removeAllListeners();
11
+ });
12
+
13
+ describe("subscribe", () => {
14
+ it("should subscribe to topic", () => {
15
+ const handler = vi.fn();
16
+ bus.subscribe("agent-1", "task:request", handler);
17
+
18
+ const stats = bus.getStats();
19
+ expect(stats.subscriptionsByAgent["agent-1"]).toBe(1);
20
+ expect(stats.subscriptionsByTopic["task:request"]).toBe(1);
21
+ });
22
+
23
+ it("should return unsubscribe function", () => {
24
+ const handler = vi.fn();
25
+ const unsubscribe = bus.subscribe("agent-1", "task:request", handler);
26
+
27
+ unsubscribe();
28
+
29
+ const stats = bus.getStats();
30
+ expect(stats.totalSubscriptions).toBe(0);
31
+ });
32
+
33
+ it("should support once option", async () => {
34
+ const handler = vi.fn();
35
+ bus.subscribe("agent-1", "task:request", handler, { once: true });
36
+
37
+ await bus.send({
38
+ from: "agent-2",
39
+ to: "agent-1",
40
+ type: "request",
41
+ topic: "task:request",
42
+ payload: { data: "test" },
43
+ correlationId: "corr-1",
44
+ });
45
+
46
+ await bus.send({
47
+ from: "agent-2",
48
+ to: "agent-1",
49
+ type: "request",
50
+ topic: "task:request",
51
+ payload: { data: "test2" },
52
+ correlationId: "corr-2",
53
+ });
54
+
55
+ expect(handler).toHaveBeenCalledTimes(1);
56
+ });
57
+
58
+ it("should support priority ordering", () => {
59
+ const calls: string[] = [];
60
+
61
+ bus.subscribe("agent-1", "topic", () => calls.push("second"));
62
+ bus.subscribe("agent-1", "topic", () => calls.push("first"), { priority: 0 });
63
+
64
+ const stats = bus.getStats();
65
+ expect(stats.subscriptionsByAgent["agent-1"]).toBe(2);
66
+ });
67
+ });
68
+
69
+ describe("unsubscribe", () => {
70
+ it("should remove handler", () => {
71
+ const handler = vi.fn();
72
+ bus.subscribe("agent-1", "topic", handler);
73
+ bus.unsubscribe("agent-1", "topic", handler);
74
+
75
+ const stats = bus.getStats();
76
+ expect(stats.totalSubscriptions).toBe(0);
77
+ });
78
+
79
+ it("should clean up empty topic", () => {
80
+ const handler = vi.fn();
81
+ bus.subscribe("agent-1", "topic", handler);
82
+ bus.unsubscribe("agent-1", "topic", handler);
83
+
84
+ const stats = bus.getStats();
85
+ expect(stats.subscriptionsByTopic["topic"]).toBeUndefined();
86
+ });
87
+
88
+ it("should clean up empty agent", () => {
89
+ const handler = vi.fn();
90
+ bus.subscribe("agent-1", "topic", handler);
91
+ bus.unsubscribe("agent-1", "topic", handler);
92
+
93
+ const stats = bus.getStats();
94
+ expect(stats.subscriptionsByAgent["agent-1"]).toBeUndefined();
95
+ });
96
+ });
97
+
98
+ describe("send", () => {
99
+ it("should deliver message to agent", async () => {
100
+ const handler = vi.fn();
101
+ bus.subscribe("agent-1", "task:request", handler);
102
+
103
+ await bus.send({
104
+ from: "agent-2",
105
+ to: "agent-1",
106
+ type: "request",
107
+ topic: "task:request",
108
+ payload: { data: "test" },
109
+ correlationId: "corr-1",
110
+ });
111
+
112
+ expect(handler).toHaveBeenCalled();
113
+ const message = handler.mock.calls[0][0] as AgentMessage;
114
+ expect(message.id).toBeDefined();
115
+ expect(message.timestamp).toBeDefined();
116
+ expect(message.payload).toEqual({ data: "test" });
117
+ });
118
+
119
+ it("should broadcast to all subscribers", async () => {
120
+ const handler1 = vi.fn();
121
+ const handler2 = vi.fn();
122
+
123
+ bus.subscribe("agent-1", "broadcast:status", handler1);
124
+ bus.subscribe("agent-2", "broadcast:status", handler2);
125
+
126
+ await bus.send({
127
+ from: "system",
128
+ to: "broadcast",
129
+ type: "event",
130
+ topic: "broadcast:status",
131
+ payload: { status: "active" },
132
+ correlationId: "corr-1",
133
+ });
134
+
135
+ expect(handler1).toHaveBeenCalled();
136
+ expect(handler2).toHaveBeenCalled();
137
+ });
138
+
139
+ it("should handle missing handlers gracefully", async () => {
140
+ await bus.send({
141
+ from: "agent-2",
142
+ to: "agent-1",
143
+ type: "request",
144
+ topic: "unknown:topic",
145
+ payload: {},
146
+ correlationId: "corr-1",
147
+ });
148
+
149
+ expect(true).toBe(true);
150
+ });
151
+
152
+ it("should handle handler errors", async () => {
153
+ const errorHandler = vi.fn().mockRejectedValue(new Error("Handler error"));
154
+ bus.subscribe("agent-1", "topic", errorHandler);
155
+
156
+ await bus.send({
157
+ from: "agent-2",
158
+ to: "agent-1",
159
+ type: "request",
160
+ topic: "topic",
161
+ payload: {},
162
+ correlationId: "corr-1",
163
+ });
164
+
165
+ expect(errorHandler).toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ describe("request/response", () => {
170
+ it("should make request and receive response", async () => {
171
+ bus.subscribe("agent-1", "task:execute", async (msg: AgentMessage) => {
172
+ await bus.respond(msg, { result: "done" });
173
+ });
174
+
175
+ const response = bus.request("agent-1", "task:execute", { task: "test" }, 1000);
176
+
177
+ await expect(response).resolves.toEqual({ result: "done" });
178
+ });
179
+
180
+ it("should timeout if no response", async () => {
181
+ const response = bus.request("agent-1", "unknown:topic", {}, 100);
182
+
183
+ await expect(response).rejects.toThrow("Request timeout");
184
+ });
185
+
186
+ it("should handle error responses", async () => {
187
+ bus.subscribe("agent-1", "task:execute", async (msg: AgentMessage) => {
188
+ const pending = (bus as unknown as { pendingRequests: Map<string, unknown> }).pendingRequests.get(msg.correlationId);
189
+ if (pending) {
190
+ (pending as { reject: (err: Error) => void }).reject(new Error("Something went wrong"));
191
+ }
192
+ });
193
+
194
+ const response = bus.request("agent-1", "task:execute", {}, 1000);
195
+
196
+ await expect(response).rejects.toThrow("Something went wrong");
197
+ });
198
+ });
199
+
200
+ describe("getStats", () => {
201
+ it("should return correct stats", async () => {
202
+ bus.subscribe("agent-1", "topic1", vi.fn());
203
+ bus.subscribe("agent-1", "topic2", vi.fn());
204
+ bus.subscribe("agent-2", "topic1", vi.fn());
205
+
206
+ await bus.send({
207
+ from: "system",
208
+ to: "agent-1",
209
+ type: "event",
210
+ topic: "topic1",
211
+ payload: {},
212
+ correlationId: "corr-1",
213
+ });
214
+
215
+ const stats = bus.getStats();
216
+
217
+ expect(stats.totalSubscriptions).toBe(3);
218
+ expect(stats.subscriptionsByAgent["agent-1"]).toBe(2);
219
+ expect(stats.subscriptionsByAgent["agent-2"]).toBe(1);
220
+ expect(stats.messagesProcessed).toBe(1);
221
+ });
222
+
223
+ it("should track pending responses", () => {
224
+ const requestPromise = bus.request("agent-1", "topic", {}, 5000);
225
+
226
+ const stats = bus.getStats();
227
+ expect(stats.pendingResponses).toBe(1);
228
+ });
229
+ });
230
+
231
+ describe("clearAgent", () => {
232
+ it("should clear agent handlers", () => {
233
+ bus.subscribe("agent-1", "topic", vi.fn());
234
+ bus.clearAgent("agent-1");
235
+
236
+ const stats = bus.getStats();
237
+ expect(stats.subscriptionsByAgent["agent-1"]).toBeUndefined();
238
+ });
239
+
240
+ it("should reject pending requests for agent", async () => {
241
+ const requestPromise = bus.request("agent-1", "topic", {}, 5000);
242
+
243
+ bus.clearAgent("agent-1");
244
+
245
+ await expect(requestPromise).rejects.toThrow("disconnected");
246
+ });
247
+ });
248
+
249
+ describe("clear", () => {
250
+ it("should clear all state", async () => {
251
+ bus.subscribe("agent-1", "topic", vi.fn());
252
+ const requestPromise = bus.request("agent-2", "topic", {}, 5000);
253
+
254
+ bus.clear();
255
+
256
+ const stats = bus.getStats();
257
+ expect(stats.totalSubscriptions).toBe(0);
258
+ expect(stats.messagesProcessed).toBe(0);
259
+ expect(stats.pendingResponses).toBe(0);
260
+
261
+ await expect(requestPromise).rejects.toThrow("Bus cleared");
262
+ });
263
+ });
264
+ });
@@ -0,0 +1,279 @@
1
+ import { logger } from "../utils/logger.ts";
2
+ import { eventBus } from "../events/event-bus.ts";
3
+
4
+ export interface AgentMessage {
5
+ id: string;
6
+ from: string;
7
+ to: string;
8
+ type: "request" | "response" | "event" | "error" | "ack";
9
+ topic: string;
10
+ payload: unknown;
11
+ correlationId: string;
12
+ timestamp: number;
13
+ ttl?: number;
14
+ metadata?: Record<string, unknown>;
15
+ }
16
+
17
+ export interface AgentMessageHandler {
18
+ (message: AgentMessage): Promise<unknown> | unknown;
19
+ }
20
+
21
+ export interface SubscriptionOptions {
22
+ once?: boolean;
23
+ priority?: number;
24
+ }
25
+
26
+ export interface BusStats {
27
+ totalSubscriptions: number;
28
+ subscriptionsByAgent: Record<string, number>;
29
+ subscriptionsByTopic: Record<string, number>;
30
+ messagesProcessed: number;
31
+ pendingResponses: number;
32
+ }
33
+
34
+ interface PendingRequest {
35
+ resolve: (value: unknown) => void;
36
+ reject: (error: Error) => void;
37
+ timeoutId: ReturnType<typeof setTimeout>;
38
+ }
39
+
40
+ export class InterAgentBus {
41
+ private handlers: Map<string, Map<string, AgentMessageHandler[]>> = new Map();
42
+ private pendingRequests: Map<string, PendingRequest> = new Map();
43
+ private messagesProcessed = 0;
44
+ private log = logger.child("inter-agent-bus");
45
+
46
+ subscribe(
47
+ agentId: string,
48
+ topic: string,
49
+ handler: AgentMessageHandler,
50
+ options?: SubscriptionOptions
51
+ ): () => void {
52
+ if (!this.handlers.has(agentId)) {
53
+ this.handlers.set(agentId, new Map());
54
+ }
55
+
56
+ const agentHandlers = this.handlers.get(agentId)!;
57
+
58
+ if (options?.once) {
59
+ const wrappedHandler: AgentMessageHandler = async (msg) => {
60
+ this.unsubscribe(agentId, topic, wrappedHandler);
61
+ return handler(msg);
62
+ };
63
+ if (!agentHandlers.has(topic)) {
64
+ agentHandlers.set(topic, []);
65
+ }
66
+ agentHandlers.get(topic)!.push(wrappedHandler);
67
+ this.log.debug(`Subscribed (once): ${agentId} -> ${topic}`);
68
+ return () => this.unsubscribe(agentId, topic, wrappedHandler);
69
+ }
70
+
71
+ if (!agentHandlers.has(topic)) {
72
+ agentHandlers.set(topic, []);
73
+ }
74
+
75
+ const handlers = agentHandlers.get(topic)!;
76
+ if (options?.priority !== undefined) {
77
+ handlers.splice(options.priority, 0, handler);
78
+ } else {
79
+ handlers.push(handler);
80
+ }
81
+
82
+ this.log.debug(`Subscribed: ${agentId} -> ${topic}`);
83
+
84
+ return () => this.unsubscribe(agentId, topic, handler);
85
+ }
86
+
87
+ unsubscribe(agentId: string, topic: string, handler: AgentMessageHandler): void {
88
+ const agentHandlers = this.handlers.get(agentId);
89
+ if (!agentHandlers) return;
90
+
91
+ const topicHandlers = agentHandlers.get(topic);
92
+ if (!topicHandlers) return;
93
+
94
+ const index = topicHandlers.indexOf(handler);
95
+ if (index > -1) {
96
+ topicHandlers.splice(index, 1);
97
+ this.log.debug(`Unsubscribed: ${agentId} -> ${topic}`);
98
+ }
99
+
100
+ if (topicHandlers.length === 0) {
101
+ agentHandlers.delete(topic);
102
+ }
103
+
104
+ if (agentHandlers.size === 0) {
105
+ this.handlers.delete(agentId);
106
+ }
107
+ }
108
+
109
+ async send(message: Omit<AgentMessage, "id" | "timestamp">): Promise<void> {
110
+ const fullMessage: AgentMessage = {
111
+ ...message,
112
+ id: crypto.randomUUID(),
113
+ timestamp: Date.now(),
114
+ };
115
+
116
+ this.messagesProcessed++;
117
+
118
+ if (message.to === "broadcast") {
119
+ await this.broadcast(fullMessage);
120
+ } else {
121
+ await this.deliver(message.to, fullMessage);
122
+ }
123
+
124
+ eventBus.emit("tool:completed", {
125
+ toolName: `inter-agent:${message.topic}`,
126
+ result: { delivered: true },
127
+ duration: 0,
128
+ success: true,
129
+ });
130
+ }
131
+
132
+ async request(
133
+ to: string,
134
+ topic: string,
135
+ payload: unknown,
136
+ timeout = 30000
137
+ ): Promise<unknown> {
138
+ const correlationId = crypto.randomUUID();
139
+
140
+ return new Promise((resolve, reject) => {
141
+ const timeoutId = setTimeout(() => {
142
+ this.pendingRequests.delete(correlationId);
143
+ reject(new Error(`Request timeout: ${topic} -> ${to}`));
144
+ }, timeout);
145
+
146
+ this.pendingRequests.set(correlationId, { resolve, reject, timeoutId });
147
+
148
+ this.send({
149
+ from: "bus",
150
+ to,
151
+ type: "request",
152
+ topic,
153
+ payload,
154
+ correlationId,
155
+ });
156
+ });
157
+ }
158
+
159
+ async respond(originalMessage: AgentMessage, response: unknown): Promise<void> {
160
+ await this.send({
161
+ from: originalMessage.to,
162
+ to: originalMessage.from,
163
+ type: "response",
164
+ topic: originalMessage.topic,
165
+ payload: response,
166
+ correlationId: originalMessage.correlationId,
167
+ });
168
+ }
169
+
170
+ handleResponse(message: AgentMessage): void {
171
+ const pending = this.pendingRequests.get(message.correlationId);
172
+ if (!pending) return;
173
+
174
+ clearTimeout(pending.timeoutId);
175
+ this.pendingRequests.delete(message.correlationId);
176
+
177
+ if (message.type === "error") {
178
+ pending.reject(new Error(String(message.payload)));
179
+ } else {
180
+ pending.resolve(message.payload);
181
+ }
182
+ }
183
+
184
+ async broadcast(message: AgentMessage): Promise<void> {
185
+ const deliveries: Promise<void>[] = [];
186
+
187
+ for (const [agentId, topics] of this.handlers) {
188
+ if (topics.has(message.topic)) {
189
+ deliveries.push(this.deliver(agentId, message));
190
+ }
191
+ }
192
+
193
+ await Promise.allSettled(deliveries);
194
+ }
195
+
196
+ private async deliver(agentId: string, message: AgentMessage): Promise<void> {
197
+ if (message.type === "response") {
198
+ this.handleResponse(message);
199
+ return;
200
+ }
201
+
202
+ const agentHandlers = this.handlers.get(agentId);
203
+ if (!agentHandlers) {
204
+ this.log.debug(`No handlers for agent: ${agentId}`);
205
+ return;
206
+ }
207
+
208
+ const topicHandlers = agentHandlers.get(message.topic);
209
+ if (!topicHandlers || topicHandlers.length === 0) {
210
+ this.log.debug(`No handlers for topic: ${message.topic} at ${agentId}`);
211
+ return;
212
+ }
213
+
214
+ for (const handler of topicHandlers) {
215
+ try {
216
+ await handler(message);
217
+ } catch (error) {
218
+ this.log.error(`Handler error for ${message.topic}`, {
219
+ agentId,
220
+ error: (error as Error).message,
221
+ });
222
+ }
223
+ }
224
+ }
225
+
226
+ getStats(): BusStats {
227
+ const subscriptionsByAgent: Record<string, number> = {};
228
+ const subscriptionsByTopic: Record<string, number> = {};
229
+
230
+ for (const [agentId, topics] of this.handlers) {
231
+ subscriptionsByAgent[agentId] = Array.from(topics.values()).reduce(
232
+ (sum, handlers) => sum + handlers.length,
233
+ 0
234
+ );
235
+
236
+ for (const [topic, handlers] of topics) {
237
+ subscriptionsByTopic[topic] = (subscriptionsByTopic[topic] ?? 0) + handlers.length;
238
+ }
239
+ }
240
+
241
+ const totalSubscriptions = Object.values(subscriptionsByAgent).reduce(
242
+ (sum, n) => sum + n,
243
+ 0
244
+ );
245
+
246
+ return {
247
+ totalSubscriptions,
248
+ subscriptionsByAgent,
249
+ subscriptionsByTopic,
250
+ messagesProcessed: this.messagesProcessed,
251
+ pendingResponses: this.pendingRequests.size,
252
+ };
253
+ }
254
+
255
+ clearAgent(agentId: string): void {
256
+ this.handlers.delete(agentId);
257
+
258
+ for (const [correlationId, pending] of this.pendingRequests) {
259
+ pending.reject(new Error(`Agent ${agentId} disconnected`));
260
+ this.pendingRequests.delete(correlationId);
261
+ }
262
+
263
+ this.log.info(`Cleared all handlers for agent: ${agentId}`);
264
+ }
265
+
266
+ clear(): void {
267
+ for (const pending of this.pendingRequests.values()) {
268
+ clearTimeout(pending.timeoutId);
269
+ pending.reject(new Error("Bus cleared"));
270
+ }
271
+
272
+ this.handlers.clear();
273
+ this.pendingRequests.clear();
274
+ this.messagesProcessed = 0;
275
+ this.log.info("Bus cleared");
276
+ }
277
+ }
278
+
279
+ export const interAgentBus = new InterAgentBus();