diff --git a/API-Reference.md b/API-Reference.md new file mode 100644 index 0000000..c2f0673 --- /dev/null +++ b/API-Reference.md @@ -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 +``` + +Returns the created or loaded persona memory space. + +### `createDailySchedule(datetime, message)` + +```ts +createDailySchedule(datetime: DateTimeInput, message: string): Promise +``` + +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 +``` + +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 +``` + +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 +``` + +Alias for `deleteSchedulesBefore(datetime)`. + +### `getTodayScheduledAvailability(datetime)` + +```ts +getTodayScheduledAvailability(datetime: DateTimeInput): Promise +``` + +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; +}): Promise +``` + +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 +``` + +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 +``` + +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; +} +``` + +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; +} +``` + +### `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; +} +``` + +```ts +interface RewriteDecision { + rewrite: boolean; + draft?: OutgoingMessageDraft; + reason?: string; +} +``` + +### `MemoryExtractionModel` + +```ts +interface MemoryExtractionModel { + extract(input: MemoryExtractionInput): Promise; +} +``` + +```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; + getSpace(spaceId: string): Promise; + addFact(spaceId: string, fact: FactDraft): Promise; + listFacts(spaceId: string): Promise; + findFacts(spaceId: string, topics: string[]): Promise; + saveScheduleEntries(spaceId: string, entries: ScheduleEntry[]): Promise; + listScheduleEntries(spaceId: string, fromInclusive: string, toExclusive: string): Promise; + deleteScheduleEntriesBefore(spaceId: string, cutoffExclusive: string): Promise; +} +``` + +### `InMemoryMemoryStore` + +Test/demo store backed by maps: + +- `spaces: Map` +- `facts: Map` +- `schedules: Map` + +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 +``` + +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; +} +``` + +### `FactDraft` + +```ts +interface FactDraft { + statement: string; + topics: string[]; + confidence?: number; + source?: string; + metadata?: Record; +} +``` + +### `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; +} +``` + +### `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? +``` diff --git a/Getting-Started.md b/Getting-Started.md new file mode 100644 index 0000000..9f871e2 --- /dev/null +++ b/Getting-Started.md @@ -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` diff --git a/Home.md b/Home.md index e69de29..26ad53c 100644 --- a/Home.md +++ b/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?' }, + ], +}); +``` diff --git a/Human-Like-Runtime.md b/Human-Like-Runtime.md new file mode 100644 index 0000000..56c5bb5 --- /dev/null +++ b/Human-Like-Runtime.md @@ -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. diff --git a/Source-Layout.md b/Source-Layout.md new file mode 100644 index 0000000..676fc5e --- /dev/null +++ b/Source-Layout.md @@ -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 +``` diff --git a/_Sidebar.md b/_Sidebar.md new file mode 100644 index 0000000..6cb112f --- /dev/null +++ b/_Sidebar.md @@ -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)