feat: add debug command for debugging schedule generation

This commit is contained in:
2026-06-06 23:32:48 +09:00
parent c422a0c0d4
commit a19fc3e508
10 changed files with 635 additions and 74 deletions

View File

@@ -399,3 +399,55 @@ describe("S8: regression on existing methods", () => {
expect(typeof Brain.load).toBe("function");
});
});
describe("Brain.createDebug", () => {
test("D1: returns a Brain with debug=true, the supplied personality, and no disk file created", async () => {
const { existsSync } = await import("fs");
const { resolve } = await import("path");
const before = existsSync(resolve(process.cwd(), "brainbox.db"));
const brain = await Brain.createDebug({ personality: "test-personality-Q" });
expect(brain).toBeInstanceOf(Brain);
expect(brain.debug).toBe(true);
expect(brain.brainbase.baseSystemPrompt).toBe("test-personality-Q");
expect(brain.brainbase.displayName).toBe("Debug Brain");
const after = existsSync(resolve(process.cwd(), "brainbox.db"));
expect(after).toBe(before);
});
test("D2: createDailySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
const brain = await Brain.createDebug({ personality: "p" });
const today = new Date(2026, 5, 5);
const tomorrow = new Date(2026, 5, 6);
const tomorrowKey = formatDateKey(tomorrow);
const schedule = await brain.createDailySchedule(today, "msg");
expect(schedule).not.toBeNull();
expect(schedule).toHaveLength(48);
const facts = await brain.db.getTopicFacts(`daily-schedule:${tomorrowKey}`, {
spaceName: brain.space.name,
});
expect(facts).toHaveLength(0);
});
test("D3: createMonthlySchedule on a debug brain returns a schedule and does NOT add a fact to the DB", async () => {
const brain = await Brain.createDebug({ personality: "p" });
const today = new Date(2026, 0, 15);
const expected = nextMonth(today);
const monthKey = `${expected.year}-${String(expected.month + 1).padStart(2, "0")}`;
const schedule = await brain.createMonthlySchedule(today, "msg");
expect(schedule).not.toBeNull();
expect(schedule).toHaveLength(expected.daysInMonth);
const facts = await brain.db.getTopicFacts(
`monthly-schedule:${monthKey}`,
{ spaceName: brain.space.name },
);
expect(facts).toHaveLength(0);
});
});

View File

@@ -21,6 +21,11 @@ import {
nextMonth,
pad2,
} from "./schedule";
import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
export interface DebugOptions {
personality: string;
}
export class Brain {
private availabilityCache: Map<string, Availability[]> = new Map();
@@ -29,6 +34,7 @@ export class Brain {
public db: IdentityDB,
public space: Space,
public brainbase: BrainItem,
public debug: boolean = false,
) {}
async createDailySchedule(
@@ -62,37 +68,44 @@ export class Brain {
jsonSchema: dailyScheduleSchema,
});
await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Daily schedule for ${dateKey} (${schedule.length} slots)`,
source: "createDailySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: dateKey,
category: "temporal",
granularity: "concrete",
role: "date",
},
],
});
if (!this.debug) {
await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Daily schedule for ${dateKey} (${schedule.length} slots)`,
source: "createDailySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "daily-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: dateKey,
category: "temporal",
granularity: "concrete",
role: "date",
},
],
});
}
return schedule;
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
let reason =
error instanceof Error
? error.message + `(${error.name})`
: String(error);
if (error instanceof BadRequestResponseError)
reason = reason + `${error.body}`;
logger.error(`createDailySchedule failed: ${reason}`);
return null;
}
@@ -125,33 +138,35 @@ export class Brain {
jsonSchema: monthlyScheduleSchema,
});
await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Monthly schedule for ${monthKey} (${schedule.length} days)`,
source: "createMonthlySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "monthly-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: monthKey,
category: "temporal",
granularity: "concrete",
role: "period",
},
],
});
if (!this.debug) {
await this.db.addFact({
spaceName: this.space.name,
statement: JSON.stringify(schedule),
summary: `Monthly schedule for ${monthKey} (${schedule.length} days)`,
source: "createMonthlySchedule",
confidence: 1.0,
topics: [
{
name: topicName,
category: "temporal",
granularity: "concrete",
role: "schedule",
},
{
name: "monthly-schedule",
category: "concept",
granularity: "abstract",
role: "schedule",
},
{
name: monthKey,
category: "temporal",
granularity: "concrete",
role: "period",
},
],
});
}
return schedule;
} catch (error) {
@@ -169,6 +184,13 @@ export class Brain {
const cached = this.availabilityCache.get(dateKey);
if (cached) return cached;
if (this.debug) {
logger.warn(
"getTodayScheduledAvailability requires a persisted daily schedule; debug brains have no DB. Use deriveAvailabilityFromSchedule(schedule) instead.",
);
return null;
}
const topicName = `daily-schedule:${dateKey}`;
const facts = await this.db.getTopicFacts(topicName, {
spaceName: this.space.name,
@@ -176,19 +198,8 @@ export class Brain {
if (facts.length === 0) return null;
const dailySchedule = JSON.parse(facts[0]!.statement) as DailySchedule;
const instruction = await loadPrompt("SCHEDULE_AVAILABILITY");
const promptMessage = JSON.stringify({
schedule: dailySchedule,
personality: this.brainbase.baseSystemPrompt,
});
const availability = await llm.call<Availability[]>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "availability",
jsonSchema: availabilitySchema,
});
const availability =
await this.deriveAvailabilityFromSchedule(dailySchedule);
this.availabilityCache.set(dateKey, availability);
return availability;
@@ -199,11 +210,35 @@ export class Brain {
}
}
async deriveAvailabilityFromSchedule(
schedule: DailySchedule,
): Promise<Availability[]> {
try {
const instruction = await loadPrompt("SCHEDULE_AVAILABILITY");
const promptMessage = JSON.stringify({
schedule,
personality: this.brainbase.baseSystemPrompt,
});
return await llm.call<Availability[]>(llm.models.identity, {
instruction,
message: promptMessage,
jsonSchemaName: "availability",
jsonSchema: availabilitySchema,
});
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
logger.error(`deriveAvailabilityFromSchedule failed: ${reason}`);
throw error;
}
}
removeScheduledAvailability(): void {
this.availabilityCache.clear();
}
private async getMonthlySummaryForDay(target: Date): Promise<string | null> {
if (this.debug) return null;
try {
const monthKey = formatMonthKey(target);
const topicName = `monthly-schedule:${monthKey}`;
@@ -222,6 +257,7 @@ export class Brain {
}
private async getHistoryFacts(): Promise<string> {
if (this.debug) return "";
try {
const topics = await this.db.listTopics({
spaceName: this.space.name,
@@ -306,4 +342,25 @@ export class Brain {
return new Brain(db, space, brain);
}
static async createDebug(options: DebugOptions): Promise<Brain> {
const db = await IdentityDB.connect({
client: "sqlite",
filename: ":memory:",
});
await db.initialize();
const space = await db.upsertSpace({
name: "debug",
description: "Debug Brain",
});
const brainbase: BrainItem = {
brainId: "debug",
spaceName: "debug",
displayName: "Debug Brain",
baseSystemPrompt: options.personality,
};
return new Brain(db, space, brainbase, true);
}
}