docs: add BoxBrain framework wiki

2026-05-15 13:20:02 +09:00
parent ac4cd25b7c
commit 6c15d737cc
6 changed files with 1268 additions and 0 deletions

530
API-Reference.md Normal file

@@ -0,0 +1,530 @@
# API Reference
This reference describes the public exports visible from `src/index.ts`:
```ts
export * from './types';
export * from './memory';
export * from './schedule';
export * from './conversation';
export * from './persona';
```
## `Persona`
Main framework class.
### Constructors
```ts
new Persona(displayName: string, seedMessage: string, options?: PersonaOptions)
new Persona(spaceId: string, options?: PersonaOptions)
```
Create mode:
```ts
const persona = new Persona('Mina', 'Mina likes quiet cafes.', { memory, models });
```
Load mode:
```ts
const persona = new Persona(existingSpaceId, { memory, models });
```
Behavior:
- Create mode calls `memory.createSpace({ displayName, seedMessage, now })`.
- Load mode calls `memory.getSpace(spaceId)` and throws if missing.
- Initialization starts immediately in the constructor and is awaited through `ready()`.
- If `options.memory` is omitted, an `InMemoryMemoryStore` is used.
### `ready()`
```ts
ready(): Promise<MemorySpace>
```
Returns the created or loaded persona memory space.
### `createDailySchedule(datetime, message)`
```ts
createDailySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]>
```
Creates tomorrow's 10-minute schedule.
If `datetime` is May 1, the schedule covers May 2 00:00 through May 3 00:00 and contains 144 entries.
Side effects:
- saves entries through `memory.saveScheduleEntries`
- refreshes the in-memory availability snapshot
- emits `persona.schedule.daily.generated`
Current deterministic mapping:
- message containing travel/trip/여행 -> daytime `travel`
- message containing study/exam/공부/시험 -> daytime `study`
- message containing job/취업/구직 -> daytime `job-search`
- message containing work/일/회사 -> daytime `work`
- otherwise daytime defaults to `work`
### `createMonthlySchedule(datetime, message)`
```ts
createMonthlySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]>
```
Creates 30 day-level entries starting tomorrow.
Side effects:
- saves entries through `memory.saveScheduleEntries`
- refreshes availability
- emits `persona.schedule.monthly.generated`
### `deleteSchedulesBefore(cutoffExclusive)`
```ts
deleteSchedulesBefore(cutoffExclusive: DateTimeInput): Promise<number>
```
Deletes or marks schedule entries before a caller-provided cutoff.
Behavior depends on the memory store:
- `InMemoryMemoryStore` physically removes entries whose `endAt <= cutoffExclusive` and returns the deleted count.
- `IdentityDbMemoryStore` currently cannot physically delete schedule facts through IdentityDB's public append-oriented API, so it returns `0` at the store layer.
- The `Persona` layer always records a fact with topic `persona.schedule.deleted` and source `boxbrain.schedule.prune`.
### `deleteSchedulesOlderThan(datetime)`
```ts
deleteSchedulesOlderThan(datetime: DateTimeInput): Promise<number>
```
Alias for `deleteSchedulesBefore(datetime)`.
### `getTodayScheduledAvailability(datetime)`
```ts
getTodayScheduledAvailability(datetime: DateTimeInput): Promise<ScheduledAvailabilitySnapshot>
```
Returns the cached schedule-derived availability snapshot.
Window rule:
- start: UTC start of `datetime`'s day
- end: two UTC days later
If the snapshot is missing or the day changed, BoxBrain reloads schedule entries from memory and rebuilds the snapshot.
### `sendMessage(input)`
```ts
sendMessage(input: {
datetime: DateTimeInput;
messageHistory: PersonaMessage[];
getLatestMessageHistory?: () => Promise<PersonaMessage[]>;
}): Promise<OutgoingMessageDraft>
```
Generates a reply to the user's message history.
Requires:
```ts
options.models.conversation
```
Pipeline:
1. Await persona readiness.
2. Load current availability.
3. Build mandatory conversation context:
- formatted `messageHistory`
- schedule entries from yesterday through tomorrow
- availability snapshot
- facts tagged with `persona`, the persona display name, or `user`
4. Emit `persona.conversation.context.loaded`.
5. Call `models.conversation.generateReply` with mode `reply`.
6. Trim blank messages from the draft.
7. If `getLatestMessageHistory` and `models.rewrite` are present, check whether a newer history arrived and optionally rewrite.
8. Emit `persona.conversation.reply.generated`.
9. Return `OutgoingMessageDraft`.
Throws if no conversation model is configured.
### `startConversation(input)`
```ts
startConversation(input: {
datetime: DateTimeInput;
messageHistory: PersonaMessage[];
}): Promise<OutgoingMessageDraft>
```
Generates a proactive opener from the persona.
Requires:
```ts
options.models.conversation
```
Uses the same mandatory context pipeline as `sendMessage`, but passes `mode: 'start-conversation'` to the model.
Emits `persona.conversation.started`.
### `sleepMemory(input)`
```ts
sleepMemory(input: {
datetime: DateTimeInput;
messageHistory: PersonaMessage[];
}): Promise<FactDraft[]>
```
Extracts durable facts from a period of messages and persists them.
Requires:
```ts
options.models.memoryExtraction
```
Pipeline:
1. Find current context facts tagged with `persona`, the persona display name, or `user`.
2. Format the provided message history.
3. Call `models.memoryExtraction.extract` with the objectivization instruction.
4. Persist each returned fact through `memory.addFact`.
5. Add the topic `sleepMemory` to each persisted fact.
6. Use source `boxbrain.sleepMemory` if the draft did not specify a source.
7. Emit `persona.memory.sleep.persisted`.
8. Return the extracted drafts.
Recommended cadence: daily around midnight, passing the previous day's messages.
## Options and models
### `PersonaOptions`
```ts
interface PersonaOptions {
memory?: BoxBrainMemoryStore;
models?: PersonaModels;
debug?: DebugHook;
now?: DateTimeInput;
}
```
- `memory`: storage adapter. Defaults to `InMemoryMemoryStore`.
- `models`: provider-agnostic LLM adapters.
- `debug`: optional async event hook.
- `now`: deterministic initialization time for tests or replay.
### `PersonaModels`
```ts
interface PersonaModels {
initialization?: PersonaInitializationModel;
conversation?: ConversationModel;
rewrite?: RewriteModel;
memoryExtraction?: MemoryExtractionModel;
}
```
### `PersonaInitializationModel`
```ts
interface PersonaInitializationModel {
extractInitialFacts(input: {
displayName: string;
seedMessage: string;
now: string;
}): Promise<FactDraft[]>;
}
```
Called during persona creation. If omitted, BoxBrain stores a default seed fact. If it returns `[]`, BoxBrain stores no fallback fact.
### `ConversationModel`
```ts
interface ConversationModel {
generateReply(input: ReplyGenerationInput): Promise<OutgoingMessageDraft>;
}
```
### `ReplyGenerationInput`
```ts
interface ReplyGenerationInput {
persona: MemorySpace;
now: string;
mode: 'reply' | 'start-conversation';
context: MandatoryConversationContext;
userMessage?: string;
instruction: string;
}
```
### `OutgoingMessageDraft`
```ts
interface OutgoingMessageDraft {
messages: string[];
reasoning?: string;
}
```
The host app should deliver each `messages[]` item as a separate messenger message.
### `RewriteModel`
```ts
interface RewriteModel {
decide(input: RewriteDecisionInput): Promise<RewriteDecision>;
}
```
```ts
interface RewriteDecision {
rewrite: boolean;
draft?: OutgoingMessageDraft;
reason?: string;
}
```
### `MemoryExtractionModel`
```ts
interface MemoryExtractionModel {
extract(input: MemoryExtractionInput): Promise<FactDraft[]>;
}
```
```ts
interface MemoryExtractionInput {
persona: MemorySpace;
now: string;
formattedMessageHistory: string;
contextFacts: StoredFact[];
instruction: string;
}
```
## Memory stores
### `BoxBrainMemoryStore`
```ts
interface BoxBrainMemoryStore {
createSpace(input: { displayName: string; seedMessage: string; now: string }): Promise<MemorySpace>;
getSpace(spaceId: string): Promise<MemorySpace | null>;
addFact(spaceId: string, fact: FactDraft): Promise<StoredFact>;
listFacts(spaceId: string): Promise<StoredFact[]>;
findFacts(spaceId: string, topics: string[]): Promise<StoredFact[]>;
saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise<void>;
listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise<ScheduleEntry[]>;
deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise<number>;
}
```
### `InMemoryMemoryStore`
Test/demo store backed by maps:
- `spaces: Map<string, MemorySpace>`
- `facts: Map<string, StoredFact[]>`
- `schedules: Map<string, ScheduleEntry[]>`
Useful for unit tests and demos. Not persistent.
### `IdentityDbMemoryStore`
Persistent store backed by IdentityDB.
```ts
import { IdentityDB } from 'identitydb';
import { IdentityDbMemoryStore } from 'boxbrain';
const db = await IdentityDB.connect({ client: 'sqlite', filename: '.data/personas.sqlite' });
await db.initialize();
const memory = new IdentityDbMemoryStore({ db });
```
### `createSqliteIdentityMemoryStore(filename)`
```ts
createSqliteIdentityMemoryStore(filename: string): Promise<IdentityDbMemoryStore>
```
Convenience helper that connects and initializes an IdentityDB SQLite database.
## Core types
### `DateTimeInput`
```ts
type DateTimeInput = Date | string | number;
```
All datetime inputs are converted through `new Date(...)`. Invalid dates throw.
### `MemorySpace`
```ts
interface MemorySpace {
id: string;
displayName: string;
createdAt: string;
metadata: Record<string, unknown>;
}
```
### `FactDraft`
```ts
interface FactDraft {
statement: string;
topics: string[];
confidence?: number;
source?: string;
metadata?: Record<string, unknown>;
}
```
### `StoredFact`
```ts
interface StoredFact extends FactDraft {
id: string;
createdAt: string;
}
```
### `PersonaMessage`
```ts
interface PersonaMessage {
sender: 'persona' | 'user';
time: DateTimeInput;
content: string;
}
```
### `ScheduleActivity`
```ts
type ScheduleActivity =
| 'sleep'
| 'rest'
| 'meal'
| 'commute'
| 'work'
| 'study'
| 'job-search'
| 'travel'
| 'exercise'
| 'social'
| 'errand'
| 'free-time';
```
### `ScheduleEntry`
```ts
interface ScheduleEntry {
id: string;
spaceId: string;
startAt: string;
endAt: string;
activity: ScheduleActivity;
title: string;
description?: string;
granularity: 'day' | 'ten-minute';
sourceMessage?: string;
metadata: Record<string, unknown>;
}
```
### `AvailabilityMode`
```ts
type AvailabilityMode = 'online' | 'do-not-disturb' | 'offline';
```
### `AvailabilityRange`
```ts
interface AvailabilityRange {
startAt: string;
endAt: string;
mode: AvailabilityMode;
sourceScheduleIds: string[];
reason: string;
}
```
### `ScheduledAvailabilitySnapshot`
```ts
interface ScheduledAvailabilitySnapshot {
generatedAt: string;
windowStartAt: string;
windowEndAt: string;
ranges: AvailabilityRange[];
}
```
### `MandatoryConversationContext`
```ts
interface MandatoryConversationContext {
formattedMessageHistory: string;
conversationWindowLabel: string;
memorySummary: string;
personaAndUserFacts: StoredFact[];
scheduleEntries: ScheduleEntry[];
availability: ScheduledAvailabilitySnapshot;
}
```
## Schedule helper exports
The following helpers are exported for applications and tests:
```ts
toDate(input)
toIso(input)
startOfUtcDay(input)
addUtcDays(input, days)
scheduleTargetDay(now)
createTenMinuteDailySchedule(input)
createMonthlyScheduleEntries(input)
availabilityModeForEntry(entry)
buildAvailabilitySnapshot(input)
dateKeysAround(input)
```
## Conversation helper exports
```ts
formatMessageHistory({ personaName, messages })
conversationInstruction()
memoryExtractionInstruction(now)
buildMandatoryConversationContext(input)
```
`formatMessageHistory` converts structured messages into model-readable text:
```text
Mina@2026-04-30T23:00:00.000Z: See you later.
user@2026-05-01T12:00:00.000Z: What are you doing?
```

255
Getting-Started.md Normal file

@@ -0,0 +1,255 @@
# Getting Started
This page shows the shortest path from installation to a working BoxBrain persona.
## Requirements
- Bun `>= 1.2.0`
- TypeScript
- A model adapter object that implements BoxBrain's provider-agnostic interfaces
- Optional but recommended: a SQLite-backed IdentityDB store via `createSqliteIdentityMemoryStore`
## Install
For local development inside the repository:
```bash
bun install
bun run test
bun run check
bun run build
```
For a consuming app, install the package normally if it is available in your registry:
```bash
bun add boxbrain
```
If consuming directly from the Gitea repository, pin a commit for reproducibility:
```bash
bun add git+https://git.psw.kr/p-sw/BoxBrain.git#49f75af
```
## 1. Create a memory store
Use `InMemoryMemoryStore` for tests and demos:
```ts
import { InMemoryMemoryStore } from 'boxbrain';
const memory = new InMemoryMemoryStore();
```
Use SQLite + IdentityDB for persistent persona memory:
```ts
import { createSqliteIdentityMemoryStore } from 'boxbrain';
const memory = await createSqliteIdentityMemoryStore('.data/personas.sqlite');
```
## 2. Provide model adapters
BoxBrain does not hard-code OpenAI, xAI, OpenRouter, Anthropic, or any other provider. The host app supplies objects that match the model interfaces.
A minimal conversation model:
```ts
import type { ConversationModel } from 'boxbrain';
const conversation: ConversationModel = {
async generateReply(input) {
// Call your LLM here.
// input.instruction contains BoxBrain's chat-like send_message guidance.
// input.context contains message history, memory, schedule, and availability.
return {
messages: ['I was just studying a bit.', 'How about you?'],
reasoning: 'Short casual reply matching the persona.',
};
},
};
```
A minimal memory extraction model:
```ts
import type { MemoryExtractionModel } from 'boxbrain';
const memoryExtraction: MemoryExtractionModel = {
async extract(input) {
// Call your LLM here and return durable, objectivized facts.
return [
{
statement: 'The user started TypeScript in 2025.',
topics: ['user', 'TypeScript', '2025'],
confidence: 0.9,
},
];
},
};
```
Optional model hooks:
- `models.initialization`: extracts initial persona facts from the seed message.
- `models.rewrite`: decides whether to discard a stale draft if new messages arrived while generating.
## 3. Create a persona
```ts
import { Persona } from 'boxbrain';
const persona = new Persona(
'Mina',
'Mina is a careful student who likes quiet cafes and is preparing for exams.',
{
memory,
models: { conversation, memoryExtraction },
now: '2026-05-01T10:00:00.000Z',
debug: (event) => console.log(event),
},
);
const space = await persona.ready();
console.log(space.id);
```
Creation behavior:
- `new Persona(displayName, seedMessage, options)` creates a new isolated persona space.
- The seed message is the freeform source for personality, history, likes, dislikes, relationships, and other persona facts.
- If `models.initialization` is provided, BoxBrain asks it for initial facts.
- If no initialization model is provided, BoxBrain stores a minimal seed fact.
- If the initialization model intentionally returns an empty list, BoxBrain does not add the fallback fact.
Load an existing persona:
```ts
const loaded = new Persona(space.id, { memory, models: { conversation, memoryExtraction } });
await loaded.ready();
```
## 4. Generate schedules
Create tomorrow's 10-minute schedule:
```ts
const entries = await persona.createDailySchedule(
'2026-05-01T10:00:00.000Z',
'Keep a normal work day.',
);
console.log(entries.length); // 144
```
Create a 30-day day-level outline, starting tomorrow:
```ts
await persona.createMonthlySchedule(
'2026-05-01T10:00:00.000Z',
'Mostly study, with occasional rest.',
);
```
Current schedule generation is deterministic:
- sleep before 07:00 and after 23:00
- meal blocks around 07:00, 12:00, and 18:00
- commute around 08:00 and 17:00
- work/study/job-search/travel based on keywords in the message
- rest/free-time in the evening
## 5. Read availability
```ts
const availability = await persona.getTodayScheduledAvailability('2026-05-01T12:00:00.000Z');
```
The snapshot covers:
- today 00:00
- through tomorrow 24:00
For example, if `datetime` is May 1, the window is May 1 00:00 through May 3 00:00.
Availability is derived from schedule entries:
- `sleep` -> `offline`
- `work`, `study`, `job-search`, `travel`, `commute` -> `do-not-disturb`
- `rest`, `meal`, `exercise`, `social`, `errand`, `free-time` -> `online`
## 6. Reply to a user message
```ts
const reply = await persona.sendMessage({
datetime: '2026-05-01T12:00:00.000Z',
messageHistory: [
{ sender: 'persona', time: '2026-04-30T23:00:00.000Z', content: 'See you later.' },
{ sender: 'user', time: '2026-05-01T12:00:00.000Z', content: 'What are you doing?' },
],
});
for (const message of reply.messages) {
await deliverToMessenger(message);
}
```
BoxBrain formats the structured history for the model as:
```text
Mina@2026-04-30T23:00:00.000Z: See you later.
user@2026-05-01T12:00:00.000Z: What are you doing?
```
## 7. Start a proactive conversation
```ts
const opener = await persona.startConversation({
datetime: '2026-05-01T20:00:00.000Z',
messageHistory: [],
});
```
The same mandatory context pipeline is used, but the model input mode is `start-conversation`.
## 8. Run sleep memory at midnight
Recommended cadence: run once near midnight with the previous day's messages.
```ts
await persona.sleepMemory({
datetime: '2026-05-02T00:00:00.000Z',
messageHistory: messagesFromMay1,
});
```
`sleepMemory` asks the memory extraction model to objectivize durable facts, then persists them to the persona's memory store.
## 9. Add debug tracing
```ts
const persona = new Persona('Mina', seed, {
memory,
models,
debug(event) {
// Write to a log file, messenger admin channel, dashboard, etc.
console.log(`[${event.time}] ${event.name}`, event.data);
},
});
```
Common debug events:
- `persona.initialized`
- `persona.loaded`
- `persona.schedule.daily.generated`
- `persona.schedule.monthly.generated`
- `persona.schedule.deleted`
- `persona.availability.refreshed`
- `persona.conversation.context.loaded`
- `persona.conversation.rewrite.checked`
- `persona.conversation.reply.generated`
- `persona.conversation.started`
- `persona.memory.sleep.persisted`

89
Home.md

@@ -0,0 +1,89 @@
# BoxBrain Wiki
BoxBrain is a TypeScript framework for building LLM harnesses that make an AI-driven persona feel less like a generic assistant and more like a specific, continuing person.
The framework is intentionally not a messenger bot, UI, or one-provider chatbot product. It supplies the core runtime primitives a host application needs:
- isolated persona spaces backed by IdentityDB-compatible memory stores
- persona initialization from a freeform seed message
- realistic schedule entries and schedule-derived availability
- reply and proactive conversation entry points
- sleep-time memory extraction into durable facts
- debug hooks for observing the framework flow and persona reasoning pipeline
> Source of truth: this wiki is written against the current compact MVP source tree (`src/types.ts`, `src/memory.ts`, `src/schedule.ts`, `src/conversation.ts`, `src/persona.ts`, `src/index.ts`) at repository commit `49f75af`.
## Why BoxBrain exists
Most LLM chat integrations feel artificial because every response is generated in isolation: the model is always available, always has the same tone, and often pretends to remember things it has not actually loaded.
BoxBrain exists to design the harness around the illusion of an ongoing person:
1. **A persona has a private memory space.** Each persona is initialized into an isolated IdentityDB-style space. Facts about the persona, the user, their relationship, schedules, and durable memories live there.
2. **A persona has time.** Schedules and availability make the persona online, busy, or offline depending on what they are supposed to be doing.
3. **A persona can speak first.** `startConversation` supports proactive messages instead of only reactive replies.
4. **A persona forgets unless memory is explicitly retrieved.** Mandatory memory retrieval tells the response model what was actually found; if nothing is found, the context explicitly says `기억이 없음`.
5. **The host app controls delivery realism.** BoxBrain returns outgoing message drafts. The application should add typing indicators, status updates, delays, and delivery policy.
## Current implementation status
BoxBrain currently implements the framework core, not a complete human simulator.
Implemented today:
- `Persona` constructor overloads for creating/loading a persona space
- `InMemoryMemoryStore`
- `IdentityDbMemoryStore`
- `createSqliteIdentityMemoryStore(filename)`
- deterministic daily/monthly schedule helpers
- schedule-derived availability snapshots
- mandatory conversation context assembly
- `sendMessage`, `startConversation`, and stale-draft rewrite checks
- `sleepMemory` extraction pipeline
- debug events
Important current limitations:
- Schedule generation is deterministic and rule-based in the current source, not yet an LLM-generated planner.
- BoxBrain does not send messages itself. It returns `OutgoingMessageDraft.messages`; the host messenger integration must deliver them.
- Typing indicators and typing delays are not built into the library yet. They should be implemented by the host app using the returned draft messages and availability snapshot.
- The IdentityDB-backed store uses IdentityDB facts as its persistence layer. Schedule entries are stored in fact metadata. Because the public IdentityDB API is append-oriented, `IdentityDbMemoryStore.deleteScheduleEntriesBefore` currently returns `0`; the `Persona` layer records a deletion fact, while the in-memory store physically prunes entries.
## Reading order
1. [Getting Started](Getting-Started)
2. [Human-Like Runtime Guide](Human-Like-Runtime)
3. [API Reference](API-Reference)
4. [Source Layout](Source-Layout)
## Minimal example
```ts
import { Persona, createSqliteIdentityMemoryStore } from 'boxbrain';
const memory = await createSqliteIdentityMemoryStore('.data/mina.sqlite');
const persona = new Persona(
'Mina',
'Mina is a careful student who likes quiet cafes and is preparing for exams.',
{
memory,
models: {
conversation: yourConversationModel,
memoryExtraction: yourMemoryExtractionModel,
},
debug: (event) => console.log(event.name, event.data),
},
);
const space = await persona.ready();
await persona.createDailySchedule(new Date(), 'Keep a normal study day.');
const availability = await persona.getTodayScheduledAvailability(new Date());
const reply = await persona.sendMessage({
datetime: new Date(),
messageHistory: [
{ sender: 'user', time: new Date(), content: 'What are you doing?' },
],
});
```

275
Human-Like-Runtime.md Normal file

@@ -0,0 +1,275 @@
# Human-Like Runtime Guide
BoxBrain's API is intentionally lower-level than a chatbot. It creates the reasoning and memory pipeline, while the host app controls messenger behavior: when to call the API, whether to show typing, how long to delay, and whether the persona should be available.
This division is important. A real-person feeling comes from the whole runtime loop, not only from the LLM prompt.
## The illusion stack
A convincing persona usually needs all of these layers:
1. **Persona identity** — stable display name, seed facts, personality, history, preferences, and relationships.
2. **Durable memory** — IdentityDB-backed facts that can be retrieved instead of invented.
3. **Recent conversation context** — yesterday/today messages passed by the API user as `messageHistory`.
4. **Schedule** — ordinary days, work/study/rest/sleep, and occasional special events.
5. **Availability/status**`online`, `do-not-disturb`, or `offline` derived from schedule.
6. **Delivery timing** — typing indicators, realistic delays, multi-message bursts, and deferral while offline.
7. **Proactive behavior** — the persona sometimes starts conversations when schedule and relationship context make it plausible.
8. **Sleep memory** — end-of-day summarization into objective durable facts.
9. **Debug visibility** — trace hooks so developers can inspect why a persona did something.
BoxBrain implements layers 1-5, 7's core conversation entry point, 8, and 9. The host app should implement delivery timing and product-specific policies.
## Recommended daily loop
Assume today is May 1.
### On app startup
1. Create or load the persona.
2. Ensure yesterday/today/tomorrow schedules exist for the persona.
3. Call `getTodayScheduledAvailability(now)` to rebuild the in-memory availability snapshot.
4. Sync the host platform status from the snapshot.
```ts
const persona = new Persona(spaceId, { memory, models, debug });
await persona.ready();
const availability = await persona.getTodayScheduledAvailability(new Date());
await updatePlatformPresence(availability);
```
The availability snapshot is stored in memory, not separately persisted. If the process restarts, loading the persona and calling `getTodayScheduledAvailability` rebuilds it from schedule entries in the memory store.
### During the day
- On inbound user messages, call `sendMessage`.
- Before delivering replies, check current availability.
- If `offline`, defer or suppress unless your product wants emergency replies.
- If `do-not-disturb`, reply more slowly or only for high-priority messages.
- If `online`, reply normally.
- Periodically evaluate whether a proactive `startConversation` is appropriate.
### Before midnight
Generate tomorrow's 10-minute schedule:
```ts
await persona.createDailySchedule(now, 'Keep tomorrow realistic and ordinary.');
```
Because `createDailySchedule(datetime, message)` targets the day after `datetime`, running it on May 1 creates May 2 00:00 through May 3 00:00.
### At midnight
Run sleep memory on the previous day's messages:
```ts
await persona.sleepMemory({
datetime: '2026-05-02T00:00:00.000Z',
messageHistory: messagesFromMay1,
});
```
Then drop local yesterday-only caches according to your application retention policy.
## Message delivery policy
`sendMessage` and `startConversation` return an `OutgoingMessageDraft`:
```ts
{
messages: ['I was studying.', 'Kinda sleepy now.'],
reasoning: 'optional model reasoning'
}
```
BoxBrain does not call a messenger API. The host app should deliver each item in `messages` as one chat message.
Recommended behavior:
```ts
for (const message of draft.messages) {
await messenger.showTyping(personaId);
await wait(typingDelayFor(message));
await messenger.sendMessage(channelId, message);
await wait(interMessagePause(message));
}
```
A practical starting point:
- typing delay: about `0.05` to `0.08` seconds per character
- minimum delay: `600ms` to avoid instant bot-like replies
- maximum delay: cap around `8-15s` unless the persona is intentionally slow
- inter-message pause: `400-1500ms` between split messages
Keep this deterministic-testable by injecting a random source or delay function in your host app.
## One sentence per message
The conversation instruction generated by BoxBrain tells the response model:
- act as the persona, not a generic assistant
- conceptually use a `send_message` tool
- return one or more outgoing messages
- unless the persona strongly prefers otherwise, keep each outgoing message to at most one sentence
- prefer short, natural chat wording
- split one thought across multiple messages when that feels human
This is why `OutgoingMessageDraft.messages` is an array.
## Availability-driven status
`getTodayScheduledAvailability(datetime)` returns ranges like:
```ts
{
generatedAt: '2026-05-01T12:00:00.000Z',
windowStartAt: '2026-05-01T00:00:00.000Z',
windowEndAt: '2026-05-03T00:00:00.000Z',
ranges: [
{
startAt: '2026-05-02T00:00:00.000Z',
endAt: '2026-05-02T07:00:00.000Z',
mode: 'offline',
sourceScheduleIds: ['...'],
reason: 'Sleep'
}
]
}
```
Map it to platform presence like this:
| BoxBrain mode | Messenger behavior | Suggested reply policy |
| --- | --- | --- |
| `online` | online/available | normal replies and proactive messages |
| `do-not-disturb` | busy/DND/idle | slower replies, fewer proactive messages |
| `offline` | offline/invisible | defer replies and suppress proactive messages |
The current snapshot gives ranges, not a single `currentMode` helper. Host apps should compute the active range for `now`.
## Proactive conversations
`startConversation` lets the persona speak first:
```ts
const opener = await persona.startConversation({ datetime: now, messageHistory });
```
Recommended proactive gate:
1. Compute active availability.
2. Skip if `offline`.
3. Strongly reduce probability if `do-not-disturb`.
4. Check recent conversation cooldown.
5. Check whether a schedule event makes the opener plausible.
6. Call `startConversation`.
7. Deliver with typing delay.
Example policy:
```ts
if (active.mode === 'online' && minutesSinceLastMessage > 90) {
if (Math.random() < 0.08) {
const draft = await persona.startConversation({ datetime: now, messageHistory });
await deliverDraftWithTyping(draft);
}
}
```
Good proactive openers usually refer to current context lightly instead of demanding attention:
- after study/work: "I'm finally done for a bit."
- during rest/free time: "It's weirdly quiet today."
- around a known event: "I kept thinking about what you said yesterday."
## Stale draft rewrite
Real messaging is concurrent. A user may send another message while the model is generating a reply.
`sendMessage` supports this through `getLatestMessageHistory` plus `models.rewrite`:
```ts
const reply = await persona.sendMessage({
datetime: now,
messageHistory: initialHistory,
getLatestMessageHistory: () => loadLatestMessagesFromMessenger(),
});
```
If latest history has more messages than the original history, BoxBrain asks the rewrite model whether the draft should be discarded. If `rewrite: true`, BoxBrain uses the rewrite model's draft or regenerates with the latest context.
Use this for:
- corrections: "아 맞다..."
- urgent additions: "wait, actually don't answer that"
- rapid multi-message user input
## Sleep memory and objectivization
`sleepMemory` is the end-of-day memory pipeline.
The memory extraction instruction tells the model to objectivize subjective statements before storage:
```text
"I started TypeScript in 2025" -> "The user started TypeScript in 2025."
```
Recommended extraction targets:
- durable user facts
- persona facts
- preferences and dislikes
- relationship facts
- stable history
- schedule-relevant future events
- recurring emotional context
Avoid storing:
- transient small talk
- one-off jokes with no future use
- raw message dumps
- facts contradicted by the current conversation unless the model explains the update
## Debug hooks for observability
Use `debug(event)` to make the invisible harness visible:
```ts
debug(event) {
trace.write({
name: event.name,
time: event.time,
spaceId: event.spaceId,
data: event.data,
});
}
```
Recommended debug sinks:
- local JSONL trace file
- admin dashboard
- developer-only messenger channel
- test snapshots
Useful event groups:
- initialization: `persona.initialized`, `persona.loaded`
- schedule/status: `persona.schedule.daily.generated`, `persona.schedule.monthly.generated`, `persona.schedule.deleted`, `persona.availability.refreshed`
- conversation: `persona.conversation.context.loaded`, `persona.conversation.rewrite.checked`, `persona.conversation.reply.generated`, `persona.conversation.started`
- memory: `persona.memory.sleep.persisted`
## What not to fake
To preserve trust in the harness, do not make the model pretend it knows unavailable information.
BoxBrain already helps here: when mandatory persona/user memory lookup returns no facts, the context says `기억이 없음`. Let the persona react naturally:
- "I don't think I know that yet."
- "Did you tell me before?"
- "I might be missing that memory."
That is usually more human than a confident hallucination.

112
Source-Layout.md Normal file

@@ -0,0 +1,112 @@
# Source Layout
BoxBrain's current implementation is intentionally compact. The repository focuses on a framework core rather than a full chatbot product.
## Root files
| Path | Purpose |
| --- | --- |
| `README.md` | High-level project overview and quick API examples |
| `package.json` | Package metadata, Bun scripts, runtime dependencies |
| `tsconfig.json` | TypeScript configuration |
| `bun.lock` | Bun lockfile |
| `.gitea/workflows/ci.yml` | Gitea CI workflow |
## Source files
| Path | Purpose |
| --- | --- |
| `src/index.ts` | Root barrel export for public APIs |
| `src/types.ts` | Public TypeScript contracts: memory, schedules, models, messages, debug events |
| `src/memory.ts` | `InMemoryMemoryStore`, `IdentityDbMemoryStore`, SQLite helper |
| `src/schedule.ts` | Datetime helpers, deterministic schedule generation, availability derivation |
| `src/conversation.ts` | Message-history formatting, prompts/instructions, mandatory context assembly |
| `src/persona.ts` | Main `Persona` class and orchestration pipeline |
## Tests
| Path | Covered behavior |
| --- | --- |
| `tests/persona.test.ts` | persona creation/loading, initialization facts, debug events |
| `tests/schedule.test.ts` | daily schedule generation, availability, pruning |
| `tests/conversation.test.ts` | mandatory context, missing memory marker, stale rewrite, proactive opener |
| `tests/sleep-memory.test.ts` | objectivized memory extraction and persistence |
| `tests/memory.test.ts` | fact listing for in-memory and SQLite IdentityDB stores |
## Public export shape
`src/index.ts` exports everything from:
```ts
export * from './types';
export * from './memory';
export * from './schedule';
export * from './conversation';
export * from './persona';
```
This means consumers can import framework types and helper functions from `boxbrain` directly.
## Implementation notes
### Persona initialization
`src/persona.ts` provides constructor overloads:
```ts
new Persona(displayName, seedMessage, options)
new Persona(spaceId, options)
```
Create mode stores a new memory space. Load mode retrieves an existing space. `ready()` awaits the initialization promise.
### Memory storage
`src/memory.ts` has two concrete stores:
- `InMemoryMemoryStore` for tests/demos
- `IdentityDbMemoryStore` for persistent IdentityDB-backed spaces
Schedule entries in the IdentityDB store are saved as facts with `metadata.scheduleEntry`.
### Schedule and availability
`src/schedule.ts` currently uses a deterministic routine:
- 144 ten-minute blocks for a daily schedule
- 30 day-level entries for monthly schedules
- keyword-based activity choice for work/study/job-search/travel
- availability ranges derived from schedule activity metadata
### Conversation context
`src/conversation.ts` centralizes mandatory context:
- formatted message history
- yesterday/today/tomorrow schedule lookup
- persona/user fact lookup
- `기억이 없음` marker when no mandatory memory is found
- schedule-derived availability snapshot
### Persona orchestration
`src/persona.ts` ties the pieces together:
- schedule creation and pruning
- availability refresh/cache
- reply generation
- proactive conversation start
- stale-draft rewrite checks
- sleep-memory persistence
- debug event emission
## Verification commands
From the repository root:
```bash
bun install
bun run test
bun run check
bun run build
```

7
_Sidebar.md Normal file

@@ -0,0 +1,7 @@
# BoxBrain Wiki
- [Home](Home)
- [Getting Started](Getting-Started)
- [Human-Like Runtime Guide](Human-Like-Runtime)
- [API Reference](API-Reference)
- [Source Layout](Source-Layout)