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