@p-sw/brainbox 0.1.0 → 0.1.2-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/index.js +5319 -0
  2. package/package.json +10 -4
  3. package/scripts/smoke_providers.ts +0 -176
  4. package/src/brain/index.ts +0 -936
  5. package/src/brain/manager.ts +0 -144
  6. package/src/brain/memory.ts +0 -99
  7. package/src/brain/messageHistory.ts +0 -26
  8. package/src/brain/schedule.ts +0 -11
  9. package/src/brain/types.ts +0 -26
  10. package/src/channel/base.ts +0 -527
  11. package/src/channel/discord.ts +0 -227
  12. package/src/channel/telegram.ts +0 -150
  13. package/src/commands/auth.tsx +0 -306
  14. package/src/commands/brain.ts +0 -119
  15. package/src/commands/daemon/commands.ts +0 -56
  16. package/src/commands/daemon/pairingCommand.ts +0 -15
  17. package/src/commands/daemon/restartCommand.ts +0 -17
  18. package/src/commands/daemon.ts +0 -168
  19. package/src/commands/index.ts +0 -16
  20. package/src/commands/model.tsx +0 -138
  21. package/src/commands/onboard.tsx +0 -473
  22. package/src/commands/pairing.ts +0 -32
  23. package/src/commands/restart.ts +0 -24
  24. package/src/config/file/auth.ts +0 -105
  25. package/src/config/file/root.ts +0 -37
  26. package/src/config/index.ts +0 -21
  27. package/src/config/loader.ts +0 -115
  28. package/src/index.ts +0 -61
  29. package/src/provider/index.ts +0 -122
  30. package/src/provider/llm.ts +0 -191
  31. package/src/provider/promptLoader.ts +0 -36
  32. package/src/provider/providers/302ai.ts +0 -17
  33. package/src/provider/providers/MiniMax.ts +0 -17
  34. package/src/provider/providers/anthropic.ts +0 -257
  35. package/src/provider/providers/azure_cognitive.ts +0 -40
  36. package/src/provider/providers/azure_openai.ts +0 -49
  37. package/src/provider/providers/baseten.ts +0 -17
  38. package/src/provider/providers/bedrock.ts +0 -312
  39. package/src/provider/providers/cerebras.ts +0 -17
  40. package/src/provider/providers/cloudflare_gateway.ts +0 -34
  41. package/src/provider/providers/cloudflare_workers.ts +0 -178
  42. package/src/provider/providers/copilot.ts +0 -22
  43. package/src/provider/providers/cortecs.ts +0 -17
  44. package/src/provider/providers/deepinfra.ts +0 -17
  45. package/src/provider/providers/deepseek.ts +0 -17
  46. package/src/provider/providers/digitalocean.ts +0 -17
  47. package/src/provider/providers/fireworks.ts +0 -17
  48. package/src/provider/providers/gitlab_duo.ts +0 -184
  49. package/src/provider/providers/gmi.ts +0 -17
  50. package/src/provider/providers/groq.ts +0 -17
  51. package/src/provider/providers/helicone.ts +0 -17
  52. package/src/provider/providers/huggingface.ts +0 -17
  53. package/src/provider/providers/ionet.ts +0 -17
  54. package/src/provider/providers/llamacpp.ts +0 -17
  55. package/src/provider/providers/llmgateway.ts +0 -17
  56. package/src/provider/providers/lmstudio.ts +0 -17
  57. package/src/provider/providers/mistral.ts +0 -17
  58. package/src/provider/providers/moonshot.ts +0 -17
  59. package/src/provider/providers/nebius.ts +0 -17
  60. package/src/provider/providers/nvidia.ts +0 -17
  61. package/src/provider/providers/ollama.ts +0 -17
  62. package/src/provider/providers/ollama_cloud.ts +0 -17
  63. package/src/provider/providers/openai.ts +0 -17
  64. package/src/provider/providers/openai_compatible.ts +0 -293
  65. package/src/provider/providers/openrouter.ts +0 -175
  66. package/src/provider/providers/ovhcloud.ts +0 -17
  67. package/src/provider/providers/sap_aicore.ts +0 -22
  68. package/src/provider/providers/scaleway.ts +0 -17
  69. package/src/provider/providers/snowflake_cortex.ts +0 -207
  70. package/src/provider/providers/stackit.ts +0 -17
  71. package/src/provider/providers/together.ts +0 -17
  72. package/src/provider/providers/venice.ts +0 -17
  73. package/src/provider/providers/vercel.ts +0 -17
  74. package/src/provider/providers/vertex.ts +0 -229
  75. package/src/provider/providers/xai.ts +0 -17
  76. package/src/provider/providers/zai.ts +0 -17
  77. package/src/provider/providers/zenmux.ts +0 -17
  78. package/src/provider/schema.ts +0 -143
  79. package/src/ui/TextInput.tsx +0 -114
  80. package/src/utils/daemonClient.ts +0 -85
  81. package/src/utils/logger.ts +0 -204
  82. package/tsconfig.json +0 -34
  83. /package/{prompts → dist/prompts}/daily_schedule.md +0 -0
  84. /package/{prompts → dist/prompts}/memoir.md +0 -0
  85. /package/{prompts → dist/prompts}/monthly_schedule.md +0 -0
  86. /package/{prompts → dist/prompts}/objectifier.md +0 -0
  87. /package/{prompts → dist/prompts}/persona_base_system_prompt.md +0 -0
  88. /package/{prompts → dist/prompts}/persona_base_system_prompt_fixed.md +0 -0
  89. /package/{prompts → dist/prompts}/persona_init.md +0 -0
  90. /package/{prompts → dist/prompts}/schedule_availability.md +0 -0
  91. /package/{prompts → dist/prompts}/send_message.md +0 -0
  92. /package/{prompts → dist/prompts}/start_conversation.md +0 -0
@@ -1,936 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import Supermemory from "supermemory";
3
- import { config } from "@/config";
4
- import { llm } from "@/provider";
5
- import { loadPrompt } from "@/provider/promptLoader";
6
- import {
7
- availabilitySchema,
8
- baseSystemPromptSchema,
9
- dailyScheduleSchema,
10
- monthlyScheduleSchema,
11
- type Availability,
12
- type AvailabilityWindows,
13
- type BaseSystemPromptGeneration,
14
- type DailySchedule,
15
- type DailySlot,
16
- type MonthlySchedule,
17
- } from "@/provider/schema";
18
- import { logger } from "@/utils/logger";
19
- import { BadRequestResponseError } from "@openrouter/sdk/models/errors";
20
-
21
- const log = logger.child("brain");
22
- import type {
23
- ChatAssistantMessage,
24
- ChatChoice,
25
- ChatFunctionTool,
26
- ChatMessages,
27
- } from "@/provider";
28
- import {
29
- brainManager,
30
- type BrainItem,
31
- type BrainItemWithChannel,
32
- } from "./manager";
33
- import {
34
- translateMessageHistory,
35
- type MessageHistoryEntry,
36
- } from "./messageHistory";
37
- import { formatDateKey, formatMonthKey, pad2 } from "./schedule";
38
- import type { Space } from "./types";
39
- import { Memory } from "./memory";
40
-
41
- export interface BrainCreateResult {
42
- brain: Brain;
43
- description: string;
44
- baseSystemPrompt: string;
45
- }
46
-
47
- export class Brain<BB extends BrainItem = BrainItem> {
48
- private availabilityCache: Map<string, AvailabilityWindows> = new Map();
49
-
50
- constructor(
51
- private db: Supermemory,
52
- private space: Space,
53
- public brainbase: BB,
54
- public memory: Memory = new Memory(this.db, this.space),
55
- ) {
56
- log.debug(
57
- `Brain constructed: id=${brainbase.brainId} name=${brainbase.displayName} space=${space.name}`,
58
- );
59
- }
60
-
61
- // ---------------------------------------------------------------------------
62
- // Domain methods
63
- // ---------------------------------------------------------------------------
64
-
65
- async createDailySchedule(datetime: Date): Promise<DailySchedule | null> {
66
- const dateKey = formatDateKey(datetime);
67
- log.debug(`createDailySchedule: starting for ${dateKey}`);
68
- try {
69
- const existing = await this.memory.get(`daily-schedule:${dateKey}`);
70
- if (existing) {
71
- log.debug(`createDailySchedule: cache hit for ${dateKey}`);
72
- try {
73
- return JSON.parse(existing.content) as DailySchedule;
74
- } catch (parseErr) {
75
- log.debug(
76
- `createDailySchedule: stored schedule malformed, regenerating: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
77
- );
78
- // fall through to regeneration if stored content is malformed
79
- }
80
- }
81
-
82
- const twoDaysAgo = new Date(datetime);
83
- twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
84
- const twoDaysAgoKey = formatDateKey(twoDaysAgo);
85
- log.debug(
86
- `createDailySchedule: gathering context (monthly, history, ${twoDaysAgoKey})`,
87
- );
88
- const [monthlySummary, history, twoDaysAgoStored] = await Promise.all([
89
- this.getMonthlySummaryForDay(datetime),
90
- this.getHistoryFacts(),
91
- this.memory.get(`daily-schedule:${twoDaysAgoKey}`),
92
- ]);
93
- let twoDaysAgoSchedule: DailySchedule | null = null;
94
- if (twoDaysAgoStored) {
95
- try {
96
- twoDaysAgoSchedule = JSON.parse(
97
- twoDaysAgoStored.content,
98
- ) as DailySchedule;
99
- log.debug(
100
- `createDailySchedule: loaded prior schedule with ${twoDaysAgoSchedule.items.length} slots`,
101
- );
102
- } catch {
103
- twoDaysAgoSchedule = null;
104
- log.debug(`createDailySchedule: prior schedule malformed, ignoring`);
105
- }
106
- }
107
-
108
- const instruction = await loadPrompt("DAILY_SCHEDULE");
109
- const promptMessage = [
110
- `Target date: ${dateKey} (${datetime.toLocaleDateString("en-US", { weekday: "long" })})`,
111
- `Personality: ${this.brainbase.baseSystemPrompt}`,
112
- monthlySummary
113
- ? `Monthly summary for this day: ${monthlySummary}`
114
- : "(no monthly summary available for this date)",
115
- `Recent schedule (${twoDaysAgoKey}, 2 days ago): ${
116
- twoDaysAgoSchedule
117
- ? twoDaysAgoSchedule.items
118
- .map((s) => `${s.start} ${s.activity}`)
119
- .join(", ")
120
- : "(no schedule on file for 2 days ago)"
121
- }`,
122
- `Recent history (facts):`,
123
- history,
124
- ].join("\n\n");
125
-
126
- log.debug(`createDailySchedule: calling identity model`);
127
- const schedule = await llm.call<DailySchedule>(llm.models.identity, {
128
- instruction,
129
- message: promptMessage,
130
- jsonSchemaName: "daily-schedule",
131
- jsonSchema: dailyScheduleSchema,
132
- });
133
- log.debug(
134
- `createDailySchedule: model returned ${schedule.items.length} slots`,
135
- );
136
-
137
- await this.memory.add({
138
- customId: `daily-schedule:${dateKey}`,
139
- content: JSON.stringify(schedule),
140
- metadata: {
141
- kind: "schedule",
142
- source: "createDailySchedule",
143
- date: dateKey,
144
- },
145
- });
146
- log.debug(`createDailySchedule: persisted ${dateKey}`);
147
-
148
- return schedule;
149
- } catch (error) {
150
- let reason =
151
- error instanceof Error
152
- ? error.message + `(${error.name})`
153
- : String(error);
154
- if (error instanceof BadRequestResponseError)
155
- reason = reason + `${error.body}`;
156
- logger.error(`createDailySchedule failed: ${reason}`);
157
- return null;
158
- }
159
- }
160
-
161
- async regenerateSchedules(): Promise<void> {
162
- const today = new Date();
163
- const tomorrow = new Date(
164
- today.getFullYear(),
165
- today.getMonth(),
166
- today.getDate() + 1,
167
- );
168
- await this.createDailySchedule(tomorrow);
169
- await this.createDailySchedule(today);
170
- const nextMonth = new Date(
171
- today.getFullYear(),
172
- today.getMonth() + 1,
173
- today.getDate(),
174
- );
175
- await this.createMonthlySchedule(nextMonth);
176
- }
177
-
178
- async createMonthlySchedule(datetime: Date): Promise<MonthlySchedule | null> {
179
- const year =
180
- datetime.getMonth() === 11
181
- ? datetime.getFullYear() + 1
182
- : datetime.getFullYear();
183
- const month = (datetime.getMonth() + 1) % 12;
184
- const daysInMonth = new Date(year, month + 1, 0).getDate();
185
- const monthKey = `${year}-${pad2(month + 1)}`;
186
- log.debug(`createMonthlySchedule: starting for ${monthKey}`);
187
- try {
188
- const existing = await this.memory.get(`monthly-schedule:${monthKey}`);
189
- if (existing) {
190
- log.debug(`createMonthlySchedule: cache hit for ${monthKey}`);
191
- try {
192
- return JSON.parse(existing.content) as MonthlySchedule;
193
- } catch (parseErr) {
194
- log.debug(
195
- `createMonthlySchedule: stored schedule malformed, regenerating: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
196
- );
197
- // fall through to regeneration if stored content is malformed
198
- }
199
- }
200
-
201
- const twoMonthsAgo = new Date(year, month - 2, 1);
202
- const twoMonthsAgoKey = `${twoMonthsAgo.getFullYear()}-${pad2(twoMonthsAgo.getMonth() + 1)}`;
203
- log.debug(
204
- `createMonthlySchedule: gathering context (history, ${twoMonthsAgoKey})`,
205
- );
206
- const [history, twoMonthsAgoStored] = await Promise.all([
207
- this.getHistoryFacts(),
208
- this.memory.get(`monthly-schedule:${twoMonthsAgoKey}`),
209
- ]);
210
- let twoMonthsAgoSchedule: MonthlySchedule | null = null;
211
- if (twoMonthsAgoStored) {
212
- try {
213
- twoMonthsAgoSchedule = JSON.parse(
214
- twoMonthsAgoStored.content,
215
- ) as MonthlySchedule;
216
- log.debug(
217
- `createMonthlySchedule: loaded prior schedule with ${twoMonthsAgoSchedule.items.length} days`,
218
- );
219
- } catch {
220
- twoMonthsAgoSchedule = null;
221
- log.debug(
222
- `createMonthlySchedule: prior schedule malformed, ignoring`,
223
- );
224
- }
225
- }
226
-
227
- const instruction = await loadPrompt("MONTHLY_SCHEDULE");
228
- const promptMessage = [
229
- `Target month: ${monthKey} (${daysInMonth} days)`,
230
- `Personality: ${this.brainbase.baseSystemPrompt}`,
231
- `Recent schedule (${twoMonthsAgoKey}, 2 months ago): ${
232
- twoMonthsAgoSchedule
233
- ? twoMonthsAgoSchedule.items
234
- .map((s) => `Day ${s.day}: ${s.summary}`)
235
- .join(", ")
236
- : "(no schedule on file for 2 months ago)"
237
- }`,
238
- `Recent history (facts):`,
239
- history,
240
- ].join("\n\n");
241
-
242
- log.debug(`createMonthlySchedule: calling identity model`);
243
- const schedule = await llm.call<MonthlySchedule>(llm.models.identity, {
244
- instruction,
245
- message: promptMessage,
246
- jsonSchemaName: "monthly-schedule",
247
- jsonSchema: monthlyScheduleSchema,
248
- });
249
- log.debug(
250
- `createMonthlySchedule: model returned ${schedule.items.length} days`,
251
- );
252
-
253
- await this.memory.add({
254
- customId: `monthly-schedule:${monthKey}`,
255
- content: JSON.stringify(schedule),
256
- metadata: {
257
- kind: "schedule",
258
- source: "createMonthlySchedule",
259
- month: monthKey,
260
- },
261
- });
262
- log.debug(`createMonthlySchedule: persisted ${monthKey}`);
263
-
264
- return schedule;
265
- } catch (error) {
266
- const reason = error instanceof Error ? error.message : String(error);
267
- logger.error(`createMonthlySchedule failed: ${reason}`);
268
- return null;
269
- }
270
- }
271
-
272
- async sleepMemory(
273
- datetime: Date = new Date(),
274
- history: ReadonlyArray<MessageHistoryEntry>,
275
- ): Promise<string | null> {
276
- if (history.length === 0) {
277
- log.debug(`sleepMemory: no history, skipping`);
278
- return null;
279
- }
280
- log.debug(`sleepMemory: starting, ${history.length} messages`);
281
- try {
282
- const dateKey = formatDateKey(datetime);
283
- const instruction = await loadPrompt("MEMOIR");
284
- const historyBlock = translateMessageHistory(
285
- this.brainbase.displayName,
286
- history,
287
- );
288
- const promptMessage = [
289
- `Date: ${dateKey}`,
290
- `Personality: ${this.brainbase.baseSystemPrompt}`,
291
- `Conversation log:`,
292
- historyBlock,
293
- ].join("\n\n");
294
-
295
- log.debug(`sleepMemory: calling identity model`);
296
- const memoir = await llm.call<string>(llm.models.identity, {
297
- instruction,
298
- message: promptMessage,
299
- });
300
- log.debug(`sleepMemory: model returned ${memoir.length} chars`);
301
-
302
- await this.memory.add({
303
- customId: `daily-journal:${dateKey}`,
304
- content: memoir,
305
- metadata: {
306
- kind: "daily-journal",
307
- source: "sleepMemory",
308
- date: dateKey,
309
- },
310
- });
311
- log.debug(`sleepMemory: journal persisted for ${dateKey}`);
312
-
313
- return memoir;
314
- } catch (error) {
315
- const reason = error instanceof Error ? error.message : String(error);
316
- logger.error(`sleepMemory failed: ${reason}`);
317
- return null;
318
- }
319
- }
320
-
321
- async getTodayScheduledAvailability(
322
- datetime: Date,
323
- ): Promise<AvailabilityWindows | null> {
324
- const dateKey = formatDateKey(datetime);
325
- try {
326
- const cached = this.availabilityCache.get(dateKey);
327
- if (cached) {
328
- log.debug(`getTodayScheduledAvailability: cache hit for ${dateKey}`);
329
- return cached;
330
- }
331
-
332
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
333
- if (!stored) {
334
- log.debug(`getTodayScheduledAvailability: no schedule for ${dateKey}`);
335
- return null;
336
- }
337
-
338
- const dailySchedule = JSON.parse(stored.content) as DailySchedule;
339
- const availability =
340
- await this.deriveAvailabilityFromSchedule(dailySchedule);
341
-
342
- this.availabilityCache.set(dateKey, availability);
343
- log.debug(
344
- `getTodayScheduledAvailability: cached ${availability.items.length} windows for ${dateKey}`,
345
- );
346
- return availability;
347
- } catch (error) {
348
- const reason = error instanceof Error ? error.message : String(error);
349
- logger.error(`getTodayScheduledAvailability failed: ${reason}`);
350
- return null;
351
- }
352
- }
353
-
354
- async getAvailability(datetime: Date = new Date()): Promise<Availability> {
355
- const h = datetime.getHours();
356
- const m = datetime.getMinutes();
357
- const hhmm = `${pad2(h)}:${pad2(m)}`;
358
- const current = h * 60 + m;
359
- const toMinutes = (s: string): number => {
360
- const [hh = 0, mm = 0] = s.split(":").map((x) => parseInt(x, 10));
361
- return hh * 60 + mm;
362
- };
363
- const windows = await this.getTodayScheduledAvailability(datetime);
364
- const match = windows?.items.find(
365
- (w) => toMinutes(w.start) <= current && current < toMinutes(w.end),
366
- );
367
- const result = match ?? { start: hhmm, end: hhmm, status: "offline" };
368
- log.debug(
369
- `getAvailability: now=${hhmm} status=${result.status} (${windows?.items.length ?? 0} windows on file)`,
370
- );
371
- return result;
372
- }
373
-
374
- async getCurrentAndAdjacentSlots(now: Date): Promise<DailySlot[]> {
375
- const dateKey = formatDateKey(now);
376
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
377
- if (!stored) {
378
- log.debug(`getCurrentAndAdjacentSlots: no schedule for ${dateKey}`);
379
- return [];
380
- }
381
- let schedule: DailySchedule;
382
- try {
383
- schedule = JSON.parse(stored.content) as DailySchedule;
384
- } catch (parseErr) {
385
- log.debug(
386
- `getCurrentAndAdjacentSlots: stored schedule malformed: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
387
- );
388
- return [];
389
- }
390
- const currentMinutes = now.getHours() * 60 + now.getMinutes();
391
- const toMinutes = (hhmm: string): number => {
392
- const [h = 0, m = 0] = hhmm.split(":").map((x) => parseInt(x, 10));
393
- return h * 60 + m;
394
- };
395
- const index = schedule.items.findIndex(
396
- (slot) =>
397
- toMinutes(slot.start) <= currentMinutes &&
398
- currentMinutes < toMinutes(slot.end),
399
- );
400
- if (index === -1) {
401
- log.debug(
402
- `getCurrentAndAdjacentSlots: no matching slot at ${now.toTimeString().slice(0, 5)}`,
403
- );
404
- return [];
405
- }
406
- const slice = schedule.items.slice(Math.max(0, index - 1), index + 2);
407
- log.debug(
408
- `getCurrentAndAdjacentSlots: index=${index} returned ${slice.length} slots`,
409
- );
410
- return slice;
411
- }
412
-
413
- async deriveAvailabilityFromSchedule(
414
- schedule: DailySchedule,
415
- ): Promise<AvailabilityWindows> {
416
- log.debug(
417
- `deriveAvailabilityFromSchedule: ${schedule.items.length} slots → model`,
418
- );
419
- try {
420
- const instruction = await loadPrompt("SCHEDULE_AVAILABILITY");
421
- const promptMessage = JSON.stringify({
422
- schedule,
423
- personality: this.brainbase.baseSystemPrompt,
424
- });
425
-
426
- const result = await llm.call<AvailabilityWindows>(llm.models.identity, {
427
- instruction,
428
- message: promptMessage,
429
- jsonSchemaName: "availability",
430
- jsonSchema: availabilitySchema,
431
- });
432
- log.debug(
433
- `deriveAvailabilityFromSchedule: ${result.items.length} windows`,
434
- );
435
- return result;
436
- } catch (error) {
437
- const reason = error instanceof Error ? error.message : String(error);
438
- logger.error(`deriveAvailabilityFromSchedule failed: ${reason}`);
439
- throw error;
440
- }
441
- }
442
-
443
- invalidateScheduledAvailability(datetime: Date = new Date()): void {
444
- const todayKey = formatDateKey(datetime);
445
- let removed = 0;
446
- for (const key of this.availabilityCache.keys()) {
447
- if (key < todayKey) {
448
- this.availabilityCache.delete(key);
449
- removed += 1;
450
- }
451
- }
452
- if (removed > 0) {
453
- log.debug(
454
- `invalidateScheduledAvailability: dropped ${removed} stale cache entries (<${todayKey})`,
455
- );
456
- }
457
- }
458
-
459
- async persistBrainBase(): Promise<void> {
460
- log.debug(
461
- `persistBrainBase: id=${this.brainbase.brainId} name=${this.brainbase.displayName}`,
462
- );
463
- await brainManager.saveBrain(this.brainbase.brainId, this.brainbase);
464
- }
465
-
466
- async sendMessage(
467
- history: ReadonlyArray<MessageHistoryEntry>,
468
- newMessages: ReadonlyArray<MessageHistoryEntry>,
469
- options: {
470
- now?: Date;
471
- maxSteps?: number;
472
- initiate?: boolean;
473
- send?: (text: string) => Promise<void>;
474
- } = {},
475
- ): Promise<string[]> {
476
- const now = options.now ?? new Date();
477
- const maxSteps = options.maxSteps ?? 20;
478
- const initiate = options.initiate ?? false;
479
- const send = options.send ?? (async () => {});
480
-
481
- log.debug(
482
- `sendMessage: start initiate=${initiate} history=${history.length} new=${newMessages.length} maxSteps=${maxSteps}`,
483
- );
484
-
485
- const replyMessages: string[] = [];
486
- const tools: ChatFunctionTool[] = buildSendMessageTools();
487
- const historyBlock = translateMessageHistory(
488
- this.brainbase.displayName,
489
- history,
490
- );
491
- const newBlock = translateMessageHistory(
492
- this.brainbase.displayName,
493
- newMessages,
494
- );
495
- const memoryBlock = await this.buildMemoryBlock();
496
- const scheduleBlock = await this.buildScheduleBlock(now);
497
- const datetimeBlock = formatDatetime(now);
498
-
499
- const instruction = initiate
500
- ? await loadPrompt("START_CONVERSATION")
501
- : await loadPrompt("SEND_MESSAGE");
502
- const userPrompt = initiate
503
- ? [
504
- `Current date and time: ${datetimeBlock}`,
505
- scheduleBlock,
506
- memoryBlock,
507
- `Conversation so far:`,
508
- historyBlock.length > 0 ? historyBlock : "(no prior messages)",
509
- `You are opening this chat. The user has not sent a message.`,
510
- ].join("\n\n")
511
- : [
512
- `Current date and time: ${datetimeBlock}`,
513
- scheduleBlock,
514
- memoryBlock,
515
- `Conversation so far:`,
516
- historyBlock.length > 0 ? historyBlock : "(no prior messages)",
517
- `New user message(s) to which you must reply:`,
518
- newBlock.length > 0 ? newBlock : "(none — open turn)",
519
- ].join("\n\n");
520
-
521
- const messages: ChatMessages[] = [
522
- {
523
- role: "user",
524
- content: userPrompt,
525
- },
526
- ];
527
-
528
- for (let step = 0; step < maxSteps; step += 1) {
529
- log.debug(`sendMessage: step ${step + 1}/${maxSteps} → model`);
530
- let choice: ChatChoice;
531
- try {
532
- choice = await llm.chatWithTools(llm.models.conversation, {
533
- instruction: `${this.brainbase.baseSystemPrompt}\n\n${instruction}`,
534
- messages,
535
- tools,
536
- });
537
- } catch (error) {
538
- const reason = error instanceof Error ? error.message : String(error);
539
- logger.error(`sendMessage: LLM call failed at step ${step}: ${reason}`);
540
- return replyMessages;
541
- }
542
-
543
- const assistantMessage = choice.message;
544
- const toolCalls = assistantMessage.toolCalls ?? [];
545
- const hasContent =
546
- typeof assistantMessage.content === "string" &&
547
- assistantMessage.content.length > 0;
548
-
549
- log.debug(
550
- `sendMessage: step ${step + 1} → toolCalls=${toolCalls.length} hasContent=${hasContent}`,
551
- );
552
-
553
- if (toolCalls.length === 0) {
554
- log.debug(
555
- `sendMessage: model returned no tool calls; finalising with ${replyMessages.length} replies`,
556
- );
557
- return replyMessages;
558
- }
559
-
560
- messages.push(stripAssistantForHistory(assistantMessage));
561
-
562
- for (const call of toolCalls) {
563
- if (call.function.name === "addReplyMessage") {
564
- const content = parseAddReplyMessageArguments(
565
- call.function.arguments,
566
- );
567
- if (content !== null) {
568
- log.debug(
569
- `sendMessage: addReplyMessage[${replyMessages.length}] (${content.length} chars)`,
570
- );
571
- send(content);
572
- replyMessages.push(content);
573
- } else {
574
- log.debug(
575
- `sendMessage: addReplyMessage rejected (invalid arguments: ${call.function.arguments})`,
576
- );
577
- }
578
- messages.push({
579
- role: "tool",
580
- toolCallId: call.id,
581
- content:
582
- content === null
583
- ? JSON.stringify({ ok: false, error: "invalid arguments" })
584
- : JSON.stringify({ ok: true, index: replyMessages.length - 1 }),
585
- });
586
- continue;
587
- }
588
- if (call.function.name === "searchMemory") {
589
- log.debug(`sendMessage: searchMemory tool call`);
590
- const result = await this.executeSearchTool(call.function.arguments);
591
- messages.push({
592
- role: "tool",
593
- toolCallId: call.id,
594
- content: result,
595
- });
596
- continue;
597
- }
598
- log.debug(`sendMessage: unknown tool "${call.function.name}"`);
599
- messages.push({
600
- role: "tool",
601
- toolCallId: call.id,
602
- content: JSON.stringify({
603
- ok: false,
604
- error: `Unknown tool: ${call.function.name}`,
605
- }),
606
- });
607
- }
608
-
609
- if (
610
- !hasContent &&
611
- toolCalls.every((c) => c.function.name === "searchMemory")
612
- ) {
613
- log.debug(`sendMessage: step was pure searchMemory, looping back`);
614
- continue;
615
- }
616
- }
617
-
618
- logger.warn(
619
- `sendMessage: reached maxSteps (${maxSteps}) without final reply`,
620
- );
621
- return replyMessages;
622
- }
623
-
624
- private async buildMemoryBlock(): Promise<string> {
625
- const facts = await this.getHistoryFacts();
626
- return `Known facts about the persona and the user:\n${facts || "(none indexed)"}`;
627
- }
628
-
629
- private async buildScheduleBlock(now: Date): Promise<string> {
630
- const dateKey = formatDateKey(now);
631
- const currentSlots = await this.getCurrentAndAdjacentSlots(now);
632
- const currentBlock =
633
- currentSlots.length > 0
634
- ? `Currently (around ${now.toTimeString().slice(0, 5)}):\n${currentSlots
635
- .map(
636
- (s) =>
637
- ` ${s.start}-${s.end} ${s.activity}${s.notes ? ` (${s.notes})` : ""}`,
638
- )
639
- .join("\n")}`
640
- : `Currently (${dateKey} ${now.toTimeString().slice(0, 5)}): (no matching slot in today's schedule)`;
641
-
642
- const days: { label: string; date: Date }[] = [
643
- {
644
- label: "Yesterday",
645
- date: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1),
646
- },
647
- {
648
- label: "Tomorrow",
649
- date: new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1),
650
- },
651
- ];
652
- const blocks: string[] = [];
653
- for (const { label, date } of days) {
654
- const key = formatDateKey(date);
655
- const summary = await this.getDailyScheduleSummary(key);
656
- blocks.push(
657
- `${label} (${key}): ${summary ?? "(no daily schedule on file)"}`,
658
- );
659
- }
660
- return `Schedule context:\n${currentBlock}\n\n${blocks.join("\n")}`;
661
- }
662
-
663
- private async getDailyScheduleSummary(
664
- dateKey: string,
665
- ): Promise<string | null> {
666
- try {
667
- const stored = await this.memory.get(`daily-schedule:${dateKey}`);
668
- if (!stored) return null;
669
- const schedule = JSON.parse(stored.content) as DailySchedule;
670
- const first = schedule.items[0];
671
- const last = schedule.items[schedule.items.length - 1];
672
- if (!first || !last) return null;
673
- const total = schedule.items.length;
674
- return `starts ${first.activity}@${first.start}, ends ${last.activity}@${last.end} (${total} slots)`;
675
- } catch {
676
- return null;
677
- }
678
- }
679
-
680
- private async executeSearchTool(argumentsJson: string): Promise<string> {
681
- const query = parseSearchArguments(argumentsJson);
682
- if (!query) {
683
- log.debug(`executeSearchTool: missing/invalid query in args`);
684
- return JSON.stringify({ ok: false, error: "missing query" });
685
- }
686
- log.debug(`executeSearchTool: query="${query}"`);
687
- try {
688
- const hits = await this.memory.search(query, 5);
689
- const compact = hits.map((hit) => ({
690
- content: hit.content,
691
- score: hit.score,
692
- }));
693
- log.debug(`executeSearchTool: ${compact.length} hits returned to model`);
694
- return JSON.stringify({ ok: true, hits: compact });
695
- } catch (error) {
696
- const reason = error instanceof Error ? error.message : String(error);
697
- log.debug(`executeSearchTool: failed: ${reason}`);
698
- return JSON.stringify({ ok: false, error: reason });
699
- }
700
- }
701
-
702
- async getMonthlySummaryForDay(target: Date): Promise<string | null> {
703
- try {
704
- const monthKey = formatMonthKey(target);
705
- const stored = await this.memory.get(`monthly-schedule:${monthKey}`);
706
- if (!stored) {
707
- log.debug(
708
- `getMonthlySummaryForDay: no monthly schedule for ${monthKey}`,
709
- );
710
- return null;
711
- }
712
-
713
- const monthly = JSON.parse(stored.content) as MonthlySchedule;
714
- const day = target.getDate();
715
- const entry = monthly.items.find((d) => d.day === day);
716
- log.debug(
717
- `getMonthlySummaryForDay: month=${monthKey} day=${day} ${entry ? "hit" : "miss"}`,
718
- );
719
- return entry?.summary ?? null;
720
- } catch (parseErr) {
721
- log.debug(
722
- `getMonthlySummaryForDay: parse failed: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`,
723
- );
724
- return null;
725
- }
726
- }
727
-
728
- async getHistoryFacts(): Promise<string> {
729
- try {
730
- const docs = await this.memory.list();
731
- const text = docs
732
- .map((d) => d.content)
733
- .slice(-30)
734
- .join("\n");
735
- log.debug(`getHistoryFacts: ${docs.length} docs, ${text.length} chars`);
736
- return text;
737
- } catch (err) {
738
- log.debug(
739
- `getHistoryFacts: failed: ${err instanceof Error ? err.message : String(err)}`,
740
- );
741
- return "";
742
- }
743
- }
744
-
745
- static async create(
746
- displayName: string,
747
- seed: string,
748
- ): Promise<{ brainId: string; brain: Brain } | null> {
749
- log.debug(`Brain.create: starting name="${displayName}"`);
750
- try {
751
- const personaInitInstruction = await loadPrompt("PERSONA_INIT");
752
- log.debug(`Brain.create: generating description`);
753
- const description = await llm.call<string>(llm.models.identity, {
754
- instruction: personaInitInstruction,
755
- message: seed,
756
- });
757
- log.debug(
758
- `Brain.create: description returned (${description.length} chars)`,
759
- );
760
-
761
- const personaSystemInstruction = await loadPrompt(
762
- "PERSONA_BASE_SYSTEM_PROMPT",
763
- );
764
- log.debug(`Brain.create: generating base system prompt + dials`);
765
- const generated = await llm.call<BaseSystemPromptGeneration>(
766
- llm.models.identity,
767
- {
768
- instruction: personaSystemInstruction,
769
- message: description,
770
- jsonSchemaName: "base-system-prompt",
771
- jsonSchema: baseSystemPromptSchema,
772
- },
773
- );
774
- log.debug(
775
- `Brain.create: dials dndProb=${generated.dndReplyProbability} startCountThreshold=${generated.startConversationCountThreshold} startTimeThreshold=${generated.startConversationTimeThreshold}`,
776
- );
777
-
778
- const personaSystemFixed = await loadPrompt(
779
- "PERSONA_BASE_SYSTEM_PROMPT_FIXED",
780
- );
781
- const baseSystemPrompt = `${generated.baseSystemPrompt}\n\n${personaSystemFixed}`;
782
-
783
- const db = new Supermemory({ apiKey: config.supermemoryApiKey });
784
- const brainId = randomUUID();
785
- const space: Space = {
786
- name: `brain:${brainId}`,
787
- description: displayName,
788
- };
789
-
790
- const brainbase: BrainItem = {
791
- brainId,
792
- spaceName: space.name,
793
- displayName,
794
- baseSystemPrompt,
795
- dndReplyProbability: generated.dndReplyProbability,
796
- startConversationCountThreshold:
797
- generated.startConversationCountThreshold,
798
- startConversationTimeThreshold:
799
- generated.startConversationTimeThreshold,
800
- activated: false,
801
- };
802
-
803
- const memory = new Memory(db, space);
804
-
805
- await memory.add({
806
- customId: "persona",
807
- content: description,
808
- metadata: { kind: "persona", source: "persona-init" },
809
- });
810
- log.debug(`Brain.create: persona description stored`);
811
-
812
- await brainManager.saveBrain(brainId, brainbase);
813
- log.debug(`Brain.create: brainbase saved (id=${brainId})`);
814
- return { brainId, brain: new Brain(db, space, brainbase, memory) };
815
- } catch (error) {
816
- const reason = error instanceof Error ? error.message : String(error);
817
- logger.error(`Failed to create brain "${displayName}": ${reason}`);
818
- return null;
819
- }
820
- }
821
-
822
- static async delete(brainId: string): Promise<boolean> {
823
- log.debug(`Brain.delete: id=${brainId}`);
824
- const brainbase = await brainManager.loadBrain(brainId);
825
- if (!brainbase) {
826
- log.debug(`Brain.delete: no brainbase found`);
827
- return false;
828
- }
829
-
830
- const db = new Supermemory({ apiKey: config.supermemoryApiKey });
831
- const memory = new Memory(db, { name: brainbase.spaceName });
832
- try {
833
- await memory.clear();
834
- } catch (error) {
835
- const reason = error instanceof Error ? error.message : String(error);
836
- logger.warn(
837
- `Failed to clear memory for brain "${brainbase.displayName}": ${reason}`,
838
- );
839
- }
840
-
841
- await brainManager.deleteBrain(brainId);
842
- log.debug(`Brain.delete: done id=${brainId}`);
843
- return true;
844
- }
845
-
846
- static async load(brainId: string): Promise<Brain | null> {
847
- log.debug(`Brain.load: id=${brainId}`);
848
- const brainbase = await brainManager.loadBrain(brainId);
849
- if (!brainbase || !brainManager.isBrainReady(brainbase)) {
850
- log.debug(`Brain.load: not loadable (missing or not ready)`);
851
- return null;
852
- }
853
-
854
- const db = new Supermemory({ apiKey: config.supermemoryApiKey });
855
- const space: Space = { name: brainbase.spaceName };
856
- log.debug(`Brain.load: ready (channel=${brainbase.channel})`);
857
- return new Brain(db, space, brainbase);
858
- }
859
- }
860
-
861
- function formatDatetime(now: Date): string {
862
- const pad = (n: number) => n.toString().padStart(2, "0");
863
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(
864
- now.getHours(),
865
- )}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
866
- }
867
-
868
- function buildSendMessageTools(): ChatFunctionTool[] {
869
- return [
870
- {
871
- name: "addReplyMessage",
872
- description:
873
- "Append one chat bubble to the reply stream. Call once per bubble you want to send. Do not call when you are done — just return text without tool calls.",
874
- parameters: {
875
- type: "object",
876
- additionalProperties: false,
877
- properties: {
878
- content: { type: "string", description: "The bubble text." },
879
- },
880
- required: ["content"],
881
- },
882
- },
883
- {
884
- name: "searchMemory",
885
- description:
886
- "Semantic search over the long-term memory of facts about the persona and the user. Returns the most relevant stored content for a natural-language query.",
887
- parameters: {
888
- type: "object",
889
- additionalProperties: false,
890
- properties: {
891
- query: {
892
- type: "string",
893
- description:
894
- "Natural-language query describing the fact you want to recall.",
895
- },
896
- },
897
- required: ["query"],
898
- },
899
- },
900
- ];
901
- }
902
-
903
- function parseAddReplyMessageArguments(json: string): string | null {
904
- try {
905
- const parsed = JSON.parse(json) as { content?: unknown };
906
- if (typeof parsed.content === "string" && parsed.content.length > 0) {
907
- return parsed.content;
908
- }
909
- } catch {
910
- return null;
911
- }
912
- return null;
913
- }
914
-
915
- function parseSearchArguments(json: string): string | null {
916
- try {
917
- const parsed = JSON.parse(json) as { query?: unknown };
918
- if (typeof parsed.query === "string" && parsed.query.trim().length > 0) {
919
- return parsed.query;
920
- }
921
- } catch {
922
- return null;
923
- }
924
- return null;
925
- }
926
-
927
- function stripAssistantForHistory(message: {
928
- content?: string;
929
- toolCalls?: ChatAssistantMessage["toolCalls"];
930
- }): ChatAssistantMessage {
931
- return {
932
- role: "assistant",
933
- content: message.content,
934
- toolCalls: message.toolCalls,
935
- };
936
- }