@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,310 @@
1
+ export interface SessionState {
2
+ id: string;
3
+ agentId: string;
4
+ channel: string;
5
+ userId: string;
6
+ createdAt: number;
7
+ lastActivityAt: number;
8
+ messageCount: number;
9
+ status: "active" | "idle" | "closed";
10
+ }
11
+
12
+ export interface AgentState {
13
+ id: string;
14
+ name: string;
15
+ status: "ready" | "busy" | "error";
16
+ currentSessionId?: string;
17
+ lastError?: string;
18
+ }
19
+
20
+ export interface ChannelState {
21
+ name: string;
22
+ accountId: string;
23
+ status: "connected" | "disconnected" | "error";
24
+ lastActivity?: number;
25
+ error?: string;
26
+ }
27
+
28
+ export interface MetricsState {
29
+ totalMessages: number;
30
+ totalSessions: number;
31
+ totalToolCalls: number;
32
+ averageResponseTime: number;
33
+ errors: number;
34
+ startedAt: number;
35
+ }
36
+
37
+ export interface HiveState {
38
+ sessions: Map<string, SessionState>;
39
+ agents: Map<string, AgentState>;
40
+ channels: Map<string, ChannelState>;
41
+ metrics: MetricsState;
42
+ }
43
+
44
+ export interface StateSnapshot {
45
+ id: string;
46
+ timestamp: number;
47
+ state: HiveState;
48
+ reason?: string;
49
+ action?: string;
50
+ correlationId?: string;
51
+ }
52
+
53
+ interface StateStoreOptions {
54
+ maxSnapshots?: number;
55
+ enableSnapshots?: boolean;
56
+ }
57
+
58
+ const defaultMetrics: MetricsState = {
59
+ totalMessages: 0,
60
+ totalSessions: 0,
61
+ totalToolCalls: 0,
62
+ averageResponseTime: 0,
63
+ errors: 0,
64
+ startedAt: Date.now(),
65
+ };
66
+
67
+ export class StateStore {
68
+ private state: HiveState;
69
+ private snapshots: StateSnapshot[] = [];
70
+ private readonly maxSnapshots: number;
71
+ private readonly enableSnapshots: boolean;
72
+ private listeners: Set<(state: Readonly<HiveState>) => void> = new Set();
73
+ private correlationId?: string;
74
+
75
+ constructor(options: StateStoreOptions = {}) {
76
+ this.maxSnapshots = options.maxSnapshots ?? 100;
77
+ this.enableSnapshots = options.enableSnapshots ?? true;
78
+ this.state = this.createInitialState();
79
+ }
80
+
81
+ setCorrelationId(id: string): void {
82
+ this.correlationId = id;
83
+ }
84
+
85
+ getCorrelationId(): string | undefined {
86
+ return this.correlationId;
87
+ }
88
+
89
+ clearCorrelationId(): void {
90
+ this.correlationId = undefined;
91
+ }
92
+
93
+ private createInitialState(): HiveState {
94
+ return {
95
+ sessions: new Map(),
96
+ agents: new Map(),
97
+ channels: new Map(),
98
+ metrics: { ...defaultMetrics },
99
+ };
100
+ }
101
+
102
+ getState(): Readonly<HiveState> {
103
+ return this.state;
104
+ }
105
+
106
+ update(updater: (draft: HiveState) => void, reason?: string): void {
107
+ const newState = this.cloneState(this.state);
108
+ updater(newState);
109
+
110
+ if (this.enableSnapshots) {
111
+ this.saveSnapshot(newState, reason);
112
+ }
113
+
114
+ this.state = newState;
115
+ this.notifyListeners();
116
+ }
117
+
118
+ private cloneState(state: HiveState): HiveState {
119
+ return {
120
+ sessions: new Map(state.sessions),
121
+ agents: new Map(state.agents),
122
+ channels: new Map(state.channels),
123
+ metrics: { ...state.metrics },
124
+ };
125
+ }
126
+
127
+ private saveSnapshot(state: HiveState, reason?: string): void {
128
+ const snapshot: StateSnapshot = {
129
+ id: crypto.randomUUID(),
130
+ timestamp: Date.now(),
131
+ state: this.cloneState(state),
132
+ reason,
133
+ action: reason,
134
+ correlationId: this.correlationId,
135
+ };
136
+
137
+ this.snapshots.push(snapshot);
138
+
139
+ if (this.snapshots.length > this.maxSnapshots) {
140
+ this.snapshots.shift();
141
+ }
142
+ }
143
+
144
+ getSnapshotAt(timestamp: number): StateSnapshot | undefined {
145
+ return this.snapshots.find((s) => s.timestamp >= timestamp);
146
+ }
147
+
148
+ getSnapshotById(id: string): StateSnapshot | undefined {
149
+ return this.snapshots.find((s) => s.id === id);
150
+ }
151
+
152
+ getAllSnapshots(): StateSnapshot[] {
153
+ return [...this.snapshots];
154
+ }
155
+
156
+ getRecentSnapshots(count: number = 10): StateSnapshot[] {
157
+ return this.snapshots.slice(-count);
158
+ }
159
+
160
+ subscribe(listener: (state: Readonly<HiveState>) => void): () => void {
161
+ this.listeners.add(listener);
162
+ return () => this.listeners.delete(listener);
163
+ }
164
+
165
+ private notifyListeners(): void {
166
+ const state = this.getState();
167
+ for (const listener of this.listeners) {
168
+ try {
169
+ listener(state);
170
+ } catch (error) {
171
+ console.error("[StateStore] Listener error:", error);
172
+ }
173
+ }
174
+ }
175
+
176
+ createSession(session: Omit<SessionState, "createdAt" | "lastActivityAt" | "messageCount" | "status">): SessionState {
177
+ const newSession: SessionState = {
178
+ ...session,
179
+ createdAt: Date.now(),
180
+ lastActivityAt: Date.now(),
181
+ messageCount: 0,
182
+ status: "active",
183
+ };
184
+
185
+ this.update((state) => {
186
+ state.sessions.set(session.id, newSession);
187
+ state.metrics.totalSessions++;
188
+ }, `Session created: ${session.id}`);
189
+
190
+ return newSession;
191
+ }
192
+
193
+ updateSession(sessionId: string, updates: Partial<SessionState>): void {
194
+ this.update((state) => {
195
+ const session = state.sessions.get(sessionId);
196
+ if (session) {
197
+ state.sessions.set(sessionId, { ...session, ...updates });
198
+ }
199
+ }, `Session updated: ${sessionId}`);
200
+ }
201
+
202
+ closeSession(sessionId: string): void {
203
+ this.update((state) => {
204
+ const session = state.sessions.get(sessionId);
205
+ if (session) {
206
+ state.sessions.set(sessionId, { ...session, status: "closed" });
207
+ }
208
+ }, `Session closed: ${sessionId}`);
209
+ }
210
+
211
+ incrementMessageCount(sessionId: string): void {
212
+ this.update((state) => {
213
+ const session = state.sessions.get(sessionId);
214
+ if (session) {
215
+ session.messageCount++;
216
+ session.lastActivityAt = Date.now();
217
+ state.sessions.set(sessionId, session);
218
+ state.metrics.totalMessages++;
219
+ }
220
+ });
221
+ }
222
+
223
+ registerAgent(agent: Omit<AgentState, "status">): void {
224
+ this.update((state) => {
225
+ state.agents.set(agent.id, { ...agent, status: "ready" });
226
+ }, `Agent registered: ${agent.id}`);
227
+ }
228
+
229
+ updateAgent(agentId: string, updates: Partial<AgentState>): void {
230
+ this.update((state) => {
231
+ const agent = state.agents.get(agentId);
232
+ if (agent) {
233
+ state.agents.set(agentId, { ...agent, ...updates });
234
+ }
235
+ }, `Agent updated: ${agentId}`);
236
+ }
237
+
238
+ updateChannel(channelName: string, accountId: string, updates: Partial<ChannelState>): void {
239
+ this.update((state) => {
240
+ const key = `${channelName}:${accountId}`;
241
+ const channel = state.channels.get(key);
242
+ if (channel) {
243
+ state.channels.set(key, { ...channel, ...updates });
244
+ } else {
245
+ state.channels.set(key, {
246
+ name: channelName,
247
+ accountId,
248
+ status: "disconnected",
249
+ ...updates,
250
+ });
251
+ }
252
+ }, `Channel updated: ${channelName}:${accountId}`);
253
+ }
254
+
255
+ recordToolCall(duration: number, success: boolean): void {
256
+ this.update((state) => {
257
+ state.metrics.totalToolCalls++;
258
+ if (!success) {
259
+ state.metrics.errors++;
260
+ }
261
+ const total = state.metrics.totalToolCalls;
262
+ const prevAvg = state.metrics.averageResponseTime;
263
+ state.metrics.averageResponseTime = prevAvg + (duration - prevAvg) / total;
264
+ });
265
+ }
266
+
267
+ reset(): void {
268
+ this.state = this.createInitialState();
269
+ this.snapshots = [];
270
+ this.saveSnapshot(this.state, "Store reset");
271
+ }
272
+
273
+ exportState(): string {
274
+ const exportable = {
275
+ sessions: Object.fromEntries(this.state.sessions),
276
+ agents: Object.fromEntries(this.state.agents),
277
+ channels: Object.fromEntries(this.state.channels),
278
+ metrics: this.state.metrics,
279
+ };
280
+ return JSON.stringify(exportable, null, 2);
281
+ }
282
+
283
+ export(): string {
284
+ return this.exportState();
285
+ }
286
+
287
+ getStats(): {
288
+ sessionsCount: number;
289
+ activeSessions: number;
290
+ agentsCount: number;
291
+ channelsCount: number;
292
+ snapshotsCount: number;
293
+ uptime: number;
294
+ } {
295
+ const activeSessions = Array.from(this.state.sessions.values()).filter(
296
+ (s) => s.status === "active"
297
+ ).length;
298
+
299
+ return {
300
+ sessionsCount: this.state.sessions.size,
301
+ activeSessions,
302
+ agentsCount: this.state.agents.size,
303
+ channelsCount: this.state.channels.size,
304
+ snapshotsCount: this.snapshots.length,
305
+ uptime: Date.now() - this.state.metrics.startedAt,
306
+ };
307
+ }
308
+ }
309
+
310
+ export const stateStore = new StateStore();
@@ -1,7 +1,7 @@
1
1
  import { Database } from "bun:sqlite";
2
2
  import { logger } from "../utils/logger.ts";
3
3
  import * as path from "node:path";
4
- import * as fs from "node:fs";
4
+ import { existsSync, mkdirSync } from "node:fs";
5
5
 
6
6
  export interface ChatMessageRow {
7
7
  id: string;
@@ -30,8 +30,8 @@ export class DatabaseService {
30
30
 
31
31
  // Ensure directory exists
32
32
  const dir = path.dirname(targetPath);
33
- if (!fs.existsSync(dir)) {
34
- fs.mkdirSync(dir, { recursive: true });
33
+ if (!existsSync(dir)) {
34
+ mkdirSync(dir, { recursive: true });
35
35
  }
36
36
 
37
37
  this.db = new Database(targetPath, { create: true });
package/src/tools/cron.ts CHANGED
@@ -13,11 +13,51 @@ export interface CronJob {
13
13
  createdAt: Date;
14
14
  }
15
15
 
16
- export function createCronTools(_config: Config): Tool[] {
16
+ function matchCron(expr: string, date: Date): boolean {
17
+ try {
18
+ const parts = expr.split(/\s+/);
19
+ if (parts.length !== 5) return false;
20
+ const [min, hour, dom, month, dow] = parts;
21
+ const match = (val: number, p: string) => {
22
+ if (p === "*") return true;
23
+ if (p.startsWith("*/")) return val % parseInt(p.slice(2), 10) === 0;
24
+ return parseInt(p, 10) === val;
25
+ };
26
+ return match(date.getMinutes(), min) &&
27
+ match(date.getHours(), hour) &&
28
+ match(date.getDate(), dom) &&
29
+ match(date.getMonth() + 1, month) &&
30
+ match(date.getDay(), dow);
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ let cronInterval: ReturnType<typeof setInterval> | null = null;
37
+
38
+ export function createCronTools(_config: Config, onTrigger?: (sessionId: string, task: string) => void): Tool[] {
17
39
  const log = logger.child("cron");
18
40
  const jobs: Map<string, CronJob> = new Map();
19
41
  let jobIdCounter = 0;
20
42
 
43
+ if (cronInterval) clearInterval(cronInterval);
44
+ cronInterval = setInterval(() => {
45
+ const now = new Date();
46
+ for (const job of jobs.values()) {
47
+ if (!job.enabled) continue;
48
+
49
+ const lastRunMin = job.lastRun ? Math.floor(job.lastRun.getTime() / 60000) : 0;
50
+ const currentMin = Math.floor(now.getTime() / 60000);
51
+ if (lastRunMin === currentMin) continue;
52
+
53
+ if (matchCron(job.expression, now)) {
54
+ job.lastRun = now;
55
+ log.info(`Cron triggered: ${job.id} for task: ${job.task}`);
56
+ if (onTrigger) onTrigger(job.sessionId, job.task);
57
+ }
58
+ }
59
+ }, 30000);
60
+
21
61
  const cronAdd: Tool = {
22
62
  name: "cron_add",
23
63
  description: "Add a scheduled task",
@@ -45,7 +85,7 @@ export function createCronTools(_config: Config): Tool[] {
45
85
  const sessionId = (params.sessionId as string) ?? "agent:main:main";
46
86
 
47
87
  const id = `cron-${++jobIdCounter}`;
48
-
88
+
49
89
  const job: CronJob = {
50
90
  id,
51
91
  sessionId,
@@ -0,0 +1,226 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { DynamicToolRegistry } from "../tools/dynamic-registry.ts";
3
+
4
+ describe("DynamicToolRegistry", () => {
5
+ let registry: DynamicToolRegistry;
6
+
7
+ beforeEach(() => {
8
+ registry = new DynamicToolRegistry();
9
+ });
10
+
11
+ it("should register a tool", () => {
12
+ registry.register({
13
+ name: "calculator_add",
14
+ description: "Sumar dos números",
15
+ parameters: {
16
+ a: { type: "number", description: "Primer número", required: true },
17
+ b: { type: "number", description: "Segundo número", required: true },
18
+ },
19
+ execute: async ({ a, b }) => (a as number) + (b as number),
20
+ });
21
+
22
+ const tool = registry.get("calculator_add");
23
+ expect(tool).toBeDefined();
24
+ expect(tool?.description).toBe("Sumar dos números");
25
+ });
26
+
27
+ it("should validate parameters on execution", async () => {
28
+ registry.register({
29
+ name: "validated_tool",
30
+ description: "A tool with validation",
31
+ parameters: {
32
+ value: { type: "number", required: true },
33
+ },
34
+ execute: async ({ value }) => value,
35
+ });
36
+
37
+ const tool = registry.get("validated_tool");
38
+ expect(tool).toBeDefined();
39
+
40
+ await expect(tool!.execute({ value: 42 })).resolves.toBe(42);
41
+
42
+ await expect(tool!.execute({ value: "invalid" })).rejects.toThrow();
43
+ });
44
+
45
+ it("should unregister a tool", () => {
46
+ registry.register({
47
+ name: "temp_tool",
48
+ description: "Temporary",
49
+ parameters: {},
50
+ execute: async () => null,
51
+ });
52
+
53
+ expect(registry.has("temp_tool")).toBe(true);
54
+
55
+ registry.unregister("temp_tool");
56
+
57
+ expect(registry.has("temp_tool")).toBe(false);
58
+ });
59
+
60
+ it("should list all tools", () => {
61
+ registry.register({
62
+ name: "tool1",
63
+ description: "Tool 1",
64
+ parameters: {},
65
+ execute: async () => null,
66
+ });
67
+ registry.register({
68
+ name: "tool2",
69
+ description: "Tool 2",
70
+ parameters: {},
71
+ execute: async () => null,
72
+ });
73
+
74
+ const tools = registry.list();
75
+ expect(tools.length).toBe(2);
76
+ expect(tools.map((t) => t.name)).toContain("tool1");
77
+ expect(tools.map((t) => t.name)).toContain("tool2");
78
+ });
79
+
80
+ it("should list tool names", () => {
81
+ registry.register({
82
+ name: "named_tool",
83
+ description: "Named tool",
84
+ parameters: {},
85
+ execute: async () => null,
86
+ });
87
+
88
+ const names = registry.listNames();
89
+ expect(names).toContain("named_tool");
90
+ });
91
+
92
+ it("should handle optional parameters", async () => {
93
+ registry.register({
94
+ name: "optional_params",
95
+ description: "Tool with optional params",
96
+ parameters: {
97
+ required: { type: "string", required: true },
98
+ optional: { type: "string", required: false },
99
+ },
100
+ execute: async (args) => args,
101
+ });
102
+
103
+ const tool = registry.get("optional_params");
104
+
105
+ await expect(tool!.execute({ required: "test" })).resolves.toEqual({
106
+ required: "test",
107
+ });
108
+
109
+ await expect(
110
+ tool!.execute({ required: "test", optional: "value" })
111
+ ).resolves.toEqual({ required: "test", optional: "value" });
112
+ });
113
+
114
+ it("should handle enum parameters", async () => {
115
+ registry.register({
116
+ name: "enum_tool",
117
+ description: "Tool with enum",
118
+ parameters: {
119
+ status: {
120
+ type: "string",
121
+ enum: ["active", "inactive", "pending"],
122
+ required: true,
123
+ },
124
+ },
125
+ execute: async ({ status }) => status,
126
+ });
127
+
128
+ const tool = registry.get("enum_tool");
129
+
130
+ await expect(tool!.execute({ status: "active" })).resolves.toBe("active");
131
+
132
+ await expect(tool!.execute({ status: "invalid" })).rejects.toThrow();
133
+ });
134
+
135
+ it("should handle array parameters", async () => {
136
+ registry.register({
137
+ name: "array_tool",
138
+ description: "Tool with array",
139
+ parameters: {
140
+ items: {
141
+ type: "array",
142
+ items: { type: "string" },
143
+ required: true,
144
+ },
145
+ },
146
+ execute: async ({ items }) => items,
147
+ });
148
+
149
+ const tool = registry.get("array_tool");
150
+
151
+ await expect(tool!.execute({ items: ["a", "b", "c"] })).resolves.toEqual([
152
+ "a",
153
+ "b",
154
+ "c",
155
+ ]);
156
+ });
157
+
158
+ it("should track stats", () => {
159
+ registry.register({
160
+ name: "stats_tool",
161
+ description: "For stats",
162
+ parameters: {},
163
+ execute: async () => null,
164
+ });
165
+
166
+ const stats = registry.getStats();
167
+ expect(stats.total).toBe(1);
168
+ expect(stats.byPlugin["core"]).toBe(1);
169
+ });
170
+
171
+ it("should convert to Tool interface", () => {
172
+ registry.register({
173
+ name: "convert_tool",
174
+ description: "For conversion",
175
+ parameters: {
176
+ input: { type: "string", description: "Input value" },
177
+ },
178
+ execute: async () => "result",
179
+ });
180
+
181
+ const tool = registry.toToolInterface("convert_tool");
182
+ expect(tool).toBeDefined();
183
+ expect(tool?.name).toBe("convert_tool");
184
+ expect(tool?.parameters.type).toBe("object");
185
+ expect(tool?.parameters.properties).toHaveProperty("input");
186
+ });
187
+
188
+ it("should register tools from plugins", () => {
189
+ registry.register(
190
+ {
191
+ name: "plugin_tool",
192
+ description: "From plugin",
193
+ parameters: {},
194
+ execute: async () => null,
195
+ },
196
+ "test-plugin"
197
+ );
198
+
199
+ const tool = registry.get("plugin_tool");
200
+ expect(tool?.pluginName).toBe("test-plugin");
201
+
202
+ const pluginTools = registry.listByPlugin("test-plugin");
203
+ expect(pluginTools.length).toBe(1);
204
+ });
205
+
206
+ it("should clear all tools", () => {
207
+ registry.register({
208
+ name: "clear1",
209
+ description: "To clear",
210
+ parameters: {},
211
+ execute: async () => null,
212
+ });
213
+ registry.register({
214
+ name: "clear2",
215
+ description: "To clear",
216
+ parameters: {},
217
+ execute: async () => null,
218
+ });
219
+
220
+ expect(registry.list().length).toBe(2);
221
+
222
+ registry.clear();
223
+
224
+ expect(registry.list().length).toBe(0);
225
+ });
226
+ });