hebbian 0.3.3 → 0.3.4

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.
@@ -1237,6 +1237,29 @@ function logEpisode(brainRoot, type, path, detail) {
1237
1237
  "utf8"
1238
1238
  );
1239
1239
  }
1240
+ function readEpisodes(brainRoot) {
1241
+ const logDir = join12(brainRoot, SESSION_LOG_DIR);
1242
+ if (!existsSync11(logDir)) return [];
1243
+ const episodes = [];
1244
+ let entries;
1245
+ try {
1246
+ entries = readdirSync7(logDir);
1247
+ } catch {
1248
+ return [];
1249
+ }
1250
+ for (const entry of entries) {
1251
+ if (!entry.startsWith("memory") || !entry.endsWith(".neuron")) continue;
1252
+ try {
1253
+ const content = readFileSync4(join12(logDir, entry), "utf8");
1254
+ if (content.trim()) {
1255
+ episodes.push(JSON.parse(content));
1256
+ }
1257
+ } catch {
1258
+ }
1259
+ }
1260
+ episodes.sort((a, b) => a.ts.localeCompare(b.ts));
1261
+ return episodes;
1262
+ }
1240
1263
  function getNextSlot(logDir) {
1241
1264
  let maxSlot = 0;
1242
1265
  try {
@@ -2171,6 +2194,263 @@ var init_digest = __esm({
2171
2194
  }
2172
2195
  });
2173
2196
 
2197
+ // src/evolve.ts
2198
+ var evolve_exports = {};
2199
+ __export(evolve_exports, {
2200
+ buildBrainSummary: () => buildBrainSummary,
2201
+ buildPrompt: () => buildPrompt,
2202
+ callGemini: () => callGemini,
2203
+ executeActions: () => executeActions,
2204
+ parseActions: () => parseActions,
2205
+ runEvolve: () => runEvolve,
2206
+ validateActions: () => validateActions
2207
+ });
2208
+ async function runEvolve(brainRoot, dryRun) {
2209
+ const apiKey = process.env.GEMINI_API_KEY;
2210
+ if (!apiKey) {
2211
+ console.error("\u274C GEMINI_API_KEY not set. Get one at https://aistudio.google.com/apikey");
2212
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2213
+ }
2214
+ const episodes = readEpisodes(brainRoot);
2215
+ const brain = scanBrain(brainRoot);
2216
+ const summary = buildBrainSummary(brain);
2217
+ const prompt = buildPrompt(summary, episodes);
2218
+ let rawActions;
2219
+ try {
2220
+ rawActions = await callGemini(prompt, apiKey);
2221
+ } catch (err) {
2222
+ const msg = err.message;
2223
+ console.log(`\u23ED\uFE0F evolve skipped: ${msg}`);
2224
+ logEpisode(brainRoot, "evolve-error", "", msg);
2225
+ return { actions: [], executed: 0, skipped: 0, dryRun };
2226
+ }
2227
+ const actions = validateActions(rawActions, brain);
2228
+ const skipped = rawActions.length - actions.length;
2229
+ if (actions.length === 0) {
2230
+ console.log("\u{1F9E0} evolve: no valid actions proposed");
2231
+ return { actions: [], executed: 0, skipped, dryRun };
2232
+ }
2233
+ if (dryRun) {
2234
+ console.log(`\u{1F9E0} evolve (dry-run): ${actions.length} action(s) proposed`);
2235
+ for (const action of actions) {
2236
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path} \u2014 ${action.reason}`);
2237
+ }
2238
+ return { actions, executed: 0, skipped, dryRun: true };
2239
+ }
2240
+ const executed = executeActions(brainRoot, actions);
2241
+ logEpisode(brainRoot, "evolve", "", `${executed} action(s) executed, ${skipped} skipped`);
2242
+ console.log(`\u{1F9E0} evolve: ${executed} action(s) executed, ${skipped} skipped`);
2243
+ return { actions, executed, skipped, dryRun: false };
2244
+ }
2245
+ function buildBrainSummary(brain) {
2246
+ const lines = ["# Brain State\n"];
2247
+ for (const region of brain.regions) {
2248
+ const neurons = region.neurons;
2249
+ if (neurons.length === 0 && !region.hasBomb) continue;
2250
+ lines.push(`## ${region.name} (P${REGION_PRIORITY[region.name]})`);
2251
+ if (region.hasBomb) lines.push("\u26A0\uFE0F BOMB active \u2014 region blocked");
2252
+ for (const neuron of neurons) {
2253
+ const flags = [];
2254
+ if (neuron.isDormant) flags.push("dormant");
2255
+ if (neuron.hasBomb) flags.push("bomb");
2256
+ if (neuron.hasMemory) flags.push("memory");
2257
+ if (neuron.dopamine > 0) flags.push(`dopamine:${neuron.dopamine}`);
2258
+ const flagStr = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
2259
+ lines.push(`- ${neuron.path} (counter:${neuron.counter}, intensity:${neuron.intensity})${flagStr}`);
2260
+ }
2261
+ lines.push("");
2262
+ }
2263
+ return lines.join("\n");
2264
+ }
2265
+ function buildPrompt(summary, episodes) {
2266
+ const episodeLines = episodes.length > 0 ? episodes.map((e) => `- [${e.ts}] ${e.type}: ${e.path} \u2014 ${e.detail}`).join("\n") : "(no recent episodes)";
2267
+ return `You are the evolve engine for a hebbian brain \u2014 a filesystem-based memory system for AI agents.
2268
+
2269
+ ## Axioms
2270
+ - Folder = Neuron, File = Firing Trace, Counter = Activation strength
2271
+ - 7 regions in subsumption cascade: brainstem(P0) > limbic(P1) > hippocampus(P2) > sensors(P3) > cortex(P4) > ego(P5) > prefrontal(P6)
2272
+ - Lower priority ALWAYS overrides higher priority
2273
+ - PROTECTED regions (brainstem, limbic, sensors): NEVER propose mutations for these
2274
+
2275
+ ## Current Brain
2276
+ ${summary}
2277
+
2278
+ ## Recent Episodes (last ${episodes.length})
2279
+ ${episodeLines}
2280
+
2281
+ ## Available Actions
2282
+ - grow: Create a new neuron at the given path (region/name). Use for recurring patterns that deserve permanent memory.
2283
+ - fire: Increment an existing neuron's counter. Use for strengthening well-confirmed rules.
2284
+ - signal: Add dopamine (reward), bomb (block), or memory signal. Use sparingly.
2285
+ - prune: Decrement a neuron's counter. Use for rules that aren't working or cause issues.
2286
+ - decay: Mark inactive neurons as dormant. Use for stale rules with no recent activity.
2287
+
2288
+ ## Constraints
2289
+ - Max ${MAX_ACTIONS} actions per cycle
2290
+ - PREFER fire over grow \u2014 strengthen existing neurons before creating new ones
2291
+ - NEVER target brainstem, limbic, or sensors regions
2292
+ - Each action needs a "reason" explaining why
2293
+
2294
+ ## Task
2295
+ Analyze the brain state and recent episodes. Propose actions to improve the brain.
2296
+ Focus on: strengthening repeatedly-used rules, pruning ineffective ones, growing new neurons from repeated patterns.
2297
+
2298
+ Respond with a JSON array of actions:
2299
+ [{"type":"fire","path":"cortex/NO_console_log","reason":"fired 3 times in recent sessions"}]`;
2300
+ }
2301
+ async function callGemini(prompt, apiKey) {
2302
+ const model = process.env.EVOLVE_MODEL || DEFAULT_MODEL;
2303
+ const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
2304
+ const body = {
2305
+ contents: [{ parts: [{ text: prompt }] }],
2306
+ generationConfig: {
2307
+ responseMimeType: "application/json",
2308
+ temperature: 0.2
2309
+ }
2310
+ };
2311
+ let lastError = null;
2312
+ for (let attempt = 0; attempt < 2; attempt++) {
2313
+ if (attempt > 0) {
2314
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
2315
+ }
2316
+ try {
2317
+ const res = await fetch(url, {
2318
+ method: "POST",
2319
+ headers: { "Content-Type": "application/json" },
2320
+ body: JSON.stringify(body),
2321
+ signal: AbortSignal.timeout(API_TIMEOUT)
2322
+ });
2323
+ if (!res.ok) {
2324
+ lastError = new Error(`Gemini API ${res.status}: ${res.statusText}`);
2325
+ continue;
2326
+ }
2327
+ const data = await res.json();
2328
+ if (data.error) {
2329
+ lastError = new Error(`Gemini error: ${data.error.message || "unknown"}`);
2330
+ continue;
2331
+ }
2332
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
2333
+ if (!text) {
2334
+ lastError = new Error("Gemini returned empty response");
2335
+ continue;
2336
+ }
2337
+ return parseActions(text);
2338
+ } catch (err) {
2339
+ lastError = err;
2340
+ continue;
2341
+ }
2342
+ }
2343
+ throw lastError || new Error("Gemini call failed");
2344
+ }
2345
+ function parseActions(text) {
2346
+ let parsed;
2347
+ try {
2348
+ parsed = JSON.parse(text);
2349
+ } catch {
2350
+ throw new Error(`Failed to parse LLM response as JSON: ${text.slice(0, 100)}`);
2351
+ }
2352
+ if (!Array.isArray(parsed)) {
2353
+ throw new Error("LLM response is not an array");
2354
+ }
2355
+ const validTypes = /* @__PURE__ */ new Set(["grow", "fire", "signal", "prune", "decay"]);
2356
+ const actions = [];
2357
+ for (const item of parsed) {
2358
+ if (!item || typeof item !== "object") continue;
2359
+ const { type, path, reason, signal } = item;
2360
+ if (typeof type !== "string" || !validTypes.has(type)) continue;
2361
+ if (typeof path !== "string" || path.length === 0) continue;
2362
+ if (typeof reason !== "string") continue;
2363
+ const action = { type, path, reason };
2364
+ if (type === "signal" && typeof signal === "string") {
2365
+ action.signal = signal;
2366
+ }
2367
+ actions.push(action);
2368
+ }
2369
+ return actions;
2370
+ }
2371
+ function validateActions(actions, _brain) {
2372
+ return actions.filter((action) => {
2373
+ const region = action.path.split("/")[0];
2374
+ if (!region || PROTECTED_REGIONS.includes(region)) {
2375
+ console.log(` \u{1F6E1}\uFE0F blocked: ${action.type} ${action.path} (protected region)`);
2376
+ return false;
2377
+ }
2378
+ if (!REGIONS.includes(region)) {
2379
+ console.log(` \u26A0\uFE0F skipped: ${action.type} ${action.path} (invalid region)`);
2380
+ return false;
2381
+ }
2382
+ if (action.type === "signal" && action.signal && !["dopamine", "bomb", "memory"].includes(action.signal)) {
2383
+ console.log(` \u26A0\uFE0F skipped: signal ${action.path} (invalid signal type: ${action.signal})`);
2384
+ return false;
2385
+ }
2386
+ return true;
2387
+ }).slice(0, MAX_ACTIONS);
2388
+ }
2389
+ function executeActions(brainRoot, actions) {
2390
+ let executed = 0;
2391
+ for (const action of actions) {
2392
+ try {
2393
+ switch (action.type) {
2394
+ case "fire":
2395
+ fireNeuron(brainRoot, action.path);
2396
+ break;
2397
+ case "grow":
2398
+ growNeuron(brainRoot, action.path);
2399
+ break;
2400
+ case "signal":
2401
+ signalNeuron(brainRoot, action.path, action.signal || "dopamine");
2402
+ break;
2403
+ case "prune":
2404
+ rollbackNeuron(brainRoot, action.path);
2405
+ break;
2406
+ case "decay":
2407
+ runDecay(brainRoot, 0);
2408
+ break;
2409
+ }
2410
+ console.log(` ${actionIcon(action.type)} ${action.type} ${action.path}`);
2411
+ executed++;
2412
+ } catch (err) {
2413
+ console.log(` \u26A0\uFE0F failed: ${action.type} ${action.path} \u2014 ${err.message}`);
2414
+ }
2415
+ }
2416
+ return executed;
2417
+ }
2418
+ function actionIcon(type) {
2419
+ switch (type) {
2420
+ case "fire":
2421
+ return "\u{1F525}";
2422
+ case "grow":
2423
+ return "\u{1F331}";
2424
+ case "signal":
2425
+ return "\u26A1";
2426
+ case "prune":
2427
+ return "\u2702\uFE0F";
2428
+ case "decay":
2429
+ return "\u{1F4A4}";
2430
+ default:
2431
+ return "\u2753";
2432
+ }
2433
+ }
2434
+ var MAX_ACTIONS, PROTECTED_REGIONS, DEFAULT_MODEL, API_TIMEOUT, RETRY_DELAY;
2435
+ var init_evolve = __esm({
2436
+ "src/evolve.ts"() {
2437
+ "use strict";
2438
+ init_episode();
2439
+ init_scanner();
2440
+ init_constants();
2441
+ init_fire();
2442
+ init_grow();
2443
+ init_signal();
2444
+ init_rollback();
2445
+ init_decay();
2446
+ MAX_ACTIONS = 10;
2447
+ PROTECTED_REGIONS = ["brainstem", "limbic", "sensors"];
2448
+ DEFAULT_MODEL = "gemini-2.0-flash-lite";
2449
+ API_TIMEOUT = 3e4;
2450
+ RETRY_DELAY = 5e3;
2451
+ }
2452
+ });
2453
+
2174
2454
  // src/cli.ts
2175
2455
  init_constants();
2176
2456
  import { parseArgs } from "util";
@@ -2199,6 +2479,7 @@ COMMANDS:
2199
2479
  inbox Process corrections inbox
2200
2480
  claude install|uninstall|status Manage Claude Code hooks
2201
2481
  digest [--transcript <path>] Extract corrections from conversation
2482
+ evolve [--dry-run] LLM-powered brain evolution (Gemini)
2202
2483
  diag Print brain diagnostics
2203
2484
  stats Print brain statistics
2204
2485
 
@@ -2213,6 +2494,7 @@ EXAMPLES:
2213
2494
  hebbian fire cortex/frontend/NO_console_log --brain ./my-brain
2214
2495
  hebbian emit claude --brain ./my-brain
2215
2496
  hebbian emit all
2497
+ GEMINI_API_KEY=... hebbian evolve --dry-run
2216
2498
  `.trim();
2217
2499
  function readStdin() {
2218
2500
  return new Promise((resolve4) => {
@@ -2238,6 +2520,7 @@ async function main(argv) {
2238
2520
  days: { type: "string", short: "d" },
2239
2521
  port: { type: "string", short: "p" },
2240
2522
  transcript: { type: "string", short: "t" },
2523
+ "dry-run": { type: "boolean" },
2241
2524
  help: { type: "boolean", short: "h" },
2242
2525
  version: { type: "boolean", short: "v" }
2243
2526
  },
@@ -2401,6 +2684,12 @@ async function main(argv) {
2401
2684
  }
2402
2685
  break;
2403
2686
  }
2687
+ case "evolve": {
2688
+ const dryRun = values["dry-run"] === true;
2689
+ const { runEvolve: runEvolve2 } = await Promise.resolve().then(() => (init_evolve(), evolve_exports));
2690
+ await runEvolve2(brainRoot, dryRun);
2691
+ break;
2692
+ }
2404
2693
  case "diag":
2405
2694
  case "stats": {
2406
2695
  const { scanBrain: scanBrain2 } = await Promise.resolve().then(() => (init_scanner(), scanner_exports));