@sesamespace/hivemind 0.8.13 → 0.11.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.
@@ -1343,14 +1343,15 @@ var Agent = class {
1343
1343
  });
1344
1344
  if (relevantEpisodes.length > 0) {
1345
1345
  const episodeIds = relevantEpisodes.map((e) => e.id);
1346
- this.memory.recordCoAccess(episodeIds).catch(() => {
1347
- });
1346
+ this.memory.recordCoAccess(episodeIds).catch((err) => console.warn("[memory] recordCoAccess failed:", err.message));
1348
1347
  for (const ep of relevantEpisodes) {
1349
- this.memory.recordAccess(ep.id).catch(() => {
1350
- });
1348
+ this.memory.recordAccess(ep.id).catch((err) => console.warn("[memory] recordAccess failed:", err.message));
1351
1349
  }
1352
1350
  }
1353
- const l3Knowledge = await this.memory.getL3Knowledge(contextName).catch(() => []);
1351
+ const l3Knowledge = await this.memory.getL3Knowledge(contextName).catch((err) => {
1352
+ console.warn("[memory] getL3Knowledge failed:", err.message);
1353
+ return [];
1354
+ });
1354
1355
  const systemPromptResult = buildSystemPrompt({
1355
1356
  config: this.config.agent,
1356
1357
  episodes: relevantEpisodes,
@@ -1587,6 +1588,9 @@ var Agent = class {
1587
1588
  getActiveContext() {
1588
1589
  return this.contextManager.getActiveContext();
1589
1590
  }
1591
+ getConversationHistories() {
1592
+ return this.conversationHistories;
1593
+ }
1590
1594
  };
1591
1595
 
1592
1596
  // packages/runtime/src/events.ts
@@ -4745,29 +4749,43 @@ function parseQuery(url) {
4745
4749
  }
4746
4750
  return params;
4747
4751
  }
4748
- async function proxyMemory(memoryUrl, path, method, res) {
4752
+ function readBody(req) {
4753
+ return new Promise((resolve21, reject) => {
4754
+ const chunks = [];
4755
+ req.on("data", (chunk) => chunks.push(chunk));
4756
+ req.on("end", () => resolve21(Buffer.concat(chunks).toString()));
4757
+ req.on("error", reject);
4758
+ });
4759
+ }
4760
+ async function proxyMemory(memoryUrl, path, method, res, body) {
4749
4761
  try {
4750
- const resp = await fetch(`${memoryUrl}${path}`, { method });
4751
- const body = await resp.text();
4762
+ const opts = { method };
4763
+ if (body && (method === "POST" || method === "PATCH" || method === "PUT")) {
4764
+ opts.body = body;
4765
+ opts.headers = { "Content-Type": "application/json" };
4766
+ }
4767
+ const resp = await fetch(`${memoryUrl}${path}`, opts);
4768
+ const respBody = await resp.text();
4752
4769
  res.writeHead(resp.status, {
4753
4770
  "Content-Type": resp.headers.get("content-type") ?? "application/json",
4754
4771
  "Access-Control-Allow-Origin": "*"
4755
4772
  });
4756
- res.end(body);
4773
+ res.end(respBody);
4757
4774
  } catch (err) {
4758
4775
  json(res, { error: err.message }, 502);
4759
4776
  }
4760
4777
  }
4761
- function startDashboardServer(requestLogger, memoryConfig) {
4778
+ function startDashboardServer(requestLogger, memoryConfig, getL1) {
4762
4779
  const memoryUrl = memoryConfig.daemon_url;
4763
4780
  const server = createServer(async (req, res) => {
4764
4781
  const method = req.method ?? "GET";
4765
4782
  const rawUrl = req.url ?? "/";
4766
4783
  const urlPath = rawUrl.split("?")[0];
4784
+ const queryStr = rawUrl.includes("?") ? rawUrl.slice(rawUrl.indexOf("?")) : "";
4767
4785
  if (method === "OPTIONS") {
4768
4786
  res.writeHead(204, {
4769
4787
  "Access-Control-Allow-Origin": "*",
4770
- "Access-Control-Allow-Methods": "GET, DELETE, OPTIONS",
4788
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
4771
4789
  "Access-Control-Allow-Headers": "Content-Type"
4772
4790
  });
4773
4791
  res.end();
@@ -4800,10 +4818,32 @@ function startDashboardServer(requestLogger, memoryConfig) {
4800
4818
  }
4801
4819
  return;
4802
4820
  }
4821
+ if (method === "GET" && urlPath === "/api/health") {
4822
+ await proxyMemory(memoryUrl, "/health", "GET", res);
4823
+ return;
4824
+ }
4825
+ if (method === "GET" && urlPath === "/api/stats") {
4826
+ await proxyMemory(memoryUrl, "/stats", "GET", res);
4827
+ return;
4828
+ }
4829
+ if (method === "GET" && urlPath === "/api/search") {
4830
+ await proxyMemory(memoryUrl, `/search${queryStr}`, "GET", res);
4831
+ return;
4832
+ }
4833
+ if (method === "GET" && urlPath === "/api/search/cross-context") {
4834
+ await proxyMemory(memoryUrl, `/search/cross-context${queryStr}`, "GET", res);
4835
+ return;
4836
+ }
4803
4837
  if (method === "GET" && urlPath === "/api/contexts") {
4804
4838
  await proxyMemory(memoryUrl, "/contexts", "GET", res);
4805
4839
  return;
4806
4840
  }
4841
+ const ctxDeleteMatch = urlPath.match(/^\/api\/contexts\/([^/]+)$/);
4842
+ if (method === "DELETE" && ctxDeleteMatch) {
4843
+ const name = decodeURIComponent(ctxDeleteMatch[1]);
4844
+ await proxyMemory(memoryUrl, `/contexts/${encodeURIComponent(name)}`, "DELETE", res);
4845
+ return;
4846
+ }
4807
4847
  const episodesMatch = urlPath.match(/^\/api\/contexts\/([^/]+)\/episodes$/);
4808
4848
  if (method === "GET" && episodesMatch) {
4809
4849
  const name = decodeURIComponent(episodesMatch[1]);
@@ -4821,12 +4861,69 @@ function startDashboardServer(requestLogger, memoryConfig) {
4821
4861
  );
4822
4862
  return;
4823
4863
  }
4824
- const l3DeleteMatch = urlPath.match(/^\/api\/l3\/([^/]+)$/);
4825
- if (method === "DELETE" && l3DeleteMatch) {
4826
- const id = decodeURIComponent(l3DeleteMatch[1]);
4864
+ const scoringGetMatch = urlPath.match(/^\/api\/contexts\/([^/]+)\/scoring$/);
4865
+ if (method === "GET" && scoringGetMatch) {
4866
+ const name = decodeURIComponent(scoringGetMatch[1]);
4867
+ await proxyMemory(memoryUrl, `/scoring/${encodeURIComponent(name)}`, "GET", res);
4868
+ return;
4869
+ }
4870
+ if (method === "POST" && scoringGetMatch) {
4871
+ const name = decodeURIComponent(scoringGetMatch[1]);
4872
+ const body = await readBody(req);
4873
+ await proxyMemory(memoryUrl, `/contexts/${encodeURIComponent(name)}/scoring`, "POST", res, body);
4874
+ return;
4875
+ }
4876
+ const l3IdMatch = urlPath.match(/^\/api\/l3\/([^/]+)$/);
4877
+ if (method === "DELETE" && l3IdMatch) {
4878
+ const id = decodeURIComponent(l3IdMatch[1]);
4827
4879
  await proxyMemory(memoryUrl, `/promotion/l3/${encodeURIComponent(id)}`, "DELETE", res);
4828
4880
  return;
4829
4881
  }
4882
+ if (method === "PATCH" && l3IdMatch) {
4883
+ const id = decodeURIComponent(l3IdMatch[1]);
4884
+ const body = await readBody(req);
4885
+ await proxyMemory(memoryUrl, `/promotion/l3/${encodeURIComponent(id)}`, "PATCH", res, body);
4886
+ return;
4887
+ }
4888
+ if (method === "POST" && urlPath === "/api/promotion/run") {
4889
+ await proxyMemory(memoryUrl, `/promotion/run${queryStr}`, "POST", res);
4890
+ return;
4891
+ }
4892
+ if (method === "GET" && urlPath === "/api/access/top") {
4893
+ await proxyMemory(memoryUrl, `/access/top${queryStr}`, "GET", res);
4894
+ return;
4895
+ }
4896
+ const accessMatch = urlPath.match(/^\/api\/episodes\/([^/]+)\/access$/);
4897
+ if (method === "GET" && accessMatch) {
4898
+ const id = decodeURIComponent(accessMatch[1]);
4899
+ await proxyMemory(memoryUrl, `/access/${encodeURIComponent(id)}`, "GET", res);
4900
+ return;
4901
+ }
4902
+ if (method === "GET" && urlPath === "/api/l1") {
4903
+ if (!getL1) {
4904
+ json(res, { contexts: {} });
4905
+ return;
4906
+ }
4907
+ const histories = getL1();
4908
+ const result = {};
4909
+ for (const [ctx, msgs] of histories) {
4910
+ result[ctx] = { count: msgs.length };
4911
+ }
4912
+ json(res, { contexts: result });
4913
+ return;
4914
+ }
4915
+ const l1CtxMatch = urlPath.match(/^\/api\/l1\/([^/]+)$/);
4916
+ if (method === "GET" && l1CtxMatch) {
4917
+ if (!getL1) {
4918
+ json(res, { messages: [] });
4919
+ return;
4920
+ }
4921
+ const ctx = decodeURIComponent(l1CtxMatch[1]);
4922
+ const histories = getL1();
4923
+ const messages = histories.get(ctx) || [];
4924
+ json(res, { messages: messages.map((m) => ({ role: m.role, content: m.content ?? "" })) });
4925
+ return;
4926
+ }
4830
4927
  json(res, { error: "Not found" }, 404);
4831
4928
  } catch (err) {
4832
4929
  console.error("[dashboard] Request error:", err.message);
@@ -6039,6 +6136,7 @@ import { mkdirSync as mkdirSync9, existsSync as existsSync13 } from "fs";
6039
6136
  import { randomUUID as randomUUID5 } from "crypto";
6040
6137
  var MAX_OUTPUT2 = 5e4;
6041
6138
  var browserInstance = null;
6139
+ var contextInstances = /* @__PURE__ */ new Map();
6042
6140
  var lastUsed = 0;
6043
6141
  var idleTimer = null;
6044
6142
  var IDLE_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -6047,7 +6145,16 @@ async function getBrowser() {
6047
6145
  const modName = "playwright";
6048
6146
  const pw = await Function("m", "return import(m)")(modName);
6049
6147
  if (!browserInstance) {
6050
- browserInstance = await pw.chromium.launch({ headless: true });
6148
+ browserInstance = await pw.chromium.launch({
6149
+ headless: true,
6150
+ args: [
6151
+ "--disable-dev-shm-usage",
6152
+ "--disable-setuid-sandbox",
6153
+ "--no-sandbox",
6154
+ "--disable-web-security",
6155
+ "--disable-features=VizDisplayCompositor"
6156
+ ]
6157
+ });
6051
6158
  if (!idleTimer) {
6052
6159
  idleTimer = setInterval(async () => {
6053
6160
  if (browserInstance && Date.now() - lastUsed > IDLE_TIMEOUT_MS) {
@@ -6064,7 +6171,43 @@ async function getBrowser() {
6064
6171
  );
6065
6172
  }
6066
6173
  }
6174
+ async function getBrowserContext(sessionId = "default", options = {}) {
6175
+ const browser = await getBrowser();
6176
+ if (!contextInstances.has(sessionId)) {
6177
+ const contextOptions = {
6178
+ userAgent: options.userAgent || "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6179
+ viewport: options.viewport || { width: 1280, height: 720 },
6180
+ locale: options.locale || "en-US",
6181
+ timezoneId: options.timezone || "America/New_York",
6182
+ deviceScaleFactor: options.deviceScaleFactor || 1,
6183
+ isMobile: options.isMobile || false,
6184
+ hasTouch: options.hasTouch || false,
6185
+ colorScheme: options.colorScheme || "light",
6186
+ reducedMotion: options.reducedMotion || "no-preference",
6187
+ permissions: options.permissions || [],
6188
+ geolocation: options.geolocation,
6189
+ offline: options.offline || false,
6190
+ acceptDownloads: options.downloadBehavior !== "deny"
6191
+ };
6192
+ const context = await browser.newContext(contextOptions);
6193
+ context.on("request", (request) => {
6194
+ console.log(`\u2192 ${request.method()} ${request.url()}`);
6195
+ });
6196
+ context.on("response", (response) => {
6197
+ console.log(`\u2190 ${response.status()} ${response.url()}`);
6198
+ });
6199
+ contextInstances.set(sessionId, context);
6200
+ }
6201
+ return contextInstances.get(sessionId);
6202
+ }
6067
6203
  async function closeBrowser() {
6204
+ for (const [sessionId, context] of contextInstances) {
6205
+ try {
6206
+ await context.close();
6207
+ } catch {
6208
+ }
6209
+ }
6210
+ contextInstances.clear();
6068
6211
  if (browserInstance) {
6069
6212
  try {
6070
6213
  await browserInstance.close();
@@ -6082,14 +6225,26 @@ function registerBrowserTools(registry, workspaceDir) {
6082
6225
  registry.register(
6083
6226
  "browse",
6084
6227
  [
6085
- "Navigate to a URL and interact with the page using a headless browser.",
6228
+ "Navigate to a URL and interact with the page using an enhanced headless browser with session persistence.",
6086
6229
  "Actions:",
6087
6230
  " extract (default) \u2014 get the readable text content of the page",
6088
6231
  " screenshot \u2014 take a PNG screenshot and return the file path",
6089
6232
  " click \u2014 click an element matching a CSS selector",
6090
6233
  " type \u2014 type text into an element matching a CSS selector",
6091
6234
  " evaluate \u2014 run arbitrary JavaScript in the page and return the result",
6092
- "Supports waitForSelector to wait for dynamic content before acting."
6235
+ " form_fill \u2014 fill out a form with multiple fields at once",
6236
+ " scroll \u2014 scroll the page (up, down, to element, or to coordinates)",
6237
+ " hover \u2014 hover over an element",
6238
+ " select \u2014 select an option from a dropdown",
6239
+ " upload \u2014 upload a file to a file input",
6240
+ " download \u2014 download a file and return the path",
6241
+ " pdf \u2014 generate a PDF of the page",
6242
+ " wait_for \u2014 wait for various conditions (selector, text, url, timeout)",
6243
+ " network_logs \u2014 get network request/response logs",
6244
+ " console_logs \u2014 get browser console logs",
6245
+ " cookies \u2014 get/set/clear cookies",
6246
+ " storage \u2014 get/set/clear localStorage/sessionStorage",
6247
+ "Supports session persistence, mobile simulation, and advanced browser features."
6093
6248
  ].join("\n"),
6094
6249
  {
6095
6250
  type: "object",
@@ -6100,21 +6255,101 @@ function registerBrowserTools(registry, workspaceDir) {
6100
6255
  },
6101
6256
  action: {
6102
6257
  type: "string",
6103
- enum: ["extract", "screenshot", "click", "type", "evaluate"],
6258
+ enum: [
6259
+ "extract",
6260
+ "screenshot",
6261
+ "click",
6262
+ "type",
6263
+ "evaluate",
6264
+ "form_fill",
6265
+ "scroll",
6266
+ "hover",
6267
+ "select",
6268
+ "upload",
6269
+ "download",
6270
+ "pdf",
6271
+ "wait_for",
6272
+ "network_logs",
6273
+ "console_logs",
6274
+ "cookies",
6275
+ "storage"
6276
+ ],
6104
6277
  description: "Action to perform (default: extract)."
6105
6278
  },
6106
6279
  selector: {
6107
6280
  type: "string",
6108
- description: "CSS selector for click/type actions."
6281
+ description: "CSS selector for element-based actions."
6109
6282
  },
6110
6283
  text: {
6111
6284
  type: "string",
6112
- description: "Text to type (for 'type' action)."
6285
+ description: "Text to type or search for."
6113
6286
  },
6114
6287
  javascript: {
6115
6288
  type: "string",
6116
6289
  description: "JavaScript to evaluate in the page (for 'evaluate' action)."
6117
6290
  },
6291
+ formData: {
6292
+ type: "object",
6293
+ description: "Key-value pairs for form filling (selector: value)."
6294
+ },
6295
+ scrollDirection: {
6296
+ type: "string",
6297
+ enum: ["up", "down", "left", "right", "top", "bottom"],
6298
+ description: "Scroll direction or position."
6299
+ },
6300
+ scrollDistance: {
6301
+ type: "number",
6302
+ description: "Pixels to scroll (default: 500)."
6303
+ },
6304
+ coordinates: {
6305
+ type: "object",
6306
+ properties: {
6307
+ x: { type: "number" },
6308
+ y: { type: "number" }
6309
+ },
6310
+ description: "X,Y coordinates for scrolling or clicking."
6311
+ },
6312
+ filePath: {
6313
+ type: "string",
6314
+ description: "Path to file for upload action."
6315
+ },
6316
+ waitCondition: {
6317
+ type: "string",
6318
+ enum: ["selector", "text", "url", "timeout", "networkidle"],
6319
+ description: "What to wait for."
6320
+ },
6321
+ waitValue: {
6322
+ type: "string",
6323
+ description: "Value to wait for (selector, text, URL pattern)."
6324
+ },
6325
+ cookieData: {
6326
+ type: "object",
6327
+ description: "Cookie data for cookie operations."
6328
+ },
6329
+ storageData: {
6330
+ type: "object",
6331
+ description: "Storage data for localStorage/sessionStorage operations."
6332
+ },
6333
+ session: {
6334
+ type: "string",
6335
+ description: "Session ID for persistent browser context (default: 'default')."
6336
+ },
6337
+ viewport: {
6338
+ type: "object",
6339
+ properties: {
6340
+ width: { type: "number" },
6341
+ height: { type: "number" }
6342
+ },
6343
+ description: "Browser viewport size."
6344
+ },
6345
+ userAgent: {
6346
+ type: "string",
6347
+ description: "Custom user agent string."
6348
+ },
6349
+ mobile: {
6350
+ type: "boolean",
6351
+ description: "Simulate mobile device."
6352
+ },
6118
6353
  waitForSelector: {
6119
6354
  type: "string",
6120
6355
  description: "CSS selector to wait for before performing the action."
@@ -6132,19 +6367,33 @@ function registerBrowserTools(registry, workspaceDir) {
6132
6367
  const selector = params.selector;
6133
6368
  const text = params.text;
6134
6369
  const javascript = params.javascript;
6370
+ const formData = params.formData;
6371
+ const scrollDirection = params.scrollDirection;
6372
+ const scrollDistance = params.scrollDistance || 500;
6373
+ const coordinates = params.coordinates;
6374
+ const filePath = params.filePath;
6375
+ const waitCondition = params.waitCondition;
6376
+ const waitValue = params.waitValue;
6377
+ const cookieData = params.cookieData;
6378
+ const storageData = params.storageData;
6379
+ const sessionId = params.session || "default";
6380
+ const viewport = params.viewport;
6381
+ const userAgent = params.userAgent;
6382
+ const mobile = params.mobile;
6135
6383
  const waitForSelector = params.waitForSelector;
6136
6384
  const timeout = params.timeout || 3e4;
6137
- let browser;
6138
- try {
6139
- browser = await getBrowser();
6140
- } catch (err) {
6141
- return err.message;
6142
- }
6385
+ let context;
6143
6386
  let page;
6144
6387
  try {
6145
- page = await browser.newPage({
6146
- userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
6147
- });
6388
+ const browserOptions = {
6389
+ session: sessionId,
6390
+ viewport,
6391
+ userAgent,
6392
+ isMobile: mobile,
6393
+ hasTouch: mobile
6394
+ };
6395
+ context = await getBrowserContext(sessionId, browserOptions);
6396
+ page = await context.newPage();
6148
6397
  page.setDefaultTimeout(timeout);
6149
6398
  await page.goto(url, { waitUntil: "domcontentloaded", timeout });
6150
6399
  if (waitForSelector) {
@@ -6170,12 +6419,31 @@ function registerBrowserTools(registry, workspaceDir) {
6170
6419
  await page.screenshot({ path: filepath, fullPage: true });
6171
6420
  return `Screenshot saved: ${filepath}`;
6172
6421
  }
6422
+ case "pdf": {
6423
+ if (!existsSync13(screenshotDir)) {
6424
+ mkdirSync9(screenshotDir, { recursive: true });
6425
+ }
6426
+ const filename = `${randomUUID5()}.pdf`;
6427
+ const filepath = resolve12(screenshotDir, filename);
6428
+ await page.pdf({
6429
+ path: filepath,
6430
+ format: "A4",
6431
+ printBackground: true,
6432
+ margin: { top: "1cm", right: "1cm", bottom: "1cm", left: "1cm" }
6433
+ });
6434
+ return `PDF saved: ${filepath}`;
6435
+ }
6173
6436
  case "click": {
6174
6437
  if (!selector) {
6175
6438
  return "Error: 'selector' parameter is required for click action.";
6176
6439
  }
6177
- await page.click(selector);
6178
- return `Clicked: ${selector}`;
6440
+ if (coordinates) {
6441
+ await page.mouse.click(coordinates.x, coordinates.y);
6442
+ return `Clicked at coordinates: (${coordinates.x}, ${coordinates.y})`;
6443
+ } else {
6444
+ await page.click(selector);
6445
+ return `Clicked: ${selector}`;
6446
+ }
6179
6447
  }
6180
6448
  case "type": {
6181
6449
  if (!selector) {
@@ -6187,6 +6455,169 @@ function registerBrowserTools(registry, workspaceDir) {
6187
6455
  await page.fill(selector, text);
6188
6456
  return `Typed into ${selector}: "${text}"`;
6189
6457
  }
6458
+ case "form_fill": {
6459
+ if (!formData) {
6460
+ return "Error: 'formData' parameter is required for form_fill action.";
6461
+ }
6462
+ const results = [];
6463
+ for (const [sel, value] of Object.entries(formData)) {
6464
+ await page.fill(sel, value);
6465
+ results.push(`${sel}: "${value}"`);
6466
+ }
6467
+ return `Form filled:
6468
+ ${results.join("\n")}`;
6469
+ }
6470
+ case "scroll": {
6471
+ if (coordinates) {
6472
+ await page.evaluate(({ x, y }) => window.scrollTo(x, y), coordinates);
6473
+ return `Scrolled to coordinates: (${coordinates.x}, ${coordinates.y})`;
6474
+ } else if (selector) {
6475
+ await page.locator(selector).scrollIntoViewIfNeeded();
6476
+ return `Scrolled to element: ${selector}`;
6477
+ } else if (scrollDirection) {
6478
+ switch (scrollDirection) {
6479
+ case "up":
6480
+ await page.keyboard.press("PageUp");
6481
+ break;
6482
+ case "down":
6483
+ await page.keyboard.press("PageDown");
6484
+ break;
6485
+ case "top":
6486
+ await page.keyboard.press("Home");
6487
+ break;
6488
+ case "bottom":
6489
+ await page.keyboard.press("End");
6490
+ break;
6491
+ default:
6492
+ await page.mouse.wheel(0, scrollDirection === "down" ? scrollDistance : -scrollDistance);
6493
+ }
6494
+ return `Scrolled ${scrollDirection}`;
6495
+ }
6496
+ return "Error: scroll requires coordinates, selector, or scrollDirection";
6497
+ }
6498
+ case "hover": {
6499
+ if (!selector) {
6500
+ return "Error: 'selector' parameter is required for hover action.";
6501
+ }
6502
+ await page.hover(selector);
6503
+ return `Hovered over: ${selector}`;
6504
+ }
6505
+ case "select": {
6506
+ if (!selector || !text) {
6507
+ return "Error: 'selector' and 'text' parameters are required for select action.";
6508
+ }
6509
+ await page.selectOption(selector, text);
6510
+ return `Selected "${text}" in ${selector}`;
6511
+ }
6512
+ case "upload": {
6513
+ if (!selector || !filePath) {
6514
+ return "Error: 'selector' and 'filePath' parameters are required for upload action.";
6515
+ }
6516
+ const absolutePath = resolve12(workspaceDir, filePath);
6517
+ if (!existsSync13(absolutePath)) {
6518
+ return `Error: File not found: ${absolutePath}`;
6519
+ }
6520
+ await page.setInputFiles(selector, absolutePath);
6521
+ return `Uploaded file ${filePath} to ${selector}`;
6522
+ }
6523
+ case "download": {
6524
+ const downloadPromise = page.waitForEvent("download");
6525
+ if (selector) {
6526
+ await page.click(selector);
6527
+ }
6528
+ const download = await downloadPromise;
6529
+ const filename = download.suggestedFilename() || `download_${randomUUID5()}`;
6530
+ const downloadPath = resolve12(workspaceDir, "downloads", filename);
6531
+ if (!existsSync13(resolve12(workspaceDir, "downloads"))) {
6532
+ mkdirSync9(resolve12(workspaceDir, "downloads"), { recursive: true });
6533
+ }
6534
+ await download.saveAs(downloadPath);
6535
+ return `Downloaded: ${downloadPath}`;
6536
+ }
6537
+ case "wait_for": {
6538
+ if (!waitCondition || !waitValue) {
6539
+ return "Error: 'waitCondition' and 'waitValue' parameters are required for wait_for action.";
6540
+ }
6541
+ switch (waitCondition) {
6542
+ case "selector":
6543
+ await page.waitForSelector(waitValue, { timeout });
6544
+ return `Waited for selector: ${waitValue}`;
6545
+ case "text":
6546
+ await page.waitForFunction(
6547
+ (text2) => document.body.innerText.includes(text2),
6548
+ waitValue,
6549
+ { timeout }
6550
+ );
6551
+ return `Waited for text: ${waitValue}`;
6552
+ case "url":
6553
+ await page.waitForURL(waitValue, { timeout });
6554
+ return `Waited for URL: ${waitValue}`;
6555
+ case "networkidle":
6556
+ await page.waitForLoadState("networkidle", { timeout });
6557
+ return "Waited for network idle";
6558
+ case "timeout":
6559
+ await page.waitForTimeout(parseInt(waitValue));
6560
+ return `Waited for ${waitValue}ms`;
6561
+ default:
6562
+ return `Unknown wait condition: ${waitCondition}`;
6563
+ }
6564
+ }
6565
+ case "network_logs": {
6566
+ const logs = await page.evaluate(() => {
6567
+ return "Network logging not yet implemented - use browser dev tools";
6568
+ });
6569
+ return logs;
6570
+ }
6571
+ case "console_logs": {
6572
+ const logs = [];
6573
+ page.on("console", (msg) => {
6574
+ logs.push(`[${msg.type()}] ${msg.text()}`);
6575
+ });
6576
+ await page.waitForTimeout(1e3);
6577
+ return logs.length > 0 ? logs.join("\n") : "No console logs";
6578
+ }
6579
+ case "cookies": {
6580
+ if (cookieData) {
6581
+ if (cookieData.action === "set") {
6582
+ await context.addCookies([cookieData.cookie]);
6583
+ return `Cookie set: ${cookieData.cookie.name}`;
6584
+ } else if (cookieData.action === "clear") {
6585
+ await context.clearCookies();
6586
+ return "Cookies cleared";
6587
+ }
6588
+ }
6589
+ const cookies = await context.cookies();
6590
+ return JSON.stringify(cookies, null, 2);
6591
+ }
6592
+ case "storage": {
6593
+ if (storageData) {
6594
+ const storageType = storageData.type || "localStorage";
6595
+ if (storageData.action === "set") {
6596
+ await page.evaluate(({ type, key, value }) => {
6597
+ if (type === "localStorage") {
6598
+ localStorage.setItem(key, value);
6599
+ } else {
6600
+ sessionStorage.setItem(key, value);
6601
+ }
6602
+ }, { type: storageType, key: storageData.key, value: storageData.value });
6603
+ return `${storageType} set: ${storageData.key}`;
6604
+ } else if (storageData.action === "clear") {
6605
+ await page.evaluate((type) => {
6606
+ if (type === "localStorage") {
6607
+ localStorage.clear();
6608
+ } else {
6609
+ sessionStorage.clear();
6610
+ }
6611
+ }, storageType);
6612
+ return `${storageType} cleared`;
6613
+ }
6614
+ }
6615
+ const storage = await page.evaluate(() => ({
6616
+ localStorage: Object.fromEntries(Object.entries(localStorage)),
6617
+ sessionStorage: Object.fromEntries(Object.entries(sessionStorage))
6618
+ }));
6619
+ return JSON.stringify(storage, null, 2);
6620
+ }
6190
6621
  case "evaluate": {
6191
6622
  if (!javascript) {
6192
6623
  return "Error: 'javascript' parameter is required for evaluate action.";
@@ -6200,7 +6631,7 @@ function registerBrowserTools(registry, workspaceDir) {
6200
6631
  return str ?? "(no result)";
6201
6632
  }
6202
6633
  default:
6203
- return `Unknown action: ${action}. Use: extract, screenshot, click, type, evaluate.`;
6634
+ return `Unknown action: ${action}. Available actions: extract, screenshot, click, type, evaluate, form_fill, scroll, hover, select, upload, download, pdf, wait_for, network_logs, console_logs, cookies, storage.`;
6204
6635
  }
6205
6636
  } catch (err) {
6206
6637
  return `browse error: ${err.message}`;
@@ -6214,6 +6645,98 @@ function registerBrowserTools(registry, workspaceDir) {
6214
6645
  }
6215
6646
  }
6216
6647
  );
6648
+ registry.register(
6649
+ "browser_session",
6650
+ [
6651
+ "Manage browser sessions for persistent contexts across multiple browse operations.",
6652
+ "Actions:",
6653
+ " list \u2014 list all active browser sessions",
6654
+ " create \u2014 create a new browser session with custom options",
6655
+ " close \u2014 close a specific browser session",
6656
+ " close_all \u2014 close all browser sessions",
6657
+ " info \u2014 get information about a specific session"
6658
+ ].join("\n"),
6659
+ {
6660
+ type: "object",
6661
+ properties: {
6662
+ action: {
6663
+ type: "string",
6664
+ enum: ["list", "create", "close", "close_all", "info"],
6665
+ description: "Session management action."
6666
+ },
6667
+ sessionId: {
6668
+ type: "string",
6669
+ description: "Session ID for create, close, or info actions."
6670
+ },
6671
+ options: {
6672
+ type: "object",
6673
+ description: "Browser options for create action (viewport, userAgent, mobile, etc.)."
6674
+ }
6675
+ },
6676
+ required: ["action"]
6677
+ },
6678
+ async (params) => {
6679
+ const action = params.action;
6680
+ const sessionId = params.sessionId;
6681
+ const options = params.options;
6682
+ try {
6683
+ switch (action) {
6684
+ case "list": {
6685
+ const sessions = Array.from(contextInstances.keys());
6686
+ return sessions.length > 0 ? `Active sessions: ${sessions.join(", ")}` : "No active browser sessions";
6687
+ }
6688
+ case "create": {
6689
+ if (!sessionId) {
6690
+ return "Error: 'sessionId' parameter is required for create action.";
6691
+ }
6692
+ if (contextInstances.has(sessionId)) {
6693
+ return `Error: Session '${sessionId}' already exists.`;
6694
+ }
6695
+ await getBrowserContext(sessionId, options || {});
6696
+ return `Created browser session: ${sessionId}`;
6697
+ }
6698
+ case "close": {
6699
+ if (!sessionId) {
6700
+ return "Error: 'sessionId' parameter is required for close action.";
6701
+ }
6702
+ const context = contextInstances.get(sessionId);
6703
+ if (!context) {
6704
+ return `Error: Session '${sessionId}' not found.`;
6705
+ }
6706
+ await context.close();
6707
+ contextInstances.delete(sessionId);
6708
+ return `Closed browser session: ${sessionId}`;
6709
+ }
6710
+ case "close_all": {
6711
+ const sessionCount = contextInstances.size;
6712
+ for (const [id, context] of contextInstances) {
6713
+ try {
6714
+ await context.close();
6715
+ } catch {
6716
+ }
6717
+ }
6718
+ contextInstances.clear();
6719
+ return `Closed ${sessionCount} browser sessions`;
6720
+ }
6721
+ case "info": {
6722
+ if (!sessionId) {
6723
+ return "Error: 'sessionId' parameter is required for info action.";
6724
+ }
6725
+ const context = contextInstances.get(sessionId);
6726
+ if (!context) {
6727
+ return `Error: Session '${sessionId}' not found.`;
6728
+ }
6729
+ const pages = context.pages();
6730
+ return `Session '${sessionId}': ${pages.length} active pages`;
6731
+ }
6732
+ default:
6733
+ return `Unknown action: ${action}. Available actions: list, create, close, close_all, info.`;
6734
+ }
6735
+ } catch (err) {
6736
+ return `browser_session error: ${err.message}`;
6737
+ }
6738
+ }
6739
+ );
6217
6740
  }
6218
6741
 
6219
6742
  // packages/runtime/src/tools/system.ts
@@ -7351,6 +7874,428 @@ ${output || err.message}`;
7351
7874
  );
7352
7875
  }
7353
7876
 
7877
+ // packages/runtime/src/tools/learn.ts
7878
+ function slugify(topic) {
7879
+ return topic.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
7880
+ }
7881
+ async function discoverResearch(topic, depth, registry) {
7882
+ if (!registry.has("web_search")) {
7883
+ throw new Error("web_search tool not available \u2014 is BRAVE_API_KEY set?");
7884
+ }
7885
+ const sources = [];
7886
+ const primary = await registry.execute("web_search", { query: topic, count: 8 });
7887
+ if (primary.startsWith("Error:")) throw new Error(primary);
7888
+ sources.push(...parseSearchResults(primary));
7889
+ if (depth === "deep") {
7890
+ const refinedQueries = [
7891
+ `"${topic}" tutorial`,
7892
+ `"${topic}" best practices`
7893
+ ];
7894
+ for (const q of refinedQueries) {
7895
+ const result = await registry.execute("web_search", { query: q, count: 5 });
7896
+ if (!result.startsWith("Error:")) {
7897
+ sources.push(...parseSearchResults(result));
7898
+ }
7899
+ }
7900
+ }
7901
+ const seen = /* @__PURE__ */ new Set();
7902
+ return sources.filter((s) => {
7903
+ if (seen.has(s.url)) return false;
7904
+ seen.add(s.url);
7905
+ return true;
7906
+ });
7907
+ }
7908
+ function parseSearchResults(raw) {
7909
+ const entries = [];
7910
+ const blocks = raw.split(/\n\n(?=\d+\.\s)/);
7911
+ for (const block of blocks) {
7912
+ const titleMatch = block.match(/\*\*(.+?)\*\*/);
7913
+ const urlMatch = block.match(/\s+(https?:\/\/\S+)/);
7914
+ const lines = block.split("\n");
7915
+ const snippet = lines.length >= 3 ? lines.slice(2).join(" ").trim() : "";
7916
+ if (titleMatch && urlMatch) {
7917
+ entries.push({
7918
+ title: titleMatch[1],
7919
+ url: urlMatch[1],
7920
+ snippet
7921
+ });
7922
+ }
7923
+ }
7924
+ return entries;
7925
+ }
7926
+ async function exploreResearch(topic, sources, depth, focusAreas, registry, llm) {
7927
+ if (!registry.has("web_fetch")) {
7928
+ throw new Error("web_fetch tool not available");
7929
+ }
7930
+ const maxSources = depth === "deep" ? 6 : 3;
7931
+ const toFetch = sources.slice(0, maxSources);
7932
+ const findings = [];
7933
+ for (const source of toFetch) {
7934
+ try {
7935
+ const content = await registry.execute("web_fetch", {
7936
+ url: source.url,
7937
+ max_chars: 15e3
7938
+ });
7939
+ if (content.startsWith("Fetch failed:") || content.startsWith("Fetch error:")) {
7940
+ continue;
7941
+ }
7942
+ const focusClause = focusAreas.length > 0 ? `Focus especially on: ${focusAreas.join(", ")}.` : "";
7943
+ const messages = [
7944
+ {
7945
+ role: "system",
7946
+ content: "You are a research assistant. Extract key facts concisely. Return a numbered list of 5-10 key points, one per line."
7947
+ },
7948
+ {
7949
+ role: "user",
7950
+ content: `Extract 5-10 key facts about "${topic}" from this text. ${focusClause}
7951
+
7952
+ Source: ${source.title} (${source.url})
7953
+
7954
+ ${content.slice(0, 12e3)}`
7955
+ }
7956
+ ];
7957
+ const response = await llm.chat(messages);
7958
+ const keyPoints = response.content.split("\n").map((l) => l.replace(/^\d+[\.\)]\s*/, "").trim()).filter((l) => l.length > 0);
7959
+ findings.push({ source: `${source.title} \u2014 ${source.url}`, keyPoints });
7960
+ } catch {
7961
+ }
7962
+ }
7963
+ return findings;
7964
+ }
7965
+ async function discoverCodebase(topic, registry) {
7966
+ if (!registry.has("shell")) {
7967
+ throw new Error("shell tool not available");
7968
+ }
7969
+ const sources = [];
7970
+ let targetDir = topic;
7971
+ if (topic.startsWith("http://") || topic.startsWith("https://") || topic.startsWith("git@")) {
7972
+ const slug = slugify(topic.split("/").pop()?.replace(".git", "") || "repo");
7973
+ const cloneDir = `/tmp/learn-${slug}`;
7974
+ const cloneResult = await registry.execute("shell", {
7975
+ command: `rm -rf ${cloneDir} && git clone --depth 1 ${topic} ${cloneDir} 2>&1 | tail -1`
7976
+ });
7977
+ if (cloneResult.toLowerCase().includes("fatal")) {
7978
+ throw new Error(`Failed to clone: ${cloneResult}`);
7979
+ }
7980
+ targetDir = cloneDir;
7981
+ }
7982
+ const fileList = await registry.execute("shell", {
7983
+ command: `find ${targetDir} -type f \\( -name '*.ts' -o -name '*.tsx' -o -name '*.js' -o -name '*.jsx' -o -name '*.py' -o -name '*.rs' -o -name '*.go' -o -name '*.java' \\) ! -path '*/node_modules/*' ! -path '*/.git/*' ! -path '*/dist/*' ! -path '*/build/*' | head -50`
7984
+ });
7985
+ for (const line of fileList.split("\n").filter(Boolean)) {
7986
+ sources.push({ title: line.split("/").pop() || line, url: line, snippet: "" });
7987
+ }
7988
+ if (registry.has("read_file")) {
7989
+ const readmeResult = await registry.execute("read_file", { path: `${targetDir}/README.md` });
7990
+ if (!readmeResult.startsWith("Error:")) {
7991
+ sources.unshift({ title: "README.md", url: `${targetDir}/README.md`, snippet: readmeResult.slice(0, 200) });
7992
+ }
7993
+ }
7994
+ const manifestCmd = `cat ${targetDir}/package.json 2>/dev/null || cat ${targetDir}/Cargo.toml 2>/dev/null || cat ${targetDir}/go.mod 2>/dev/null || cat ${targetDir}/pyproject.toml 2>/dev/null || echo ""`;
7995
+ const manifestResult = await registry.execute("shell", { command: manifestCmd });
7996
+ if (manifestResult.trim()) {
7997
+ sources.unshift({ title: "manifest", url: `${targetDir}/package.json`, snippet: manifestResult.slice(0, 200) });
7998
+ }
7999
+ return sources;
8000
+ }
8001
+ async function exploreCodebase(topic, sources, depth, focusAreas, registry, llm) {
8002
+ const maxFiles = depth === "deep" ? 12 : 5;
8003
+ const toRead = sources.slice(0, maxFiles);
8004
+ const findings = [];
8005
+ for (const source of toRead) {
8006
+ try {
8007
+ let content;
8008
+ if (registry.has("read_file")) {
8009
+ content = await registry.execute("read_file", { path: source.url });
8010
+ } else {
8011
+ content = await registry.execute("shell", { command: `cat "${source.url}" 2>/dev/null | head -200` });
8012
+ }
8013
+ if (!content || content.startsWith("Error:")) continue;
8014
+ const focusClause = focusAreas.length > 0 ? `Pay special attention to: ${focusAreas.join(", ")}.` : "";
8015
+ const messages = [
8016
+ {
8017
+ role: "system",
8018
+ content: "You are a code analyst. Analyze the given code file concisely. Return a numbered list of key observations."
8019
+ },
8020
+ {
8021
+ role: "user",
8022
+ content: `Analyze this code file from a project about "${topic}". Extract: purpose, key exports, patterns used, dependencies. ${focusClause}
8023
+
8024
+ File: ${source.title}
8025
+
8026
+ ${content.slice(0, 1e4)}`
8027
+ }
8028
+ ];
8029
+ const response = await llm.chat(messages);
8030
+ const keyPoints = response.content.split("\n").map((l) => l.replace(/^\d+[\.\)]\s*/, "").trim()).filter((l) => l.length > 0);
8031
+ findings.push({ source: source.title, keyPoints });
8032
+ } catch {
8033
+ }
8034
+ }
8035
+ return findings;
8036
+ }
8037
+ async function synthesize(topic, findings, focusAreas, llm) {
8038
+ const findingText = findings.map((f) => `### ${f.source}
8039
+ ${f.keyPoints.map((p) => `- ${p}`).join("\n")}`).join("\n\n");
8040
+ const focusClause = focusAreas.length > 0 ? `
8041
+ Focus areas: ${focusAreas.join(", ")}` : "";
8042
+ const messages = [
8043
+ {
8044
+ role: "system",
8045
+ content: [
8046
+ "You are a knowledge synthesizer. Distill research findings into structured, actionable knowledge.",
8047
+ "Output format:",
8048
+ "## Core Concepts",
8049
+ "(key ideas and definitions)",
8050
+ "## Key Relationships",
8051
+ "(how concepts connect)",
8052
+ "## Important Details",
8053
+ "(specifics worth remembering)",
8054
+ "## Practical Implications",
8055
+ "(how to apply this knowledge)",
8056
+ "",
8057
+ "Be concise but thorough. Maximum 8000 characters."
8058
+ ].join("\n")
8059
+ },
8060
+ {
8061
+ role: "user",
8062
+ content: `Synthesize all findings about "${topic}" into structured knowledge.${focusClause}
8063
+
8064
+ ${findingText}`
8065
+ }
8066
+ ];
8067
+ const response = await llm.chat(messages);
8068
+ return response.content.slice(0, 8e3);
8069
+ }
8070
+ async function storeInMemory(contextName, topic, findings, synthesis, daemonUrl) {
8071
+ const storedIds = [];
8072
+ try {
8073
+ await fetch(`${daemonUrl}/api/contexts`, {
8074
+ method: "POST",
8075
+ headers: { "Content-Type": "application/json" },
8076
+ body: JSON.stringify({
8077
+ name: contextName,
8078
+ description: `Learned knowledge: ${topic}`
8079
+ })
8080
+ });
8081
+ } catch {
8082
+ }
8083
+ for (const finding of findings) {
8084
+ const content = `[Source: ${finding.source}]
8085
+ ${finding.keyPoints.join("\n")}`;
8086
+ try {
8087
+ const resp = await fetch(`${daemonUrl}/api/episodes`, {
8088
+ method: "POST",
8089
+ headers: { "Content-Type": "application/json" },
8090
+ body: JSON.stringify({
8091
+ context_name: contextName,
8092
+ role: "system",
8093
+ content
8094
+ })
8095
+ });
8096
+ if (resp.ok) {
8097
+ const data = await resp.json();
8098
+ if (data.id) storedIds.push(data.id);
8099
+ }
8100
+ } catch {
8101
+ }
8102
+ }
8103
+ try {
8104
+ const resp = await fetch(`${daemonUrl}/api/episodes`, {
8105
+ method: "POST",
8106
+ headers: { "Content-Type": "application/json" },
8107
+ body: JSON.stringify({
8108
+ context_name: contextName,
8109
+ role: "assistant",
8110
+ content: `[SYNTHESIS: ${topic}]
8111
+ ${synthesis}`
8112
+ })
8113
+ });
8114
+ if (resp.ok) {
8115
+ const data = await resp.json();
8116
+ if (data.id) storedIds.push(data.id);
8117
+ }
8118
+ } catch {
8119
+ }
8120
+ try {
8121
+ await fetch(`${daemonUrl}/api/promotion/run?context=${encodeURIComponent(contextName)}`, {
8122
+ method: "POST"
8123
+ });
8124
+ } catch {
8125
+ }
8126
+ return storedIds;
8127
+ }
8128
+ function registerLearnTools(registry, config) {
8129
+ let llm = null;
8130
+ if (config.llmConfig) {
8131
+ try {
8132
+ llm = new LLMClient({
8133
+ ...config.llmConfig,
8134
+ max_tokens: 2048
8135
+ });
8136
+ } catch {
8137
+ }
8138
+ }
8139
+ let synthesisLlm = null;
8140
+ if (config.llmConfig) {
8141
+ try {
8142
+ synthesisLlm = new LLMClient({
8143
+ ...config.llmConfig,
8144
+ max_tokens: 4096
8145
+ });
8146
+ } catch {
8147
+ }
8148
+ }
8149
+ registry.register(
8150
+ "learn",
8151
+ [
8152
+ "Deeply research a topic and store organized knowledge in memory.",
8153
+ "Supports two modes: 'research' (web search + fetch) and 'codebase' (local/remote repo analysis).",
8154
+ "Findings are synthesized via LLM and stored as L2 episodes in a dedicated memory context,",
8155
+ "eligible for L3 promotion. Use this when you need thorough understanding of a topic."
8156
+ ].join(" "),
8157
+ {
8158
+ type: "object",
8159
+ properties: {
8160
+ topic: {
8161
+ type: "string",
8162
+ description: "What to learn about (e.g., 'WebSocket protocol', '/path/to/repo', 'https://github.com/user/repo')"
8163
+ },
8164
+ type: {
8165
+ type: "string",
8166
+ enum: ["research", "codebase"],
8167
+ description: "Learning type: 'research' for web-based topics, 'codebase' for analyzing code (default: research)"
8168
+ },
8169
+ depth: {
8170
+ type: "string",
8171
+ enum: ["shallow", "deep"],
8172
+ description: "How many sources to explore: shallow (3 sources) or deep (6+ sources) (default: shallow)"
8173
+ },
8174
+ context: {
8175
+ type: "string",
8176
+ description: "Custom memory context name (default: auto-generated learn-{slug})"
8177
+ },
8178
+ focus_areas: {
8179
+ type: "array",
8180
+ items: { type: "string" },
8181
+ description: "Specific aspects to focus on (e.g., ['performance', 'security'])"
8182
+ },
8183
+ store: {
8184
+ type: "boolean",
8185
+ description: "Whether to persist findings in memory (default: true)"
8186
+ }
8187
+ },
8188
+ required: ["topic"]
8189
+ },
8190
+ async (params) => {
8191
+ const topic = params.topic;
8192
+ const type = params.type || "research";
8193
+ const depth = params.depth || "shallow";
8194
+ const contextName = params.context || `learn-${slugify(topic)}`;
8195
+ const focusAreas = params.focus_areas || [];
8196
+ const shouldStore = params.store !== false;
8197
+ if (!llm || !synthesisLlm) {
8198
+ return "Error: LLM not configured \u2014 learn tool requires an LLM for synthesis. Check your llm config.";
8199
+ }
8200
+ try {
8201
+ let sources;
8202
+ if (type === "codebase") {
8203
+ sources = await discoverCodebase(topic, registry);
8204
+ } else {
8205
+ sources = await discoverResearch(topic, depth, registry);
8206
+ }
8207
+ if (sources.length === 0) {
8208
+ return `No sources found for "${topic}". Try a different query or check connectivity.`;
8209
+ }
8210
+ let findings;
8211
+ if (type === "codebase") {
8212
+ findings = await exploreCodebase(topic, sources, depth, focusAreas, registry, llm);
8213
+ } else {
8214
+ findings = await exploreResearch(topic, sources, depth, focusAreas, registry, llm);
8215
+ }
8216
+ if (findings.length === 0) {
8217
+ return `Found ${sources.length} sources but could not extract meaningful findings for "${topic}".`;
8218
+ }
8219
+ const synthesis = await synthesize(topic, findings, focusAreas, synthesisLlm);
8220
+ let storageNote = "";
8221
+ if (shouldStore) {
8222
+ const storedIds = await storeInMemory(
8223
+ contextName,
8224
+ topic,
8225
+ findings,
8226
+ synthesis,
8227
+ config.memoryDaemonUrl
8228
+ );
8229
+ storageNote = storedIds.length > 0 ? `
8230
+
8231
+ ---
8232
+ Stored ${storedIds.length} episodes in memory context "${contextName}". Use learn_recall to retrieve later.` : "\n\n---\nNote: Could not store in memory (daemon may be offline). Knowledge is shown above but not persisted.";
8233
+ }
8234
+ return `# Learned: ${topic}
8235
+
8236
+ Sources explored: ${findings.length}
8237
+ Type: ${type} | Depth: ${depth}
8238
+
8239
+ ${synthesis}${storageNote}`;
8240
+ } catch (err) {
8241
+ return `Learn failed: ${err.message}`;
8242
+ }
8243
+ }
8244
+ );
8245
+ registry.register(
8246
+ "learn_recall",
8247
+ "Retrieve knowledge from a previous learn session. Searches across learn-* memory contexts for stored findings and synthesis.",
8248
+ {
8249
+ type: "object",
8250
+ properties: {
8251
+ topic: {
8252
+ type: "string",
8253
+ description: "Search query to find previously learned knowledge"
8254
+ },
8255
+ context: {
8256
+ type: "string",
8257
+ description: "Specific learn context to search (e.g., 'learn-websocket-protocol'). If omitted, searches all learn contexts."
8258
+ }
8259
+ },
8260
+ required: ["topic"]
8261
+ },
8262
+ async (params) => {
8263
+ const topic = params.topic;
8264
+ const context = params.context;
8265
+ const daemonUrl = config.memoryDaemonUrl;
8266
+ try {
8267
+ if (context) {
8268
+ const resp2 = await fetch(`${daemonUrl}/api/search`, {
8269
+ method: "POST",
8270
+ headers: { "Content-Type": "application/json" },
8271
+ body: JSON.stringify({ query: topic, context_name: context, top_k: 10 })
8272
+ });
8273
+ if (!resp2.ok) return `Memory search failed: ${resp2.status}`;
8274
+ const data2 = await resp2.json();
8275
+ if (!data2.results?.length) return `No learned knowledge found for "${topic}" in context "${context}".`;
8276
+ return data2.results.map((r, i) => `${i + 1}. [score: ${r.score.toFixed(3)}] [${r.role}]
8277
+ ${r.content.slice(0, 500)}`).join("\n\n");
8278
+ }
8279
+ const resp = await fetch(`${daemonUrl}/api/search/cross-context`, {
8280
+ method: "POST",
8281
+ headers: { "Content-Type": "application/json" },
8282
+ body: JSON.stringify({ query: topic, top_k: 15 })
8283
+ });
8284
+ if (!resp.ok) return `Memory cross-search failed: ${resp.status}`;
8285
+ const data = await resp.json();
8286
+ const learnResults = data.results?.filter((r) => r.context_name.startsWith("learn-")) || [];
8287
+ if (learnResults.length === 0) {
8288
+ return `No previously learned knowledge found for "${topic}". Use the learn tool first.`;
8289
+ }
8290
+ return learnResults.map((r, i) => `${i + 1}. [score: ${r.score.toFixed(3)}] [${r.context_name}] [${r.role}]
8291
+ ${r.content.slice(0, 500)}`).join("\n\n");
8292
+ } catch (err) {
8293
+ return `Memory daemon unreachable: ${err.message}`;
8294
+ }
8295
+ }
8296
+ );
8297
+ }
8298
+
7354
8299
  // packages/runtime/src/tools/register.ts
7355
8300
  import { resolve as resolve18 } from "path";
7356
8301
  import { mkdirSync as mkdirSync13, existsSync as existsSync18 } from "fs";
@@ -7381,6 +8326,11 @@ function registerAllTools(hivemindHome, config) {
7381
8326
  registerMacOSTools(registry, workspaceDir);
7382
8327
  registerDataTools(registry, workspaceDir);
7383
8328
  registerCodingAgentTools(registry, workspaceDir);
8329
+ registerLearnTools(registry, {
8330
+ llmConfig: config?.llmConfig,
8331
+ memoryDaemonUrl: config?.memoryDaemonUrl || "http://localhost:3434",
8332
+ workspaceDir
8333
+ });
7384
8334
  return registry;
7385
8335
  }
7386
8336
 
@@ -7674,8 +8624,8 @@ async function startPipeline(configPath) {
7674
8624
  }
7675
8625
  }
7676
8626
  const requestLogger = new RequestLogger(resolve20(dirname8(configPath), "data", "dashboard.db"));
7677
- startDashboardServer(requestLogger, config.memory);
7678
8627
  const agent = new Agent(config);
8628
+ startDashboardServer(requestLogger, config.memory, () => agent.getConversationHistories());
7679
8629
  agent.setRequestLogger(requestLogger);
7680
8630
  const hivemindHome = process.env.HIVEMIND_HOME || resolve20(process.env.HOME || "/root", "hivemind");
7681
8631
  const toolRegistry = registerAllTools(hivemindHome, {
@@ -7683,7 +8633,8 @@ async function startPipeline(configPath) {
7683
8633
  workspace: config.agent.workspace || "workspace",
7684
8634
  braveApiKey: process.env.BRAVE_API_KEY,
7685
8635
  memoryDaemonUrl: config.memory.daemon_url,
7686
- configPath
8636
+ configPath,
8637
+ llmConfig: config.llm
7687
8638
  });
7688
8639
  const workspaceDir = resolve20(hivemindHome, config.agent.workspace || "workspace");
7689
8640
  const skillsEngine = new SkillsEngine(workspaceDir, toolRegistry);
@@ -8029,7 +8980,7 @@ var WorkerServer = class {
8029
8980
  return this.handleHealth(res);
8030
8981
  }
8031
8982
  if (method === "POST" && path === "/assign") {
8032
- const body = await readBody(req);
8983
+ const body = await readBody2(req);
8033
8984
  return this.handleAssign(body, res);
8034
8985
  }
8035
8986
  if (method === "DELETE" && path.startsWith("/assign/")) {
@@ -8040,7 +8991,7 @@ var WorkerServer = class {
8040
8991
  return this.handleStatus(res);
8041
8992
  }
8042
8993
  if (method === "POST" && path === "/sync/push") {
8043
- const body = await readBody(req);
8994
+ const body = await readBody2(req);
8044
8995
  return this.handleSyncPush(body, res);
8045
8996
  }
8046
8997
  sendJson(res, 404, { error: "Not found" });
@@ -8118,7 +9069,7 @@ var WorkerServer = class {
8118
9069
  sendJson(res, 200, result);
8119
9070
  }
8120
9071
  };
8121
- function readBody(req) {
9072
+ function readBody2(req) {
8122
9073
  return new Promise((resolve21, reject) => {
8123
9074
  const chunks = [];
8124
9075
  req.on("data", (chunk) => chunks.push(chunk));
@@ -8464,4 +9415,4 @@ smol-toml/dist/index.js:
8464
9415
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
8465
9416
  *)
8466
9417
  */
8467
- //# sourceMappingURL=chunk-ZM7RK5YV.js.map
9418
+ //# sourceMappingURL=chunk-OB6OXLPC.js.map