mirror of
https://github.com/zooai/brain.git
synced 2026-10-30 14:50:48 +00:00
The single binary brain that compounds knowledge across every channel, every agent, every runtime. Drop markdown into ~/.hanzo/workspace/. Edges auto-extract (zero LLM). Facts queryable via MCP. One SQLite file at ~/.hanzo/brain/brain.db. Same file readable by every Hanzo runtime — TS, Python, Rust, Go. Packages - brain meta-pack registering memory + graph-links + recipes - memory pluggable BrainStore (SQLite default; FTS5 hybrid search) - graph-links zero-LLM typed-edge extractor (6 edge types) - recipes-brain YAML daily-life automations (email flagship) Sister runtimes - TS canonical ships in @hanzo/bot (this monorepo is the source) - Python hanzo-memory (graph_links + recipes ports) - Rust hanzoai/mcp::brain crate - Go hanzoai/bot-go (single static binary) Canonical paths - ~/.hanzo/brain/brain.db the brain - ~/.hanzo/workspace/ markdown source — auto-ingested - ~/.hanzo/recipes/ user-authored YAML recipes - ~/.hanzo/config.toml per-machine config - ~/.hanzo/cache/ embedding cache, tool-output cache - ~/.hanzo/logs/ structured logs A brain.db written by ANY runtime is byte-identical to one written by every other. Same schema, same FTS5 setup, same pages / edges / facts shape. Pluggable backends — same BrainStore contract everywhere: sqlite (default) · qdrant · meilisearch · zapdb · replicate · vfs · postgres.
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
import { mkdtemp, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
import { SqliteStore } from "./sqlite.js";
|
|
|
|
describe("memory: SQLite (canonical default)", () => {
|
|
let dir: string;
|
|
let store: SqliteStore;
|
|
|
|
beforeAll(async () => {
|
|
dir = await mkdtemp(join(tmpdir(), "hanzo-brain-test-"));
|
|
store = new SqliteStore({ dbPath: join(dir, "brain.db") });
|
|
try {
|
|
await store.init();
|
|
} catch (e: any) {
|
|
// CI runners without bun:sqlite or better-sqlite3 — skip the suite.
|
|
if (String(e.message).includes("neither bun:sqlite nor better-sqlite3")) {
|
|
console.warn("[memory test] SQLite driver unavailable, skipping suite");
|
|
// Mark as skipped by short-circuiting subsequent tests.
|
|
store = null as any;
|
|
} else {
|
|
throw e;
|
|
}
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await store?.close?.();
|
|
await rm(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("upserts and reads a page", async () => {
|
|
if (!store) return;
|
|
await store.upsertPage("people/alice", "Alice is the CEO of Acme.", { type: "person" });
|
|
const got = await store.getPage("people/alice");
|
|
expect(got?.slug).toBe("people/alice");
|
|
expect(got?.content).toContain("Alice");
|
|
});
|
|
|
|
it("upserts edges and reads them back", async () => {
|
|
if (!store) return;
|
|
await store.upsertEdges("meetings/m1", [
|
|
{ source: "meetings/m1", target: "people/alice", type: "attended" },
|
|
{ source: "meetings/m1", target: "people/bob", type: "attended" },
|
|
]);
|
|
const out = await store.edgesFor("meetings/m1", "out");
|
|
expect(out).toHaveLength(2);
|
|
const inAlice = await store.edgesFor("people/alice", "in");
|
|
expect(inAlice).toHaveLength(1);
|
|
});
|
|
|
|
it("upserts and recalls facts", async () => {
|
|
if (!store) return;
|
|
await store.upsertFact({
|
|
subject: "people/alice",
|
|
predicate: "preference",
|
|
object: "Always replies in under 2 hours",
|
|
});
|
|
const facts = await store.recall("people/alice");
|
|
expect(facts.length).toBeGreaterThan(0);
|
|
expect(facts[0].predicate).toBe("preference");
|
|
});
|
|
|
|
it("hybrid search returns FTS hits", async () => {
|
|
if (!store) return;
|
|
const hits = await store.hybridSearch("Acme");
|
|
expect(hits.length).toBeGreaterThan(0);
|
|
expect(hits[0].slug).toBe("people/alice");
|
|
});
|
|
});
|
|
|
|
describe("memory: pluggability", () => {
|
|
it("registerBackend exposes new backends to open()", async () => {
|
|
const { open, registerBackend, listBackends } = await import("./index.js");
|
|
// Stub backend that records init() being called.
|
|
let initialized = false;
|
|
registerBackend("test-stub", () => ({
|
|
init: async () => {
|
|
initialized = true;
|
|
},
|
|
upsertPage: async () => {},
|
|
getPage: async () => null,
|
|
upsertEdges: async () => {},
|
|
edgesFor: async () => [],
|
|
upsertFact: async () => {},
|
|
recall: async () => [],
|
|
hybridSearch: async () => [],
|
|
close: async () => {},
|
|
}));
|
|
expect(listBackends()).toContain("test-stub");
|
|
expect(listBackends()).toContain("sqlite");
|
|
await open({ backend: "test-stub" });
|
|
expect(initialized).toBe(true);
|
|
});
|
|
|
|
it("unknown backend throws with a helpful message", async () => {
|
|
const { open } = await import("./index.js");
|
|
await expect(open({ backend: "no-such-thing" })).rejects.toThrow(/unknown backend/);
|
|
});
|
|
});
|