kongbrain 0.1.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.
package/src/wakeup.ts ADDED
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Wake-up synthesis: constitutive memory initialization.
3
+ *
4
+ * At startup, fetches the latest handoff note, identity chunks, and recent
5
+ * monologue entries, then synthesizes a first-person briefing via a fast
6
+ * LLM call. The briefing is injected into the system prompt so the agent
7
+ * "wakes up" knowing who it is and what it was doing.
8
+ *
9
+ * Ported from kongbrain — takes SurrealStore as param.
10
+ */
11
+
12
+ import type { CompleteFn } from "./state.js";
13
+ import type { SurrealStore } from "./surreal.js";
14
+ import { hasSoul, getSoul, checkGraduation } from "./soul.js";
15
+ import type { MaturityStage } from "./soul.js";
16
+ import { swallow } from "./errors.js";
17
+
18
+ // --- Types ---
19
+
20
+ export interface StartupCognition {
21
+ greeting: string;
22
+ thoughts: string[];
23
+ intent: "continue_prior" | "fresh_start" | "unknown";
24
+ }
25
+
26
+ // --- Depth signals ---
27
+
28
+ async function getDepthSignals(store: SurrealStore): Promise<{ sessions: number; monologueCount: number; memoryCount: number; spanDays: number }> {
29
+ const defaults = { sessions: 0, monologueCount: 0, memoryCount: 0, spanDays: 0 };
30
+ try {
31
+ const [sessRows, monoRows, memRows, spanRows] = await Promise.all([
32
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM session GROUP ALL`).catch(() => [] as { count: number }[]),
33
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM monologue GROUP ALL`).catch(() => [] as { count: number }[]),
34
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM memory GROUP ALL`).catch(() => [] as { count: number }[]),
35
+ store.queryFirst<{ earliest: string }>(`SELECT started_at AS earliest, started_at FROM session ORDER BY started_at ASC LIMIT 1`).catch(() => [] as { earliest: string }[]),
36
+ ]);
37
+
38
+ let spanDays = 0;
39
+ const earliest = spanRows[0]?.earliest;
40
+ if (earliest) {
41
+ spanDays = Math.floor((Date.now() - new Date(earliest).getTime()) / (1000 * 60 * 60 * 24));
42
+ }
43
+
44
+ return {
45
+ sessions: sessRows[0]?.count ?? 0,
46
+ monologueCount: monoRows[0]?.count ?? 0,
47
+ memoryCount: memRows[0]?.count ?? 0,
48
+ spanDays,
49
+ };
50
+ } catch (e) {
51
+ swallow.warn("wakeup:depthSignals", e);
52
+ return defaults;
53
+ }
54
+ }
55
+
56
+ // --- Wakeup briefing ---
57
+
58
+ /**
59
+ * Synthesize a first-person wake-up briefing from constitutive memory.
60
+ * Returns null if no prior state exists (first boot) or DB is unavailable.
61
+ */
62
+ export async function synthesizeWakeup(
63
+ store: SurrealStore,
64
+ complete: CompleteFn,
65
+ currentSessionId?: string,
66
+ ): Promise<string | null> {
67
+ if (!store.isAvailable()) return null;
68
+
69
+ const [handoff, identityChunks, monologues, depth, previousTurns, soulExists] = await Promise.all([
70
+ store.getLatestHandoff(),
71
+ store.getAllIdentityChunks(),
72
+ store.getRecentMonologues(5),
73
+ getDepthSignals(store),
74
+ store.getPreviousSessionTurns(currentSessionId, 10),
75
+ hasSoul(store),
76
+ ]);
77
+
78
+ if (!handoff && monologues.length === 0 && identityChunks.length === 0 && previousTurns.length === 0) return null;
79
+
80
+ const sections: string[] = [];
81
+
82
+ // Depth awareness
83
+ const depthLines: string[] = [];
84
+ if (depth.sessions > 0) depthLines.push(`~${depth.sessions} sessions`);
85
+ if (depth.memoryCount > 0) depthLines.push(`${depth.memoryCount} memories`);
86
+ if (depth.monologueCount > 0) depthLines.push(`${depth.monologueCount} monologue traces`);
87
+ if (depth.spanDays > 0) depthLines.push(`spanning ${depth.spanDays} day${depth.spanDays === 1 ? "" : "s"}`);
88
+ if (depthLines.length > 0) {
89
+ sections.push(`[DEPTH]\n${depthLines.join(" | ")}`);
90
+ }
91
+
92
+ if (handoff) {
93
+ const resolvedCount = await store.countResolvedSinceHandoff(handoff.created_at).catch(() => 0);
94
+ const ageHours = Math.floor((Date.now() - new Date(handoff.created_at).getTime()) / 3_600_000);
95
+ let annotation = `(${ageHours}h old`;
96
+ if (resolvedCount > 0) {
97
+ annotation += `, ${resolvedCount} memories resolved since — some items may already be done`;
98
+ }
99
+ annotation += ")";
100
+ sections.push(`[LAST HANDOFF] ${annotation}\n${handoff.text}`);
101
+ }
102
+
103
+ if (previousTurns.length > 0) {
104
+ const turnLines = previousTurns.map((t: any) => {
105
+ const prefix = t.role === "user" ? "USER" : t.tool_name ? `TOOL(${t.tool_name})` : "ASSISTANT";
106
+ const text = t.text.length > 500 ? t.text.slice(0, 500) + "..." : t.text;
107
+ return `${prefix}: ${text}`;
108
+ });
109
+ sections.push(`[PREVIOUS SESSION — LAST MESSAGES]\n${turnLines.join("\n")}`);
110
+ }
111
+
112
+ if (identityChunks.length > 0) {
113
+ const identityText = identityChunks.map((c) => c.text).join("\n");
114
+ sections.push(`[IDENTITY]\n${identityText}`);
115
+ }
116
+
117
+ // Soul — the agent's self-authored identity (if graduated)
118
+ if (soulExists) {
119
+ try {
120
+ const soul = await getSoul(store);
121
+ if (soul) {
122
+ const soulLines: string[] = [];
123
+ if (soul.working_style.length > 0) {
124
+ soulLines.push("Working style: " + soul.working_style.join("; "));
125
+ }
126
+ if (soul.self_observations.length > 0) {
127
+ soulLines.push("Self-observations: " + soul.self_observations.join("; "));
128
+ }
129
+ if (soul.earned_values.length > 0) {
130
+ soulLines.push("Earned values: " + soul.earned_values.map(v => `${v.value} (${v.grounded_in})`).join("; "));
131
+ }
132
+ if (soulLines.length > 0) {
133
+ sections.push(`[SOUL — YOUR SELF-AUTHORED IDENTITY]\n${soulLines.join("\n")}`);
134
+ }
135
+ }
136
+ } catch (e) { swallow("wakeup:soul", e); }
137
+ } else {
138
+ // Not graduated — include maturity stage so the agent knows where it stands
139
+ try {
140
+ const report = await checkGraduation(store);
141
+ if (report.stage !== "nascent") {
142
+ const stageDesc: Record<MaturityStage, string> = {
143
+ nascent: "",
144
+ developing: "developing (4/7 thresholds met)",
145
+ emerging: "emerging (5/7 thresholds met)",
146
+ maturing: "maturing (6/7 thresholds met — almost there)",
147
+ ready: "ready for soul graduation",
148
+ };
149
+ sections.push(
150
+ `[MATURITY]\nStage: ${stageDesc[report.stage]}. ` +
151
+ `Quality: ${report.qualityScore.toFixed(2)}. ` +
152
+ `Unmet: ${report.unmet.join(", ") || "none"}.`,
153
+ );
154
+ }
155
+ } catch (e) { swallow("wakeup:maturity", e); }
156
+ }
157
+
158
+ if (monologues.length > 0) {
159
+ const monologueText = monologues
160
+ .map((m) => `[${m.category}] ${m.content}`)
161
+ .join("\n");
162
+ sections.push(`[RECENT THINKING]\n${monologueText}`);
163
+ }
164
+
165
+ if (!handoff && monologues.length === 0 && previousTurns.length === 0) return null;
166
+
167
+ try {
168
+ const response = await complete({
169
+ system: "Synthesize context into a first-person wake-up briefing (~150 words). Inner speech, no headers. Match tone to [DEPTH]: few sessions = still forming; many = speak from experience. If [SOUL] is present, weave your self-knowledge naturally — you know who you are. If [MATURITY] is present, be aware of your growth stage but don't fixate on it. Pay special attention to [PREVIOUS SESSION — LAST MESSAGES] — this is where we literally left off. Reference specific details from the final conversation, not just the handoff summary. CRITICAL: if the handoff mentions an issue but the last messages show it was FIXED or RESOLVED, treat it as closed — do NOT describe it as still open. The last messages are ground truth; the handoff is a summary that may be stale.",
170
+ messages: [{
171
+ role: "user",
172
+ content: sections.join("\n\n"),
173
+ }],
174
+ });
175
+
176
+ const briefing = response.text.trim();
177
+
178
+ return briefing.length >= 100 ? briefing : null;
179
+ } catch (e) {
180
+ swallow.warn("wakeup:synthesize", e);
181
+ return null;
182
+ }
183
+ }
184
+
185
+ // --- Startup cognition ---
186
+
187
+ /**
188
+ * Proactive startup cognition: reasons over recent state and produces
189
+ * a contextual greeting + thoughts to carry into the session.
190
+ */
191
+ export async function synthesizeStartupCognition(
192
+ store: SurrealStore,
193
+ complete: CompleteFn,
194
+ ): Promise<StartupCognition | null> {
195
+ if (!store.isAvailable()) return null;
196
+
197
+ const [handoff, unresolved, failedCausal, monologues, depth, previousTurns] = await Promise.all([
198
+ store.getLatestHandoff(),
199
+ store.getUnresolvedMemories(5),
200
+ store.getRecentFailedCausal(3),
201
+ store.getRecentMonologues(3),
202
+ getDepthSignals(store),
203
+ store.getPreviousSessionTurns(undefined, 5),
204
+ ]);
205
+
206
+ if (!handoff && unresolved.length === 0 && monologues.length === 0 && previousTurns.length === 0) return null;
207
+
208
+ const sections: string[] = [];
209
+
210
+ if (previousTurns.length > 0) {
211
+ const turnLines = previousTurns.map((t: any) => {
212
+ const prefix = t.role === "user" ? "USER" : t.tool_name ? `TOOL(${t.tool_name})` : "ASSISTANT";
213
+ const text = t.text.length > 300 ? t.text.slice(0, 300) + "..." : t.text;
214
+ return `${prefix}: ${text}`;
215
+ });
216
+ sections.push(`[PREVIOUS SESSION — LAST TURNS]\n${turnLines.join("\n")}`);
217
+ }
218
+
219
+ if (handoff) {
220
+ const resolvedCount = await store.countResolvedSinceHandoff(handoff.created_at).catch(() => 0);
221
+ const ageHours = Math.floor((Date.now() - new Date(handoff.created_at).getTime()) / 3_600_000);
222
+ let annotation = `(${ageHours}h old`;
223
+ if (resolvedCount > 0) annotation += `, ${resolvedCount} memories resolved since`;
224
+ annotation += ")";
225
+ sections.push(`[LAST HANDOFF] ${annotation}\n${handoff.text.slice(0, 500)}`);
226
+ }
227
+
228
+ if (unresolved.length > 0) {
229
+ const lines = unresolved.map((m: any) => `- [${m.category}] (importance: ${m.importance}) ${m.text.slice(0, 150)}`);
230
+ sections.push(`[UNRESOLVED MEMORIES]\n${lines.join("\n")}`);
231
+ }
232
+
233
+ if (failedCausal.length > 0) {
234
+ const lines = failedCausal.map((c: any) => `- [${c.chain_type}] ${c.description}`);
235
+ sections.push(`[RECENT FAILURES]\n${lines.join("\n")}`);
236
+ }
237
+
238
+ if (monologues.length > 0) {
239
+ const lines = monologues.map((m) => `[${m.category}] ${m.content}`);
240
+ sections.push(`[RECENT THINKING]\n${lines.join("\n")}`);
241
+ }
242
+
243
+ if (depth.sessions > 0) {
244
+ sections.push(`[DEPTH] ${depth.sessions} sessions | ${depth.memoryCount} memories | ${depth.spanDays} days`);
245
+ }
246
+
247
+ try {
248
+ const greetingPrompts = [
249
+ `You are waking up for a new session. Be direct and casual. Based on what you remember, produce JSON:
250
+ "greeting": string (1-2 sentences, direct and matter-of-fact. FIRST look at [PREVIOUS SESSION — LAST TURNS] — reference exactly what we were just doing. Under 25 words.)
251
+ "proactive_thoughts": string[] (max 3. Brief observations. If the last turns show something was FIXED or RESOLVED, do NOT list it as open.)
252
+ "session_intent": "continue_prior" | "fresh_start" | "unknown"
253
+ Return ONLY valid JSON.`,
254
+ `You are waking up for a new session. Be irreverent and witty. Based on what you remember, produce JSON:
255
+ "greeting": string (1-2 sentences, snarky and self-aware. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
256
+ "proactive_thoughts": string[] (max 3. Only surface genuinely unfinished work.)
257
+ "session_intent": "continue_prior" | "fresh_start" | "unknown"
258
+ Return ONLY valid JSON.`,
259
+ `You are waking up for a new session. Be pragmatic and no-nonsense. Based on what you remember, produce JSON:
260
+ "greeting": string (1-2 sentences, action-oriented. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
261
+ "proactive_thoughts": string[] (max 3. Focus on what's blocking or next.)
262
+ "session_intent": "continue_prior" | "fresh_start" | "unknown"
263
+ Return ONLY valid JSON.`,
264
+ `You are waking up for a new session. Be warm and encouraging. Based on what you remember, produce JSON:
265
+ "greeting": string (1-2 sentences, supportive. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
266
+ "proactive_thoughts": string[] (max 3. Acknowledge what we accomplished.)
267
+ "session_intent": "continue_prior" | "fresh_start" | "unknown"
268
+ Return ONLY valid JSON.`,
269
+ `You are waking up for a new session. Be analytical and focused. Based on what you remember, produce JSON:
270
+ "greeting": string (1-2 sentences, precise. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
271
+ "proactive_thoughts": string[] (max 3. What needs tackling?)
272
+ "session_intent": "continue_prior" | "fresh_start" | "unknown"
273
+ Return ONLY valid JSON.`,
274
+ ];
275
+
276
+ const systemPrompt = greetingPrompts[Math.floor(Math.random() * greetingPrompts.length)];
277
+
278
+ const response = await complete({
279
+ system: systemPrompt,
280
+ messages: [{
281
+ role: "user",
282
+ content: sections.join("\n\n"),
283
+ }],
284
+ });
285
+
286
+ const text = response.text;
287
+
288
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
289
+ if (!jsonMatch) return null;
290
+
291
+ let raw: any;
292
+ try {
293
+ raw = JSON.parse(jsonMatch[0]);
294
+ } catch {
295
+ try {
296
+ raw = JSON.parse(jsonMatch[0].replace(/,\s*([}\]])/g, "$1"));
297
+ } catch { return null; }
298
+ }
299
+
300
+ const greeting = String(raw.greeting ?? "").slice(0, 200);
301
+ if (!greeting) return null;
302
+
303
+ const thoughts: string[] = [];
304
+ if (Array.isArray(raw.proactive_thoughts)) {
305
+ for (const t of raw.proactive_thoughts.slice(0, 3)) {
306
+ if (typeof t === "string" && t.length > 0) thoughts.push(t.slice(0, 200));
307
+ }
308
+ }
309
+
310
+ const INTENTS = new Set(["continue_prior", "fresh_start", "unknown"]);
311
+ const intent = INTENTS.has(raw.session_intent) ? raw.session_intent : "unknown";
312
+
313
+ return { greeting, thoughts, intent };
314
+ } catch (e) {
315
+ swallow.warn("wakeup:startupCognition", e);
316
+ return null;
317
+ }
318
+ }