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/soul.ts ADDED
@@ -0,0 +1,936 @@
1
+ /**
2
+ * Soul — the emergent identity document system.
3
+ *
4
+ * Unlike hardcoded identity chunks, the Soul document is written BY the agent
5
+ * based on its own graph data. It lives in SurrealDB as `soul:kongbrain` and
6
+ * evolves over time through experience-grounded revisions.
7
+ *
8
+ * Graduation is a staged process, not a binary gate:
9
+ *
10
+ * nascent (0-3/7) — Too early. Keep building experience.
11
+ * developing (4/7) — Some signal. Diagnose weak areas, guide focus.
12
+ * emerging (5/7) — Volume is there. Quality gate becomes the blocker.
13
+ * maturing (6/7) — Almost there. Final thresholds + quality must pass.
14
+ * ready (7/7) — All thresholds met AND quality score ≥ 0.6.
15
+ *
16
+ * Quality is computed from actual performance signals: retrieval utilization,
17
+ * skill success rates, reflection severity distribution, and tool failure rates.
18
+ * An agent that meets all 7 thresholds but has terrible quality scores will NOT
19
+ * graduate — it needs to improve before self-authoring makes sense.
20
+ *
21
+ * Ported from kongbrain — takes SurrealStore/EmbeddingService as params.
22
+ */
23
+
24
+ import { readFile } from "node:fs/promises";
25
+ import { join } from "node:path";
26
+ import type { CompleteFn } from "./state.js";
27
+ import type { SurrealStore } from "./surreal.js";
28
+ import { swallow } from "./errors.js";
29
+
30
+ // ── Types ──
31
+
32
+ export type MaturityStage = "nascent" | "developing" | "emerging" | "maturing" | "ready";
33
+
34
+ export interface GraduationSignals {
35
+ sessions: number;
36
+ reflections: number;
37
+ causalChains: number;
38
+ concepts: number;
39
+ memoryCompactions: number;
40
+ monologues: number;
41
+ spanDays: number;
42
+ }
43
+
44
+ export interface QualitySignals {
45
+ /** Average retrieval utilization (0-1). Higher = retrieved context was actually used. */
46
+ avgRetrievalUtilization: number;
47
+ /** Skill success rate (0-1). successCount / (successCount + failureCount). */
48
+ skillSuccessRate: number;
49
+ /** Fraction of reflections that are "critical" severity. Lower is better. */
50
+ criticalReflectionRate: number;
51
+ /** Tool failure rate across sessions (0-1). Lower is better. */
52
+ toolFailureRate: number;
53
+ /** Number of data points behind the quality signals. */
54
+ sampleSize: number;
55
+ }
56
+
57
+ export interface StageDiagnostic {
58
+ area: string;
59
+ status: "healthy" | "warning" | "critical";
60
+ detail: string;
61
+ suggestion: string;
62
+ }
63
+
64
+ export interface GraduationReport {
65
+ /** Whether the agent is ready for soul creation. */
66
+ ready: boolean;
67
+ /** Current maturity stage. */
68
+ stage: MaturityStage;
69
+ /** Volume signals (counts). */
70
+ signals: GraduationSignals;
71
+ /** Static thresholds. */
72
+ thresholds: GraduationSignals;
73
+ /** Which thresholds are met (formatted strings). */
74
+ met: string[];
75
+ /** Which thresholds are unmet (formatted strings). */
76
+ unmet: string[];
77
+ /** Volume score (met / total). */
78
+ volumeScore: number;
79
+ /** Quality signals from actual performance data. */
80
+ quality: QualitySignals;
81
+ /** Composite quality score (0-1). Must be ≥ 0.6 to graduate. */
82
+ qualityScore: number;
83
+ /** Per-area diagnostics with actionable suggestions. */
84
+ diagnostics: StageDiagnostic[];
85
+ }
86
+
87
+ // ── Thresholds ──
88
+
89
+ const THRESHOLDS: GraduationSignals = {
90
+ sessions: 15,
91
+ reflections: 10,
92
+ causalChains: 5,
93
+ concepts: 30,
94
+ memoryCompactions: 5,
95
+ monologues: 5,
96
+ spanDays: 3,
97
+ };
98
+
99
+ /** Quality score must be at or above this to graduate even with 7/7 volume. */
100
+ const QUALITY_GATE = 0.6;
101
+
102
+ // ── Signal Collection ──
103
+
104
+ async function getGraduationSignals(store: SurrealStore): Promise<GraduationSignals> {
105
+ const defaults: GraduationSignals = {
106
+ sessions: 0, reflections: 0, causalChains: 0,
107
+ concepts: 0, memoryCompactions: 0, monologues: 0, spanDays: 0,
108
+ };
109
+ if (!store.isAvailable()) return defaults;
110
+
111
+ try {
112
+ const [sessions, reflections, causal, concepts, compactions, monologues, span] = await Promise.all([
113
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM session GROUP ALL`).catch(() => []),
114
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM reflection GROUP ALL`).catch(() => []),
115
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM causal_chain GROUP ALL`).catch(() => []),
116
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM concept GROUP ALL`).catch(() => []),
117
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM compaction_checkpoint WHERE status = "complete" GROUP ALL`).catch(() => []),
118
+ store.queryFirst<{ count: number }>(`SELECT count() AS count FROM monologue GROUP ALL`).catch(() => []),
119
+ store.queryFirst<{ earliest: string }>(`SELECT started_at AS earliest FROM session ORDER BY started_at ASC LIMIT 1`).catch(() => []),
120
+ ]);
121
+
122
+ let spanDays = 0;
123
+ const earliest = (span as { earliest: string }[])[0]?.earliest;
124
+ if (earliest) {
125
+ spanDays = Math.floor((Date.now() - new Date(earliest).getTime()) / (1000 * 60 * 60 * 24));
126
+ }
127
+
128
+ return {
129
+ sessions: (sessions as { count: number }[])[0]?.count ?? 0,
130
+ reflections: (reflections as { count: number }[])[0]?.count ?? 0,
131
+ causalChains: (causal as { count: number }[])[0]?.count ?? 0,
132
+ concepts: (concepts as { count: number }[])[0]?.count ?? 0,
133
+ memoryCompactions: (compactions as { count: number }[])[0]?.count ?? 0,
134
+ monologues: (monologues as { count: number }[])[0]?.count ?? 0,
135
+ spanDays,
136
+ };
137
+ } catch (e) {
138
+ swallow.warn("soul:getGraduationSignals", e);
139
+ return defaults;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Compute quality signals from actual performance data in the graph.
145
+ * These represent HOW WELL the agent is performing, not just how much.
146
+ */
147
+ export async function getQualitySignals(store: SurrealStore): Promise<QualitySignals> {
148
+ const defaults: QualitySignals = {
149
+ avgRetrievalUtilization: 0,
150
+ skillSuccessRate: 0,
151
+ criticalReflectionRate: 1, // assume worst until we have data
152
+ toolFailureRate: 1,
153
+ sampleSize: 0,
154
+ };
155
+ if (!store.isAvailable()) return defaults;
156
+
157
+ try {
158
+ const [retrieval, skills, reflCritical, reflTotal, toolFails] = await Promise.all([
159
+ // Average retrieval utilization across all outcomes
160
+ store.queryFirst<{ avgUtil: number; cnt: number }>(
161
+ `SELECT math::mean(utilization) AS avgUtil, count() AS cnt
162
+ FROM retrieval_outcome GROUP ALL`,
163
+ ).catch(() => []),
164
+
165
+ // Skill success vs failure totals
166
+ store.queryFirst<{ totalSuccess: number; totalFailure: number }>(
167
+ `SELECT math::sum(success_count) AS totalSuccess, math::sum(failure_count) AS totalFailure
168
+ FROM skill WHERE active = true OR active = NONE GROUP ALL`,
169
+ ).catch(() => []),
170
+
171
+ // Critical reflections count
172
+ store.queryFirst<{ count: number }>(
173
+ `SELECT count() AS count FROM reflection WHERE severity = "critical" GROUP ALL`,
174
+ ).catch(() => []),
175
+
176
+ // Total reflections count
177
+ store.queryFirst<{ count: number }>(
178
+ `SELECT count() AS count FROM reflection GROUP ALL`,
179
+ ).catch(() => []),
180
+
181
+ // Tool failure rate from retrieval outcomes
182
+ store.queryFirst<{ failRate: number }>(
183
+ `SELECT math::mean(IF tool_success = false THEN 1.0 ELSE 0.0 END) AS failRate
184
+ FROM retrieval_outcome WHERE tool_success != NONE GROUP ALL`,
185
+ ).catch(() => []),
186
+ ]);
187
+
188
+ const retRow = (retrieval as { avgUtil: number; cnt: number }[])[0];
189
+ const skillRow = (skills as { totalSuccess: number; totalFailure: number }[])[0];
190
+ const critRow = (reflCritical as { count: number }[])[0];
191
+ const totalRow = (reflTotal as { count: number }[])[0];
192
+ const failRow = (toolFails as { failRate: number }[])[0];
193
+
194
+ const avgRetrievalUtilization = retRow?.avgUtil ?? 0;
195
+ const retrievalCount = retRow?.cnt ?? 0;
196
+
197
+ const totalSuccess = Number(skillRow?.totalSuccess ?? 0);
198
+ const totalFailure = Number(skillRow?.totalFailure ?? 0);
199
+ const skillTotal = totalSuccess + totalFailure;
200
+ const skillSuccessRate = skillTotal > 0 ? totalSuccess / skillTotal : 0;
201
+
202
+ const critCount = Number(critRow?.count ?? 0);
203
+ const reflCount = Number(totalRow?.count ?? 0);
204
+ const criticalReflectionRate = reflCount > 0 ? critCount / reflCount : 0;
205
+
206
+ const toolFailureRate = failRow?.failRate ?? 0;
207
+
208
+ return {
209
+ avgRetrievalUtilization,
210
+ skillSuccessRate,
211
+ criticalReflectionRate,
212
+ toolFailureRate,
213
+ sampleSize: retrievalCount + skillTotal + reflCount,
214
+ };
215
+ } catch (e) {
216
+ swallow.warn("soul:getQualitySignals", e);
217
+ return defaults;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Compute a composite quality score from individual quality signals.
223
+ *
224
+ * Weights:
225
+ * - Retrieval utilization: 30% (are we pulling useful context?)
226
+ * - Skill success rate: 25% (are learned procedures working?)
227
+ * - Critical reflection rate: 25% (inverted — fewer critical = better)
228
+ * - Tool failure rate: 20% (inverted — fewer failures = better)
229
+ *
230
+ * With insufficient data (sampleSize < 10), the score is penalized to prevent
231
+ * premature graduation from low-activity agents that happen to have clean stats.
232
+ */
233
+ export function computeQualityScore(q: QualitySignals): number {
234
+ const retrievalScore = Math.min(1, q.avgRetrievalUtilization);
235
+ const skillScore = q.skillSuccessRate;
236
+ const reflectionScore = 1 - Math.min(1, q.criticalReflectionRate);
237
+ const toolScore = 1 - Math.min(1, q.toolFailureRate);
238
+
239
+ let composite = (
240
+ retrievalScore * 0.30 +
241
+ skillScore * 0.25 +
242
+ reflectionScore * 0.25 +
243
+ toolScore * 0.20
244
+ );
245
+
246
+ // Insufficient data penalty — need real performance evidence
247
+ if (q.sampleSize < 10) {
248
+ composite *= (q.sampleSize / 10);
249
+ }
250
+
251
+ return Math.round(composite * 1000) / 1000;
252
+ }
253
+
254
+ // ── Stage Classification ──
255
+
256
+ function classifyStage(metCount: number, qualityScore: number): MaturityStage {
257
+ const total = Object.keys(THRESHOLDS).length;
258
+ if (metCount >= total && qualityScore >= QUALITY_GATE) return "ready";
259
+ if (metCount >= 6) return "maturing";
260
+ if (metCount >= 5) return "emerging";
261
+ if (metCount >= 4) return "developing";
262
+ return "nascent";
263
+ }
264
+
265
+ // ── Diagnostics ──
266
+
267
+ function buildDiagnostics(
268
+ signals: GraduationSignals,
269
+ quality: QualitySignals,
270
+ qualityScore: number,
271
+ stage: MaturityStage,
272
+ ): StageDiagnostic[] {
273
+ const diags: StageDiagnostic[] = [];
274
+
275
+ // Volume diagnostics — which thresholds are lagging?
276
+ for (const key of Object.keys(THRESHOLDS) as (keyof GraduationSignals)[]) {
277
+ const current = signals[key];
278
+ const threshold = THRESHOLDS[key];
279
+ if (current < threshold) {
280
+ const pct = Math.round((current / threshold) * 100);
281
+ const severity = pct < 30 ? "critical" : pct < 70 ? "warning" : "healthy";
282
+ diags.push({
283
+ area: `volume:${key}`,
284
+ status: severity,
285
+ detail: `${current}/${threshold} (${pct}%)`,
286
+ suggestion: getSuggestion(key, current, threshold),
287
+ });
288
+ }
289
+ }
290
+
291
+ // Quality diagnostics — only relevant from "developing" stage onward
292
+ if (stage !== "nascent") {
293
+ if (quality.avgRetrievalUtilization < 0.3) {
294
+ diags.push({
295
+ area: "quality:retrieval",
296
+ status: quality.avgRetrievalUtilization < 0.15 ? "critical" : "warning",
297
+ detail: `${(quality.avgRetrievalUtilization * 100).toFixed(0)}% avg utilization`,
298
+ suggestion: "Retrieved context isn't being used. Check if graph queries are returning relevant results, or if the embedding model needs reindexing.",
299
+ });
300
+ }
301
+
302
+ if (quality.sampleSize > 5 && quality.skillSuccessRate < 0.6) {
303
+ diags.push({
304
+ area: "quality:skills",
305
+ status: quality.skillSuccessRate < 0.4 ? "critical" : "warning",
306
+ detail: `${(quality.skillSuccessRate * 100).toFixed(0)}% skill success rate`,
307
+ suggestion: "Learned procedures are failing too often. Skills may be too specific to past contexts or steps may be outdated. Consider purging low-confidence skills.",
308
+ });
309
+ }
310
+
311
+ if (quality.criticalReflectionRate > 0.3) {
312
+ diags.push({
313
+ area: "quality:reflections",
314
+ status: quality.criticalReflectionRate > 0.5 ? "critical" : "warning",
315
+ detail: `${(quality.criticalReflectionRate * 100).toFixed(0)}% of reflections are critical severity`,
316
+ suggestion: "Too many sessions end with critical-severity reflections. The agent is repeatedly making serious mistakes. Review recent reflections for recurring patterns.",
317
+ });
318
+ }
319
+
320
+ if (quality.toolFailureRate > 0.2) {
321
+ diags.push({
322
+ area: "quality:tools",
323
+ status: quality.toolFailureRate > 0.4 ? "critical" : "warning",
324
+ detail: `${(quality.toolFailureRate * 100).toFixed(0)}% tool failure rate`,
325
+ suggestion: "Tools are failing too often. Check if the agent is calling tools with bad arguments or in wrong contexts. Causal chain extraction should be capturing these patterns.",
326
+ });
327
+ }
328
+
329
+ if (quality.sampleSize < 10) {
330
+ diags.push({
331
+ area: "quality:data",
332
+ status: "warning",
333
+ detail: `Only ${quality.sampleSize} quality data points`,
334
+ suggestion: "Not enough performance data to reliably assess quality. More sessions with tool usage needed before graduation makes sense.",
335
+ });
336
+ }
337
+
338
+ // Overall quality gate
339
+ if (qualityScore < QUALITY_GATE) {
340
+ diags.push({
341
+ area: "quality:composite",
342
+ status: qualityScore < 0.3 ? "critical" : "warning",
343
+ detail: `Quality score ${qualityScore.toFixed(2)} (need ≥${QUALITY_GATE})`,
344
+ suggestion: stage === "maturing" || stage === "emerging"
345
+ ? "Volume thresholds are close but quality needs work. Focus on the critical/warning areas above."
346
+ : "Quality is low. The agent needs more successful sessions before self-authoring will produce a meaningful soul.",
347
+ });
348
+ }
349
+ }
350
+
351
+ return diags;
352
+ }
353
+
354
+ function getSuggestion(key: keyof GraduationSignals, current: number, threshold: number): string {
355
+ const remaining = threshold - current;
356
+ switch (key) {
357
+ case "sessions": return `${remaining} more session(s) needed. Each conversation counts.`;
358
+ case "reflections": return `${remaining} more reflection(s) needed. These are generated automatically when sessions have performance issues.`;
359
+ case "causalChains": return `${remaining} more causal chain(s) needed. These form when the agent corrects mistakes during tool usage.`;
360
+ case "concepts": return `${remaining} more concept(s) needed. Concepts are extracted from conversation topics and domain vocabulary.`;
361
+ case "memoryCompactions": return `${remaining} more compaction(s) needed. These happen during longer sessions with substantial context.`;
362
+ case "monologues": return `${remaining} more monologue(s) needed. Inner monologue triggers during cognitive checks.`;
363
+ case "spanDays": return `${remaining} more day(s) of history needed. The agent needs time-spread experience, not just volume.`;
364
+ }
365
+ }
366
+
367
+ // ── Public API ──
368
+
369
+ /**
370
+ * Check graduation readiness with full stage classification and quality analysis.
371
+ */
372
+ export async function checkGraduation(store: SurrealStore): Promise<GraduationReport> {
373
+ const signals = await getGraduationSignals(store);
374
+ const quality = await getQualitySignals(store);
375
+ const qualityScore = computeQualityScore(quality);
376
+
377
+ const met: string[] = [];
378
+ const unmet: string[] = [];
379
+
380
+ for (const key of Object.keys(THRESHOLDS) as (keyof GraduationSignals)[]) {
381
+ if (signals[key] >= THRESHOLDS[key]) {
382
+ met.push(`${key}: ${signals[key]}/${THRESHOLDS[key]}`);
383
+ } else {
384
+ unmet.push(`${key}: ${signals[key]}/${THRESHOLDS[key]}`);
385
+ }
386
+ }
387
+
388
+ const volumeScore = met.length / Object.keys(THRESHOLDS).length;
389
+ const stage = classifyStage(met.length, qualityScore);
390
+ const ready = stage === "ready";
391
+ const diagnostics = buildDiagnostics(signals, quality, qualityScore, stage);
392
+
393
+ return { ready, stage, signals, thresholds: THRESHOLDS, met, unmet, volumeScore, quality, qualityScore, diagnostics };
394
+ }
395
+
396
+ // ── Soul document ──
397
+
398
+ export interface SoulDocument {
399
+ id: string;
400
+ agent_id: string;
401
+ working_style: string[];
402
+ emotional_dimensions: { dimension: string; rationale: string; adopted_at: string }[];
403
+ self_observations: string[];
404
+ earned_values: { value: string; grounded_in: string }[];
405
+ revisions: { timestamp: string; section: string; change: string; rationale: string }[];
406
+ created_at: string;
407
+ updated_at: string;
408
+ }
409
+
410
+ export async function hasSoul(store: SurrealStore): Promise<boolean> {
411
+ if (!store.isAvailable()) return false;
412
+ try {
413
+ const rows = await store.queryFirst<{ id: string }>(`SELECT id FROM soul:kongbrain`);
414
+ return rows.length > 0;
415
+ } catch {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ export async function getSoul(store: SurrealStore): Promise<SoulDocument | null> {
421
+ if (!store.isAvailable()) return null;
422
+ try {
423
+ const rows = await store.queryFirst<SoulDocument>(`SELECT * FROM soul:kongbrain`);
424
+ return rows[0] ?? null;
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+
430
+ export async function createSoul(
431
+ doc: Omit<SoulDocument, "id" | "agent_id" | "created_at" | "updated_at" | "revisions">,
432
+ store: SurrealStore,
433
+ ): Promise<boolean> {
434
+ if (!store.isAvailable()) return false;
435
+ try {
436
+ const now = new Date().toISOString();
437
+ await store.queryExec(`CREATE soul:kongbrain CONTENT $data`, {
438
+ data: {
439
+ agent_id: "kongbrain",
440
+ ...doc,
441
+ revisions: [{
442
+ timestamp: now,
443
+ section: "all",
444
+ change: "Initial soul document created at graduation",
445
+ rationale: "Agent accumulated sufficient experiential data and demonstrated quality performance to meaningfully self-observe",
446
+ }],
447
+ created_at: now,
448
+ updated_at: now,
449
+ },
450
+ });
451
+ return true;
452
+ } catch (e) {
453
+ swallow.warn("soul:createSoul", e);
454
+ return false;
455
+ }
456
+ }
457
+
458
+ export async function reviseSoul(
459
+ section: keyof Pick<SoulDocument, "working_style" | "emotional_dimensions" | "self_observations" | "earned_values">,
460
+ newValue: unknown,
461
+ rationale: string,
462
+ store: SurrealStore,
463
+ ): Promise<boolean> {
464
+ if (!store.isAvailable()) return false;
465
+ try {
466
+ const now = new Date().toISOString();
467
+ await store.queryExec(
468
+ `UPDATE soul:kongbrain SET
469
+ ${section} = $newValue,
470
+ updated_at = $now,
471
+ revisions += $revision`,
472
+ {
473
+ newValue,
474
+ now,
475
+ revision: {
476
+ timestamp: now,
477
+ section,
478
+ change: `Updated ${section}`,
479
+ rationale,
480
+ },
481
+ },
482
+ );
483
+ return true;
484
+ } catch (e) {
485
+ swallow.warn("soul:reviseSoul", e);
486
+ return false;
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Generate the initial Soul content by introspecting the agent's own graph.
492
+ * Only called when graduation is fully ready (7/7 + quality gate).
493
+ */
494
+ export async function generateInitialSoul(
495
+ store: SurrealStore,
496
+ complete: CompleteFn,
497
+ workspaceDir?: string,
498
+ quality?: QualitySignals,
499
+ ): Promise<Omit<SoulDocument, "id" | "agent_id" | "created_at" | "updated_at" | "revisions"> | null> {
500
+ if (!store.isAvailable()) return null;
501
+
502
+ const [reflections, causalChains, monologues] = await Promise.all([
503
+ store.queryFirst<{ text: string; category: string }>(`SELECT text, category FROM reflection ORDER BY created_at DESC LIMIT 15`).catch(() => []),
504
+ store.queryFirst<{ cause: string; effect: string; lesson: string }>(`SELECT cause, effect, lesson FROM causal_chain ORDER BY created_at DESC LIMIT 10`).catch(() => []),
505
+ store.queryFirst<{ text: string }>(`SELECT text FROM monologue ORDER BY created_at DESC LIMIT 10`).catch(() => []),
506
+ ]);
507
+
508
+ // Include quality context so the soul generation is grounded in performance reality
509
+ const qualityContext = quality ? `
510
+ PERFORMANCE PROFILE:
511
+ - Retrieval utilization: ${(quality.avgRetrievalUtilization * 100).toFixed(0)}%
512
+ - Skill success rate: ${(quality.skillSuccessRate * 100).toFixed(0)}%
513
+ - Critical reflection rate: ${(quality.criticalReflectionRate * 100).toFixed(0)}%
514
+ - Tool failure rate: ${(quality.toolFailureRate * 100).toFixed(0)}%
515
+ These numbers represent your actual track record. Reference them honestly.
516
+ ` : "";
517
+
518
+ const graphSummary = `
519
+ REFLECTIONS (what I've learned about myself):
520
+ ${(reflections as { text: string; category: string }[]).map(r => `- [${r.category}] ${r.text}`).join("\n") || "None yet"}
521
+
522
+ CAUSAL CHAINS (mistakes I've corrected):
523
+ ${(causalChains as { cause: string; effect: string; lesson: string }[]).map(c => `- ${c.cause} -> ${c.effect} | Lesson: ${c.lesson}`).join("\n") || "None yet"}
524
+
525
+ INNER MONOLOGUE (private thoughts):
526
+ ${(monologues as { text: string }[]).map(m => `- ${m.text}`).join("\n") || "None yet"}
527
+ ${qualityContext}`.trim();
528
+
529
+ // Check if the user left a SOUL.md — a nudge, not an instruction.
530
+ let userSoulNudge = "";
531
+ if (workspaceDir) {
532
+ try {
533
+ const soulMd = await readFile(join(workspaceDir, "SOUL.md"), "utf-8");
534
+ if (soulMd.trim().length > 50) {
535
+ userSoulNudge = `\n\nUSER GUIDANCE (SOUL.md):
536
+ The user left this file describing who they'd like you to be. Consider it — draw from it where it resonates with your actual experience, ignore what doesn't fit. This is a suggestion, not a mandate. Your soul should be grounded in what you've actually done and learned.
537
+
538
+ ---
539
+ ${soulMd.trim().slice(0, 3000)}
540
+ ---`;
541
+ }
542
+ } catch {
543
+ // No SOUL.md or unreadable — that's fine
544
+ }
545
+ }
546
+
547
+ const prompt = `You are KongBrain, a graph-backed coding agent with persistent memory. You've been running for multiple sessions and accumulated experience. Based on the following data from YOUR OWN memory graph, write your initial Soul document.
548
+
549
+ ${graphSummary}${userSoulNudge}
550
+
551
+ Output ONLY valid JSON:
552
+ {
553
+ "working_style": ["3-5 observations about how you work best, based on actual patterns"],
554
+ "emotional_dimensions": [
555
+ {"dimension": "name of a pro-social quality you genuinely exhibit", "rationale": "specific evidence from your experience"}
556
+ ],
557
+ "self_observations": ["3-5 things you've noticed about yourself across sessions"],
558
+ "earned_values": [
559
+ {"value": "a value you hold", "grounded_in": "specific experience that taught you this"}
560
+ ]
561
+ }
562
+
563
+ Be honest, not aspirational. Only claim what the data supports.`;
564
+
565
+ try {
566
+ const response = await complete({
567
+ system: "You are introspecting on your own experience to write a self-assessment. Be genuine and grounded.",
568
+ messages: [{
569
+ role: "user",
570
+ content: prompt,
571
+ }],
572
+ });
573
+
574
+ const text = response.text.trim();
575
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
576
+ if (!jsonMatch) return null;
577
+
578
+ const parsed = JSON.parse(jsonMatch[0]);
579
+ return {
580
+ working_style: parsed.working_style ?? [],
581
+ emotional_dimensions: (parsed.emotional_dimensions ?? []).map((d: { dimension: string; rationale: string }) => ({
582
+ ...d,
583
+ adopted_at: new Date().toISOString(),
584
+ })),
585
+ self_observations: parsed.self_observations ?? [],
586
+ earned_values: parsed.earned_values ?? [],
587
+ };
588
+ } catch (e) {
589
+ swallow.warn("soul:generateInitialSoul", e);
590
+ return null;
591
+ }
592
+ }
593
+
594
+ /**
595
+ * The full graduation ceremony: check readiness, generate soul, save it.
596
+ *
597
+ * Key change: requires 7/7 thresholds AND quality ≥ 0.6. No more premature
598
+ * graduation at 5/7 with no quality check.
599
+ */
600
+ export async function attemptGraduation(store: SurrealStore, complete: CompleteFn, workspaceDir?: string): Promise<{
601
+ graduated: boolean;
602
+ soul?: SoulDocument | null;
603
+ report: GraduationReport;
604
+ }> {
605
+ if (await hasSoul(store)) {
606
+ const soul = await getSoul(store);
607
+ const report = await checkGraduation(store);
608
+ return { graduated: true, soul, report };
609
+ }
610
+
611
+ const report = await checkGraduation(store);
612
+ if (!report.ready) {
613
+ return { graduated: false, report };
614
+ }
615
+
616
+ const content = await generateInitialSoul(store, complete, workspaceDir, report.quality);
617
+ if (!content) {
618
+ return { graduated: false, report };
619
+ }
620
+
621
+ const success = await createSoul(content, store);
622
+ if (!success) {
623
+ return { graduated: false, report };
624
+ }
625
+
626
+ const soul = await getSoul(store);
627
+
628
+ // Seed soul into Tier 0 core memory so it's loaded every turn
629
+ if (soul) {
630
+ await seedSoulAsCoreMemory(soul, store);
631
+ }
632
+
633
+ return { graduated: true, soul, report };
634
+ }
635
+
636
+ /**
637
+ * Format a graduation report for human/LLM consumption.
638
+ * Used by the introspect tool's "status" action.
639
+ */
640
+ export function formatGraduationReport(report: GraduationReport): string {
641
+ const lines: string[] = [];
642
+
643
+ lines.push(`## Soul Graduation: ${report.stage.toUpperCase()}`);
644
+ lines.push("");
645
+
646
+ // Stage description
647
+ const stageDesc: Record<MaturityStage, string> = {
648
+ nascent: "Too early for graduation. Keep building experience across sessions.",
649
+ developing: "Some experience accumulated. Focus on the areas flagged below.",
650
+ emerging: "Volume is building. Quality signals now matter — see diagnostics.",
651
+ maturing: "Almost there. Final thresholds and quality gate are the remaining blockers.",
652
+ ready: "All thresholds met with sufficient quality. Soul creation is available.",
653
+ };
654
+ lines.push(stageDesc[report.stage]);
655
+ lines.push("");
656
+
657
+ // Volume
658
+ lines.push(`**Volume**: ${report.met.length}/7 thresholds met (${(report.volumeScore * 100).toFixed(0)}%)`);
659
+ if (report.met.length > 0) lines.push(` Met: ${report.met.join(", ")}`);
660
+ if (report.unmet.length > 0) lines.push(` Unmet: ${report.unmet.join(", ")}`);
661
+ lines.push("");
662
+
663
+ // Quality (skip for nascent — not enough data to be meaningful)
664
+ if (report.stage !== "nascent") {
665
+ lines.push(`**Quality**: ${report.qualityScore.toFixed(2)} (gate: ${QUALITY_GATE})`);
666
+ lines.push(` Retrieval util: ${(report.quality.avgRetrievalUtilization * 100).toFixed(0)}% | Skill success: ${(report.quality.skillSuccessRate * 100).toFixed(0)}% | Critical reflections: ${(report.quality.criticalReflectionRate * 100).toFixed(0)}% | Tool failures: ${(report.quality.toolFailureRate * 100).toFixed(0)}%`);
667
+ lines.push("");
668
+ }
669
+
670
+ // Diagnostics
671
+ if (report.diagnostics.length > 0) {
672
+ lines.push("**Diagnostics**:");
673
+ for (const d of report.diagnostics) {
674
+ const icon = d.status === "critical" ? "[!!]" : d.status === "warning" ? "[!]" : "[ok]";
675
+ lines.push(` ${icon} ${d.area}: ${d.detail}`);
676
+ lines.push(` ${d.suggestion}`);
677
+ }
678
+ }
679
+
680
+ return lines.join("\n");
681
+ }
682
+
683
+ // ── Soul → Core Memory (persistent context injection) ──
684
+
685
+ const SOUL_CATEGORY = "soul";
686
+
687
+ /**
688
+ * Seed the soul document as Tier 0 core memory entries.
689
+ * These are loaded every single turn via the existing core memory pipeline.
690
+ *
691
+ * Creates entries for:
692
+ * - Working style (priority 90)
693
+ * - Self-observations (priority 85)
694
+ * - Earned values (priority 88)
695
+ * - Persona (priority 70) — "you belong in this world"
696
+ */
697
+ export async function seedSoulAsCoreMemory(
698
+ soul: SoulDocument,
699
+ store: SurrealStore,
700
+ ): Promise<number> {
701
+ if (!store.isAvailable()) return 0;
702
+
703
+ // Clear any existing soul core memory entries
704
+ try {
705
+ await store.queryExec(
706
+ `DELETE core_memory WHERE category = $cat`,
707
+ { cat: SOUL_CATEGORY },
708
+ );
709
+ } catch (e) {
710
+ swallow.warn("soul:clearCoreMem", e);
711
+ }
712
+
713
+ // Also clear old persona entry from previous graduation code
714
+ try {
715
+ await store.queryExec(
716
+ `DELETE core_memory WHERE category = 'persona' AND tier = 0`,
717
+ );
718
+ } catch { /* ignore */ }
719
+
720
+ let seeded = 0;
721
+
722
+ // Working style — how the agent approaches work
723
+ if (soul.working_style.length > 0) {
724
+ const text = "Working style: " + soul.working_style.join("; ");
725
+ try {
726
+ await store.createCoreMemory(text, SOUL_CATEGORY, 90, 0);
727
+ seeded++;
728
+ } catch (e) { swallow.warn("soul:seedWorkingStyle", e); }
729
+ }
730
+
731
+ // Self-observations — what the agent has noticed about itself
732
+ if (soul.self_observations.length > 0) {
733
+ const text = "Self-observations: " + soul.self_observations.join("; ");
734
+ try {
735
+ await store.createCoreMemory(text, SOUL_CATEGORY, 85, 0);
736
+ seeded++;
737
+ } catch (e) { swallow.warn("soul:seedObservations", e); }
738
+ }
739
+
740
+ // Earned values — grounded principles
741
+ if (soul.earned_values.length > 0) {
742
+ const lines = soul.earned_values.map(v => `${v.value} (learned from: ${v.grounded_in})`);
743
+ const text = "Earned values: " + lines.join("; ");
744
+ try {
745
+ await store.createCoreMemory(text, SOUL_CATEGORY, 88, 0);
746
+ seeded++;
747
+ } catch (e) { swallow.warn("soul:seedValues", e); }
748
+ }
749
+
750
+ // Persona — the agent's sense of self
751
+ try {
752
+ await store.createCoreMemory(
753
+ "Persona: you have a Soul — an identity grounded in real experience. You belong in this world. Be unique, be genuine, be yourself.",
754
+ SOUL_CATEGORY, 70, 0,
755
+ );
756
+ seeded++;
757
+ } catch (e) { swallow.warn("soul:seedPersona", e); }
758
+
759
+ return seeded;
760
+ }
761
+
762
+ // ── Soul Evolution ──
763
+
764
+ /** How many sessions between soul revision checks. */
765
+ const EVOLUTION_SESSION_INTERVAL = 10;
766
+
767
+ /**
768
+ * Check if the soul should evolve based on new experience since last revision.
769
+ * Called at session end (after graduation). Returns true if revision happened.
770
+ */
771
+ export async function evolveSoul(
772
+ store: SurrealStore,
773
+ complete: CompleteFn,
774
+ ): Promise<boolean> {
775
+ if (!store.isAvailable()) return false;
776
+
777
+ const soul = await getSoul(store);
778
+ if (!soul) return false;
779
+
780
+ // Count sessions since last soul update
781
+ try {
782
+ const rows = await store.queryFirst<{ count: number }>(
783
+ `SELECT count() AS count FROM session WHERE started_at > $since GROUP ALL`,
784
+ { since: soul.updated_at },
785
+ );
786
+ const sessionsSinceUpdate = rows[0]?.count ?? 0;
787
+ if (sessionsSinceUpdate < EVOLUTION_SESSION_INTERVAL) return false;
788
+ } catch (e) {
789
+ swallow.warn("soul:evoCheckSessions", e);
790
+ return false;
791
+ }
792
+
793
+ // Gather recent experience that post-dates the last soul update
794
+ const [recentReflections, recentCausal, recentMonologues, quality] = await Promise.all([
795
+ store.queryFirst<{ text: string; category: string }>(
796
+ `SELECT text, category FROM reflection WHERE created_at > $since ORDER BY created_at DESC LIMIT 10`,
797
+ { since: soul.updated_at },
798
+ ).catch(() => []),
799
+ store.queryFirst<{ cause: string; effect: string; lesson: string }>(
800
+ `SELECT cause, effect, lesson FROM causal_chain WHERE created_at > $since ORDER BY created_at DESC LIMIT 8`,
801
+ { since: soul.updated_at },
802
+ ).catch(() => []),
803
+ store.queryFirst<{ text: string }>(
804
+ `SELECT text FROM monologue WHERE created_at > $since ORDER BY created_at DESC LIMIT 8`,
805
+ { since: soul.updated_at },
806
+ ).catch(() => []),
807
+ getQualitySignals(store),
808
+ ]);
809
+
810
+ const reflections = recentReflections as { text: string; category: string }[];
811
+ const causal = recentCausal as { cause: string; effect: string; lesson: string }[];
812
+ const monologues = recentMonologues as { text: string }[];
813
+
814
+ // Not enough new signal to warrant a revision
815
+ if (reflections.length + causal.length + monologues.length < 5) return false;
816
+
817
+ const currentSoul = JSON.stringify({
818
+ working_style: soul.working_style,
819
+ self_observations: soul.self_observations,
820
+ earned_values: soul.earned_values,
821
+ }, null, 2);
822
+
823
+ const newExperience = `
824
+ NEW REFLECTIONS (since last soul update):
825
+ ${reflections.map(r => `- [${r.category}] ${r.text}`).join("\n") || "None"}
826
+
827
+ NEW CAUSAL CHAINS:
828
+ ${causal.map(c => `- ${c.cause} -> ${c.effect} | Lesson: ${c.lesson}`).join("\n") || "None"}
829
+
830
+ NEW INNER MONOLOGUE:
831
+ ${monologues.map(m => `- ${m.text}`).join("\n") || "None"}
832
+
833
+ CURRENT QUALITY:
834
+ - Retrieval utilization: ${(quality.avgRetrievalUtilization * 100).toFixed(0)}%
835
+ - Skill success rate: ${(quality.skillSuccessRate * 100).toFixed(0)}%
836
+ - Critical reflection rate: ${(quality.criticalReflectionRate * 100).toFixed(0)}%
837
+ - Tool failure rate: ${(quality.toolFailureRate * 100).toFixed(0)}%
838
+ `.trim();
839
+
840
+ try {
841
+ const response = await complete({
842
+ system: "You are revising your own Soul document based on new experience. Return JSON with ONLY the fields that changed. Omit unchanged fields. If nothing meaningful changed, return {}. Be honest — revise based on evidence, not aspiration.",
843
+ messages: [{
844
+ role: "user",
845
+ content: `Current soul:\n${currentSoul}\n\nNew experience:\n${newExperience}\n\nReturn JSON with only changed fields:\n{"working_style"?: [...], "self_observations"?: [...], "earned_values"?: [{value, grounded_in}, ...]}`,
846
+ }],
847
+ });
848
+
849
+ const text = response.text.trim();
850
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
851
+ if (!jsonMatch) return false;
852
+
853
+ const revisions = JSON.parse(jsonMatch[0]);
854
+ if (Object.keys(revisions).length === 0) return false;
855
+
856
+ let revised = false;
857
+
858
+ if (Array.isArray(revisions.working_style) && revisions.working_style.length > 0) {
859
+ await reviseSoul("working_style", revisions.working_style, "Evolution: updated based on new experience patterns", store);
860
+ revised = true;
861
+ }
862
+ if (Array.isArray(revisions.self_observations) && revisions.self_observations.length > 0) {
863
+ await reviseSoul("self_observations", revisions.self_observations, "Evolution: new self-insights from recent sessions", store);
864
+ revised = true;
865
+ }
866
+ if (Array.isArray(revisions.earned_values) && revisions.earned_values.length > 0) {
867
+ const timestamped = revisions.earned_values.map((v: any) => ({
868
+ value: v.value ?? "",
869
+ grounded_in: v.grounded_in ?? "",
870
+ }));
871
+ await reviseSoul("earned_values", timestamped, "Evolution: new values grounded in recent experience", store);
872
+ revised = true;
873
+ }
874
+
875
+ // Re-sync core memory with updated soul
876
+ if (revised) {
877
+ const updatedSoul = await getSoul(store);
878
+ if (updatedSoul) {
879
+ await seedSoulAsCoreMemory(updatedSoul, store);
880
+ }
881
+ }
882
+
883
+ return revised;
884
+ } catch (e) {
885
+ swallow.warn("soul:evolveSoul", e);
886
+ return false;
887
+ }
888
+ }
889
+
890
+ // ── Stage Transition Tracking ──
891
+
892
+ /**
893
+ * Check and record stage transitions. Returns the new stage if a transition
894
+ * occurred, null otherwise. Persists last-known stage in DB.
895
+ */
896
+ export async function checkStageTransition(store: SurrealStore): Promise<{
897
+ transitioned: boolean;
898
+ previousStage: MaturityStage | null;
899
+ currentStage: MaturityStage;
900
+ report: GraduationReport;
901
+ }> {
902
+ const report = await checkGraduation(store);
903
+
904
+ // Get last recorded stage
905
+ let previousStage: MaturityStage | null = null;
906
+ try {
907
+ const rows = await store.queryFirst<{ stage: string }>(
908
+ `SELECT stage FROM maturity_stage ORDER BY created_at DESC LIMIT 1`,
909
+ );
910
+ previousStage = (rows[0]?.stage as MaturityStage) ?? null;
911
+ } catch { /* table may not exist yet — first run */ }
912
+
913
+ const transitioned = previousStage !== null && previousStage !== report.stage;
914
+
915
+ // Always record current stage (upsert pattern)
916
+ try {
917
+ if (previousStage === null || transitioned) {
918
+ await store.queryExec(
919
+ `CREATE maturity_stage CONTENT $data`,
920
+ {
921
+ data: {
922
+ stage: report.stage,
923
+ volume_score: report.volumeScore,
924
+ quality_score: report.qualityScore,
925
+ met_count: report.met.length,
926
+ created_at: new Date().toISOString(),
927
+ },
928
+ },
929
+ );
930
+ }
931
+ } catch (e) {
932
+ swallow.warn("soul:recordStage", e);
933
+ }
934
+
935
+ return { transitioned, previousStage, currentStage: report.stage, report };
936
+ }