Compare commits
4 Commits
8bd6926a95
...
v0.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ef1b89a2d | |||
| f9f37b0835 | |||
| 43b5147f45 | |||
| 239d63dff7 |
4
bun.lock
4
bun.lock
@@ -5,7 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "boxbrain",
|
"name": "boxbrain",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"identitydb": "0.2.1",
|
"identitydb": "^0.2.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
@@ -249,7 +249,7 @@
|
|||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
"identitydb": ["identitydb@0.2.1", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-e+caNqI7F6JaqgyIFQbdiT5/2Frs5PEJgy3mmF+qUVspHZ4z6QtFF5jonDnpYtJpZ9guPWfeQ/xteeaiwOJ5zA=="],
|
"identitydb": ["identitydb@0.2.2", "", { "dependencies": { "kysely": "^0.28.8", "mysql2": "^3.15.3", "pg": "^8.16.0" } }, "sha512-dAgmGIGgLoW/hsh1tWkXGAVZ3bkBK2s7uJmRBeD5K22SfH75lc1N5UPlODJ87JjU/MFbdM6Kc2UlWcKzUuZlig=="],
|
||||||
|
|
||||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "boxbrain",
|
"name": "boxbrain",
|
||||||
"version": "0.3.0",
|
"version": "0.3.2",
|
||||||
"description": "Human-like persona harness framework powered by LLMs and IdentityDB.",
|
"description": "Human-like persona harness framework powered by LLMs and IdentityDB.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"prepublishOnly": "bun run check && bun run test && bun run build && bun run pack:check"
|
"prepublishOnly": "bun run check && bun run test && bun run build && bun run pack:check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"identitydb": "0.2.1"
|
"identitydb": "^0.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
|||||||
@@ -5,36 +5,39 @@ import type {
|
|||||||
MemorySpace,
|
MemorySpace,
|
||||||
PersonaMessage,
|
PersonaMessage,
|
||||||
ScheduledAvailabilitySnapshot,
|
ScheduledAvailabilitySnapshot,
|
||||||
} from './types';
|
} from "./types";
|
||||||
import { addUtcDays, buildAvailabilitySnapshot, dateKeysAround, startOfUtcDay, toIso } from './schedule';
|
import { addUtcDays, dateKeysAround, startOfUtcDay, toIso } from "./schedule";
|
||||||
|
|
||||||
export function formatMessageHistory(input: { personaName: string; messages: PersonaMessage[] }): string {
|
export function formatMessageHistory(input: {
|
||||||
|
personaName: string;
|
||||||
|
messages: PersonaMessage[];
|
||||||
|
}): string {
|
||||||
return input.messages
|
return input.messages
|
||||||
.map((message) => {
|
.map((message) => {
|
||||||
const sender = message.sender === 'persona' ? input.personaName : 'user';
|
const sender = message.sender === "persona" ? input.personaName : "user";
|
||||||
return `${sender}@${toIso(message.time)}: ${message.content}`;
|
return `${sender}@${toIso(message.time)}: ${message.content}`;
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function conversationInstruction(): string {
|
export function conversationInstruction(): string {
|
||||||
return [
|
return [
|
||||||
'You are controlling the persona, not a generic assistant.',
|
"You are controlling the persona, not a generic assistant.",
|
||||||
'Use the send_message tool conceptually: return one or more outgoing messages.',
|
"Use the send_message tool conceptually: return one or more outgoing messages.",
|
||||||
'Unless the persona strongly prefers otherwise, keep each outgoing message to at most one sentence.',
|
"Unless the persona strongly prefers otherwise, keep each outgoing message to at most one sentence.",
|
||||||
'Prefer short, natural, chat-like wording and allow splitting one thought into multiple messages.',
|
"Prefer short, natural, chat-like wording and allow splitting one thought into multiple messages.",
|
||||||
'If mandatory memory says "기억이 없음", the persona may naturally wonder about missing context instead of pretending to remember.',
|
'If mandatory memory says "기억이 없음", the persona may naturally wonder about missing context instead of pretending to remember.',
|
||||||
].join('\n');
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function memoryExtractionInstruction(now: string): string {
|
export function memoryExtractionInstruction(now: string): string {
|
||||||
return [
|
return [
|
||||||
`Current objective time: ${now}.`,
|
`Current objective time: ${now}.`,
|
||||||
'Read the message history and extract durable facts worth remembering.',
|
"Read the message history and extract durable facts worth remembering.",
|
||||||
'Objectivize subjective statements before storage.',
|
"Objectivize subjective statements before storage.",
|
||||||
'Example: "I started TypeScript in 2025" becomes "The user started TypeScript in 2025."',
|
'Example: "I started TypeScript in 2025" becomes "The user started TypeScript in 2025."',
|
||||||
'Prefer facts about the persona, the user, their relationship, preferences, history, schedule-relevant events, and stable traits.',
|
"Prefer facts about the persona, the user, their relationship, preferences, history, schedule-relevant events, and stable traits.",
|
||||||
].join('\n');
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buildMandatoryConversationContext(input: {
|
export async function buildMandatoryConversationContext(input: {
|
||||||
@@ -47,15 +50,27 @@ export async function buildMandatoryConversationContext(input: {
|
|||||||
const now = startOfUtcDay(input.now);
|
const now = startOfUtcDay(input.now);
|
||||||
const from = addUtcDays(now, -1).toISOString();
|
const from = addUtcDays(now, -1).toISOString();
|
||||||
const to = addUtcDays(now, 2).toISOString();
|
const to = addUtcDays(now, 2).toISOString();
|
||||||
const scheduleEntries = await input.memory.listScheduleEntries(input.persona.id, from, to);
|
const scheduleEntries = await input.memory.listScheduleEntries(
|
||||||
const personaAndUserFacts = await input.memory.findFacts(input.persona.id, ['persona', input.persona.displayName, 'user']);
|
input.persona.id,
|
||||||
const memorySummary = personaAndUserFacts.length === 0
|
from,
|
||||||
? '기억이 없음'
|
to,
|
||||||
: personaAndUserFacts.map((fact) => `- ${fact.statement}`).join('\n');
|
);
|
||||||
|
const personaAndUserFacts = await input.memory.findFacts(input.persona.id, [
|
||||||
|
"persona",
|
||||||
|
input.persona.displayName,
|
||||||
|
"user",
|
||||||
|
]);
|
||||||
|
const memorySummary =
|
||||||
|
personaAndUserFacts.length === 0
|
||||||
|
? "기억이 없음"
|
||||||
|
: personaAndUserFacts.map((fact) => `- ${fact.statement}`).join("\n");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
formattedMessageHistory: formatMessageHistory({ personaName: input.persona.displayName, messages: input.messages }),
|
formattedMessageHistory: formatMessageHistory({
|
||||||
conversationWindowLabel: `Required conversation window: yesterday/today. Schedule dates: ${dateKeysAround(input.now).join(', ')}.`,
|
personaName: input.persona.displayName,
|
||||||
|
messages: input.messages,
|
||||||
|
}),
|
||||||
|
conversationWindowLabel: `Required conversation window: yesterday/today. Schedule dates: ${dateKeysAround(input.now).join(", ")}.`,
|
||||||
memorySummary,
|
memorySummary,
|
||||||
personaAndUserFacts,
|
personaAndUserFacts,
|
||||||
scheduleEntries,
|
scheduleEntries,
|
||||||
|
|||||||
282
src/persona.ts
282
src/persona.ts
@@ -1,4 +1,4 @@
|
|||||||
import { InMemoryMemoryStore } from './memory';
|
import { InMemoryMemoryStore } from "./memory";
|
||||||
import {
|
import {
|
||||||
addUtcDays,
|
addUtcDays,
|
||||||
blocksToDailySchedule,
|
blocksToDailySchedule,
|
||||||
@@ -9,13 +9,13 @@ import {
|
|||||||
scheduleTargetDay,
|
scheduleTargetDay,
|
||||||
startOfUtcDay,
|
startOfUtcDay,
|
||||||
toIso,
|
toIso,
|
||||||
} from './schedule';
|
} from "./schedule";
|
||||||
import {
|
import {
|
||||||
buildMandatoryConversationContext,
|
buildMandatoryConversationContext,
|
||||||
conversationInstruction,
|
conversationInstruction,
|
||||||
formatMessageHistory,
|
formatMessageHistory,
|
||||||
memoryExtractionInstruction,
|
memoryExtractionInstruction,
|
||||||
} from './conversation';
|
} from "./conversation";
|
||||||
import type {
|
import type {
|
||||||
BoxBrainMemoryStore,
|
BoxBrainMemoryStore,
|
||||||
DateTimeInput,
|
DateTimeInput,
|
||||||
@@ -27,28 +27,31 @@ import type {
|
|||||||
PersonaOptions,
|
PersonaOptions,
|
||||||
ScheduleEntry,
|
ScheduleEntry,
|
||||||
ScheduledAvailabilitySnapshot,
|
ScheduledAvailabilitySnapshot,
|
||||||
} from './types';
|
} from "./types";
|
||||||
|
|
||||||
interface CreateMode {
|
interface CreateMode {
|
||||||
type: 'create';
|
type: "create";
|
||||||
displayName: string;
|
displayName: string;
|
||||||
seedMessage: string;
|
seedMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoadMode {
|
interface LoadMode {
|
||||||
type: 'load';
|
type: "load";
|
||||||
spaceId: string;
|
spaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mode = CreateMode | LoadMode;
|
type Mode = CreateMode | LoadMode;
|
||||||
|
|
||||||
function defaultInitialFact(displayName: string, seedMessage: string): FactDraft {
|
function defaultInitialFact(
|
||||||
|
displayName: string,
|
||||||
|
seedMessage: string,
|
||||||
|
): FactDraft {
|
||||||
return {
|
return {
|
||||||
statement: `${displayName} is a BoxBrain persona initialized from this seed: ${seedMessage}`,
|
statement: `${displayName} is a BoxBrain persona initialized from this seed: ${seedMessage}`,
|
||||||
topics: ['persona', displayName],
|
topics: ["persona", displayName],
|
||||||
source: 'boxbrain.persona.initialization',
|
source: "boxbrain.persona.initialization",
|
||||||
confidence: 1,
|
confidence: 1,
|
||||||
metadata: { boxbrainType: 'persona-initial-fact' },
|
metadata: { boxbrainType: "persona-initial-fact" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,14 +69,22 @@ export class Persona {
|
|||||||
private readonly readyPromise: Promise<MemorySpace>;
|
private readonly readyPromise: Promise<MemorySpace>;
|
||||||
private availabilitySnapshot?: ScheduledAvailabilitySnapshot;
|
private availabilitySnapshot?: ScheduledAvailabilitySnapshot;
|
||||||
|
|
||||||
constructor(displayName: string, seedMessage: string, options?: PersonaOptions);
|
constructor(
|
||||||
|
displayName: string,
|
||||||
|
seedMessage: string,
|
||||||
|
options?: PersonaOptions,
|
||||||
|
);
|
||||||
constructor(spaceId: string, options?: PersonaOptions);
|
constructor(spaceId: string, options?: PersonaOptions);
|
||||||
constructor(first: string, second?: string | PersonaOptions, third?: PersonaOptions) {
|
constructor(
|
||||||
if (typeof second === 'string') {
|
first: string,
|
||||||
this.mode = { type: 'create', displayName: first, seedMessage: second };
|
second?: string | PersonaOptions,
|
||||||
|
third?: PersonaOptions,
|
||||||
|
) {
|
||||||
|
if (typeof second === "string") {
|
||||||
|
this.mode = { type: "create", displayName: first, seedMessage: second };
|
||||||
this.options = third ?? {};
|
this.options = third ?? {};
|
||||||
} else {
|
} else {
|
||||||
this.mode = { type: 'load', spaceId: first };
|
this.mode = { type: "load", spaceId: first };
|
||||||
this.options = second ?? {};
|
this.options = second ?? {};
|
||||||
}
|
}
|
||||||
this.memory = this.options.memory ?? new InMemoryMemoryStore();
|
this.memory = this.options.memory ?? new InMemoryMemoryStore();
|
||||||
@@ -84,10 +95,17 @@ export class Persona {
|
|||||||
return this.readyPromise;
|
return this.readyPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createDailySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]> {
|
get spaceId(): Promise<string> {
|
||||||
|
return this.readyPromise.then((v) => v.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createDailySchedule(
|
||||||
|
datetime: DateTimeInput,
|
||||||
|
message: string,
|
||||||
|
): Promise<ScheduleEntry[]> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
if (!this.options.models?.schedule) {
|
if (!this.options.models?.schedule) {
|
||||||
throw new Error('createDailySchedule requires options.models.schedule.');
|
throw new Error("createDailySchedule requires options.models.schedule.");
|
||||||
}
|
}
|
||||||
const targetDay = scheduleTargetDay(datetime);
|
const targetDay = scheduleTargetDay(datetime);
|
||||||
const blocks = await this.options.models.schedule.generateDailySchedule({
|
const blocks = await this.options.models.schedule.generateDailySchedule({
|
||||||
@@ -96,17 +114,31 @@ export class Persona {
|
|||||||
message,
|
message,
|
||||||
instruction: scheduleInstruction(),
|
instruction: scheduleInstruction(),
|
||||||
});
|
});
|
||||||
const entries = blocksToDailySchedule({ persona, targetDay, message, blocks });
|
const entries = blocksToDailySchedule({
|
||||||
await this.emit('persona.schedule.daily.generated', { targetDay: targetDay.toISOString(), count: entries.length, message });
|
persona,
|
||||||
|
targetDay,
|
||||||
|
message,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
await this.emit("persona.schedule.daily.generated", {
|
||||||
|
targetDay: targetDay.toISOString(),
|
||||||
|
count: entries.length,
|
||||||
|
message,
|
||||||
|
});
|
||||||
await this.memory.saveScheduleEntries(persona.id, entries);
|
await this.memory.saveScheduleEntries(persona.id, entries);
|
||||||
await this.refreshAvailability(datetime);
|
await this.refreshAvailability(datetime);
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createMonthlySchedule(datetime: DateTimeInput, message: string): Promise<ScheduleEntry[]> {
|
async createMonthlySchedule(
|
||||||
|
datetime: DateTimeInput,
|
||||||
|
message: string,
|
||||||
|
): Promise<ScheduleEntry[]> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
if (!this.options.models?.schedule) {
|
if (!this.options.models?.schedule) {
|
||||||
throw new Error('createMonthlySchedule requires options.models.schedule.');
|
throw new Error(
|
||||||
|
"createMonthlySchedule requires options.models.schedule.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const fromDay = scheduleTargetDay(datetime);
|
const fromDay = scheduleTargetDay(datetime);
|
||||||
const days = daysInMonth(fromDay);
|
const days = daysInMonth(fromDay);
|
||||||
@@ -117,8 +149,16 @@ export class Persona {
|
|||||||
days,
|
days,
|
||||||
instruction: scheduleInstruction(),
|
instruction: scheduleInstruction(),
|
||||||
});
|
});
|
||||||
const entries = blocksToMonthlySchedule({ persona, fromDay, message, blocks });
|
const entries = blocksToMonthlySchedule({
|
||||||
await this.emit('persona.schedule.monthly.generated', { count: entries.length, message });
|
persona,
|
||||||
|
fromDay,
|
||||||
|
message,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
await this.emit("persona.schedule.monthly.generated", {
|
||||||
|
count: entries.length,
|
||||||
|
message,
|
||||||
|
});
|
||||||
await this.memory.saveScheduleEntries(persona.id, entries);
|
await this.memory.saveScheduleEntries(persona.id, entries);
|
||||||
await this.refreshAvailability(datetime);
|
await this.refreshAvailability(datetime);
|
||||||
return entries;
|
return entries;
|
||||||
@@ -127,14 +167,24 @@ export class Persona {
|
|||||||
async deleteSchedulesBefore(cutoffExclusive: DateTimeInput): Promise<number> {
|
async deleteSchedulesBefore(cutoffExclusive: DateTimeInput): Promise<number> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
const cutoff = toIso(cutoffExclusive);
|
const cutoff = toIso(cutoffExclusive);
|
||||||
const deleted = await this.memory.deleteScheduleEntriesBefore(persona.id, cutoff);
|
const deleted = await this.memory.deleteScheduleEntriesBefore(
|
||||||
|
persona.id,
|
||||||
|
cutoff,
|
||||||
|
);
|
||||||
await this.memory.addFact(persona.id, {
|
await this.memory.addFact(persona.id, {
|
||||||
statement: `Schedules before ${cutoff} were deleted or marked inactive.`,
|
statement: `Schedules before ${cutoff} were deleted or marked inactive.`,
|
||||||
topics: ['persona.schedule.deleted', 'schedule', cutoff.slice(0, 10)],
|
topics: ["persona.schedule.deleted", "schedule", cutoff.slice(0, 10)],
|
||||||
source: 'boxbrain.schedule.prune',
|
source: "boxbrain.schedule.prune",
|
||||||
metadata: { boxbrainType: 'schedule-deletion', cutoffExclusive: cutoff, deleted },
|
metadata: {
|
||||||
|
boxbrainType: "schedule-deletion",
|
||||||
|
cutoffExclusive: cutoff,
|
||||||
|
deleted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.emit("persona.schedule.deleted", {
|
||||||
|
cutoffExclusive: cutoff,
|
||||||
|
deleted,
|
||||||
});
|
});
|
||||||
await this.emit('persona.schedule.deleted', { cutoffExclusive: cutoff, deleted });
|
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +192,15 @@ export class Persona {
|
|||||||
return this.deleteSchedulesBefore(datetime);
|
return this.deleteSchedulesBefore(datetime);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTodayScheduledAvailability(datetime: DateTimeInput): Promise<ScheduledAvailabilitySnapshot> {
|
async getTodayScheduledAvailability(
|
||||||
|
datetime: DateTimeInput,
|
||||||
|
): Promise<ScheduledAvailabilitySnapshot> {
|
||||||
if (!this.availabilitySnapshot) {
|
if (!this.availabilitySnapshot) {
|
||||||
await this.refreshAvailability(datetime);
|
await this.refreshAvailability(datetime);
|
||||||
}
|
}
|
||||||
const snapshot = this.availabilitySnapshot;
|
const snapshot = this.availabilitySnapshot;
|
||||||
if (!snapshot) throw new Error('Availability snapshot was not initialized.');
|
if (!snapshot)
|
||||||
|
throw new Error("Availability snapshot was not initialized.");
|
||||||
|
|
||||||
const today = startOfUtcDay(datetime).toISOString();
|
const today = startOfUtcDay(datetime).toISOString();
|
||||||
if (snapshot.windowStartAt !== today) {
|
if (snapshot.windowStartAt !== today) {
|
||||||
@@ -155,7 +208,8 @@ export class Persona {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshed = this.availabilitySnapshot;
|
const refreshed = this.availabilitySnapshot;
|
||||||
if (!refreshed) throw new Error('Availability snapshot was not initialized.');
|
if (!refreshed)
|
||||||
|
throw new Error("Availability snapshot was not initialized.");
|
||||||
return refreshed;
|
return refreshed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,9 +220,11 @@ export class Persona {
|
|||||||
}): Promise<OutgoingMessageDraft> {
|
}): Promise<OutgoingMessageDraft> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
if (!this.options.models?.conversation) {
|
if (!this.options.models?.conversation) {
|
||||||
throw new Error('sendMessage requires options.models.conversation.');
|
throw new Error("sendMessage requires options.models.conversation.");
|
||||||
}
|
}
|
||||||
const availability = await this.getTodayScheduledAvailability(input.datetime);
|
const availability = await this.getTodayScheduledAvailability(
|
||||||
|
input.datetime,
|
||||||
|
);
|
||||||
const context = await buildMandatoryConversationContext({
|
const context = await buildMandatoryConversationContext({
|
||||||
persona,
|
persona,
|
||||||
now: input.datetime,
|
now: input.datetime,
|
||||||
@@ -176,20 +232,24 @@ export class Persona {
|
|||||||
messages: input.messageHistory,
|
messages: input.messageHistory,
|
||||||
availability,
|
availability,
|
||||||
});
|
});
|
||||||
await this.emit('persona.conversation.context.loaded', {
|
await this.emit("persona.conversation.context.loaded", {
|
||||||
factCount: context.personaAndUserFacts.length,
|
factCount: context.personaAndUserFacts.length,
|
||||||
scheduleEntryCount: context.scheduleEntries.length,
|
scheduleEntryCount: context.scheduleEntries.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
const userMessage = [...input.messageHistory].reverse().find((message) => message.sender === 'user')?.content;
|
const userMessage = [...input.messageHistory]
|
||||||
let draft = ensureDraft(await this.options.models.conversation.generateReply({
|
.reverse()
|
||||||
persona,
|
.find((message) => message.sender === "user")?.content;
|
||||||
now: toIso(input.datetime),
|
let draft = ensureDraft(
|
||||||
mode: 'reply',
|
await this.options.models.conversation.generateReply({
|
||||||
context,
|
persona,
|
||||||
...(userMessage === undefined ? {} : { userMessage }),
|
now: toIso(input.datetime),
|
||||||
instruction: conversationInstruction(),
|
mode: "reply",
|
||||||
}));
|
context,
|
||||||
|
...(userMessage === undefined ? {} : { userMessage }),
|
||||||
|
instruction: conversationInstruction(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
if (input.getLatestMessageHistory && this.options.models.rewrite) {
|
if (input.getLatestMessageHistory && this.options.models.rewrite) {
|
||||||
const latest = await input.getLatestMessageHistory();
|
const latest = await input.getLatestMessageHistory();
|
||||||
@@ -209,20 +269,28 @@ export class Persona {
|
|||||||
draft,
|
draft,
|
||||||
context: latestContext,
|
context: latestContext,
|
||||||
});
|
});
|
||||||
await this.emit('persona.conversation.rewrite.checked', { rewrite: decision.rewrite, reason: decision.reason ?? null });
|
await this.emit("persona.conversation.rewrite.checked", {
|
||||||
|
rewrite: decision.rewrite,
|
||||||
|
reason: decision.reason ?? null,
|
||||||
|
});
|
||||||
if (decision.rewrite) {
|
if (decision.rewrite) {
|
||||||
draft = ensureDraft(decision.draft ?? await this.options.models.conversation.generateReply({
|
draft = ensureDraft(
|
||||||
persona,
|
decision.draft ??
|
||||||
now: toIso(input.datetime),
|
(await this.options.models.conversation.generateReply({
|
||||||
mode: 'reply',
|
persona,
|
||||||
context: latestContext,
|
now: toIso(input.datetime),
|
||||||
instruction: conversationInstruction(),
|
mode: "reply",
|
||||||
}));
|
context: latestContext,
|
||||||
|
instruction: conversationInstruction(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.emit('persona.conversation.reply.generated', { messageCount: draft.messages.length });
|
await this.emit("persona.conversation.reply.generated", {
|
||||||
|
messageCount: draft.messages.length,
|
||||||
|
});
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,9 +300,13 @@ export class Persona {
|
|||||||
}): Promise<OutgoingMessageDraft> {
|
}): Promise<OutgoingMessageDraft> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
if (!this.options.models?.conversation) {
|
if (!this.options.models?.conversation) {
|
||||||
throw new Error('startConversation requires options.models.conversation.');
|
throw new Error(
|
||||||
|
"startConversation requires options.models.conversation.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const availability = await this.getTodayScheduledAvailability(input.datetime);
|
const availability = await this.getTodayScheduledAvailability(
|
||||||
|
input.datetime,
|
||||||
|
);
|
||||||
const context = await buildMandatoryConversationContext({
|
const context = await buildMandatoryConversationContext({
|
||||||
persona,
|
persona,
|
||||||
now: input.datetime,
|
now: input.datetime,
|
||||||
@@ -242,14 +314,18 @@ export class Persona {
|
|||||||
messages: input.messageHistory,
|
messages: input.messageHistory,
|
||||||
availability,
|
availability,
|
||||||
});
|
});
|
||||||
const draft = ensureDraft(await this.options.models.conversation.generateReply({
|
const draft = ensureDraft(
|
||||||
persona,
|
await this.options.models.conversation.generateReply({
|
||||||
now: toIso(input.datetime),
|
persona,
|
||||||
mode: 'start-conversation',
|
now: toIso(input.datetime),
|
||||||
context,
|
mode: "start-conversation",
|
||||||
instruction: conversationInstruction(),
|
context,
|
||||||
}));
|
instruction: conversationInstruction(),
|
||||||
await this.emit('persona.conversation.started', { messageCount: draft.messages.length });
|
}),
|
||||||
|
);
|
||||||
|
await this.emit("persona.conversation.started", {
|
||||||
|
messageCount: draft.messages.length,
|
||||||
|
});
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,64 +335,102 @@ export class Persona {
|
|||||||
}): Promise<FactDraft[]> {
|
}): Promise<FactDraft[]> {
|
||||||
const persona = await this.ready();
|
const persona = await this.ready();
|
||||||
if (!this.options.models?.memoryExtraction) {
|
if (!this.options.models?.memoryExtraction) {
|
||||||
throw new Error('sleepMemory requires options.models.memoryExtraction.');
|
throw new Error("sleepMemory requires options.models.memoryExtraction.");
|
||||||
}
|
}
|
||||||
const contextFacts = await this.memory.findFacts(persona.id, ['persona', persona.displayName, 'user']);
|
const contextFacts = await this.memory.findFacts(persona.id, [
|
||||||
|
"persona",
|
||||||
|
persona.displayName,
|
||||||
|
"user",
|
||||||
|
]);
|
||||||
const drafts = await this.options.models.memoryExtraction.extract({
|
const drafts = await this.options.models.memoryExtraction.extract({
|
||||||
persona,
|
persona,
|
||||||
now: toIso(input.datetime),
|
now: toIso(input.datetime),
|
||||||
formattedMessageHistory: formatMessageHistory({ personaName: persona.displayName, messages: input.messageHistory }),
|
formattedMessageHistory: formatMessageHistory({
|
||||||
|
personaName: persona.displayName,
|
||||||
|
messages: input.messageHistory,
|
||||||
|
}),
|
||||||
contextFacts,
|
contextFacts,
|
||||||
instruction: memoryExtractionInstruction(toIso(input.datetime)),
|
instruction: memoryExtractionInstruction(toIso(input.datetime)),
|
||||||
});
|
});
|
||||||
for (const draft of drafts) {
|
for (const draft of drafts) {
|
||||||
await this.memory.addFact(persona.id, {
|
await this.memory.addFact(persona.id, {
|
||||||
...draft,
|
...draft,
|
||||||
topics: [...draft.topics, 'sleepMemory'],
|
topics: [...draft.topics, "sleepMemory"],
|
||||||
source: draft.source ?? 'boxbrain.sleepMemory',
|
source: draft.source ?? "boxbrain.sleepMemory",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await this.emit('persona.memory.sleep.persisted', { factCount: drafts.length });
|
await this.emit("persona.memory.sleep.persisted", {
|
||||||
|
factCount: drafts.length,
|
||||||
|
});
|
||||||
return drafts;
|
return drafts;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initialize(): Promise<MemorySpace> {
|
private async initialize(): Promise<MemorySpace> {
|
||||||
const now = toIso(this.options.now ?? new Date());
|
const now = toIso(this.options.now ?? new Date());
|
||||||
if (this.mode.type === 'load') {
|
if (this.mode.type === "load") {
|
||||||
const existing = await this.memory.getSpace(this.mode.spaceId);
|
const existing = await this.memory.getSpace(this.mode.spaceId);
|
||||||
if (!existing) throw new Error(`Persona space not found: ${this.mode.spaceId}`);
|
if (!existing)
|
||||||
await this.emit('persona.loaded', { displayName: existing.displayName });
|
throw new Error(`Persona space not found: ${this.mode.spaceId}`);
|
||||||
|
await this.emit("persona.loaded", { displayName: existing.displayName });
|
||||||
await this.refreshAvailability(now, existing);
|
await this.refreshAvailability(now, existing);
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const space = await this.memory.createSpace({ displayName: this.mode.displayName, seedMessage: this.mode.seedMessage, now });
|
const space = await this.memory.createSpace({
|
||||||
|
displayName: this.mode.displayName,
|
||||||
|
seedMessage: this.mode.seedMessage,
|
||||||
|
now,
|
||||||
|
});
|
||||||
const modelFacts = this.options.models?.initialization
|
const modelFacts = this.options.models?.initialization
|
||||||
? await this.options.models.initialization.extractInitialFacts({
|
? await this.options.models.initialization.extractInitialFacts({
|
||||||
displayName: this.mode.displayName,
|
displayName: this.mode.displayName,
|
||||||
seedMessage: this.mode.seedMessage,
|
seedMessage: this.mode.seedMessage,
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
const facts = modelFacts ?? [defaultInitialFact(this.mode.displayName, this.mode.seedMessage)];
|
const facts = modelFacts ?? [
|
||||||
|
defaultInitialFact(this.mode.displayName, this.mode.seedMessage),
|
||||||
|
];
|
||||||
for (const fact of facts) {
|
for (const fact of facts) {
|
||||||
await this.memory.addFact(space.id, fact);
|
await this.memory.addFact(space.id, fact);
|
||||||
}
|
}
|
||||||
await this.emit('persona.initialized', { displayName: space.displayName, factCount: facts.length }, space.id);
|
await this.emit(
|
||||||
|
"persona.initialized",
|
||||||
|
{ displayName: space.displayName, factCount: facts.length },
|
||||||
|
space.id,
|
||||||
|
);
|
||||||
await this.refreshAvailability(now, space);
|
await this.refreshAvailability(now, space);
|
||||||
return space;
|
return space;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshAvailability(datetime: DateTimeInput, knownPersona?: MemorySpace): Promise<void> {
|
private async refreshAvailability(
|
||||||
const persona = knownPersona ?? await this.ready();
|
datetime: DateTimeInput,
|
||||||
|
knownPersona?: MemorySpace,
|
||||||
|
): Promise<void> {
|
||||||
|
const persona = knownPersona ?? (await this.ready());
|
||||||
const start = startOfUtcDay(datetime);
|
const start = startOfUtcDay(datetime);
|
||||||
const end = addUtcDays(start, 2);
|
const end = addUtcDays(start, 2);
|
||||||
const entries = await this.memory.listScheduleEntries(persona.id, start.toISOString(), end.toISOString());
|
const entries = await this.memory.listScheduleEntries(
|
||||||
this.availabilitySnapshot = buildAvailabilitySnapshot({ now: datetime, entries });
|
persona.id,
|
||||||
await this.emit('persona.availability.refreshed', { rangeCount: this.availabilitySnapshot.ranges.length }, persona.id);
|
start.toISOString(),
|
||||||
|
end.toISOString(),
|
||||||
|
);
|
||||||
|
this.availabilitySnapshot = buildAvailabilitySnapshot({
|
||||||
|
now: datetime,
|
||||||
|
entries,
|
||||||
|
});
|
||||||
|
await this.emit(
|
||||||
|
"persona.availability.refreshed",
|
||||||
|
{ rangeCount: this.availabilitySnapshot.ranges.length },
|
||||||
|
persona.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async emit(name: string, data?: Record<string, unknown>, explicitSpaceId?: string): Promise<void> {
|
private async emit(
|
||||||
|
name: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
explicitSpaceId?: string,
|
||||||
|
): Promise<void> {
|
||||||
if (!this.options.debug) return;
|
if (!this.options.debug) return;
|
||||||
const event: DebugEvent = {
|
const event: DebugEvent = {
|
||||||
name,
|
name,
|
||||||
|
|||||||
Reference in New Issue
Block a user