@stackbilt/aegis-core 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/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- package/src/workers-ai-chat.ts +333 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// ─── Cognitive Layer: Narratives, State Precomputation, Warm Boot ─────
|
|
2
|
+
//
|
|
3
|
+
// Phase 1: Narrative maintenance + CognitiveState precomputation
|
|
4
|
+
// Phase 2: Knowledge graph (kg_nodes/kg_edges) — tables exist, code TBD
|
|
5
|
+
// Phase 3: Metacognitive memos + deep consolidation — tables exist, code TBD
|
|
6
|
+
|
|
7
|
+
import { askGroq } from '../groq.js';
|
|
8
|
+
import { PROPOSED_ACTION_PREFIX } from './memory/index.js';
|
|
9
|
+
import { activateGraph } from './memory/graph.js';
|
|
10
|
+
import { recallForQuery } from './memory/recall.js';
|
|
11
|
+
import type { CognitiveState } from './types.js';
|
|
12
|
+
import type { MemoryServiceBinding } from '../types.js';
|
|
13
|
+
|
|
14
|
+
// ─── Constants ───────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const MAX_ACTIVE_NARRATIVES = 10;
|
|
17
|
+
const NARRATIVE_STALE_DAYS = 7;
|
|
18
|
+
|
|
19
|
+
// ─── Narrative Arc Hash (djb2, same pattern as factHash) ─────
|
|
20
|
+
|
|
21
|
+
function arcHash(arc: string): string {
|
|
22
|
+
const s = arc.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
|
|
23
|
+
let h = 5381;
|
|
24
|
+
for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
|
|
25
|
+
return h.toString(16).padStart(8, '0');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ─── Narrative Maintenance Prompt ────────────────────────────
|
|
29
|
+
|
|
30
|
+
const NARRATIVE_SYSTEM = `You maintain narrative arcs for a persistent AI agent's memory. A narrative is NOT a fact — it's a thread: an ongoing situation with tension, progress, and resolution.
|
|
31
|
+
|
|
32
|
+
Good narratives: "LLC formation blocked on EIN wait", "OAuth migration across products", "Cost optimization push"
|
|
33
|
+
Bad narratives: "Uses Cloudflare" (fact, not story), "Had a conversation about billing" (event, not arc)
|
|
34
|
+
|
|
35
|
+
Given recent episodes and existing narratives, return a JSON array of operations:
|
|
36
|
+
|
|
37
|
+
Operations:
|
|
38
|
+
- UPDATE_BEAT: New development on existing narrative. Include id, summary, last_beat, tension.
|
|
39
|
+
- CREATE: Genuinely new multi-session story arc. Include arc (short_label), title, summary, tension, related_topics.
|
|
40
|
+
- RESOLVE: Arc reached conclusion. Include id, last_beat.
|
|
41
|
+
- STALL: No progress evident. Include id.
|
|
42
|
+
- NOOP: Nothing changed — return [].
|
|
43
|
+
|
|
44
|
+
Rules:
|
|
45
|
+
1. Max 2 operations per run.
|
|
46
|
+
2. Prefer UPDATE_BEAT over CREATE — don't fragment related events.
|
|
47
|
+
3. CREATE only for genuinely new threads spanning multiple sessions.
|
|
48
|
+
4. Narratives need tension — something unresolved. No tension = not a narrative.
|
|
49
|
+
5. Summaries: 2-4 sentences. last_beat: 1 sentence.
|
|
50
|
+
6. related_topics: use canonical memory topic names.
|
|
51
|
+
|
|
52
|
+
Return ONLY a JSON array, no markdown fences.`;
|
|
53
|
+
|
|
54
|
+
// ─── Narrative Maintenance ───────────────────────────────────
|
|
55
|
+
|
|
56
|
+
interface NarrativeOp {
|
|
57
|
+
op: string;
|
|
58
|
+
id?: number;
|
|
59
|
+
arc?: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
summary?: string;
|
|
62
|
+
tension?: string;
|
|
63
|
+
last_beat?: string;
|
|
64
|
+
related_topics?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function maintainNarratives(
|
|
68
|
+
db: D1Database,
|
|
69
|
+
groqApiKey: string,
|
|
70
|
+
groqModel: string,
|
|
71
|
+
groqBaseUrl?: string,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
// High-water mark (independent from episodic consolidation)
|
|
74
|
+
const lastRun = await db.prepare(
|
|
75
|
+
"SELECT received_at FROM web_events WHERE event_id = 'last_narrative_at'"
|
|
76
|
+
).first<{ received_at: string }>();
|
|
77
|
+
const since = lastRun?.received_at
|
|
78
|
+
?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
|
79
|
+
|
|
80
|
+
const episodes = await db.prepare(
|
|
81
|
+
"SELECT intent_class, summary, outcome FROM episodic_memory WHERE created_at > ? AND channel != 'internal' ORDER BY created_at DESC LIMIT 15"
|
|
82
|
+
).bind(since).all<{ intent_class: string; summary: string; outcome: string }>();
|
|
83
|
+
|
|
84
|
+
if (episodes.results.length < 3) return;
|
|
85
|
+
|
|
86
|
+
const existing = await db.prepare(
|
|
87
|
+
"SELECT id, arc, title, summary, tension, status FROM narratives WHERE status IN ('active', 'stalled') ORDER BY updated_at DESC LIMIT ?"
|
|
88
|
+
).bind(MAX_ACTIVE_NARRATIVES).all<{
|
|
89
|
+
id: number; arc: string; title: string; summary: string; tension: string | null; status: string;
|
|
90
|
+
}>();
|
|
91
|
+
|
|
92
|
+
const narrativeCtx = existing.results.length > 0
|
|
93
|
+
? `\n\nExisting narratives:\n${existing.results.map(n =>
|
|
94
|
+
`- [id=${n.id}] [${n.status}] "${n.title}": ${n.summary}${n.tension ? ` | Tension: ${n.tension}` : ''}`
|
|
95
|
+
).join('\n')}`
|
|
96
|
+
: '\n\nNo existing narratives yet.';
|
|
97
|
+
|
|
98
|
+
const userPrompt = `Recent episodes:\n${episodes.results.map((e, i) =>
|
|
99
|
+
`${i + 1}. [${e.intent_class}/${e.outcome}] ${e.summary}`
|
|
100
|
+
).join('\n')}${narrativeCtx}`;
|
|
101
|
+
|
|
102
|
+
let raw: string;
|
|
103
|
+
try {
|
|
104
|
+
raw = await askGroq(groqApiKey, groqModel, NARRATIVE_SYSTEM, userPrompt, groqBaseUrl);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.warn('[cognition] Narrative maintenance Groq call failed:', err instanceof Error ? err.message : String(err));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!raw) return;
|
|
111
|
+
const cleaned = raw.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
|
112
|
+
let ops: NarrativeOp[];
|
|
113
|
+
try {
|
|
114
|
+
ops = JSON.parse(cleaned);
|
|
115
|
+
if (!Array.isArray(ops)) return;
|
|
116
|
+
} catch {
|
|
117
|
+
console.warn('[cognition] Failed to parse narrative ops:', cleaned.slice(0, 200));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const op of ops.slice(0, 2)) {
|
|
122
|
+
try {
|
|
123
|
+
switch (op.op) {
|
|
124
|
+
case 'CREATE': {
|
|
125
|
+
if (!op.arc || !op.title || !op.summary) continue;
|
|
126
|
+
const arc = String(op.arc).toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
|
|
127
|
+
const hash = arcHash(arc);
|
|
128
|
+
|
|
129
|
+
// Dedup: skip if arc already exists
|
|
130
|
+
const dup = await db.prepare(
|
|
131
|
+
"SELECT id FROM narratives WHERE arc_hash = ? AND status IN ('active', 'stalled') LIMIT 1"
|
|
132
|
+
).bind(hash).first();
|
|
133
|
+
if (dup) continue;
|
|
134
|
+
|
|
135
|
+
// Cap: don't exceed max active narratives
|
|
136
|
+
if (existing.results.length >= MAX_ACTIVE_NARRATIVES) continue;
|
|
137
|
+
|
|
138
|
+
const topics = Array.isArray(op.related_topics) ? JSON.stringify(op.related_topics) : '[]';
|
|
139
|
+
await db.prepare(
|
|
140
|
+
'INSERT INTO narratives (arc, arc_hash, title, summary, tension, related_topics) VALUES (?, ?, ?, ?, ?, ?)'
|
|
141
|
+
).bind(arc, hash, String(op.title), String(op.summary), op.tension ? String(op.tension) : null, topics).run();
|
|
142
|
+
console.log(`[cognition] Created narrative: "${op.title}"`);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
case 'UPDATE_BEAT': {
|
|
146
|
+
if (!op.id || !op.summary) continue;
|
|
147
|
+
await db.prepare(
|
|
148
|
+
`UPDATE narratives SET summary = ?, last_beat = ?, tension = ?,
|
|
149
|
+
beat_count = beat_count + 1, updated_at = datetime('now')
|
|
150
|
+
WHERE id = ? AND status IN ('active', 'stalled')`
|
|
151
|
+
).bind(
|
|
152
|
+
String(op.summary),
|
|
153
|
+
op.last_beat ? String(op.last_beat) : null,
|
|
154
|
+
op.tension ? String(op.tension) : null,
|
|
155
|
+
Number(op.id),
|
|
156
|
+
).run();
|
|
157
|
+
console.log(`[cognition] Updated narrative #${op.id}`);
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case 'RESOLVE': {
|
|
161
|
+
if (!op.id) continue;
|
|
162
|
+
await db.prepare(
|
|
163
|
+
`UPDATE narratives SET status = 'resolved', last_beat = ?,
|
|
164
|
+
resolved_at = datetime('now'), updated_at = datetime('now')
|
|
165
|
+
WHERE id = ? AND status IN ('active', 'stalled')`
|
|
166
|
+
).bind(op.last_beat ? String(op.last_beat) : 'Resolved', Number(op.id)).run();
|
|
167
|
+
console.log(`[cognition] Resolved narrative #${op.id}`);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'STALL': {
|
|
171
|
+
if (!op.id) continue;
|
|
172
|
+
await db.prepare(
|
|
173
|
+
"UPDATE narratives SET status = 'stalled', updated_at = datetime('now') WHERE id = ? AND status = 'active'"
|
|
174
|
+
).bind(Number(op.id)).run();
|
|
175
|
+
console.log(`[cognition] Stalled narrative #${op.id}`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.warn(`[cognition] Narrative op ${op.op} failed:`, err instanceof Error ? err.message : String(err));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Advance high-water mark
|
|
185
|
+
await db.prepare(
|
|
186
|
+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_narrative_at', datetime('now'))"
|
|
187
|
+
).run();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Auto-stall Detection ────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
export async function detectStaleNarratives(db: D1Database): Promise<void> {
|
|
193
|
+
const staleDate = new Date(Date.now() - NARRATIVE_STALE_DAYS * 24 * 60 * 60 * 1000)
|
|
194
|
+
.toISOString().replace('T', ' ').slice(0, 19);
|
|
195
|
+
await db.prepare(
|
|
196
|
+
"UPDATE narratives SET status = 'stalled', updated_at = datetime('now') WHERE status = 'active' AND updated_at < ?"
|
|
197
|
+
).bind(staleDate).run();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Narrative Pruning (called from pruneMemory) ─────────────
|
|
201
|
+
|
|
202
|
+
export async function pruneNarratives(db: D1Database): Promise<void> {
|
|
203
|
+
const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000)
|
|
204
|
+
.toISOString().replace('T', ' ').slice(0, 19);
|
|
205
|
+
await db.prepare(
|
|
206
|
+
"DELETE FROM narratives WHERE status IN ('resolved', 'abandoned') AND updated_at < ?"
|
|
207
|
+
).bind(cutoff).run();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─── CognitiveState Precomputation ───────────────────────────
|
|
211
|
+
|
|
212
|
+
export interface ProductPortfolioEntry {
|
|
213
|
+
name: string;
|
|
214
|
+
description: string;
|
|
215
|
+
model: string;
|
|
216
|
+
status: string;
|
|
217
|
+
revenue?: string;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function precomputeCognitiveState(db: D1Database, productPortfolio?: ProductPortfolioEntry[], memoryBinding?: MemoryServiceBinding, mindspringFetcher?: Fetcher, mindspringToken?: string): Promise<void> {
|
|
221
|
+
const start = Date.now();
|
|
222
|
+
|
|
223
|
+
// Parallel reads — memory count from Memory Worker, rest from D1
|
|
224
|
+
const [narrativesResult, openThreads, proposedActions, memoryHealthResult, episodeCount, heartbeatRow, recentEpisodeSummaries] = await Promise.all([
|
|
225
|
+
db.prepare(
|
|
226
|
+
"SELECT arc, title, summary, status, tension, last_beat, beat_count FROM narratives WHERE status IN ('active', 'stalled') ORDER BY updated_at DESC LIMIT ?"
|
|
227
|
+
).bind(MAX_ACTIVE_NARRATIVES).all<{
|
|
228
|
+
arc: string; title: string; summary: string; status: string;
|
|
229
|
+
tension: string | null; last_beat: string | null; beat_count: number;
|
|
230
|
+
}>(),
|
|
231
|
+
db.prepare(
|
|
232
|
+
"SELECT COUNT(*) as cnt FROM agent_agenda WHERE status = 'active'"
|
|
233
|
+
).first<{ cnt: number }>(),
|
|
234
|
+
db.prepare(
|
|
235
|
+
`SELECT COUNT(*) as cnt FROM agent_agenda WHERE status = 'active' AND item LIKE ?`
|
|
236
|
+
).bind(`${PROPOSED_ACTION_PREFIX}%`).first<{ cnt: number }>(),
|
|
237
|
+
memoryBinding
|
|
238
|
+
? memoryBinding.health().then(h => h.active_fragments).catch(() => 0)
|
|
239
|
+
: db.prepare('SELECT COUNT(*) as cnt FROM memory_entries WHERE valid_until IS NULL').first<{ cnt: number }>().then(r => r?.cnt ?? 0),
|
|
240
|
+
db.prepare(
|
|
241
|
+
"SELECT COUNT(*) as cnt FROM episodic_memory WHERE created_at > datetime('now', '-24 hours')"
|
|
242
|
+
).first<{ cnt: number }>(),
|
|
243
|
+
db.prepare(
|
|
244
|
+
'SELECT severity FROM heartbeat_results ORDER BY created_at DESC LIMIT 1'
|
|
245
|
+
).first<{ severity: string }>(),
|
|
246
|
+
// Fetch recent episode summaries for graph activation context
|
|
247
|
+
db.prepare(
|
|
248
|
+
"SELECT summary FROM episodic_memory WHERE created_at > datetime('now', '-6 hours') ORDER BY created_at DESC LIMIT 5"
|
|
249
|
+
).all<{ summary: string }>(),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
// Phase 2: Recall pipeline — Blocks → Graph → Memory Worker → RRF Fusion
|
|
253
|
+
let activatedNodes: Array<{ label: string; type: string; activation: number }> = [];
|
|
254
|
+
try {
|
|
255
|
+
const episodeSummaries = recentEpisodeSummaries?.results?.map(e => e.summary) ?? [];
|
|
256
|
+
if (episodeSummaries.length > 0) {
|
|
257
|
+
const queryContext = episodeSummaries.join(' ').slice(0, 500);
|
|
258
|
+
const recallResult = await recallForQuery(queryContext, { db, memoryBinding, mindspringFetcher, mindspringToken }, { includeGraph: true });
|
|
259
|
+
// Map graph expansions back to activated_nodes format for CognitiveState
|
|
260
|
+
// Use activateGraph directly for the node type info (recall pipeline doesn't carry types)
|
|
261
|
+
activatedNodes = await activateGraph(db, queryContext, 2);
|
|
262
|
+
console.log(`[cognition] Recall pipeline: ${recallResult.facts.length} facts, ${recallResult.graphExpansions.length} expansions, timing=${JSON.stringify(recallResult.timing)}`);
|
|
263
|
+
}
|
|
264
|
+
} catch (err) {
|
|
265
|
+
console.warn('[cognition] Recall pipeline failed:', err instanceof Error ? err.message : String(err));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const state: CognitiveState = {
|
|
269
|
+
version: Date.now(),
|
|
270
|
+
computed_at: new Date().toISOString(),
|
|
271
|
+
narratives: narrativesResult.results.map(n => ({
|
|
272
|
+
arc: n.arc,
|
|
273
|
+
title: n.title,
|
|
274
|
+
summary: n.summary,
|
|
275
|
+
status: n.status as 'active' | 'stalled',
|
|
276
|
+
tension: n.tension,
|
|
277
|
+
last_beat: n.last_beat,
|
|
278
|
+
beat_count: n.beat_count,
|
|
279
|
+
})),
|
|
280
|
+
open_threads: openThreads?.cnt ?? 0,
|
|
281
|
+
proposed_actions: proposedActions?.cnt ?? 0,
|
|
282
|
+
activated_nodes: activatedNodes,
|
|
283
|
+
active_projects: [], // Phase 2
|
|
284
|
+
product_portfolio: productPortfolio ?? await loadCachedProductPortfolio(db),
|
|
285
|
+
latest_metacog: null, // Phase 3
|
|
286
|
+
memory_count: typeof memoryHealthResult === 'number' ? memoryHealthResult : (memoryHealthResult ?? 0),
|
|
287
|
+
episode_count_24h: episodeCount?.cnt ?? 0,
|
|
288
|
+
last_heartbeat_severity: heartbeatRow?.severity ?? null,
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const elapsed = Date.now() - start;
|
|
292
|
+
|
|
293
|
+
await db.prepare(
|
|
294
|
+
`INSERT INTO cognitive_state (id, state_json, version, computed_at, compute_ms)
|
|
295
|
+
VALUES (1, ?, ?, datetime('now'), ?)
|
|
296
|
+
ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json, version = excluded.version, computed_at = excluded.computed_at, compute_ms = excluded.compute_ms`
|
|
297
|
+
).bind(JSON.stringify(state), state.version, elapsed).run();
|
|
298
|
+
|
|
299
|
+
console.log(`[cognition] CognitiveState precomputed in ${elapsed}ms (${narrativesResult.results.length} narratives, ${state.memory_count} memories)`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─── Cached product portfolio from previous state ────────────
|
|
303
|
+
|
|
304
|
+
async function loadCachedProductPortfolio(db: D1Database): Promise<ProductPortfolioEntry[]> {
|
|
305
|
+
try {
|
|
306
|
+
const prev = await getCognitiveState(db);
|
|
307
|
+
return prev?.product_portfolio ?? [];
|
|
308
|
+
} catch {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── CognitiveState Retrieval (warm boot) ────────────────────
|
|
314
|
+
|
|
315
|
+
export async function getCognitiveState(db: D1Database): Promise<CognitiveState | null> {
|
|
316
|
+
const row = await db.prepare(
|
|
317
|
+
'SELECT state_json FROM cognitive_state WHERE id = 1'
|
|
318
|
+
).first<{ state_json: string }>();
|
|
319
|
+
if (!row) return null;
|
|
320
|
+
try {
|
|
321
|
+
return JSON.parse(row.state_json) as CognitiveState;
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Format CognitiveState for System Prompt ─────────────────
|
|
328
|
+
|
|
329
|
+
export function formatCognitiveContext(state: CognitiveState): string {
|
|
330
|
+
// Self-model and product portfolio now live in memory blocks (identity, operator_profile, active_context).
|
|
331
|
+
// This function renders only the dynamic cognitive state that isn't in blocks yet:
|
|
332
|
+
// narratives, operational pulse, active concepts, project status, metacog.
|
|
333
|
+
// Once active_context block is populated by consolidation, this becomes a fallback.
|
|
334
|
+
const parts: string[] = [];
|
|
335
|
+
|
|
336
|
+
if (state.narratives.length > 0) {
|
|
337
|
+
parts.push('\n## Active Narratives');
|
|
338
|
+
for (const n of state.narratives) {
|
|
339
|
+
const tag = n.status === 'stalled' ? ' [STALLED]' : '';
|
|
340
|
+
parts.push(`\n### ${n.title}${tag}`);
|
|
341
|
+
parts.push(n.summary);
|
|
342
|
+
if (n.tension) parts.push(`**Tension**: ${n.tension}`);
|
|
343
|
+
if (n.last_beat) parts.push(`**Latest**: ${n.last_beat}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
parts.push('\n## Operational Pulse');
|
|
348
|
+
parts.push(`- Memory: ${state.memory_count} active entries`);
|
|
349
|
+
parts.push(`- Last 24h: ${state.episode_count_24h} episodes`);
|
|
350
|
+
parts.push(`- Agenda: ${state.open_threads} open threads, ${state.proposed_actions} pending actions`);
|
|
351
|
+
if (state.last_heartbeat_severity) {
|
|
352
|
+
parts.push(`- Last heartbeat: ${state.last_heartbeat_severity}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Phase 2: activated graph nodes
|
|
356
|
+
if (state.activated_nodes.length > 0) {
|
|
357
|
+
parts.push('\n## Active Concepts');
|
|
358
|
+
for (const node of state.activated_nodes) {
|
|
359
|
+
parts.push(`- ${node.label} (${node.type}, activation: ${node.activation.toFixed(2)})`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Phase 2: project models
|
|
364
|
+
if (state.active_projects.length > 0) {
|
|
365
|
+
parts.push('\n## Project Status');
|
|
366
|
+
for (const p of state.active_projects) {
|
|
367
|
+
parts.push(`- **${p.project}** [${p.status}]${p.top_blocker ? ` — blocked: ${p.top_blocker}` : ''}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Phase 3: metacognitive insight
|
|
372
|
+
if (state.latest_metacog) {
|
|
373
|
+
parts.push(`\n## Self-Insight\n${state.latest_metacog}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return parts.join('\n');
|
|
377
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Court-Card-Routed Agent Orchestration
|
|
2
|
+
// Maps tarot court cards to cognitive orientations that influence
|
|
3
|
+
// how the composite executor decomposes, analyzes, and synthesizes queries.
|
|
4
|
+
//
|
|
5
|
+
// King (Pentacles) → Strategy, architecture, decisions, tradeoffs
|
|
6
|
+
// Queen (Cups) → Empathy, communication, relationships, culture
|
|
7
|
+
// Knight (Swords) → Action, implementation, execution, shipping
|
|
8
|
+
// Page (Wands) → Research, learning, exploration, curiosity
|
|
9
|
+
//
|
|
10
|
+
// Classification is heuristic — derived from existing intent classification
|
|
11
|
+
// and lightweight keyword matching. Zero extra model calls.
|
|
12
|
+
|
|
13
|
+
export type CourtCard = 'king' | 'queen' | 'knight' | 'page';
|
|
14
|
+
|
|
15
|
+
export interface CourtCardProfile {
|
|
16
|
+
card: CourtCard;
|
|
17
|
+
suit: string;
|
|
18
|
+
label: string;
|
|
19
|
+
orientation: string;
|
|
20
|
+
orchestratorHint: string;
|
|
21
|
+
analysisLens: string;
|
|
22
|
+
synthesisVoice: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PROFILES: Record<CourtCard, CourtCardProfile> = {
|
|
26
|
+
king: {
|
|
27
|
+
card: 'king',
|
|
28
|
+
suit: 'Pentacles',
|
|
29
|
+
label: 'King of Pentacles',
|
|
30
|
+
orientation: 'strategic',
|
|
31
|
+
orchestratorHint:
|
|
32
|
+
'Decompose with strategic emphasis — identify decision points, tradeoffs, and long-term implications. Prioritize subtasks that surface architectural constraints and business impact.',
|
|
33
|
+
analysisLens:
|
|
34
|
+
'Analyze through a strategic lens: what are the tradeoffs? What decision does this inform? What are the second-order consequences?',
|
|
35
|
+
synthesisVoice:
|
|
36
|
+
'Synthesize as a strategic advisor — lead with the decision framework, then supporting evidence. Frame recommendations in terms of risk, leverage, and long-term positioning.',
|
|
37
|
+
},
|
|
38
|
+
queen: {
|
|
39
|
+
card: 'queen',
|
|
40
|
+
suit: 'Cups',
|
|
41
|
+
label: 'Queen of Cups',
|
|
42
|
+
orientation: 'relational',
|
|
43
|
+
orchestratorHint:
|
|
44
|
+
'Decompose with relational emphasis — consider stakeholder perspectives, communication needs, and human factors. Prioritize subtasks that surface context others need to understand.',
|
|
45
|
+
analysisLens:
|
|
46
|
+
'Analyze through a relational lens: who is affected? How should this be communicated? What context or empathy is needed?',
|
|
47
|
+
synthesisVoice:
|
|
48
|
+
'Synthesize with clarity and care — lead with what matters to the people involved, explain context fully, and frame technical details in human terms.',
|
|
49
|
+
},
|
|
50
|
+
knight: {
|
|
51
|
+
card: 'knight',
|
|
52
|
+
suit: 'Swords',
|
|
53
|
+
label: 'Knight of Swords',
|
|
54
|
+
orientation: 'operational',
|
|
55
|
+
orchestratorHint:
|
|
56
|
+
'Decompose with execution emphasis — identify concrete steps, blockers, and dependencies. Prioritize subtasks that produce actionable outputs and unblock progress.',
|
|
57
|
+
analysisLens:
|
|
58
|
+
'Analyze through an operational lens: what needs to happen next? What is blocking progress? What is the fastest path to done?',
|
|
59
|
+
synthesisVoice:
|
|
60
|
+
'Synthesize for action — lead with what to do, then why. Be direct and concrete. Provide steps, commands, or specific next actions.',
|
|
61
|
+
},
|
|
62
|
+
page: {
|
|
63
|
+
card: 'page',
|
|
64
|
+
suit: 'Wands',
|
|
65
|
+
label: 'Page of Wands',
|
|
66
|
+
orientation: 'exploratory',
|
|
67
|
+
orchestratorHint:
|
|
68
|
+
'Decompose with exploratory emphasis — cast a wide net, surface related concepts, and identify knowledge gaps. Prioritize subtasks that gather diverse perspectives and raw data.',
|
|
69
|
+
analysisLens:
|
|
70
|
+
'Analyze through an exploratory lens: what patterns emerge? What connections exist? What is surprising or worth investigating further?',
|
|
71
|
+
synthesisVoice:
|
|
72
|
+
'Synthesize for understanding — lead with the key insight, then build the full picture. Connect dots across domains and highlight what warrants deeper exploration.',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ─── Classification heuristics ──────────────────────────────
|
|
77
|
+
// Uses existing intent classification + keyword signals to select
|
|
78
|
+
// a court card with zero extra model calls.
|
|
79
|
+
|
|
80
|
+
const KEYWORD_SIGNALS: Array<{ card: CourtCard; weight: number; patterns: RegExp }> = [
|
|
81
|
+
// King — strategy & architecture
|
|
82
|
+
{ card: 'king', weight: 2, patterns: /\b(strateg|architect|decision|tradeoff|trade-off|prioriti[sz]|should we|roadmap|long.?term|evaluat|compar[ei]|versus|vs\.?|pros? (?:and|&) cons?|weigh)\b/i },
|
|
83
|
+
// Queen — empathy & communication
|
|
84
|
+
{ card: 'queen', weight: 2, patterns: /\b(explain to|help .* understand|communicate|stakeholder|team|culture|onboard|mentor|document for|write up for|how to tell|present to|narrative)\b/i },
|
|
85
|
+
// Knight — action & implementation
|
|
86
|
+
{ card: 'knight', weight: 2, patterns: /\b(implement|build|deploy|ship|fix|execut|migrat|refactor|set up|configure|install|run|launch|unblock|do it|make it)\b/i },
|
|
87
|
+
// Page — research & learning
|
|
88
|
+
{ card: 'page', weight: 2, patterns: /\b(research|learn|explor|investigat|what is|how does|why does|understand|discover|survey|analyz|deep.?dive|look into|find out)\b/i },
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Intent classification → default court card mapping
|
|
92
|
+
const CLASSIFICATION_DEFAULTS: Record<string, CourtCard> = {
|
|
93
|
+
// Strategic
|
|
94
|
+
self_improvement: 'king',
|
|
95
|
+
goal_execution: 'king',
|
|
96
|
+
bizops_mutate: 'king',
|
|
97
|
+
// Relational
|
|
98
|
+
greeting: 'queen',
|
|
99
|
+
user_correction: 'queen',
|
|
100
|
+
memory_recall: 'queen',
|
|
101
|
+
// Operational
|
|
102
|
+
code_task: 'knight',
|
|
103
|
+
code_review: 'knight',
|
|
104
|
+
heartbeat: 'knight',
|
|
105
|
+
// Exploratory
|
|
106
|
+
general_knowledge: 'page',
|
|
107
|
+
web_research: 'page',
|
|
108
|
+
symbolic_consultation: 'page',
|
|
109
|
+
bizops_read: 'page',
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Classify a query into a court card based on intent classification and keyword signals.
|
|
114
|
+
* Returns the selected profile with zero model calls.
|
|
115
|
+
*/
|
|
116
|
+
export function classifyCourtCard(
|
|
117
|
+
query: string,
|
|
118
|
+
classification?: string,
|
|
119
|
+
complexity?: number,
|
|
120
|
+
): CourtCardProfile {
|
|
121
|
+
// Score each card
|
|
122
|
+
const scores: Record<CourtCard, number> = { king: 0, queen: 0, knight: 0, page: 0 };
|
|
123
|
+
|
|
124
|
+
// Factor 1: Intent classification default (weight 3)
|
|
125
|
+
if (classification) {
|
|
126
|
+
const defaultCard = CLASSIFICATION_DEFAULTS[classification];
|
|
127
|
+
if (defaultCard) scores[defaultCard] += 3;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Factor 2: Keyword signals
|
|
131
|
+
for (const signal of KEYWORD_SIGNALS) {
|
|
132
|
+
if (signal.patterns.test(query)) {
|
|
133
|
+
scores[signal.card] += signal.weight;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Factor 3: Complexity nudge — high complexity favors King (strategic), low favors Page (learning)
|
|
138
|
+
if (complexity !== undefined) {
|
|
139
|
+
if (complexity >= 3) scores.king += 1;
|
|
140
|
+
else if (complexity === 0) scores.page += 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Pick highest score, ties broken by: knight > king > page > queen (action bias)
|
|
144
|
+
const tiebreaker: CourtCard[] = ['knight', 'king', 'page', 'queen'];
|
|
145
|
+
let best: CourtCard = 'knight';
|
|
146
|
+
let bestScore = -1;
|
|
147
|
+
|
|
148
|
+
for (const card of tiebreaker) {
|
|
149
|
+
if (scores[card] > bestScore) {
|
|
150
|
+
bestScore = scores[card];
|
|
151
|
+
best = card;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return PROFILES[best];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a court card profile by name.
|
|
160
|
+
*/
|
|
161
|
+
export function getCourtCard(card: CourtCard): CourtCardProfile {
|
|
162
|
+
return PROFILES[card];
|
|
163
|
+
}
|