@sesamespace/hivemind 0.5.17 → 0.6.1

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.
@@ -16,28 +16,46 @@ var LLMClient = class {
16
16
  this.temperature = config.temperature;
17
17
  this.apiKey = config.api_key ?? "";
18
18
  }
19
+ /**
20
+ * Simple chat completion (no tools). Backwards compatible.
21
+ */
19
22
  async chat(messages) {
23
+ return this.chatWithTools(messages);
24
+ }
25
+ /**
26
+ * Chat completion with optional tool definitions.
27
+ * Returns tool_calls if the model wants to use tools.
28
+ */
29
+ async chatWithTools(messages, tools) {
30
+ const body = {
31
+ model: this.model,
32
+ messages,
33
+ max_tokens: this.maxTokens,
34
+ temperature: this.temperature
35
+ };
36
+ if (tools && tools.length > 0) {
37
+ body.tools = tools;
38
+ body.tool_choice = "auto";
39
+ }
20
40
  const resp = await fetch(`${this.baseUrl}/chat/completions`, {
21
41
  method: "POST",
22
42
  headers: {
23
43
  "Content-Type": "application/json",
24
44
  ...this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}
25
45
  },
26
- body: JSON.stringify({
27
- model: this.model,
28
- messages,
29
- max_tokens: this.maxTokens,
30
- temperature: this.temperature
31
- })
46
+ body: JSON.stringify(body)
32
47
  });
33
48
  if (!resp.ok) {
34
- const body = await resp.text();
35
- throw new Error(`LLM request failed: ${resp.status} ${body}`);
49
+ const text = await resp.text();
50
+ throw new Error(`LLM request failed: ${resp.status} ${text}`);
36
51
  }
37
52
  const data = await resp.json();
53
+ const choice = data.choices[0];
38
54
  return {
39
- content: data.choices[0].message.content,
55
+ content: choice.message.content ?? "",
40
56
  model: data.model,
57
+ tool_calls: choice.message.tool_calls,
58
+ finish_reason: choice.finish_reason,
41
59
  usage: data.usage
42
60
  };
43
61
  }
@@ -618,11 +636,13 @@ var Agent = class {
618
636
  llm;
619
637
  memory;
620
638
  contextManager;
639
+ toolRegistry = null;
621
640
  // Per-context conversation histories
622
641
  conversationHistories = /* @__PURE__ */ new Map();
623
642
  messageCount = 0;
624
643
  PROMOTION_INTERVAL = 10;
625
644
  // Run promotion every N messages
645
+ MAX_TOOL_ITERATIONS = 25;
626
646
  requestLogger = null;
627
647
  constructor(config, contextName = "global") {
628
648
  this.config = config;
@@ -633,6 +653,13 @@ var Agent = class {
633
653
  this.contextManager.switchContext(contextName);
634
654
  }
635
655
  }
656
+ setToolRegistry(registry) {
657
+ this.toolRegistry = registry;
658
+ const toolCount = registry.getDefinitions().length;
659
+ if (toolCount > 0) {
660
+ console.log(`[hivemind] Tool use enabled: ${toolCount} tools registered`);
661
+ }
662
+ }
636
663
  setRequestLogger(logger) {
637
664
  this.requestLogger = logger;
638
665
  }
@@ -668,7 +695,31 @@ var Agent = class {
668
695
  const systemPromptResult = buildSystemPrompt(this.config.agent, relevantEpisodes, contextName, l3Knowledge);
669
696
  const messages = buildMessages(systemPromptResult.text, conversationHistory, userMessage);
670
697
  const llmStart = Date.now();
671
- const response = await this.llm.chat(messages);
698
+ const toolDefs = this.toolRegistry?.getDefinitions() ?? [];
699
+ let response = await this.llm.chatWithTools(messages, toolDefs.length > 0 ? toolDefs : void 0);
700
+ let iterations = 0;
701
+ while (response.tool_calls && response.tool_calls.length > 0 && iterations < this.MAX_TOOL_ITERATIONS) {
702
+ iterations++;
703
+ messages.push({
704
+ role: "assistant",
705
+ content: response.content,
706
+ tool_calls: response.tool_calls
707
+ });
708
+ for (const call of response.tool_calls) {
709
+ console.log(`[tools] ${call.function.name}(${call.function.arguments.slice(0, 100)})`);
710
+ const result = await this.toolRegistry.executeCall(call);
711
+ messages.push({
712
+ role: "tool",
713
+ content: result.content,
714
+ tool_call_id: result.tool_call_id
715
+ });
716
+ console.log(`[tools] ${call.function.name} \u2192 ${result.content.slice(0, 100)}${result.content.length > 100 ? "..." : ""}`);
717
+ }
718
+ response = await this.llm.chatWithTools(messages, toolDefs);
719
+ }
720
+ if (iterations > 0) {
721
+ console.log(`[tools] Completed after ${iterations} tool iteration(s)`);
722
+ }
672
723
  const latencyMs = Date.now() - llmStart;
673
724
  if (this.requestLogger) {
674
725
  try {
@@ -751,192 +802,6 @@ var Agent = class {
751
802
  let stripped = message.replace(/^\[.+?\s+in\s+group\s+chat\]:\s*/, "");
752
803
  stripped = stripped.replace(/^\[.+?\]:\s*/, "");
753
804
  const cmdText = stripped;
754
- if (/^hm[:\s]status\b/i.test(cmdText)) {
755
- const memoryOk = await this.memory.healthCheck();
756
- const contexts = this.contextManager.listContexts();
757
- let contextStats = "";
758
- try {
759
- const daemonContexts = await this.memory.listContexts();
760
- contextStats = daemonContexts.map((c) => ` - ${c.name}: ${c.episode_count} episodes`).join("\n");
761
- } catch {
762
- contextStats = " (unable to query memory daemon)";
763
- }
764
- const response = [
765
- `## Agent Status`,
766
- ``,
767
- `**Name:** ${this.config.agent.name}`,
768
- `**Active Context:** ${activeCtx}`,
769
- `**Memory Daemon:** ${memoryOk ? "\u2705 connected" : "\u274C unreachable"} (${this.config.memory.daemon_url})`,
770
- `**Model:** ${this.config.llm.model}`,
771
- `**Temperature:** ${this.config.llm.temperature}`,
772
- `**Max Tokens:** ${this.config.llm.max_tokens}`,
773
- `**Memory Top-K:** ${this.config.memory.top_k}`,
774
- `**Messages Processed:** ${this.messageCount}`,
775
- `**L1 Contexts in Memory:** ${this.conversationHistories.size}`,
776
- ``,
777
- `### Contexts`,
778
- contextStats
779
- ].join("\n");
780
- return { content: response, model: "system", context: activeCtx };
781
- }
782
- if (/^hm[:\s]memory\s+stats\b/i.test(cmdText)) {
783
- try {
784
- const contexts = await this.memory.listContexts();
785
- let totalEpisodes = 0;
786
- let totalL3 = 0;
787
- let lines = [];
788
- for (const ctx of contexts) {
789
- totalEpisodes += ctx.episode_count;
790
- let l3Count = 0;
791
- try {
792
- const l3 = await this.memory.getL3Knowledge(ctx.name);
793
- l3Count = l3.length;
794
- totalL3 += l3Count;
795
- } catch {
796
- }
797
- lines.push(` - **${ctx.name}**: ${ctx.episode_count} L2 episodes, ${l3Count} L3 entries`);
798
- }
799
- const response = [
800
- `## Memory Stats`,
801
- ``,
802
- `**Total L2 Episodes:** ${totalEpisodes}`,
803
- `**Total L3 Knowledge:** ${totalL3}`,
804
- `**Contexts:** ${contexts.length}`,
805
- ``,
806
- `### Per Context`,
807
- ...lines
808
- ].join("\n");
809
- return { content: response, model: "system", context: activeCtx };
810
- } catch (err) {
811
- return { content: `Memory stats failed: ${err.message}`, model: "system", context: activeCtx };
812
- }
813
- }
814
- const memSearchMatch = cmdText.match(/^hm[:\s]memory\s+search\s+(.+)/i);
815
- if (memSearchMatch) {
816
- const query = memSearchMatch[1].trim();
817
- try {
818
- const results = await this.memory.search(query, activeCtx, this.config.memory.top_k);
819
- if (results.length === 0) {
820
- return { content: `No memories found for "${query}" in context "${activeCtx}".`, model: "system", context: activeCtx };
821
- }
822
- let response = `## Memory Search: "${query}"
823
-
824
- `;
825
- for (const ep of results) {
826
- const ctxLabel = ep.context_name !== activeCtx ? ` [from: ${ep.context_name}]` : "";
827
- response += `- **[${ep.role}]** (score: ${ep.score.toFixed(3)})${ctxLabel} ${ep.content.slice(0, 300)}${ep.content.length > 300 ? "..." : ""}
828
- `;
829
- }
830
- return { content: response, model: "system", context: activeCtx };
831
- } catch (err) {
832
- return { content: `Memory search failed: ${err.message}`, model: "system", context: activeCtx };
833
- }
834
- }
835
- const l3Match = cmdText.match(/^hm[:\s]memory\s+l3(?:\s+(\S+))?/i);
836
- if (l3Match) {
837
- const targetCtx = l3Match[1] || activeCtx;
838
- try {
839
- const entries = await this.memory.getL3Knowledge(targetCtx);
840
- if (entries.length === 0) {
841
- return { content: `No L3 knowledge in context "${targetCtx}".`, model: "system", context: activeCtx };
842
- }
843
- let response = `## L3 Knowledge: ${targetCtx}
844
-
845
- `;
846
- for (const entry of entries) {
847
- response += `- **[${entry.id.slice(0, 8)}]** (density: ${entry.connection_density}, accesses: ${entry.access_count})
848
- ${entry.content}
849
- `;
850
- }
851
- return { content: response, model: "system", context: activeCtx };
852
- } catch (err) {
853
- return { content: `L3 query failed: ${err.message}`, model: "system", context: activeCtx };
854
- }
855
- }
856
- if (/^hm[:\s]config\b/i.test(cmdText)) {
857
- const response = [
858
- `## Configuration`,
859
- ``,
860
- `**Agent:** ${this.config.agent.name}`,
861
- `**Personality:** ${this.config.agent.personality}`,
862
- `**Workspace:** ${this.config.agent.workspace || "(none)"}`,
863
- ``,
864
- `### LLM`,
865
- `- Model: ${this.config.llm.model}`,
866
- `- Base URL: ${this.config.llm.base_url}`,
867
- `- Max Tokens: ${this.config.llm.max_tokens}`,
868
- `- Temperature: ${this.config.llm.temperature}`,
869
- ``,
870
- `### Memory`,
871
- `- Daemon URL: ${this.config.memory.daemon_url}`,
872
- `- Top-K: ${this.config.memory.top_k}`,
873
- `- Embedding Model: ${this.config.memory.embedding_model}`,
874
- ``,
875
- `### Sesame`,
876
- `- API: ${this.config.sesame.api_url}`,
877
- `- WebSocket: ${this.config.sesame.ws_url}`,
878
- `- API Key: ${this.config.sesame.api_key ? "***" + this.config.sesame.api_key.slice(-4) : "(not set)"}`
879
- ].join("\n");
880
- return { content: response, model: "system", context: activeCtx };
881
- }
882
- if (/^hm[:\s]contexts\b/i.test(cmdText)) {
883
- const localContexts = this.contextManager.listContexts();
884
- let daemonInfo = "";
885
- try {
886
- const daemonContexts = await this.memory.listContexts();
887
- daemonInfo = "\n### Memory Daemon Contexts\n" + daemonContexts.map(
888
- (c) => ` - **${c.name}**: ${c.episode_count} episodes (created: ${c.created_at})`
889
- ).join("\n");
890
- } catch {
891
- daemonInfo = "\n(Memory daemon unreachable)";
892
- }
893
- const localInfo = localContexts.map((c) => {
894
- const active = c.name === activeCtx ? " \u2190 active" : "";
895
- const historyLen = this.conversationHistories.get(c.name)?.length || 0;
896
- return ` - **${c.name}**${active}: ${historyLen / 2} turns in L1`;
897
- }).join("\n");
898
- const response = [
899
- `## Contexts`,
900
- ``,
901
- `**Active:** ${activeCtx}`,
902
- ``,
903
- `### Local (L1 Working Memory)`,
904
- localInfo,
905
- daemonInfo
906
- ].join("\n");
907
- return { content: response, model: "system", context: activeCtx };
908
- }
909
- if (/^hm[:\s]help\b/i.test(cmdText)) {
910
- const response = [
911
- `## Available Commands`,
912
- ``,
913
- `### Introspection`,
914
- `- \`hm:status\` \u2014 Agent health, memory status, config summary`,
915
- `- \`hm:config\` \u2014 Full configuration details`,
916
- `- \`hm:contexts\` \u2014 List all contexts with L1/L2 info`,
917
- `- \`hm:memory stats\` \u2014 Episode counts, L3 entries per context`,
918
- `- \`hm:memory search <query>\` \u2014 Search L2 memories with scores`,
919
- `- \`hm:memory l3 [context]\` \u2014 Show L3 knowledge entries`,
920
- `- \`hm:help\` \u2014 This help message`,
921
- ``,
922
- `### Context Management`,
923
- `- \`switch to <name>\` \u2014 Switch active context`,
924
- `- \`create context <name>\` \u2014 Create a new context`,
925
- `- \`list contexts\` \u2014 List contexts (legacy)`,
926
- ``,
927
- `### Memory`,
928
- `- \`search all: <query>\` \u2014 Cross-context search`,
929
- `- \`share <episode_id> with <context>\` \u2014 Share episode across contexts`,
930
- ``,
931
- `### Tasks`,
932
- `- \`task add <title>\` \u2014 Create a task`,
933
- `- \`task list [status]\` \u2014 List tasks`,
934
- `- \`task start <id>\` \u2014 Start a task`,
935
- `- \`task complete <id>\` \u2014 Complete a task`,
936
- `- \`task next\` \u2014 Pick up next available task`
937
- ].join("\n");
938
- return { content: response, model: "system", context: activeCtx };
939
- }
940
805
  const searchAllMatch = cmdText.match(/^(?:search\s+all|cross-context\s+search)[:\s]+(.+)/i);
941
806
  if (searchAllMatch) {
942
807
  const query = searchAllMatch[1].trim();
@@ -1896,12 +1761,7 @@ var SesameClient2 = class {
1896
1761
  this.sdk.on("message", (event) => {
1897
1762
  const msg = event.data || event.message || event;
1898
1763
  const senderId = msg.senderId || msg.sender?.id;
1899
- if (senderId === this.agentId) {
1900
- const content = msg.content?.trim() || "";
1901
- const isHmCommand = /^hm[:\s]/i.test(content);
1902
- const isTextCommand = /^(?:list contexts|switch to|create context|search all:|task )/i.test(content);
1903
- if (!isHmCommand && !isTextCommand) return;
1904
- }
1764
+ if (senderId === this.agentId) return;
1905
1765
  if (!this.messageHandler || !msg.content) return;
1906
1766
  const channelInfo = this.channels.get(msg.channelId);
1907
1767
  this.messageHandler({
@@ -2247,14 +2107,550 @@ function startDashboardServer(requestLogger, memoryConfig) {
2247
2107
  });
2248
2108
  }
2249
2109
 
2110
+ // packages/runtime/src/tool-registry.ts
2111
+ var ToolRegistry = class {
2112
+ tools = /* @__PURE__ */ new Map();
2113
+ register(name, description, parameters, executor) {
2114
+ this.tools.set(name, {
2115
+ definition: {
2116
+ type: "function",
2117
+ function: { name, description, parameters }
2118
+ },
2119
+ executor
2120
+ });
2121
+ }
2122
+ getDefinitions() {
2123
+ return Array.from(this.tools.values()).map((t) => t.definition);
2124
+ }
2125
+ has(name) {
2126
+ return this.tools.has(name);
2127
+ }
2128
+ async execute(name, params) {
2129
+ const tool = this.tools.get(name);
2130
+ if (!tool) {
2131
+ return `Error: Unknown tool "${name}"`;
2132
+ }
2133
+ try {
2134
+ return await tool.executor(params);
2135
+ } catch (err) {
2136
+ return `Error executing ${name}: ${err.message}`;
2137
+ }
2138
+ }
2139
+ async executeCall(call) {
2140
+ let params = {};
2141
+ try {
2142
+ params = JSON.parse(call.function.arguments);
2143
+ } catch {
2144
+ return {
2145
+ tool_call_id: call.id,
2146
+ role: "tool",
2147
+ content: `Error: Invalid JSON arguments for ${call.function.name}`
2148
+ };
2149
+ }
2150
+ const result = await this.execute(call.function.name, params);
2151
+ return {
2152
+ tool_call_id: call.id,
2153
+ role: "tool",
2154
+ content: result
2155
+ };
2156
+ }
2157
+ };
2158
+
2159
+ // packages/runtime/src/tools/shell.ts
2160
+ import { execSync } from "child_process";
2161
+ import { resolve as resolve5 } from "path";
2162
+ var MAX_OUTPUT = 5e4;
2163
+ function registerShellTool(registry, workspaceDir) {
2164
+ registry.register(
2165
+ "shell",
2166
+ "Execute a shell command and return stdout/stderr. Use for running programs, checking status, installing packages, git operations, etc.",
2167
+ {
2168
+ type: "object",
2169
+ properties: {
2170
+ command: {
2171
+ type: "string",
2172
+ description: "The shell command to execute"
2173
+ },
2174
+ workdir: {
2175
+ type: "string",
2176
+ description: "Working directory (relative to workspace). Defaults to workspace root."
2177
+ },
2178
+ timeout: {
2179
+ type: "number",
2180
+ description: "Timeout in seconds. Default: 30"
2181
+ }
2182
+ },
2183
+ required: ["command"]
2184
+ },
2185
+ async (params) => {
2186
+ const command = params.command;
2187
+ const timeout = (params.timeout || 30) * 1e3;
2188
+ const cwd = params.workdir ? resolve5(workspaceDir, params.workdir) : workspaceDir;
2189
+ try {
2190
+ const output = execSync(command, {
2191
+ cwd,
2192
+ timeout,
2193
+ encoding: "utf-8",
2194
+ maxBuffer: 10 * 1024 * 1024,
2195
+ // 10MB
2196
+ shell: "/bin/sh",
2197
+ env: { ...process.env, PATH: `/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:${process.env.PATH}` }
2198
+ });
2199
+ const trimmed = output.length > MAX_OUTPUT ? output.slice(0, MAX_OUTPUT) + `
2200
+ ... (truncated, ${output.length} total chars)` : output;
2201
+ return trimmed || "(no output)";
2202
+ } catch (err) {
2203
+ const stderr = err.stderr?.toString() || "";
2204
+ const stdout = err.stdout?.toString() || "";
2205
+ const output = (stdout + "\n" + stderr).trim();
2206
+ const code = err.status ?? "unknown";
2207
+ return `Command failed (exit code ${code}):
2208
+ ${output || err.message}`;
2209
+ }
2210
+ }
2211
+ );
2212
+ }
2213
+
2214
+ // packages/runtime/src/tools/files.ts
2215
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
2216
+ import { resolve as resolve6, dirname as dirname5 } from "path";
2217
+ var MAX_READ = 1e5;
2218
+ function registerFileTools(registry, workspaceDir) {
2219
+ registry.register(
2220
+ "read_file",
2221
+ "Read the contents of a file. Returns the text content. Use offset/limit for large files.",
2222
+ {
2223
+ type: "object",
2224
+ properties: {
2225
+ path: {
2226
+ type: "string",
2227
+ description: "File path (relative to workspace or absolute)"
2228
+ },
2229
+ offset: {
2230
+ type: "number",
2231
+ description: "Line number to start reading from (1-indexed)"
2232
+ },
2233
+ limit: {
2234
+ type: "number",
2235
+ description: "Maximum number of lines to read"
2236
+ }
2237
+ },
2238
+ required: ["path"]
2239
+ },
2240
+ async (params) => {
2241
+ const filePath = resolvePath(workspaceDir, params.path);
2242
+ if (!existsSync4(filePath)) {
2243
+ return `Error: File not found: ${filePath}`;
2244
+ }
2245
+ try {
2246
+ let content = readFileSync6(filePath, "utf-8");
2247
+ if (params.offset || params.limit) {
2248
+ const lines = content.split("\n");
2249
+ const start = (params.offset || 1) - 1;
2250
+ const end = params.limit ? start + params.limit : lines.length;
2251
+ content = lines.slice(start, end).join("\n");
2252
+ }
2253
+ if (content.length > MAX_READ) {
2254
+ content = content.slice(0, MAX_READ) + `
2255
+ ... (truncated, ${content.length} total chars)`;
2256
+ }
2257
+ return content || "(empty file)";
2258
+ } catch (err) {
2259
+ return `Error reading file: ${err.message}`;
2260
+ }
2261
+ }
2262
+ );
2263
+ registry.register(
2264
+ "write_file",
2265
+ "Write content to a file. Creates the file and parent directories if they don't exist. Overwrites existing content.",
2266
+ {
2267
+ type: "object",
2268
+ properties: {
2269
+ path: {
2270
+ type: "string",
2271
+ description: "File path (relative to workspace or absolute)"
2272
+ },
2273
+ content: {
2274
+ type: "string",
2275
+ description: "Content to write to the file"
2276
+ }
2277
+ },
2278
+ required: ["path", "content"]
2279
+ },
2280
+ async (params) => {
2281
+ const filePath = resolvePath(workspaceDir, params.path);
2282
+ try {
2283
+ const dir = dirname5(filePath);
2284
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
2285
+ writeFileSync2(filePath, params.content);
2286
+ return `Written ${params.content.length} bytes to ${filePath}`;
2287
+ } catch (err) {
2288
+ return `Error writing file: ${err.message}`;
2289
+ }
2290
+ }
2291
+ );
2292
+ registry.register(
2293
+ "edit_file",
2294
+ "Edit a file by finding and replacing exact text. The old text must match exactly (including whitespace).",
2295
+ {
2296
+ type: "object",
2297
+ properties: {
2298
+ path: {
2299
+ type: "string",
2300
+ description: "File path (relative to workspace or absolute)"
2301
+ },
2302
+ old_text: {
2303
+ type: "string",
2304
+ description: "Exact text to find and replace"
2305
+ },
2306
+ new_text: {
2307
+ type: "string",
2308
+ description: "New text to replace with"
2309
+ }
2310
+ },
2311
+ required: ["path", "old_text", "new_text"]
2312
+ },
2313
+ async (params) => {
2314
+ const filePath = resolvePath(workspaceDir, params.path);
2315
+ if (!existsSync4(filePath)) {
2316
+ return `Error: File not found: ${filePath}`;
2317
+ }
2318
+ try {
2319
+ const content = readFileSync6(filePath, "utf-8");
2320
+ const oldText = params.old_text;
2321
+ const newText = params.new_text;
2322
+ if (!content.includes(oldText)) {
2323
+ return `Error: Could not find the exact text to replace in ${filePath}`;
2324
+ }
2325
+ const updated = content.replace(oldText, newText);
2326
+ writeFileSync2(filePath, updated);
2327
+ return `Edited ${filePath}: replaced ${oldText.length} chars with ${newText.length} chars`;
2328
+ } catch (err) {
2329
+ return `Error editing file: ${err.message}`;
2330
+ }
2331
+ }
2332
+ );
2333
+ registry.register(
2334
+ "list_files",
2335
+ "List files and directories in a path.",
2336
+ {
2337
+ type: "object",
2338
+ properties: {
2339
+ path: {
2340
+ type: "string",
2341
+ description: "Directory path (relative to workspace or absolute). Defaults to workspace root."
2342
+ }
2343
+ },
2344
+ required: []
2345
+ },
2346
+ async (params) => {
2347
+ const { readdirSync: readdirSync2, statSync } = await import("fs");
2348
+ const dirPath = params.path ? resolvePath(workspaceDir, params.path) : workspaceDir;
2349
+ if (!existsSync4(dirPath)) {
2350
+ return `Error: Directory not found: ${dirPath}`;
2351
+ }
2352
+ try {
2353
+ const entries = readdirSync2(dirPath);
2354
+ const results = [];
2355
+ for (const entry of entries) {
2356
+ if (entry.startsWith(".")) continue;
2357
+ try {
2358
+ const stat = statSync(resolve6(dirPath, entry));
2359
+ results.push(stat.isDirectory() ? `${entry}/` : entry);
2360
+ } catch {
2361
+ results.push(entry);
2362
+ }
2363
+ }
2364
+ return results.join("\n") || "(empty directory)";
2365
+ } catch (err) {
2366
+ return `Error listing directory: ${err.message}`;
2367
+ }
2368
+ }
2369
+ );
2370
+ }
2371
+ function resolvePath(workspace, path) {
2372
+ if (path.startsWith("/") || path.startsWith("~")) {
2373
+ return path.replace(/^~/, process.env.HOME || "/root");
2374
+ }
2375
+ return resolve6(workspace, path);
2376
+ }
2377
+
2378
+ // packages/runtime/src/tools/web.ts
2379
+ function registerWebTools(registry, config) {
2380
+ registry.register(
2381
+ "web_search",
2382
+ "Search the web using Brave Search API. Returns titles, URLs, and snippets.",
2383
+ {
2384
+ type: "object",
2385
+ properties: {
2386
+ query: {
2387
+ type: "string",
2388
+ description: "Search query string"
2389
+ },
2390
+ count: {
2391
+ type: "number",
2392
+ description: "Number of results (1-10, default 5)"
2393
+ }
2394
+ },
2395
+ required: ["query"]
2396
+ },
2397
+ async (params) => {
2398
+ const apiKey = config?.braveApiKey || process.env.BRAVE_API_KEY;
2399
+ if (!apiKey) {
2400
+ return "Error: No Brave Search API key configured. Set BRAVE_API_KEY env var.";
2401
+ }
2402
+ const query = params.query;
2403
+ const count = Math.min(Math.max(params.count || 5, 1), 10);
2404
+ try {
2405
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
2406
+ const resp = await fetch(url, {
2407
+ headers: { "X-Subscription-Token": apiKey, Accept: "application/json" }
2408
+ });
2409
+ if (!resp.ok) {
2410
+ return `Search failed: ${resp.status} ${await resp.text()}`;
2411
+ }
2412
+ const data = await resp.json();
2413
+ const results = data.web?.results || [];
2414
+ if (results.length === 0) return "No results found.";
2415
+ return results.map((r, i) => `${i + 1}. **${r.title}**
2416
+ ${r.url}
2417
+ ${r.description}`).join("\n\n");
2418
+ } catch (err) {
2419
+ return `Search error: ${err.message}`;
2420
+ }
2421
+ }
2422
+ );
2423
+ registry.register(
2424
+ "web_fetch",
2425
+ "Fetch a URL and extract readable content as markdown/text. Good for reading articles, docs, and web pages.",
2426
+ {
2427
+ type: "object",
2428
+ properties: {
2429
+ url: {
2430
+ type: "string",
2431
+ description: "HTTP or HTTPS URL to fetch"
2432
+ },
2433
+ max_chars: {
2434
+ type: "number",
2435
+ description: "Maximum characters to return (default 20000)"
2436
+ }
2437
+ },
2438
+ required: ["url"]
2439
+ },
2440
+ async (params) => {
2441
+ const url = params.url;
2442
+ const maxChars = params.max_chars || 2e4;
2443
+ try {
2444
+ const resp = await fetch(url, {
2445
+ headers: {
2446
+ "User-Agent": "Hivemind/1.0 (Agent Web Fetcher)",
2447
+ Accept: "text/html,application/xhtml+xml,text/plain,application/json"
2448
+ },
2449
+ redirect: "follow"
2450
+ });
2451
+ if (!resp.ok) {
2452
+ return `Fetch failed: ${resp.status} ${resp.statusText}`;
2453
+ }
2454
+ let text = await resp.text();
2455
+ const contentType = resp.headers.get("content-type") || "";
2456
+ if (contentType.includes("html")) {
2457
+ text = text.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "");
2458
+ text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
2459
+ text = text.replace(/<[^>]+>/g, " ");
2460
+ text = text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
2461
+ text = text.replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ");
2462
+ text = text.replace(/\s+/g, " ").trim();
2463
+ }
2464
+ if (text.length > maxChars) {
2465
+ text = text.slice(0, maxChars) + `
2466
+ ... (truncated, ${text.length} total chars)`;
2467
+ }
2468
+ return text || "(empty response)";
2469
+ } catch (err) {
2470
+ return `Fetch error: ${err.message}`;
2471
+ }
2472
+ }
2473
+ );
2474
+ }
2475
+
2476
+ // packages/runtime/src/tools/memory.ts
2477
+ function registerMemoryTools(registry, daemonUrl) {
2478
+ registry.register(
2479
+ "memory_search",
2480
+ "Search your episodic memory for relevant past conversations and experiences. Returns scored results from your memory system. Use this when you want to recall something from a previous conversation or check what you know about a topic.",
2481
+ {
2482
+ type: "object",
2483
+ properties: {
2484
+ query: {
2485
+ type: "string",
2486
+ description: "What to search for in memory"
2487
+ },
2488
+ context: {
2489
+ type: "string",
2490
+ description: "Context to search in (default: current active context)"
2491
+ },
2492
+ top_k: {
2493
+ type: "number",
2494
+ description: "Number of results to return (default: 10)"
2495
+ }
2496
+ },
2497
+ required: ["query"]
2498
+ },
2499
+ async (params) => {
2500
+ const query = params.query;
2501
+ const context = params.context || "global";
2502
+ const topK = params.top_k || 10;
2503
+ try {
2504
+ const resp = await fetch(`${daemonUrl}/api/search`, {
2505
+ method: "POST",
2506
+ headers: { "Content-Type": "application/json" },
2507
+ body: JSON.stringify({ query, context_name: context, top_k: topK })
2508
+ });
2509
+ if (!resp.ok) return `Memory search failed: ${resp.status}`;
2510
+ const data = await resp.json();
2511
+ if (!data.results?.length) return `No memories found for "${query}" in context "${context}".`;
2512
+ return data.results.map((r, i) => `${i + 1}. [score: ${r.score.toFixed(3)}] [${r.context_name}] [${r.role}] ${r.content.slice(0, 200)}`).join("\n");
2513
+ } catch (err) {
2514
+ return `Memory daemon unreachable: ${err.message}`;
2515
+ }
2516
+ }
2517
+ );
2518
+ registry.register(
2519
+ "memory_stats",
2520
+ "Get statistics about your memory system \u2014 episode counts, contexts, L3 knowledge entries. Use for self-diagnosis or understanding your memory state.",
2521
+ {
2522
+ type: "object",
2523
+ properties: {},
2524
+ required: []
2525
+ },
2526
+ async () => {
2527
+ try {
2528
+ const healthResp = await fetch(`${daemonUrl}/health`);
2529
+ const health = healthResp.ok ? await healthResp.json() : { status: "unreachable" };
2530
+ const ctxResp = await fetch(`${daemonUrl}/api/contexts`);
2531
+ const contexts = ctxResp.ok ? (await ctxResp.json()).contexts : [];
2532
+ const l3Counts = {};
2533
+ for (const ctx of contexts) {
2534
+ try {
2535
+ const l3Resp = await fetch(`${daemonUrl}/api/l3/${encodeURIComponent(ctx.name)}`);
2536
+ if (l3Resp.ok) {
2537
+ const l3Data = await l3Resp.json();
2538
+ l3Counts[ctx.name] = l3Data.entries?.length ?? 0;
2539
+ }
2540
+ } catch {
2541
+ }
2542
+ }
2543
+ const totalEpisodes = contexts.reduce((sum, c) => sum + c.episode_count, 0);
2544
+ const totalL3 = Object.values(l3Counts).reduce((sum, c) => sum + c, 0);
2545
+ let output = `Memory Daemon: ${health.status || "unknown"}
2546
+ `;
2547
+ output += `Total Episodes (L2): ${totalEpisodes}
2548
+ `;
2549
+ output += `Total Knowledge (L3): ${totalL3}
2550
+ `;
2551
+ output += `
2552
+ Contexts:
2553
+ `;
2554
+ for (const ctx of contexts) {
2555
+ output += ` - ${ctx.name}: ${ctx.episode_count} episodes, ${l3Counts[ctx.name] ?? 0} L3 entries
2556
+ `;
2557
+ }
2558
+ return output;
2559
+ } catch (err) {
2560
+ return `Memory daemon unreachable: ${err.message}`;
2561
+ }
2562
+ }
2563
+ );
2564
+ registry.register(
2565
+ "memory_l3",
2566
+ "View your promoted L3 knowledge \u2014 high-level patterns, decisions, and insights that have been distilled from your episodic memories. This is your 'long-term wisdom'.",
2567
+ {
2568
+ type: "object",
2569
+ properties: {
2570
+ context: {
2571
+ type: "string",
2572
+ description: "Context to get L3 knowledge for (default: global)"
2573
+ }
2574
+ },
2575
+ required: []
2576
+ },
2577
+ async (params) => {
2578
+ const context = params.context || "global";
2579
+ try {
2580
+ const resp = await fetch(`${daemonUrl}/api/l3/${encodeURIComponent(context)}`);
2581
+ if (!resp.ok) return `L3 query failed: ${resp.status}`;
2582
+ const data = await resp.json();
2583
+ if (!data.entries?.length) return `No L3 knowledge in context "${context}".`;
2584
+ return data.entries.map((e, i) => `${i + 1}. ${e.content} (from ${e.source_episodes} episodes, ${e.created_at})`).join("\n");
2585
+ } catch (err) {
2586
+ return `Memory daemon unreachable: ${err.message}`;
2587
+ }
2588
+ }
2589
+ );
2590
+ registry.register(
2591
+ "memory_cross_search",
2592
+ "Search across ALL memory contexts at once. Useful when you're not sure which context a memory belongs to.",
2593
+ {
2594
+ type: "object",
2595
+ properties: {
2596
+ query: {
2597
+ type: "string",
2598
+ description: "What to search for across all contexts"
2599
+ },
2600
+ top_k: {
2601
+ type: "number",
2602
+ description: "Number of results (default: 10)"
2603
+ }
2604
+ },
2605
+ required: ["query"]
2606
+ },
2607
+ async (params) => {
2608
+ const query = params.query;
2609
+ const topK = params.top_k || 10;
2610
+ try {
2611
+ const resp = await fetch(`${daemonUrl}/api/search/cross-context`, {
2612
+ method: "POST",
2613
+ headers: { "Content-Type": "application/json" },
2614
+ body: JSON.stringify({ query, top_k: topK })
2615
+ });
2616
+ if (!resp.ok) return `Cross-context search failed: ${resp.status}`;
2617
+ const data = await resp.json();
2618
+ if (!data.results?.length) return `No memories found across any context for "${query}".`;
2619
+ return data.results.map((r, i) => `${i + 1}. [${r.score.toFixed(3)}] [${r.context_name}] ${r.content.slice(0, 200)}`).join("\n");
2620
+ } catch (err) {
2621
+ return `Memory daemon unreachable: ${err.message}`;
2622
+ }
2623
+ }
2624
+ );
2625
+ }
2626
+
2627
+ // packages/runtime/src/tools/register.ts
2628
+ import { resolve as resolve7 } from "path";
2629
+ import { mkdirSync as mkdirSync3, existsSync as existsSync5 } from "fs";
2630
+ function registerAllTools(hivemindHome, config) {
2631
+ const registry = new ToolRegistry();
2632
+ if (config?.enabled === false) {
2633
+ return registry;
2634
+ }
2635
+ const workspaceDir = resolve7(hivemindHome, config?.workspace || "workspace");
2636
+ if (!existsSync5(workspaceDir)) {
2637
+ mkdirSync3(workspaceDir, { recursive: true });
2638
+ }
2639
+ registerShellTool(registry, workspaceDir);
2640
+ registerFileTools(registry, workspaceDir);
2641
+ registerWebTools(registry, { braveApiKey: config?.braveApiKey });
2642
+ registerMemoryTools(registry, config?.memoryDaemonUrl || "http://localhost:3434");
2643
+ return registry;
2644
+ }
2645
+
2250
2646
  // packages/runtime/src/pipeline.ts
2251
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, unlinkSync } from "fs";
2252
- import { resolve as resolve5, dirname as dirname5 } from "path";
2647
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3, unlinkSync } from "fs";
2648
+ import { resolve as resolve8, dirname as dirname6 } from "path";
2253
2649
  import { fileURLToPath as fileURLToPath3 } from "url";
2254
2650
  var PACKAGE_VERSION = "unknown";
2255
2651
  try {
2256
- const __dirname2 = dirname5(fileURLToPath3(import.meta.url));
2257
- const pkg = JSON.parse(readFileSync6(resolve5(__dirname2, "../package.json"), "utf-8"));
2652
+ const __dirname2 = dirname6(fileURLToPath3(import.meta.url));
2653
+ const pkg = JSON.parse(readFileSync7(resolve8(__dirname2, "../package.json"), "utf-8"));
2258
2654
  PACKAGE_VERSION = pkg.version ?? "unknown";
2259
2655
  } catch {
2260
2656
  }
@@ -2285,7 +2681,7 @@ function startHealthServer(port) {
2285
2681
  return server;
2286
2682
  }
2287
2683
  function writePidFile(path) {
2288
- writeFileSync2(path, String(process.pid));
2684
+ writeFileSync3(path, String(process.pid));
2289
2685
  console.log(`[hivemind] PID file written: ${path}`);
2290
2686
  }
2291
2687
  function cleanupPidFile(path) {
@@ -2316,10 +2712,18 @@ async function startPipeline(configPath) {
2316
2712
  memoryConnected = true;
2317
2713
  console.log("[hivemind] Memory daemon connected");
2318
2714
  }
2319
- const requestLogger = new RequestLogger(resolve5(dirname5(configPath), "data", "dashboard.db"));
2715
+ const requestLogger = new RequestLogger(resolve8(dirname6(configPath), "data", "dashboard.db"));
2320
2716
  startDashboardServer(requestLogger, config.memory);
2321
2717
  const agent = new Agent(config);
2322
2718
  agent.setRequestLogger(requestLogger);
2719
+ const hivemindHome = process.env.HIVEMIND_HOME || resolve8(process.env.HOME || "/root", "hivemind");
2720
+ const toolRegistry = registerAllTools(hivemindHome, {
2721
+ enabled: true,
2722
+ workspace: config.agent.workspace || "workspace",
2723
+ braveApiKey: process.env.BRAVE_API_KEY,
2724
+ memoryDaemonUrl: config.memory.daemon_url
2725
+ });
2726
+ agent.setToolRegistry(toolRegistry);
2323
2727
  console.log(`[hivemind] Context manager initialized (active: ${agent.getActiveContext()})`);
2324
2728
  if (config.sesame.api_key) {
2325
2729
  await startSesameLoop(config, agent);
@@ -2467,8 +2871,8 @@ ${response.content}
2467
2871
  console.error("Error:", err.message);
2468
2872
  }
2469
2873
  });
2470
- return new Promise((resolve6) => {
2471
- rl.on("close", resolve6);
2874
+ return new Promise((resolve9) => {
2875
+ rl.on("close", resolve9);
2472
2876
  });
2473
2877
  }
2474
2878
 
@@ -2497,20 +2901,20 @@ var WorkerServer = class {
2497
2901
  }
2498
2902
  /** Start listening. */
2499
2903
  async start() {
2500
- return new Promise((resolve6, reject) => {
2904
+ return new Promise((resolve9, reject) => {
2501
2905
  this.server = createServer3((req, res) => this.handleRequest(req, res));
2502
2906
  this.server.on("error", reject);
2503
- this.server.listen(this.port, () => resolve6());
2907
+ this.server.listen(this.port, () => resolve9());
2504
2908
  });
2505
2909
  }
2506
2910
  /** Stop the server. */
2507
2911
  async stop() {
2508
- return new Promise((resolve6) => {
2912
+ return new Promise((resolve9) => {
2509
2913
  if (!this.server) {
2510
- resolve6();
2914
+ resolve9();
2511
2915
  return;
2512
2916
  }
2513
- this.server.close(() => resolve6());
2917
+ this.server.close(() => resolve9());
2514
2918
  });
2515
2919
  }
2516
2920
  getPort() {
@@ -2633,10 +3037,10 @@ var WorkerServer = class {
2633
3037
  }
2634
3038
  };
2635
3039
  function readBody(req) {
2636
- return new Promise((resolve6, reject) => {
3040
+ return new Promise((resolve9, reject) => {
2637
3041
  const chunks = [];
2638
3042
  req.on("data", (chunk) => chunks.push(chunk));
2639
- req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf-8")));
3043
+ req.on("end", () => resolve9(Buffer.concat(chunks).toString("utf-8")));
2640
3044
  req.on("error", reject);
2641
3045
  });
2642
3046
  }
@@ -2963,4 +3367,4 @@ smol-toml/dist/index.js:
2963
3367
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2964
3368
  *)
2965
3369
  */
2966
- //# sourceMappingURL=chunk-OPOXV53N.js.map
3370
+ //# sourceMappingURL=chunk-7YHRRM5B.js.map