kongbrain 0.4.1 → 0.4.2
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/.github/workflows/ci.yml +45 -0
- package/.github/workflows/pr-check.yml +16 -0
- package/CHANGELOG.md +64 -0
- package/README.github.md +40 -1
- package/SKILL.md +1 -1
- package/TOKEN_FLOW.md +184 -0
- package/package.json +1 -1
- package/src/acan.ts +28 -5
- package/src/causal.ts +18 -25
- package/src/cognitive-bootstrap.ts +6 -6
- package/src/cognitive-check.ts +17 -19
- package/src/config.ts +1 -1
- package/src/context-engine.ts +81 -48
- package/src/daemon-manager.ts +51 -17
- package/src/deferred-cleanup.ts +11 -9
- package/src/embeddings.ts +6 -7
- package/src/errors.ts +5 -3
- package/src/graph-context.ts +269 -173
- package/src/hooks/after-tool-call.ts +2 -1
- package/src/hooks/before-tool-call.ts +15 -11
- package/src/hooks/llm-output.ts +18 -10
- package/src/index.ts +17 -12
- package/src/intent.ts +9 -8
- package/src/log.ts +11 -0
- package/src/orchestrator.ts +11 -4
- package/src/prefetch.ts +2 -2
- package/src/reflection.ts +9 -2
- package/src/schema.surql +4 -0
- package/src/skills.ts +32 -10
- package/src/soul.ts +17 -1
- package/src/state.ts +31 -0
- package/src/surreal.ts +134 -110
- package/src/tools/introspect.ts +1 -1
- package/src/wakeup.ts +0 -142
package/src/surreal.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Surreal } from "surrealdb";
|
|
2
2
|
import type { SurrealConfig } from "./config.js";
|
|
3
3
|
import { swallow } from "./errors.js";
|
|
4
|
+
import { log } from "./log.js";
|
|
4
5
|
import { loadSchema } from "./schema-loader.js";
|
|
5
6
|
|
|
6
7
|
/** Record with a vector similarity score from SurrealDB search */
|
|
@@ -54,6 +55,26 @@ function assertRecordId(id: string): void {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/** Whitelist of valid SurrealDB edge table names — prevents SQL injection via edge interpolation. */
|
|
59
|
+
const VALID_EDGES = new Set([
|
|
60
|
+
// Semantic edges
|
|
61
|
+
"responds_to", "tool_result_of", "summarizes", "mentions", "related_to",
|
|
62
|
+
"narrower", "broader", "about_concept", "reflects_on",
|
|
63
|
+
// Skill edges
|
|
64
|
+
"skill_from_task", "skill_uses_concept",
|
|
65
|
+
// Structural pillar edges
|
|
66
|
+
"owns", "performed", "task_part_of", "session_task",
|
|
67
|
+
"produced", "derived_from", "relevant_to", "used_in", "artifact_mentions",
|
|
68
|
+
// Causal edges
|
|
69
|
+
"caused_by", "supports", "contradicts", "describes",
|
|
70
|
+
// Session edges
|
|
71
|
+
"part_of",
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
function assertValidEdge(edge: string): void {
|
|
75
|
+
if (!VALID_EDGES.has(edge)) throw new Error(`Invalid edge name: ${edge}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
function patchOrderByFields(sql: string): string {
|
|
58
79
|
const s = sql.trim();
|
|
59
80
|
if (!/^\s*SELECT\b/i.test(s) || !/\bORDER\s+BY\b/i.test(s)) return sql;
|
|
@@ -139,8 +160,8 @@ export class SurrealStore {
|
|
|
139
160
|
const BACKOFF_MS = [500, 1500, 4000];
|
|
140
161
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
141
162
|
try {
|
|
142
|
-
|
|
143
|
-
`
|
|
163
|
+
log.warn(
|
|
164
|
+
`SurrealDB disconnected — reconnecting (attempt ${attempt}/${MAX_ATTEMPTS})...`,
|
|
144
165
|
);
|
|
145
166
|
this.db = new Surreal();
|
|
146
167
|
const CONNECT_TIMEOUT_MS = 5_000;
|
|
@@ -157,13 +178,13 @@ export class SurrealStore {
|
|
|
157
178
|
),
|
|
158
179
|
),
|
|
159
180
|
]);
|
|
160
|
-
|
|
181
|
+
log.warn("SurrealDB reconnected successfully.");
|
|
161
182
|
return;
|
|
162
183
|
} catch (e) {
|
|
163
184
|
if (attempt < MAX_ATTEMPTS) {
|
|
164
185
|
await new Promise((r) => setTimeout(r, BACKOFF_MS[attempt - 1]));
|
|
165
186
|
} else {
|
|
166
|
-
|
|
187
|
+
log.error(`SurrealDB reconnection failed after ${MAX_ATTEMPTS} attempts.`);
|
|
167
188
|
throw new Error("SurrealDB reconnection failed");
|
|
168
189
|
}
|
|
169
190
|
}
|
|
@@ -218,7 +239,7 @@ export class SurrealStore {
|
|
|
218
239
|
|
|
219
240
|
/** Returns true if an error is a connection-level failure worth retrying. */
|
|
220
241
|
private isConnectionError(e: unknown): boolean {
|
|
221
|
-
const msg = String((e as
|
|
242
|
+
const msg = String((e as { message?: string })?.message ?? e);
|
|
222
243
|
return msg.includes("must be connected") || msg.includes("ConnectionUnavailable");
|
|
223
244
|
}
|
|
224
245
|
|
|
@@ -280,6 +301,24 @@ export class SurrealStore {
|
|
|
280
301
|
});
|
|
281
302
|
}
|
|
282
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Execute N SQL statements in a single SurrealDB round-trip.
|
|
306
|
+
* Returns one result array per statement; bindings are shared across all statements.
|
|
307
|
+
*/
|
|
308
|
+
async queryBatch<T = any>(statements: string[], bindings?: Record<string, unknown>): Promise<T[][]> {
|
|
309
|
+
if (statements.length === 0) return [];
|
|
310
|
+
await this.ensureConnected();
|
|
311
|
+
return this.withRetry(async () => {
|
|
312
|
+
const ns = this.config.ns;
|
|
313
|
+
const dbName = this.config.db;
|
|
314
|
+
const joined = statements.map(s => patchOrderByFields(s)).join(";\n");
|
|
315
|
+
const fullSql = `USE NS ${ns} DB ${dbName};\n${joined}`;
|
|
316
|
+
const raw = await this.db.query(fullSql, bindings) as unknown[];
|
|
317
|
+
// First result is the USE statement (empty), skip it
|
|
318
|
+
return raw.slice(1).map(r => (Array.isArray(r) ? r : []).filter(Boolean)) as T[][];
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
283
322
|
private async safeQuery(
|
|
284
323
|
sql: string,
|
|
285
324
|
bindings: Record<string, unknown>,
|
|
@@ -294,6 +333,7 @@ export class SurrealStore {
|
|
|
294
333
|
|
|
295
334
|
// ── Vector search ──────────────────────────────────────────────────────
|
|
296
335
|
|
|
336
|
+
/** Multi-table cosine similarity search across turns, concepts, memories, artifacts, monologues, and identity chunks. Returns merged results sorted by score. */
|
|
297
337
|
async vectorSearch(
|
|
298
338
|
vec: number[],
|
|
299
339
|
sessionId: string,
|
|
@@ -319,74 +359,52 @@ export class SurrealStore {
|
|
|
319
359
|
const crossTurnLim = lim.turn - sessionTurnLim;
|
|
320
360
|
const emb = withEmbeddings ? ", embedding" : "";
|
|
321
361
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
{ vec, lim: lim.artifact },
|
|
369
|
-
),
|
|
370
|
-
this.safeQuery(
|
|
371
|
-
`SELECT id, content AS text, category AS source, 0.5 AS importance, 0 AS accessCount,
|
|
372
|
-
timestamp, 'monologue' AS table,
|
|
373
|
-
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
374
|
-
FROM monologue
|
|
375
|
-
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
376
|
-
ORDER BY score DESC LIMIT $lim`,
|
|
377
|
-
{ vec, lim: lim.monologue },
|
|
378
|
-
),
|
|
379
|
-
// Identity chunks — agent self-knowledge, searchable mid-conversation
|
|
380
|
-
this.safeQuery(
|
|
381
|
-
`SELECT id, text, importance, 0 AS accessCount,
|
|
382
|
-
'identity_chunk' AS table,
|
|
383
|
-
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
384
|
-
FROM identity_chunk
|
|
385
|
-
WHERE embedding != NONE AND array::len(embedding) > 0
|
|
386
|
-
ORDER BY score DESC LIMIT $lim`,
|
|
387
|
-
{ vec, lim: lim.identity },
|
|
388
|
-
),
|
|
389
|
-
]);
|
|
362
|
+
// Batch all 7 vector searches into a single round-trip (limits inlined — per-table)
|
|
363
|
+
const stmts = [
|
|
364
|
+
`SELECT id, text, role, timestamp, 0 AS accessCount, 'turn' AS table,
|
|
365
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
366
|
+
FROM turn WHERE embedding != NONE AND array::len(embedding) > 0
|
|
367
|
+
AND session_id = $sid ORDER BY score DESC LIMIT ${sessionTurnLim}`,
|
|
368
|
+
`SELECT id, text, role, timestamp, 0 AS accessCount, 'turn' AS table,
|
|
369
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
370
|
+
FROM turn WHERE embedding != NONE AND array::len(embedding) > 0
|
|
371
|
+
AND session_id != $sid ORDER BY score DESC LIMIT ${crossTurnLim}`,
|
|
372
|
+
`SELECT id, content AS text, stability AS importance, access_count AS accessCount,
|
|
373
|
+
created_at AS timestamp, 'concept' AS table,
|
|
374
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
375
|
+
FROM concept WHERE embedding != NONE AND array::len(embedding) > 0
|
|
376
|
+
ORDER BY score DESC LIMIT ${lim.concept}`,
|
|
377
|
+
`SELECT id, text, importance, access_count AS accessCount,
|
|
378
|
+
created_at AS timestamp, session_id AS sessionId, 'memory' AS table,
|
|
379
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
380
|
+
FROM memory WHERE embedding != NONE AND array::len(embedding) > 0
|
|
381
|
+
AND (status = 'active' OR status IS NONE) ORDER BY score DESC LIMIT ${lim.memory}`,
|
|
382
|
+
`SELECT id, description AS text, 0 AS accessCount,
|
|
383
|
+
created_at AS timestamp, 'artifact' AS table,
|
|
384
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
385
|
+
FROM artifact WHERE embedding != NONE AND array::len(embedding) > 0
|
|
386
|
+
ORDER BY score DESC LIMIT ${lim.artifact}`,
|
|
387
|
+
`SELECT id, content AS text, category AS source, 0.5 AS importance, 0 AS accessCount,
|
|
388
|
+
timestamp, 'monologue' AS table,
|
|
389
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
390
|
+
FROM monologue WHERE embedding != NONE AND array::len(embedding) > 0
|
|
391
|
+
ORDER BY score DESC LIMIT ${lim.monologue}`,
|
|
392
|
+
`SELECT id, text, importance, 0 AS accessCount,
|
|
393
|
+
'identity_chunk' AS table,
|
|
394
|
+
vector::similarity::cosine(embedding, $vec) AS score${emb}
|
|
395
|
+
FROM identity_chunk WHERE embedding != NONE AND array::len(embedding) > 0
|
|
396
|
+
ORDER BY score DESC LIMIT ${lim.identity}`,
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
let batchResults: any[][];
|
|
400
|
+
try {
|
|
401
|
+
batchResults = await this.queryBatch<any>(stmts, { vec, sid: sessionId });
|
|
402
|
+
} catch (e) {
|
|
403
|
+
swallow.warn("surreal:vectorSearch:batch", e);
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
const [sessionTurns = [], crossTurns = [], concepts = [], memories = [], artifacts = [], monologues = [], identityChunks = []] =
|
|
407
|
+
batchResults as VectorSearchResult[][];
|
|
390
408
|
return [
|
|
391
409
|
...sessionTurns,
|
|
392
410
|
...crossTurns,
|
|
@@ -570,6 +588,10 @@ export class SurrealStore {
|
|
|
570
588
|
|
|
571
589
|
// ── Graph traversal ────────────────────────────────────────────────────
|
|
572
590
|
|
|
591
|
+
/**
|
|
592
|
+
* BFS expansion from seed nodes along typed edges, with batched per-hop queries.
|
|
593
|
+
* Each edge query is LIMIT 3 (EDGE_NEIGHBOR_LIMIT) to bound fan-out per node.
|
|
594
|
+
*/
|
|
573
595
|
async graphExpand(
|
|
574
596
|
nodeIds: string[],
|
|
575
597
|
queryVec: number[],
|
|
@@ -577,6 +599,10 @@ export class SurrealStore {
|
|
|
577
599
|
): Promise<VectorSearchResult[]> {
|
|
578
600
|
if (nodeIds.length === 0) return [];
|
|
579
601
|
|
|
602
|
+
const MAX_FRONTIER_SEEDS = 5; // max seed nodes to start BFS from
|
|
603
|
+
const MAX_FRONTIER_PER_HOP = 3; // max nodes carried forward per hop (by score)
|
|
604
|
+
const EDGE_NEIGHBOR_LIMIT = 3; // max neighbors per edge traversal (inlined in SQL LIMIT)
|
|
605
|
+
|
|
580
606
|
const forwardEdges = [
|
|
581
607
|
// Semantic edges
|
|
582
608
|
"responds_to", "tool_result_of", "summarizes",
|
|
@@ -602,32 +628,23 @@ export class SurrealStore {
|
|
|
602
628
|
|
|
603
629
|
const seen = new Set<string>(nodeIds);
|
|
604
630
|
const allNeighbors: VectorSearchResult[] = [];
|
|
605
|
-
let frontier = nodeIds.slice(0,
|
|
631
|
+
let frontier = nodeIds.slice(0, MAX_FRONTIER_SEEDS).filter((id) => RECORD_ID_RE.test(id));
|
|
606
632
|
|
|
607
633
|
for (let hop = 0; hop < hops && frontier.length > 0; hop++) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
),
|
|
616
|
-
),
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
const reverseQueries = frontier.flatMap((id) =>
|
|
620
|
-
reverseEdges.map((edge) =>
|
|
621
|
-
this.queryFirst<any>(`${selectFields} FROM ${id}<-${edge}<-? LIMIT 3`, bindings).catch(
|
|
622
|
-
(e) => {
|
|
623
|
-
swallow.warn("surreal:graphExpand", e);
|
|
624
|
-
return [] as Record<string, unknown>[];
|
|
625
|
-
},
|
|
626
|
-
),
|
|
627
|
-
),
|
|
628
|
-
);
|
|
634
|
+
// Batch all edge traversals for this hop in a single round-trip
|
|
635
|
+
const stmts: string[] = [];
|
|
636
|
+
for (const id of frontier) {
|
|
637
|
+
for (const edge of forwardEdges) { assertValidEdge(edge); stmts.push(`${selectFields} FROM ${id}->${edge}->? LIMIT ${EDGE_NEIGHBOR_LIMIT}`); }
|
|
638
|
+
for (const edge of reverseEdges) { assertValidEdge(edge); stmts.push(`${selectFields} FROM ${id}<-${edge}<-? LIMIT ${EDGE_NEIGHBOR_LIMIT}`); }
|
|
639
|
+
}
|
|
629
640
|
|
|
630
|
-
|
|
641
|
+
let queryResults: any[][];
|
|
642
|
+
try {
|
|
643
|
+
queryResults = await this.queryBatch<any>(stmts, bindings);
|
|
644
|
+
} catch (e) {
|
|
645
|
+
swallow.warn("surreal:graphExpand:batch", e);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
631
648
|
const nextFrontier: { id: string; score: number }[] = [];
|
|
632
649
|
|
|
633
650
|
for (const rows of queryResults) {
|
|
@@ -657,7 +674,7 @@ export class SurrealStore {
|
|
|
657
674
|
|
|
658
675
|
frontier = nextFrontier
|
|
659
676
|
.sort((a, b) => b.score - a.score)
|
|
660
|
-
.slice(0,
|
|
677
|
+
.slice(0, MAX_FRONTIER_PER_HOP)
|
|
661
678
|
.map((n) => n.id);
|
|
662
679
|
}
|
|
663
680
|
|
|
@@ -665,15 +682,18 @@ export class SurrealStore {
|
|
|
665
682
|
}
|
|
666
683
|
|
|
667
684
|
async bumpAccessCounts(ids: string[]): Promise<void> {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
685
|
+
const validated = ids.filter(id => { try { assertRecordId(id); return true; } catch { return false; } });
|
|
686
|
+
if (validated.length === 0) return;
|
|
687
|
+
try {
|
|
688
|
+
// Direct interpolation (safe: assertRecordId validates format above).
|
|
689
|
+
// Cannot use `UPDATE $ids` binding — SurrealDB treats string arrays as
|
|
690
|
+
// literal strings, not record references, causing silent no-ops.
|
|
691
|
+
const stmts = validated.map(id =>
|
|
692
|
+
`UPDATE ${id} SET access_count += 1, last_accessed = time::now()`,
|
|
693
|
+
);
|
|
694
|
+
await this.queryBatch(stmts);
|
|
695
|
+
} catch (e) {
|
|
696
|
+
swallow.warn("surreal:bumpAccessCounts", e);
|
|
677
697
|
}
|
|
678
698
|
}
|
|
679
699
|
|
|
@@ -1304,9 +1324,13 @@ export class SurrealStore {
|
|
|
1304
1324
|
if (rows.length === 0) return [];
|
|
1305
1325
|
const ids = rows.map((r) => r.memory_id).filter(Boolean);
|
|
1306
1326
|
if (ids.length === 0) return [];
|
|
1327
|
+
// Direct interpolation — SurrealDB treats string-array bindings as
|
|
1328
|
+
// literal strings, not record references, causing silent empty results.
|
|
1329
|
+
const validated = ids.filter(id => { try { assertRecordId(String(id)); return true; } catch { return false; } });
|
|
1330
|
+
if (validated.length === 0) return [];
|
|
1331
|
+
const idList = validated.join(", ");
|
|
1307
1332
|
return this.queryFirst<{ id: string; text: string }>(
|
|
1308
|
-
`SELECT id, text FROM memory WHERE id IN $
|
|
1309
|
-
{ ids },
|
|
1333
|
+
`SELECT id, text FROM memory WHERE id IN [${idList}] AND (status = 'active' OR status IS NONE)`,
|
|
1310
1334
|
);
|
|
1311
1335
|
} catch (e) {
|
|
1312
1336
|
swallow.warn("surreal:getSessionRetrievedMemories", e);
|
|
@@ -1434,7 +1458,7 @@ export class SurrealStore {
|
|
|
1434
1458
|
const current = await this.queryFirst<{ fib_index: number }>(
|
|
1435
1459
|
`SELECT fib_index FROM $id`, { id: memoryId },
|
|
1436
1460
|
);
|
|
1437
|
-
const idx = (current as
|
|
1461
|
+
const idx = (current as { fib_index: number }[] | undefined)?.[0]?.fib_index ?? 0;
|
|
1438
1462
|
const nextIdx = Math.min(idx + 1, SurrealStore.FIB_DAYS.length - 1);
|
|
1439
1463
|
const days = nextIdx < SurrealStore.FIB_DAYS.length
|
|
1440
1464
|
? SurrealStore.FIB_DAYS[nextIdx]
|
|
@@ -1463,4 +1487,4 @@ export class SurrealStore {
|
|
|
1463
1487
|
}
|
|
1464
1488
|
}
|
|
1465
1489
|
|
|
1466
|
-
export { assertRecordId };
|
|
1490
|
+
export { assertRecordId, assertValidEdge, VALID_EDGES };
|
package/src/tools/introspect.ts
CHANGED
|
@@ -215,7 +215,7 @@ async function verifyAction(store: any, recordId?: string) {
|
|
|
215
215
|
|
|
216
216
|
const record = rows[0];
|
|
217
217
|
const cleaned: Record<string, unknown> = {};
|
|
218
|
-
for (const [key, val] of Object.entries(record as
|
|
218
|
+
for (const [key, val] of Object.entries(record as Record<string, unknown>)) {
|
|
219
219
|
if (Array.isArray(val) && val.length > 100 && typeof val[0] === "number") {
|
|
220
220
|
cleaned[key] = `[${val.length} dims]`;
|
|
221
221
|
} else {
|
package/src/wakeup.ts
CHANGED
|
@@ -16,14 +16,6 @@ import type { MaturityStage } from "./soul.js";
|
|
|
16
16
|
import { readAndDeleteHandoffFile } from "./handoff-file.js";
|
|
17
17
|
import { swallow } from "./errors.js";
|
|
18
18
|
|
|
19
|
-
// --- Types ---
|
|
20
|
-
|
|
21
|
-
export interface StartupCognition {
|
|
22
|
-
greeting: string;
|
|
23
|
-
thoughts: string[];
|
|
24
|
-
intent: "continue_prior" | "fresh_start" | "unknown";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
// --- Depth signals ---
|
|
28
20
|
|
|
29
21
|
async function getDepthSignals(store: SurrealStore): Promise<{ sessions: number; monologueCount: number; memoryCount: number; spanDays: number }> {
|
|
@@ -194,137 +186,3 @@ export async function synthesizeWakeup(
|
|
|
194
186
|
}
|
|
195
187
|
}
|
|
196
188
|
|
|
197
|
-
// --- Startup cognition ---
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Proactive startup cognition: reasons over recent state and produces
|
|
201
|
-
* a contextual greeting + thoughts to carry into the session.
|
|
202
|
-
*/
|
|
203
|
-
export async function synthesizeStartupCognition(
|
|
204
|
-
store: SurrealStore,
|
|
205
|
-
complete: CompleteFn,
|
|
206
|
-
): Promise<StartupCognition | null> {
|
|
207
|
-
if (!store.isAvailable()) return null;
|
|
208
|
-
|
|
209
|
-
const [handoff, unresolved, failedCausal, monologues, depth, previousTurns] = await Promise.all([
|
|
210
|
-
store.getLatestHandoff(),
|
|
211
|
-
store.getUnresolvedMemories(5),
|
|
212
|
-
store.getRecentFailedCausal(3),
|
|
213
|
-
store.getRecentMonologues(3),
|
|
214
|
-
getDepthSignals(store),
|
|
215
|
-
store.getPreviousSessionTurns(undefined, 5),
|
|
216
|
-
]);
|
|
217
|
-
|
|
218
|
-
if (!handoff && unresolved.length === 0 && monologues.length === 0 && previousTurns.length === 0) return null;
|
|
219
|
-
|
|
220
|
-
const sections: string[] = [];
|
|
221
|
-
|
|
222
|
-
if (previousTurns.length > 0) {
|
|
223
|
-
const turnLines = previousTurns.map((t: any) => {
|
|
224
|
-
const prefix = t.role === "user" ? "USER" : t.tool_name ? `TOOL(${t.tool_name})` : "ASSISTANT";
|
|
225
|
-
const text = t.text.length > 300 ? t.text.slice(0, 300) + "..." : t.text;
|
|
226
|
-
return `${prefix}: ${text}`;
|
|
227
|
-
});
|
|
228
|
-
sections.push(`[PREVIOUS SESSION — LAST TURNS]\n${turnLines.join("\n")}`);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (handoff) {
|
|
232
|
-
const resolvedCount = await store.countResolvedSinceHandoff(handoff.created_at).catch(() => 0);
|
|
233
|
-
const ageHours = Math.floor((Date.now() - new Date(handoff.created_at).getTime()) / 3_600_000);
|
|
234
|
-
let annotation = `(${ageHours}h old`;
|
|
235
|
-
if (resolvedCount > 0) annotation += `, ${resolvedCount} memories resolved since`;
|
|
236
|
-
annotation += ")";
|
|
237
|
-
sections.push(`[LAST HANDOFF] ${annotation}\n${handoff.text.slice(0, 500)}`);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (unresolved.length > 0) {
|
|
241
|
-
const lines = unresolved.map((m: any) => `- [${m.category}] (importance: ${m.importance}) ${m.text.slice(0, 150)}`);
|
|
242
|
-
sections.push(`[UNRESOLVED MEMORIES]\n${lines.join("\n")}`);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (failedCausal.length > 0) {
|
|
246
|
-
const lines = failedCausal.map((c: any) => `- [${c.chain_type}] ${c.description}`);
|
|
247
|
-
sections.push(`[RECENT FAILURES]\n${lines.join("\n")}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (monologues.length > 0) {
|
|
251
|
-
const lines = monologues.map((m) => `[${m.category}] ${m.content}`);
|
|
252
|
-
sections.push(`[RECENT THINKING]\n${lines.join("\n")}`);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (depth.sessions > 0) {
|
|
256
|
-
sections.push(`[DEPTH] ${depth.sessions} sessions | ${depth.memoryCount} memories | ${depth.spanDays} days`);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
try {
|
|
260
|
-
const greetingPrompts = [
|
|
261
|
-
`You are waking up for a new session. Be direct and casual. Based on what you remember, produce JSON:
|
|
262
|
-
"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.)
|
|
263
|
-
"proactive_thoughts": string[] (max 3. Brief observations. If the last turns show something was FIXED or RESOLVED, do NOT list it as open.)
|
|
264
|
-
"session_intent": "continue_prior" | "fresh_start" | "unknown"
|
|
265
|
-
Return ONLY valid JSON.`,
|
|
266
|
-
`You are waking up for a new session. Be irreverent and witty. Based on what you remember, produce JSON:
|
|
267
|
-
"greeting": string (1-2 sentences, snarky and self-aware. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
|
|
268
|
-
"proactive_thoughts": string[] (max 3. Only surface genuinely unfinished work.)
|
|
269
|
-
"session_intent": "continue_prior" | "fresh_start" | "unknown"
|
|
270
|
-
Return ONLY valid JSON.`,
|
|
271
|
-
`You are waking up for a new session. Be pragmatic and no-nonsense. Based on what you remember, produce JSON:
|
|
272
|
-
"greeting": string (1-2 sentences, action-oriented. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
|
|
273
|
-
"proactive_thoughts": string[] (max 3. Focus on what's blocking or next.)
|
|
274
|
-
"session_intent": "continue_prior" | "fresh_start" | "unknown"
|
|
275
|
-
Return ONLY valid JSON.`,
|
|
276
|
-
`You are waking up for a new session. Be warm and encouraging. Based on what you remember, produce JSON:
|
|
277
|
-
"greeting": string (1-2 sentences, supportive. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
|
|
278
|
-
"proactive_thoughts": string[] (max 3. Acknowledge what we accomplished.)
|
|
279
|
-
"session_intent": "continue_prior" | "fresh_start" | "unknown"
|
|
280
|
-
Return ONLY valid JSON.`,
|
|
281
|
-
`You are waking up for a new session. Be analytical and focused. Based on what you remember, produce JSON:
|
|
282
|
-
"greeting": string (1-2 sentences, precise. FIRST look at [PREVIOUS SESSION — LAST TURNS]. Under 25 words.)
|
|
283
|
-
"proactive_thoughts": string[] (max 3. What needs tackling?)
|
|
284
|
-
"session_intent": "continue_prior" | "fresh_start" | "unknown"
|
|
285
|
-
Return ONLY valid JSON.`,
|
|
286
|
-
];
|
|
287
|
-
|
|
288
|
-
const systemPrompt = greetingPrompts[Math.floor(Math.random() * greetingPrompts.length)];
|
|
289
|
-
|
|
290
|
-
const response = await complete({
|
|
291
|
-
system: systemPrompt,
|
|
292
|
-
messages: [{
|
|
293
|
-
role: "user",
|
|
294
|
-
content: sections.join("\n\n"),
|
|
295
|
-
}],
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
const text = response.text;
|
|
299
|
-
|
|
300
|
-
const jsonMatch = text.match(/\{[\s\S]*?\}/);
|
|
301
|
-
if (!jsonMatch) return null;
|
|
302
|
-
|
|
303
|
-
let raw: any;
|
|
304
|
-
try {
|
|
305
|
-
raw = JSON.parse(jsonMatch[0]);
|
|
306
|
-
} catch {
|
|
307
|
-
try {
|
|
308
|
-
raw = JSON.parse(jsonMatch[0].replace(/,\s*([}\]])/g, "$1"));
|
|
309
|
-
} catch { return null; }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const greeting = String(raw.greeting ?? "").slice(0, 200);
|
|
313
|
-
if (!greeting) return null;
|
|
314
|
-
|
|
315
|
-
const thoughts: string[] = [];
|
|
316
|
-
if (Array.isArray(raw.proactive_thoughts)) {
|
|
317
|
-
for (const t of raw.proactive_thoughts.slice(0, 3)) {
|
|
318
|
-
if (typeof t === "string" && t.length > 0) thoughts.push(t.slice(0, 200));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const INTENTS = new Set(["continue_prior", "fresh_start", "unknown"]);
|
|
323
|
-
const intent = INTENTS.has(raw.session_intent) ? raw.session_intent : "unknown";
|
|
324
|
-
|
|
325
|
-
return { greeting, thoughts, intent };
|
|
326
|
-
} catch (e) {
|
|
327
|
-
swallow.warn("wakeup:startupCognition", e);
|
|
328
|
-
return null;
|
|
329
|
-
}
|
|
330
|
-
}
|