Table of Contents
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:
- Persona identity — stable display name, seed facts, personality, history, preferences, and relationships.
- Durable memory — IdentityDB-backed facts that can be retrieved instead of invented.
- Recent conversation context — yesterday/today messages passed by the API user as
messageHistory. - Schedule — ordinary days, work/study/rest/sleep, and occasional special events.
- Availability/status —
online,do-not-disturb, orofflinederived from schedule. - Delivery timing — typing indicators, realistic delays, multi-message bursts, and deferral while offline.
- Proactive behavior — the persona sometimes starts conversations when schedule and relationship context make it plausible.
- Sleep memory — end-of-day summarization into objective durable facts.
- 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
- Create or load the persona.
- Ensure yesterday/today/tomorrow schedules exist for the persona.
- Call
getTodayScheduledAvailability(now)to rebuild the in-memory availability snapshot. - Sync the host platform status from the snapshot.
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 active 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
startConversationis appropriate.
Midnight automation
Run the schedule and sleep pipeline from the host app on a real timer rather than exposing them as user-facing chat commands. A common policy is one local-midnight job:
- Run sleep memory for the previous local day's messages.
- Generate the next day's schedule.
- On the last day of a month, generate the next monthly schedule window.
- Record debug/events to an admin dashboard or developer channel.
const now = new Date('2026-05-31T00:00:00.000');
await persona.sleepMemory({
datetime: now,
messageHistory: messagesFromPreviousLocalDay,
});
await persona.createDailySchedule(
now,
"Generate tomorrow's ordinary realistic day schedule.",
);
if (new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1).getDate() === 1) {
await persona.createMonthlySchedule(
now,
'Generate the coming month of ordinary long-horizon anchors and special events.',
);
}
Because createDailySchedule(datetime, message) targets the day after datetime, running it at May 1 00:00 creates May 2 00:00 through May 3 00:00. createMonthlySchedule(datetime, message) follows the same next-day convention for its first monthly entry, so monthly automation should run on the last local midnight of the month; for example, May 31 00:00 creates a monthly window that begins June 1. This keeps the persona from "planning itself" in response to a slash command and makes the schedule feel like background life state.
Then drop local yesterday-only caches according to your application retention policy.
Message delivery policy
sendMessage and startConversation return an OutgoingMessageDraft:
{
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:
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.05to0.08seconds per character - minimum delay:
600msto avoid instant bot-like replies - maximum delay: cap around
8-15sunless the persona is intentionally slow - inter-message pause:
400-1500msbetween 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_messagetool - 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:
{
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 snapshot gives ranges, not a single active-mode helper. Host apps should compute the active range for now.
Proactive conversations
startConversation lets the persona speak first:
const opener = await persona.startConversation({ datetime: now, messageHistory });
Recommended proactive gate:
- Compute active availability.
- Skip if
offline. - Strongly reduce probability if
do-not-disturb. - Check recent conversation cooldown.
- Check whether a schedule event makes the opener plausible.
- Call
startConversation. - Deliver with typing delay.
Example policy:
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 immediate 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:
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. Host apps should usually trigger it automatically from a midnight job over the previous local day's conversation log, then expose the result through observability surfaces rather than requiring a user to ask the persona to sleep.
The memory extraction instruction tells the model to objectivize subjective statements before storage:
"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 analyzed conversation unless the model explains the update
Debug hooks for observability
Use debug(event) to make the invisible harness visible:
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.