@sesamespace/hivemind 0.2.0 → 0.3.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 (72) hide show
  1. package/PLANNING.md +383 -0
  2. package/TASKS.md +60 -0
  3. package/install.sh +187 -0
  4. package/npm-package.json +28 -0
  5. package/package.json +13 -20
  6. package/packages/cli/package.json +23 -0
  7. package/{dist/chunk-DVR2KBL7.js → packages/cli/src/commands/fleet.ts} +50 -30
  8. package/packages/cli/src/commands/init.ts +230 -0
  9. package/{dist/chunk-MBS5A6BZ.js → packages/cli/src/commands/service.ts} +51 -42
  10. package/{dist/chunk-RNK5Q5GR.js → packages/cli/src/commands/start.ts} +12 -14
  11. package/{dist/main.js → packages/cli/src/main.ts} +12 -18
  12. package/packages/cli/tsconfig.json +8 -0
  13. package/packages/memory/Cargo.lock +6480 -0
  14. package/packages/memory/Cargo.toml +21 -0
  15. package/packages/memory/src/context.rs +179 -0
  16. package/packages/memory/src/embeddings.rs +51 -0
  17. package/packages/memory/src/main.rs +626 -0
  18. package/packages/memory/src/promotion.rs +637 -0
  19. package/packages/memory/src/scoring.rs +131 -0
  20. package/packages/memory/src/store.rs +460 -0
  21. package/packages/memory/src/tasks.rs +321 -0
  22. package/packages/runtime/package.json +24 -0
  23. package/packages/runtime/src/__tests__/fleet-integration.test.ts +235 -0
  24. package/packages/runtime/src/__tests__/fleet.test.ts +207 -0
  25. package/packages/runtime/src/__tests__/integration.test.ts +434 -0
  26. package/packages/runtime/src/agent.ts +255 -0
  27. package/packages/runtime/src/config.ts +130 -0
  28. package/packages/runtime/src/context.ts +192 -0
  29. package/packages/runtime/src/fleet/fleet-manager.ts +399 -0
  30. package/packages/runtime/src/fleet/memory-sync.ts +362 -0
  31. package/packages/runtime/src/fleet/primary-client.ts +285 -0
  32. package/packages/runtime/src/fleet/worker-protocol.ts +158 -0
  33. package/packages/runtime/src/fleet/worker-server.ts +246 -0
  34. package/packages/runtime/src/index.ts +57 -0
  35. package/packages/runtime/src/llm-client.ts +65 -0
  36. package/packages/runtime/src/memory-client.ts +309 -0
  37. package/packages/runtime/src/pipeline.ts +151 -0
  38. package/packages/runtime/src/prompt.ts +173 -0
  39. package/packages/runtime/src/sesame.ts +174 -0
  40. package/{dist/start.js → packages/runtime/src/start.ts} +7 -9
  41. package/packages/runtime/src/task-engine.ts +113 -0
  42. package/packages/runtime/src/worker.ts +339 -0
  43. package/packages/runtime/tsconfig.json +8 -0
  44. package/pnpm-workspace.yaml +2 -0
  45. package/run-aidan.sh +23 -0
  46. package/scripts/bootstrap.sh +196 -0
  47. package/scripts/build-npm.sh +94 -0
  48. package/scripts/com.hivemind.agent.plist +44 -0
  49. package/scripts/com.hivemind.memory.plist +31 -0
  50. package/tsconfig.json +22 -0
  51. package/tsup.config.ts +28 -0
  52. package/dist/chunk-2I2O6X5D.js +0 -1408
  53. package/dist/chunk-2I2O6X5D.js.map +0 -1
  54. package/dist/chunk-DVR2KBL7.js.map +0 -1
  55. package/dist/chunk-MBS5A6BZ.js.map +0 -1
  56. package/dist/chunk-NVJ424TB.js +0 -731
  57. package/dist/chunk-NVJ424TB.js.map +0 -1
  58. package/dist/chunk-RNK5Q5GR.js.map +0 -1
  59. package/dist/chunk-XNOWVLXD.js +0 -160
  60. package/dist/chunk-XNOWVLXD.js.map +0 -1
  61. package/dist/commands/fleet.js +0 -9
  62. package/dist/commands/fleet.js.map +0 -1
  63. package/dist/commands/init.js +0 -7
  64. package/dist/commands/init.js.map +0 -1
  65. package/dist/commands/service.js +0 -7
  66. package/dist/commands/service.js.map +0 -1
  67. package/dist/commands/start.js +0 -9
  68. package/dist/commands/start.js.map +0 -1
  69. package/dist/index.js +0 -41
  70. package/dist/index.js.map +0 -1
  71. package/dist/main.js.map +0 -1
  72. package/dist/start.js.map +0 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Shared types for Primary <-> Worker communication protocol.
3
+ *
4
+ * Communication is HTTP-based:
5
+ * - Primary calls Worker endpoints to assign contexts, check health
6
+ * - Worker calls Primary endpoints to register, report status
7
+ */
8
+
9
+ // --- Worker Identity & Registration ---
10
+
11
+ export interface WorkerInfo {
12
+ id: string;
13
+ url: string;
14
+ capabilities: WorkerCapabilities;
15
+ assigned_contexts: string[];
16
+ registered_at: string;
17
+ last_heartbeat: string;
18
+ }
19
+
20
+ export interface WorkerCapabilities {
21
+ max_contexts: number;
22
+ has_ollama: boolean;
23
+ has_memory_daemon: boolean;
24
+ available_models: string[];
25
+ }
26
+
27
+ export interface WorkerRegistrationRequest {
28
+ url: string;
29
+ capabilities: WorkerCapabilities;
30
+ }
31
+
32
+ export interface WorkerRegistrationResponse {
33
+ worker_id: string;
34
+ registered_at: string;
35
+ }
36
+
37
+ // --- Health Checks ---
38
+
39
+ export type WorkerHealthStatus = "healthy" | "degraded" | "unreachable";
40
+
41
+ export interface WorkerHealthResponse {
42
+ worker_id: string;
43
+ status: WorkerHealthStatus;
44
+ uptime_seconds: number;
45
+ assigned_contexts: string[];
46
+ active_context: string | null;
47
+ memory_daemon_ok: boolean;
48
+ ollama_ok: boolean;
49
+ }
50
+
51
+ // --- Context Assignment ---
52
+
53
+ export interface ContextAssignment {
54
+ context_name: string;
55
+ context_description: string;
56
+ assigned_at: string;
57
+ }
58
+
59
+ export interface ContextAssignRequest {
60
+ context_name: string;
61
+ context_description: string;
62
+ }
63
+
64
+ export interface ContextAssignResponse {
65
+ context_name: string;
66
+ accepted: boolean;
67
+ reason?: string;
68
+ }
69
+
70
+ // --- Worker Status Reporting ---
71
+
72
+ export type WorkerActivity = "idle" | "working" | "error";
73
+
74
+ export interface WorkerStatus {
75
+ worker_id: string;
76
+ activity: WorkerActivity;
77
+ current_context: string | null;
78
+ current_task: string | null;
79
+ error?: string;
80
+ reported_at: string;
81
+ }
82
+
83
+ export interface WorkerStatusReport {
84
+ activity: WorkerActivity;
85
+ current_context: string | null;
86
+ current_task: string | null;
87
+ error?: string;
88
+ }
89
+
90
+ // --- Memory Sync ---
91
+
92
+ /** L3 knowledge entries synced from Worker to Primary. */
93
+ export interface SyncPullRequest {
94
+ worker_id: string;
95
+ context_name: string;
96
+ entries: SyncL3Entry[];
97
+ }
98
+
99
+ /** An L3 entry with a last-modified timestamp for conflict resolution. */
100
+ export interface SyncL3Entry {
101
+ id: string;
102
+ source_episode_id: string;
103
+ context_name: string;
104
+ content: string;
105
+ promoted_at: string;
106
+ access_count: number;
107
+ connection_density: number;
108
+ updated_at: string;
109
+ }
110
+
111
+ export interface SyncPullResponse {
112
+ accepted: number;
113
+ rejected: number;
114
+ }
115
+
116
+ /** Global context updates pushed from Primary to Worker. */
117
+ export interface SyncPushRequest {
118
+ entries: SyncL3Entry[];
119
+ episodes: SyncL2Episode[];
120
+ }
121
+
122
+ /** L2 episodes synced as append-only (no conflict resolution needed). */
123
+ export interface SyncL2Episode {
124
+ id: string;
125
+ timestamp: string;
126
+ context_name: string;
127
+ role: string;
128
+ content: string;
129
+ }
130
+
131
+ export interface SyncPushResponse {
132
+ l3_accepted: number;
133
+ l2_appended: number;
134
+ }
135
+
136
+ // --- Protocol Constants ---
137
+
138
+ export const WORKER_API_PREFIX = "/workers";
139
+
140
+ /** Primary-side routes (Worker calls these) */
141
+ export const PRIMARY_ROUTES = {
142
+ register: `${WORKER_API_PREFIX}/register`,
143
+ status: (workerId: string) => `${WORKER_API_PREFIX}/${encodeURIComponent(workerId)}/status`,
144
+ syncPull: `${WORKER_API_PREFIX}/sync/pull`,
145
+ } as const;
146
+
147
+ /** Worker-side routes (Primary calls these) */
148
+ export const WORKER_ROUTES = {
149
+ health: "/health",
150
+ assign: "/assign",
151
+ unassign: (contextName: string) => `/assign/${encodeURIComponent(contextName)}`,
152
+ status: "/status",
153
+ syncPush: "/sync/push",
154
+ } as const;
155
+
156
+ export const DEFAULT_HEALTH_INTERVAL_MS = 30_000;
157
+ export const HEALTH_TIMEOUT_MS = 5_000;
158
+ export const DEFAULT_SYNC_INTERVAL_MS = 60_000;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Worker-side HTTP server that exposes endpoints for the Primary to call.
3
+ *
4
+ * Uses Node's built-in http module — no external framework needed for these few routes.
5
+ *
6
+ * Endpoints:
7
+ * GET /health — Health check
8
+ * POST /assign — Accept a context assignment
9
+ * DELETE /assign/:contextName — Release a context
10
+ * GET /status — Current activity status
11
+ */
12
+
13
+ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "http";
14
+ import type {
15
+ WorkerHealthResponse,
16
+ WorkerStatusReport,
17
+ ContextAssignRequest,
18
+ ContextAssignResponse,
19
+ ContextAssignment,
20
+ SyncPushRequest,
21
+ SyncPushResponse,
22
+ } from "./worker-protocol.js";
23
+
24
+ export interface WorkerServerOptions {
25
+ workerId: string;
26
+ port: number;
27
+ maxContexts?: number;
28
+ memoryDaemonUrl?: string;
29
+ ollamaUrl?: string;
30
+ }
31
+
32
+ export class WorkerServer {
33
+ private server: Server | null = null;
34
+ private workerId: string;
35
+ private port: number;
36
+ private startTime: number;
37
+ private assignedContexts: Map<string, ContextAssignment> = new Map();
38
+ private activeContext: string | null = null;
39
+ private currentTask: string | null = null;
40
+ private maxContexts: number;
41
+ private memoryDaemonUrl: string | null;
42
+ private ollamaUrl: string | null;
43
+ private onAssignCallback: ((contextName: string, description: string) => void) | null = null;
44
+ private onSyncPushCallback: ((req: SyncPushRequest) => Promise<SyncPushResponse>) | null = null;
45
+
46
+ constructor(opts: WorkerServerOptions) {
47
+ this.workerId = opts.workerId;
48
+ this.port = opts.port;
49
+ this.startTime = Date.now();
50
+ this.maxContexts = opts.maxContexts ?? 4;
51
+ this.memoryDaemonUrl = opts.memoryDaemonUrl ?? null;
52
+ this.ollamaUrl = opts.ollamaUrl ?? null;
53
+ }
54
+
55
+ /** Start listening. */
56
+ async start(): Promise<void> {
57
+ return new Promise((resolve, reject) => {
58
+ this.server = createServer((req, res) => this.handleRequest(req, res));
59
+
60
+ this.server.on("error", reject);
61
+ this.server.listen(this.port, () => resolve());
62
+ });
63
+ }
64
+
65
+ /** Stop the server. */
66
+ async stop(): Promise<void> {
67
+ return new Promise((resolve) => {
68
+ if (!this.server) {
69
+ resolve();
70
+ return;
71
+ }
72
+ this.server.close(() => resolve());
73
+ });
74
+ }
75
+
76
+ getPort(): number {
77
+ return this.port;
78
+ }
79
+
80
+ getAssignedContexts(): string[] {
81
+ return Array.from(this.assignedContexts.keys());
82
+ }
83
+
84
+ setActiveContext(name: string | null): void {
85
+ this.activeContext = name;
86
+ }
87
+
88
+ setCurrentTask(taskId: string | null): void {
89
+ this.currentTask = taskId;
90
+ }
91
+
92
+ /** Register a callback for when a new context is assigned. */
93
+ onContextAssigned(cb: (contextName: string, description: string) => void): void {
94
+ this.onAssignCallback = cb;
95
+ }
96
+
97
+ /** Register a handler for incoming sync push requests from Primary. */
98
+ onSyncPush(cb: (req: SyncPushRequest) => Promise<SyncPushResponse>): void {
99
+ this.onSyncPushCallback = cb;
100
+ }
101
+
102
+ // --- Request Router ---
103
+
104
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
105
+ const url = new URL(req.url ?? "/", `http://localhost:${this.port}`);
106
+ const path = url.pathname;
107
+ const method = req.method ?? "GET";
108
+
109
+ try {
110
+ if (method === "GET" && path === "/health") {
111
+ return this.handleHealth(res);
112
+ }
113
+
114
+ if (method === "POST" && path === "/assign") {
115
+ const body = await readBody(req);
116
+ return this.handleAssign(body, res);
117
+ }
118
+
119
+ // DELETE /assign/:contextName
120
+ if (method === "DELETE" && path.startsWith("/assign/")) {
121
+ const contextName = decodeURIComponent(path.slice("/assign/".length));
122
+ return this.handleUnassign(contextName, res);
123
+ }
124
+
125
+ if (method === "GET" && path === "/status") {
126
+ return this.handleStatus(res);
127
+ }
128
+
129
+ if (method === "POST" && path === "/sync/push") {
130
+ const body = await readBody(req);
131
+ return this.handleSyncPush(body, res);
132
+ }
133
+
134
+ sendJson(res, 404, { error: "Not found" });
135
+ } catch (err) {
136
+ const msg = err instanceof Error ? err.message : String(err);
137
+ sendJson(res, 500, { error: msg });
138
+ }
139
+ }
140
+
141
+ // --- Handlers ---
142
+
143
+ private handleHealth(res: ServerResponse): void {
144
+ const uptimeMs = Date.now() - this.startTime;
145
+
146
+ const body: WorkerHealthResponse = {
147
+ worker_id: this.workerId,
148
+ status: "healthy",
149
+ uptime_seconds: Math.floor(uptimeMs / 1000),
150
+ assigned_contexts: Array.from(this.assignedContexts.keys()),
151
+ active_context: this.activeContext,
152
+ memory_daemon_ok: this.memoryDaemonUrl !== null,
153
+ ollama_ok: this.ollamaUrl !== null,
154
+ };
155
+
156
+ sendJson(res, 200, body);
157
+ }
158
+
159
+ private handleAssign(raw: string, res: ServerResponse): void {
160
+ const req = JSON.parse(raw) as ContextAssignRequest;
161
+
162
+ if (this.assignedContexts.size >= this.maxContexts) {
163
+ const body: ContextAssignResponse = {
164
+ context_name: req.context_name,
165
+ accepted: false,
166
+ reason: "Worker at max context capacity",
167
+ };
168
+ sendJson(res, 200, body);
169
+ return;
170
+ }
171
+
172
+ const assignment: ContextAssignment = {
173
+ context_name: req.context_name,
174
+ context_description: req.context_description,
175
+ assigned_at: new Date().toISOString(),
176
+ };
177
+
178
+ this.assignedContexts.set(req.context_name, assignment);
179
+
180
+ // Notify the worker runtime about the new context
181
+ if (this.onAssignCallback) {
182
+ this.onAssignCallback(req.context_name, req.context_description);
183
+ }
184
+
185
+ const body: ContextAssignResponse = {
186
+ context_name: req.context_name,
187
+ accepted: true,
188
+ };
189
+ sendJson(res, 200, body);
190
+ }
191
+
192
+ private handleUnassign(contextName: string, res: ServerResponse): void {
193
+ const existed = this.assignedContexts.delete(contextName);
194
+
195
+ if (this.activeContext === contextName) {
196
+ this.activeContext = null;
197
+ this.currentTask = null;
198
+ }
199
+
200
+ sendJson(res, existed ? 200 : 404, {
201
+ context_name: contextName,
202
+ removed: existed,
203
+ });
204
+ }
205
+
206
+ private handleStatus(res: ServerResponse): void {
207
+ const report: WorkerStatusReport = {
208
+ activity: this.currentTask ? "working" : "idle",
209
+ current_context: this.activeContext,
210
+ current_task: this.currentTask,
211
+ };
212
+
213
+ sendJson(res, 200, report);
214
+ }
215
+
216
+ private async handleSyncPush(raw: string, res: ServerResponse): Promise<void> {
217
+ if (!this.onSyncPushCallback) {
218
+ sendJson(res, 501, { error: "Sync push handler not registered" });
219
+ return;
220
+ }
221
+
222
+ const req = JSON.parse(raw) as SyncPushRequest;
223
+ const result = await this.onSyncPushCallback(req);
224
+ sendJson(res, 200, result);
225
+ }
226
+ }
227
+
228
+ // --- Helpers ---
229
+
230
+ function readBody(req: IncomingMessage): Promise<string> {
231
+ return new Promise((resolve, reject) => {
232
+ const chunks: Buffer[] = [];
233
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
234
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
235
+ req.on("error", reject);
236
+ });
237
+ }
238
+
239
+ function sendJson(res: ServerResponse, status: number, body: unknown): void {
240
+ const payload = JSON.stringify(body);
241
+ res.writeHead(status, {
242
+ "Content-Type": "application/json",
243
+ "Content-Length": Buffer.byteLength(payload),
244
+ });
245
+ res.end(payload);
246
+ }
@@ -0,0 +1,57 @@
1
+ export { Agent } from "./agent.js";
2
+ export { MemoryClient } from "./memory-client.js";
3
+ export { LLMClient } from "./llm-client.js";
4
+ export { ContextManager } from "./context.js";
5
+ export { TaskEngine } from "./task-engine.js";
6
+ export { loadConfig } from "./config.js";
7
+ export { buildSystemPrompt, buildMessages } from "./prompt.js";
8
+ export { SesameClient } from "./sesame.js";
9
+ export { startPipeline } from "./pipeline.js";
10
+
11
+ export type { HivemindConfig, AgentConfig, LLMConfig, MemoryConfig, SesameConfig, WorkerModeConfig } from "./config.js";
12
+ export type { ChatMessage, ChatResponse } from "./llm-client.js";
13
+ export type {
14
+ Episode,
15
+ ScoredEpisode,
16
+ EpisodeInput,
17
+ ContextInfo,
18
+ CrossContextResult,
19
+ PromotionResult,
20
+ L3Entry,
21
+ TaskRecord,
22
+ TaskInput,
23
+ } from "./memory-client.js";
24
+ export type { AgentResponse } from "./agent.js";
25
+ export type { SesameMessage } from "./sesame.js";
26
+ export type { ContextMetadata, ContextSwitchResult } from "./context.js";
27
+ export type { TaskCommand, TaskStatus } from "./task-engine.js";
28
+
29
+ // Fleet (Phase 3)
30
+ export { PrimaryClient } from "./fleet/primary-client.js";
31
+ export { WorkerServer } from "./fleet/worker-server.js";
32
+ export { WorkerRuntime, startWorker } from "./worker.js";
33
+ export { FleetManager } from "./fleet/fleet-manager.js";
34
+ export { WorkerMemorySync, PrimaryMemorySync } from "./fleet/memory-sync.js";
35
+ export type { FleetDashboard, WorkerSummary, MigrationResult } from "./fleet/fleet-manager.js";
36
+ export type { WorkerServerOptions } from "./fleet/worker-server.js";
37
+ export type { WorkerSyncOptions, PrimarySyncOptions } from "./fleet/memory-sync.js";
38
+ export type {
39
+ WorkerInfo,
40
+ WorkerCapabilities,
41
+ WorkerRegistrationRequest,
42
+ WorkerRegistrationResponse,
43
+ WorkerHealthResponse,
44
+ WorkerHealthStatus,
45
+ WorkerStatus,
46
+ WorkerStatusReport,
47
+ WorkerActivity,
48
+ ContextAssignment,
49
+ ContextAssignRequest,
50
+ ContextAssignResponse,
51
+ SyncPullRequest,
52
+ SyncPullResponse,
53
+ SyncPushRequest,
54
+ SyncPushResponse,
55
+ SyncL3Entry,
56
+ SyncL2Episode,
57
+ } from "./fleet/worker-protocol.js";
@@ -0,0 +1,65 @@
1
+ import type { LLMConfig } from "./config.js";
2
+
3
+ export interface ChatMessage {
4
+ role: "system" | "user" | "assistant";
5
+ content: string;
6
+ }
7
+
8
+ export interface ChatResponse {
9
+ content: string;
10
+ model: string;
11
+ usage?: {
12
+ prompt_tokens: number;
13
+ completion_tokens: number;
14
+ total_tokens: number;
15
+ };
16
+ }
17
+
18
+ export class LLMClient {
19
+ private baseUrl: string;
20
+ private model: string;
21
+ private maxTokens: number;
22
+ private temperature: number;
23
+ private apiKey: string;
24
+
25
+ constructor(config: LLMConfig) {
26
+ this.baseUrl = config.base_url;
27
+ this.model = config.model;
28
+ this.maxTokens = config.max_tokens;
29
+ this.temperature = config.temperature;
30
+ this.apiKey = config.api_key ?? "";
31
+ }
32
+
33
+ async chat(messages: ChatMessage[]): Promise<ChatResponse> {
34
+ const resp = await fetch(`${this.baseUrl}/chat/completions`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}),
39
+ },
40
+ body: JSON.stringify({
41
+ model: this.model,
42
+ messages,
43
+ max_tokens: this.maxTokens,
44
+ temperature: this.temperature,
45
+ }),
46
+ });
47
+
48
+ if (!resp.ok) {
49
+ const body = await resp.text();
50
+ throw new Error(`LLM request failed: ${resp.status} ${body}`);
51
+ }
52
+
53
+ const data = (await resp.json()) as {
54
+ choices: Array<{ message: { content: string } }>;
55
+ model: string;
56
+ usage?: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
57
+ };
58
+
59
+ return {
60
+ content: data.choices[0].message.content,
61
+ model: data.model,
62
+ usage: data.usage,
63
+ };
64
+ }
65
+ }