fix: improve brain init debug output

This commit is contained in:
2026-06-07 15:17:42 +09:00
parent ebea998507
commit 968de4d4a8
3 changed files with 144 additions and 35 deletions

View File

@@ -1,6 +1,6 @@
import { randomUUID } from "node:crypto";
import { config } from "@/config";
import { IdentityDB, type Space } from "identitydb";
import { IdentityDB, type ExtractedFact, type Space } from "identitydb";
import { llm } from "@/openrouter";
import { loadPrompt } from "@/openrouter/promptLoader";
import {
@@ -28,6 +28,19 @@ export interface DebugOptions {
personality: string;
}
export interface BrainCreateResult {
brain: Brain;
description: string;
baseSystemPrompt: string;
/**
* Raw facts as returned by `factExtractor.extract(description)`. Populated
* only when `Brain.create` is called with `debug: true`; in production
* (the default), facts are persisted via `db.ingestStatements` which does
* not surface the raw extractor output to the caller.
*/
extractedFacts?: ExtractedFact[];
}
export class Brain {
private availabilityCache: Map<string, AvailabilityWindows> = new Map();
@@ -280,8 +293,8 @@ export class Brain {
static async create(
displayName: string,
seed: string,
options: { dbPath?: string; braindbPath?: string } = {},
): Promise<Brain | null> {
options: { dbPath?: string; braindbPath?: string; debug?: boolean } = {},
): Promise<BrainCreateResult | null> {
const dbPath = options.dbPath ?? config.dbPath;
const manager = options.braindbPath
? new BrainDBManager(options.braindbPath)
@@ -321,10 +334,26 @@ export class Brain {
description: displayName,
});
await db.ingestStatement(description, {
extractor: factExtractor,
spaceName,
});
let extractedFacts: ExtractedFact[] | undefined;
if (options.debug) {
extractedFacts = await factExtractor.extract(description);
for (const fact of extractedFacts) {
await db.addFact({
spaceName,
statement: fact.statement ?? description,
summary: fact.summary,
source: fact.source,
confidence: fact.confidence,
topics: fact.topics,
metadata: fact.metadata,
});
}
} else {
await db.ingestStatements(description, {
extractor: factExtractor,
spaceName,
});
}
const brainbase: BrainItem = {
brainId,
@@ -334,7 +363,8 @@ export class Brain {
};
await manager.saveBrain(brainId, brainbase);
return new Brain(db, space, brainbase);
const brain = new Brain(db, space, brainbase);
return { brain, description, baseSystemPrompt, extractedFacts };
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`Failed to create brain "${displayName}": ${reason}`);

View File

@@ -1,5 +1,8 @@
import { randomUUID } from "node:crypto";
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { existsSync, readdirSync } from "fs";
import { unlink, writeFile } from "fs/promises";
import type { ExtractedFact } from "identitydb";
import { tmpdir } from "os";
interface RecordedCall {
@@ -16,18 +19,7 @@ const llmCalls: RecordedCall[] = [];
const PERSONA_DESCRIPTION = "A 34yo night-shift nurse, hides exhaustion behind sarcasm.";
const GENERATED_BASE_SYSTEM_PROMPT =
"You are Maren. You text in lowercase. You use '...' when tired.";
const EXTRACTED_FACTS: Array<{
statement: string;
summary: string;
source: string;
confidence: number;
topics: Array<{
name: string;
category: string;
granularity: string;
role: string;
}>;
}> = [
const EXTRACTED_FACTS: ExtractedFact[] = [
{
statement: "Maren is 34 years old.",
summary: "Maren is 34 years old.",
@@ -96,6 +88,7 @@ mock.module("@/config", () => ({
}));
const { runDebugBrainInit } = await import("./brain");
const { Brain: ProdBrain } = await import("@/brain");
beforeEach(() => {
llmCalls.length = 0;
@@ -115,7 +108,7 @@ afterEach(async () => {
});
describe("runDebugBrainInit", () => {
test("B1: returns ok result with brainId, spaceName, baseSystemPrompt, and uses the supplied seed", async () => {
test("B1: returns ok result with full description, baseSystemPrompt, extractedFacts, and uses the supplied seed", async () => {
const result = await runDebugBrainInit({
displayName: "Maren",
seed: "Maren, 34, night-shift nurse, hides exhaustion behind sarcasm",
@@ -131,6 +124,8 @@ describe("runDebugBrainInit", () => {
);
expect(result.spaceName).toBe(`brain:${result.brainId}`);
expect(result.description).toBe(PERSONA_DESCRIPTION);
expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT);
expect(result.baseSystemPrompt).toContain("You exist in a text chat.");
expect(result.baseSystemPrompt).toBe(
@@ -139,6 +134,8 @@ describe("runDebugBrainInit", () => {
GENERATED_BASE_SYSTEM_PROMPT.length + 2,
),
);
expect(result.extractedFacts).toEqual(EXTRACTED_FACTS);
});
test("B2: invokes the LLM exactly 3 times — PERSONA_INIT, PERSONA_BASE_SYSTEM_PROMPT, fact-extractor", async () => {
@@ -207,3 +204,49 @@ describe("runDebugBrainInit", () => {
expect(result.ok).toBe(true);
});
});
describe("Brain.create (production path — debug: false)", () => {
test("B6: with debug omitted (default), uses db.ingestStatements and does NOT return extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren", "a prod seed", {
dbPath: ":memory:",
braindbPath,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.brain).toBeDefined();
expect(result.description).toBe(PERSONA_DESCRIPTION);
expect(result.baseSystemPrompt).toContain(GENERATED_BASE_SYSTEM_PROMPT);
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
test("B7: with debug: false (explicit), same as default — uses db.ingestStatements, no extractedFacts", async () => {
const braindbPath = `${tmpdir()}/brainbox-prod-brain-${randomUUID()}.json`;
await writeFile(braindbPath, "{}", { encoding: "utf-8" });
const result = await ProdBrain.create("ProdMaren2", "seed", {
dbPath: ":memory:",
braindbPath,
debug: false,
});
try {
expect(result).not.toBeNull();
if (!result) throw new Error("expected result");
expect(result.extractedFacts).toBeUndefined();
} finally {
try {
await unlink(braindbPath);
} catch {}
}
});
});

View File

@@ -4,6 +4,7 @@ import { tmpdir } from "os";
import { join } from "path";
import type { Command } from "commander";
import ora from "ora";
import type { ExtractedFact } from "identitydb";
import { Brain } from "@/brain";
import { logger } from "@/utils/logger";
@@ -19,18 +20,26 @@ export type BrainInitResult =
displayName: string;
brainId: string;
spaceName: string;
description: string;
baseSystemPrompt: string;
extractedFacts: ExtractedFact[];
}
| { ok: false; error: string };
/**
* Exercise the full `Brain.create` flow (PERSONA_INIT → PERSONA_BASE_SYSTEM_PROMPT
* LLM calls → SQLite DB upsert → fact extraction → braindb save) without
* touching real on-disk state.
* LLM calls → SQLite DB upsert → fact extraction via `factExtractor.extract` →
* braindb save) without touching real on-disk state.
*
* - SQLite DB uses `:memory:` (ephemeral, dies with the process).
* - The braindb JSON is written to a fresh temp file under `os.tmpdir()`
* and unlinked after the run.
*
* Prints the full text of:
* 1. the generated `description` (PERSONA_INIT output)
* 2. the concatenated `baseSystemPrompt` (generated + fixed)
* 3. the `extractedFacts` (obtained by directly calling
* `factExtractor.extract(description)`)
*/
export async function runDebugBrainInit(
opts: BrainInitOptions,
@@ -44,27 +53,53 @@ export async function runDebugBrainInit(
`Initializing brain "${opts.displayName}" with LLM (debug, no real disk state)...`,
).start();
try {
const brain = await Brain.create(opts.displayName, opts.seed, {
const result = await Brain.create(opts.displayName, opts.seed, {
dbPath: ":memory:",
braindbPath,
debug: true,
});
if (!brain) {
if (!result) {
spinner.fail("Brain initialization failed");
return { ok: false, error: "Brain initialization failed" };
}
const {
brain,
description,
baseSystemPrompt,
extractedFacts,
} = result;
const factCount = extractedFacts?.length ?? 0;
spinner.succeed(
`Brain initialized (id=${brain.brainbase.brainId}, space=${brain.brainbase.spaceName})`,
`Brain initialized (id=${brain.brainbase.brainId}, space=${brain.brainbase.spaceName}, ${factCount} fact(s) extracted)`,
);
printSection(`Description (PERSONA_INIT output)`);
console.log(description);
console.log();
printSection(`baseSystemPrompt (PERSONA_BASE_SYSTEM_PROMPT + FIXED)`);
console.log(baseSystemPrompt);
console.log();
printSection(
`Brain — ${opts.displayName} (${brain.brainbase.brainId})`,
);
console.log(`spaceName: ${brain.brainbase.spaceName}`);
console.log(`displayName: ${brain.brainbase.displayName}`);
console.log(`baseSystemPrompt (first 240 chars):`);
console.log(
` ${brain.brainbase.baseSystemPrompt.slice(0, 240).replace(/\n/g, "\n ")}${brain.brainbase.baseSystemPrompt.length > 240 ? "..." : ""}`,
`Extracted facts (factExtractor.extract — ${factCount})`,
);
if (extractedFacts && extractedFacts.length > 0) {
extractedFacts.forEach((fact, i) => {
console.log(`\n[${i + 1}/${extractedFacts.length}]`);
console.log(` statement: ${fact.statement ?? ""}`);
console.log(` summary: ${fact.summary ?? ""}`);
console.log(` source: ${fact.source ?? ""}`);
console.log(` confidence: ${fact.confidence ?? ""}`);
console.log(` topics: ${JSON.stringify(fact.topics)}`);
if (fact.metadata) {
console.log(` metadata: ${JSON.stringify(fact.metadata)}`);
}
});
} else {
console.log(" (no facts extracted)");
}
console.log();
logger.info("Debug run complete. Nothing was written to real disk.");
@@ -74,14 +109,15 @@ export async function runDebugBrainInit(
displayName: opts.displayName,
brainId: brain.brainbase.brainId,
spaceName: brain.brainbase.spaceName,
baseSystemPrompt: brain.brainbase.baseSystemPrompt,
description,
baseSystemPrompt,
extractedFacts: extractedFacts ?? [],
};
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
spinner.fail("Brain initialization failed");
return { ok: false, error: reason };
} finally {
// Clean up the temp braindb file regardless of success/failure.
try {
await unlink(braindbPath);
} catch {}