docs: add BoxBrain framework wiki
530
API-Reference.md
Normal file
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
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
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
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
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
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)
|
||||||
Reference in New Issue
Block a user