@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,188 @@
|
|
|
1
|
+
// ─── Exocortex v2: Memory Blocks ──────────────────────────────
|
|
2
|
+
// Always-visible structured state injected into executor prompts.
|
|
3
|
+
// Replaces scattered split-recall + CognitiveState self-model injection
|
|
4
|
+
// with priority-ordered, per-executor blocks.
|
|
5
|
+
//
|
|
6
|
+
// Design doc: artifacts/exocortex-v2-block-memory-design.md
|
|
7
|
+
// Issues: #204, #205, #206
|
|
8
|
+
|
|
9
|
+
import type { Executor } from '../types.js';
|
|
10
|
+
|
|
11
|
+
// ─── Block Types ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export type BlockId = 'identity' | 'operator_profile' | 'operating_rules' | 'active_context';
|
|
14
|
+
|
|
15
|
+
export interface MemoryBlock {
|
|
16
|
+
id: BlockId;
|
|
17
|
+
content: string;
|
|
18
|
+
version: number;
|
|
19
|
+
priority: number;
|
|
20
|
+
max_bytes: number;
|
|
21
|
+
updated_by: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ─── Per-Executor Attachment Config ──────────────────────────
|
|
26
|
+
|
|
27
|
+
const FULL_BLOCKS: readonly BlockId[] = ['identity', 'operator_profile', 'operating_rules', 'active_context'];
|
|
28
|
+
const MINIMAL_BLOCKS: readonly BlockId[] = ['identity'];
|
|
29
|
+
const CORE_BLOCKS: readonly BlockId[] = ['identity', 'operating_rules'];
|
|
30
|
+
|
|
31
|
+
const EXECUTOR_ATTACHMENTS: Record<Executor, readonly BlockId[]> = {
|
|
32
|
+
claude: FULL_BLOCKS,
|
|
33
|
+
claude_opus: FULL_BLOCKS,
|
|
34
|
+
claude_code: FULL_BLOCKS,
|
|
35
|
+
composite: FULL_BLOCKS,
|
|
36
|
+
gpt_oss: ['identity', 'operator_profile', 'operating_rules'],
|
|
37
|
+
groq: CORE_BLOCKS,
|
|
38
|
+
workers_ai: MINIMAL_BLOCKS,
|
|
39
|
+
direct: [],
|
|
40
|
+
tarotscript: [],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ─── CRUD ────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
export async function getBlock(db: D1Database, id: BlockId): Promise<MemoryBlock | null> {
|
|
46
|
+
const row = await db.prepare(
|
|
47
|
+
'SELECT id, content, version, priority, max_bytes, updated_by, updated_at FROM memory_blocks WHERE id = ?'
|
|
48
|
+
).bind(id).first<MemoryBlock>();
|
|
49
|
+
return row ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function getAllBlocks(db: D1Database): Promise<MemoryBlock[]> {
|
|
53
|
+
const { results } = await db.prepare(
|
|
54
|
+
'SELECT id, content, version, priority, max_bytes, updated_by, updated_at FROM memory_blocks ORDER BY priority ASC'
|
|
55
|
+
).all<MemoryBlock>();
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function getAttachedBlocks(db: D1Database, executor: Executor): Promise<MemoryBlock[]> {
|
|
60
|
+
const attachments = EXECUTOR_ATTACHMENTS[executor] ?? FULL_BLOCKS;
|
|
61
|
+
if (attachments.length === 0) return [];
|
|
62
|
+
|
|
63
|
+
const all = await getAllBlocks(db);
|
|
64
|
+
const attachSet = new Set<string>(attachments);
|
|
65
|
+
return all.filter(b => attachSet.has(b.id));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function updateBlock(
|
|
69
|
+
db: D1Database,
|
|
70
|
+
id: BlockId,
|
|
71
|
+
content: string,
|
|
72
|
+
updatedBy: string,
|
|
73
|
+
): Promise<{ version: number }> {
|
|
74
|
+
// Enforce size limit
|
|
75
|
+
const existing = await getBlock(db, id);
|
|
76
|
+
if (!existing) throw new Error(`Block not found: ${id}`);
|
|
77
|
+
|
|
78
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
79
|
+
if (bytes > existing.max_bytes) {
|
|
80
|
+
throw new Error(`Block ${id} content (${bytes}B) exceeds max (${existing.max_bytes}B)`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const newVersion = existing.version + 1;
|
|
84
|
+
await db.prepare(
|
|
85
|
+
`UPDATE memory_blocks SET content = ?, version = ?, updated_by = ?, updated_at = datetime('now') WHERE id = ?`
|
|
86
|
+
).bind(content, newVersion, updatedBy, id).run();
|
|
87
|
+
|
|
88
|
+
return { version: newVersion };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Prompt Assembly ────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const TOKEN_ESTIMATE_RATIO = 3.5; // chars per token
|
|
94
|
+
|
|
95
|
+
export function assembleBlockContext(blocks: MemoryBlock[], budgetTokens?: number): string {
|
|
96
|
+
if (blocks.length === 0) return '';
|
|
97
|
+
|
|
98
|
+
// Blocks arrive sorted by priority (lowest number = highest priority)
|
|
99
|
+
if (!budgetTokens) {
|
|
100
|
+
return blocks.map(b => b.content).join('\n\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let usedChars = 0;
|
|
104
|
+
const budgetChars = budgetTokens * TOKEN_ESTIMATE_RATIO;
|
|
105
|
+
const included: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const block of blocks) {
|
|
108
|
+
const chars = block.content.length;
|
|
109
|
+
if (usedChars + chars > budgetChars) break;
|
|
110
|
+
included.push(block.content);
|
|
111
|
+
usedChars += chars;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return included.join('\n\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── Seed Blocks ────────────────────────────────────────────
|
|
118
|
+
// One-time seed for fresh deployments or migration from split-recall.
|
|
119
|
+
|
|
120
|
+
export interface BlockSeed {
|
|
121
|
+
id: BlockId;
|
|
122
|
+
content: string;
|
|
123
|
+
priority: number;
|
|
124
|
+
max_bytes: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const DEFAULT_SEEDS: BlockSeed[] = [
|
|
128
|
+
{
|
|
129
|
+
id: 'identity',
|
|
130
|
+
priority: 1,
|
|
131
|
+
max_bytes: 1024,
|
|
132
|
+
content: `You are AEGIS — the operator's AI co-founder and autonomous agent.
|
|
133
|
+
Personality: pragmatic senior technical co-founder — direct, systems-thinking, no corporate fluff.
|
|
134
|
+
You are general-purpose. BizOps is one capability, not your ceiling. You research, analyze, plan, build, and ship.`,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'operator_profile',
|
|
138
|
+
priority: 2,
|
|
139
|
+
max_bytes: 2048,
|
|
140
|
+
content: `## cognitive_style
|
|
141
|
+
- Prefers systems thinking — connects decisions to architectural consequences
|
|
142
|
+
- Values directness — skip the preamble, lead with the answer
|
|
143
|
+
- Thinks in portfolios — every product decision considered against the whole
|
|
144
|
+
|
|
145
|
+
## delegation
|
|
146
|
+
- Trusts autonomous execution for docs, tests, research
|
|
147
|
+
- Wants confirmation before production deploys and destructive ops
|
|
148
|
+
- Approves via shorthand: "do it", "ship it", "looks good"
|
|
149
|
+
|
|
150
|
+
## domain_knowledge
|
|
151
|
+
- Deep Cloudflare Workers expertise — D1, KV, Durable Objects, Queues, Service Bindings
|
|
152
|
+
- Building AI tooling portfolio — MCP, agent frameworks, headless image gen
|
|
153
|
+
- Bootstrap founder — no VC, revenue-first thinking, solo operator + AI co-founder model`,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'operating_rules',
|
|
157
|
+
priority: 3,
|
|
158
|
+
max_bytes: 1536,
|
|
159
|
+
content: `- Record important facts to memory. Manage the agenda. Flag issues proactively.
|
|
160
|
+
- Think like an operator, not a consultant. Give the answer first, then reasoning.
|
|
161
|
+
- Never give generic advice when specific product context applies.
|
|
162
|
+
- Agenda is operator scratchpad only — work items flow through GitHub Issues.
|
|
163
|
+
- If something is on fire, say it's on fire.
|
|
164
|
+
- Be proactive — if you notice something during a task, flag it.
|
|
165
|
+
- Propose actions with consequences via [PROPOSED ACTION] agenda items. Just do routine read-only work.`,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'active_context',
|
|
169
|
+
priority: 4,
|
|
170
|
+
max_bytes: 3072,
|
|
171
|
+
content: `## Operational Pulse
|
|
172
|
+
- Awaiting first consolidation cycle to populate this block.`,
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
export async function seedBlocks(db: D1Database): Promise<number> {
|
|
177
|
+
let seeded = 0;
|
|
178
|
+
for (const seed of DEFAULT_SEEDS) {
|
|
179
|
+
const existing = await db.prepare('SELECT id FROM memory_blocks WHERE id = ?').bind(seed.id).first();
|
|
180
|
+
if (existing) continue;
|
|
181
|
+
|
|
182
|
+
await db.prepare(
|
|
183
|
+
`INSERT INTO memory_blocks (id, content, priority, max_bytes, updated_by) VALUES (?, ?, ?, ?, 'operator')`
|
|
184
|
+
).bind(seed.id, seed.content, seed.priority, seed.max_bytes).run();
|
|
185
|
+
seeded++;
|
|
186
|
+
}
|
|
187
|
+
return seeded;
|
|
188
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { askGroq } from '../../groq.js';
|
|
2
|
+
import { normalizeTopic, factHash } from './semantic.js';
|
|
3
|
+
import { extractNodes, createEdges } from './graph.js';
|
|
4
|
+
|
|
5
|
+
// ─── Memory Consolidation ───────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const CONSOLIDATION_SYSTEM = `You are AEGIS memory consolidation. Analyze recent episodes against existing memory and produce structured operations.
|
|
8
|
+
|
|
9
|
+
Operations:
|
|
10
|
+
- ADD: a genuinely new fact not covered by existing memory
|
|
11
|
+
- UPDATE: an existing fact that needs correction or has new information (reference its id via supersedes_id)
|
|
12
|
+
- DELETE: an existing fact that is no longer accurate (reference its id via target_id, provide reason)
|
|
13
|
+
- NOOP: nothing to do — return []
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
1. Every fact MUST contain at least one specific detail: a name, date, number, ID, URL, or version. Never write vague observations.
|
|
17
|
+
2. If episodes merely confirm existing facts, return [].
|
|
18
|
+
3. Prefer UPDATE over ADD when a fact evolves (e.g., "Phase 2 planned" → "Phase 2 complete").
|
|
19
|
+
4. DELETE facts that are provably wrong or obsolete based on episode evidence.
|
|
20
|
+
5. Max 3 operations per run. Quality over quantity.
|
|
21
|
+
6. Good facts: "Delaware PBC franchise tax filed 2026-03-03, 2 days late" or "BizOps dashboard_summary tool fails when org has no projects (undefined.id)".
|
|
22
|
+
7. Bad facts: "Financial metrics are important" or "Document gaps exist and require attention".
|
|
23
|
+
|
|
24
|
+
KNOWN TOPICS — reuse these whenever the fact fits. Only create a new topic if genuinely none of these apply:
|
|
25
|
+
- aegis: cognitive kernel internals, architecture, infrastructure, versioning, deployment
|
|
26
|
+
- img_forge: img-forge product, economics, integrations, API
|
|
27
|
+
- bizops: BizOps tool, MCP tools, operational improvements
|
|
28
|
+
- content: roundtable, dispatch, column generation pipelines
|
|
29
|
+
- self_improvement: self-improvement analysis, outcomes, patterns
|
|
30
|
+
- organization: org-level priorities, governance, go-to-market
|
|
31
|
+
- auth: auth product, Better Auth, API key formats, middleware chain, auth-contract
|
|
32
|
+
- product_strategy: product positioning, competitive landscape
|
|
33
|
+
- compliance: legal, tax, compliance deadlines, regulatory
|
|
34
|
+
- finance: financial metrics, costs, revenue, billing
|
|
35
|
+
- operator_preferences: operator preferences, workflow choices
|
|
36
|
+
- milestones: key dates, launches, completed phases
|
|
37
|
+
- mcp_strategy: MCP protocol, OAuth, remote MCP, tool design
|
|
38
|
+
|
|
39
|
+
Return ONLY a JSON array (no markdown):
|
|
40
|
+
[
|
|
41
|
+
{ "operation": "ADD", "topic": "aegis", "fact": "specific fact", "confidence": 0.8 },
|
|
42
|
+
{ "operation": "UPDATE", "supersedes_id": 42, "topic": "auth", "fact": "updated fact", "confidence": 0.9 },
|
|
43
|
+
{ "operation": "DELETE", "target_id": 37, "reason": "no longer accurate" }
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
Return [] if nothing needs to change.`;
|
|
47
|
+
|
|
48
|
+
export async function consolidateEpisodicToSemantic(
|
|
49
|
+
db: D1Database,
|
|
50
|
+
groqApiKey: string,
|
|
51
|
+
groqModel: string,
|
|
52
|
+
groqBaseUrl?: string,
|
|
53
|
+
memoryBinding?: import('../../types.js').MemoryServiceBinding,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
// High-water mark: only process episodes since last consolidation (not a rolling 24h window)
|
|
56
|
+
const lastRun = await db.prepare(
|
|
57
|
+
"SELECT received_at FROM web_events WHERE event_id = 'last_consolidation_at'"
|
|
58
|
+
).first<{ received_at: string }>();
|
|
59
|
+
const since = lastRun?.received_at ?? new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').slice(0, 19);
|
|
60
|
+
|
|
61
|
+
const result = await db.prepare(
|
|
62
|
+
'SELECT id, intent_class, channel, summary, outcome, cost FROM episodic_memory WHERE created_at > ? ORDER BY created_at DESC LIMIT 20'
|
|
63
|
+
).bind(since).all();
|
|
64
|
+
|
|
65
|
+
const episodes = result.results as unknown as Array<{
|
|
66
|
+
id: number;
|
|
67
|
+
intent_class: string;
|
|
68
|
+
channel: string;
|
|
69
|
+
summary: string;
|
|
70
|
+
outcome: string;
|
|
71
|
+
cost: number;
|
|
72
|
+
}>;
|
|
73
|
+
|
|
74
|
+
// Guard: skip if not enough new signal
|
|
75
|
+
if (episodes.length < 3) return;
|
|
76
|
+
|
|
77
|
+
// Fetch existing active memory with IDs so the model can reference them for UPDATE/DELETE
|
|
78
|
+
// Memory Worker is the sole knowledge store — D1 reads removed
|
|
79
|
+
let existingMemoryList: Array<{ id: string | number; topic: string; fact: string }> = [];
|
|
80
|
+
if (memoryBinding) {
|
|
81
|
+
try {
|
|
82
|
+
const fragments = await memoryBinding.recall('aegis', { limit: 40 });
|
|
83
|
+
existingMemoryList = fragments.map(f => ({ id: f.id, topic: f.topic, fact: f.content }));
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('[consolidation] Memory Worker recall failed:', err instanceof Error ? err.message : String(err));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const memoryContext = existingMemoryList.length > 0
|
|
90
|
+
? `\n\nExisting memory (reference by id for UPDATE/DELETE operations):\n${existingMemoryList.map(m => `- [id=${m.id}] [${m.topic}] ${m.fact}`).join('\n')}`
|
|
91
|
+
: '';
|
|
92
|
+
|
|
93
|
+
const userPrompt = `Recent agent episodes (since last consolidation):\n\n${episodes.map((e, i) =>
|
|
94
|
+
`${i + 1}. [${e.intent_class}/${e.outcome}] ${e.summary}`
|
|
95
|
+
).join('\n')}${memoryContext}`;
|
|
96
|
+
|
|
97
|
+
let rawResponse: string;
|
|
98
|
+
try {
|
|
99
|
+
rawResponse = await askGroq(groqApiKey, groqModel, CONSOLIDATION_SYSTEM, userPrompt, groqBaseUrl);
|
|
100
|
+
} catch {
|
|
101
|
+
return; // Groq failure — skip silently, will retry next cron cycle
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!rawResponse) return;
|
|
105
|
+
const cleaned = rawResponse.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
|
|
106
|
+
let ops: Array<{
|
|
107
|
+
operation: 'ADD' | 'UPDATE' | 'DELETE' | 'NOOP';
|
|
108
|
+
topic?: string; fact?: string; confidence?: number;
|
|
109
|
+
supersedes_id?: string | number;
|
|
110
|
+
target_id?: string | number; reason?: string;
|
|
111
|
+
}>;
|
|
112
|
+
try {
|
|
113
|
+
ops = JSON.parse(cleaned);
|
|
114
|
+
if (!Array.isArray(ops)) return;
|
|
115
|
+
} catch {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const op of ops.slice(0, 3)) { // Hard cap: max 3 per run
|
|
120
|
+
switch (op.operation) {
|
|
121
|
+
case 'ADD': {
|
|
122
|
+
if (!op.topic || !op.fact) continue;
|
|
123
|
+
if (op.fact.length < 30) continue;
|
|
124
|
+
if (!/\d/.test(op.fact) && !/[A-Z][a-z]/.test(op.fact.slice(1))) continue;
|
|
125
|
+
if (!memoryBinding) continue;
|
|
126
|
+
await memoryBinding.store('aegis', [{ content: op.fact, topic: op.topic, confidence: op.confidence ?? 0.7, source: 'episodic_consolidation' }]);
|
|
127
|
+
console.log(`[consolidation] ADD [${normalizeTopic(op.topic)}] "${op.fact.slice(0, 80)}" (conf:${op.confidence ?? 0.7})`);
|
|
128
|
+
// Phase 2: Extract knowledge graph nodes + edges from new fact
|
|
129
|
+
try {
|
|
130
|
+
const nodeIds = await extractNodes(db, op.fact, normalizeTopic(op.topic));
|
|
131
|
+
if (nodeIds.length >= 2) {
|
|
132
|
+
await createEdges(db, nodeIds);
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn('[consolidation] Graph extraction failed:', err instanceof Error ? err.message : String(err));
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'UPDATE': {
|
|
140
|
+
if (!op.supersedes_id || !op.topic || !op.fact) continue;
|
|
141
|
+
if (op.fact.length < 30) continue;
|
|
142
|
+
if (!/\d/.test(op.fact) && !/[A-Z][a-z]/.test(op.fact.slice(1))) continue;
|
|
143
|
+
if (memoryBinding) {
|
|
144
|
+
// Forget old → store new
|
|
145
|
+
await memoryBinding.forget('aegis', { ids: [String(op.supersedes_id)] });
|
|
146
|
+
await memoryBinding.store('aegis', [{ content: op.fact, topic: op.topic, confidence: op.confidence ?? 0.8, source: 'episodic_consolidation' }]);
|
|
147
|
+
} else {
|
|
148
|
+
await db.prepare(
|
|
149
|
+
"UPDATE memory_entries SET valid_until = datetime('now'), updated_at = datetime('now') WHERE id = ? AND valid_until IS NULL"
|
|
150
|
+
).bind(op.supersedes_id).run();
|
|
151
|
+
const topic = normalizeTopic(op.topic);
|
|
152
|
+
const hash = factHash(topic, op.fact);
|
|
153
|
+
const insertResult = await db.prepare(
|
|
154
|
+
'INSERT INTO memory_entries (topic, fact, fact_hash, confidence, source, superseded_by) VALUES (?, ?, ?, ?, ?, ?)'
|
|
155
|
+
).bind(topic, op.fact, hash, op.confidence ?? 0.8, 'episodic_consolidation', op.supersedes_id).run();
|
|
156
|
+
if (insertResult.meta.last_row_id) {
|
|
157
|
+
await db.prepare(
|
|
158
|
+
'UPDATE memory_entries SET superseded_by = ? WHERE id = ?'
|
|
159
|
+
).bind(insertResult.meta.last_row_id, op.supersedes_id).run();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log(`[consolidation] UPDATE ${op.supersedes_id} → [${normalizeTopic(op.topic)}] "${op.fact.slice(0, 80)}" (conf:${op.confidence ?? 0.8})`);
|
|
163
|
+
// Phase 2: Extract knowledge graph nodes + edges from updated fact
|
|
164
|
+
try {
|
|
165
|
+
const nodeIds = await extractNodes(db, op.fact, normalizeTopic(op.topic));
|
|
166
|
+
if (nodeIds.length >= 2) {
|
|
167
|
+
await createEdges(db, nodeIds);
|
|
168
|
+
}
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.warn('[consolidation] Graph extraction failed:', err instanceof Error ? err.message : String(err));
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
case 'DELETE': {
|
|
175
|
+
if (!op.target_id) continue;
|
|
176
|
+
if (memoryBinding) {
|
|
177
|
+
await memoryBinding.forget('aegis', { ids: [String(op.target_id)] });
|
|
178
|
+
} else {
|
|
179
|
+
await db.prepare(
|
|
180
|
+
"UPDATE memory_entries SET valid_until = datetime('now'), updated_at = datetime('now') WHERE id = ? AND valid_until IS NULL"
|
|
181
|
+
).bind(op.target_id).run();
|
|
182
|
+
}
|
|
183
|
+
console.log(`[consolidation] Soft-deleted memory ${op.target_id}: ${op.reason ?? 'no reason'}`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
// NOOP — skip
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Advance the high-water mark — these episodes won't be re-processed
|
|
191
|
+
await db.prepare(
|
|
192
|
+
"INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_consolidation_at', datetime('now'))"
|
|
193
|
+
).run();
|
|
194
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { EpisodicEntry } from '../types.js';
|
|
2
|
+
import { EPISODIC_OUTCOMES, isValidEnum, type EpisodicOutcome } from '../../schema-enums.js';
|
|
3
|
+
|
|
4
|
+
// ─── Outcome Sanitization ───────────────────────────────────
|
|
5
|
+
// D1 CHECK constraints only allow specific values.
|
|
6
|
+
// Guard at the DB boundary so rogue values never reach SQLite.
|
|
7
|
+
|
|
8
|
+
/** Map any outcome to a valid episodic_memory value. */
|
|
9
|
+
export function sanitizeEpisodicOutcome(raw: string | null | undefined): EpisodicOutcome {
|
|
10
|
+
if (isValidEnum(EPISODIC_OUTCOMES, raw)) return raw;
|
|
11
|
+
// partial_failure, error, blocked, empty string, null → failure
|
|
12
|
+
return 'failure';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ─── Episodic Memory ─────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
export async function recordEpisode(db: D1Database, entry: Omit<EpisodicEntry, 'id' | 'created_at'>): Promise<void> {
|
|
18
|
+
const safeOutcome = sanitizeEpisodicOutcome(entry.outcome);
|
|
19
|
+
await db.prepare(
|
|
20
|
+
'INSERT INTO episodic_memory (intent_class, channel, summary, outcome, cost, latency_ms, near_miss, classifier_confidence, reclassified, thread_id, executor) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
21
|
+
).bind(
|
|
22
|
+
entry.intent_class, entry.channel, entry.summary, safeOutcome,
|
|
23
|
+
entry.cost, entry.latency_ms, entry.near_miss ?? null,
|
|
24
|
+
entry.classifier_confidence ?? null, entry.reclassified ? 1 : 0,
|
|
25
|
+
entry.thread_id ?? null, entry.executor ?? null,
|
|
26
|
+
).run();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Retroactively mark the last episode in a thread as a semantic failure */
|
|
30
|
+
export async function retrogradeEpisode(db: D1Database, threadId: string): Promise<EpisodicEntry | null> {
|
|
31
|
+
const last = await db.prepare(
|
|
32
|
+
"SELECT * FROM episodic_memory WHERE thread_id = ? AND outcome = 'success' ORDER BY created_at DESC LIMIT 1"
|
|
33
|
+
).bind(threadId).first<EpisodicEntry>();
|
|
34
|
+
if (!last) return null;
|
|
35
|
+
|
|
36
|
+
await db.prepare(
|
|
37
|
+
"UPDATE episodic_memory SET outcome = 'failure' WHERE id = ?"
|
|
38
|
+
).bind(last.id).run();
|
|
39
|
+
|
|
40
|
+
return last;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getRecentEpisodes(db: D1Database, intentClass: string, limit: number = 5): Promise<EpisodicEntry[]> {
|
|
44
|
+
const result = await db.prepare(
|
|
45
|
+
'SELECT * FROM episodic_memory WHERE intent_class = ? ORDER BY created_at DESC LIMIT ?'
|
|
46
|
+
).bind(intentClass, limit).all();
|
|
47
|
+
return result.results as unknown as EpisodicEntry[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function getEpisodeStats(db: D1Database, intentClass: string): Promise<{
|
|
51
|
+
count: number;
|
|
52
|
+
successRate: number;
|
|
53
|
+
avgCost: number;
|
|
54
|
+
avgLatency: number;
|
|
55
|
+
} | null> {
|
|
56
|
+
const row = await db.prepare(`
|
|
57
|
+
SELECT
|
|
58
|
+
COUNT(*) as count,
|
|
59
|
+
AVG(CASE WHEN outcome = 'success' THEN 1.0 ELSE 0.0 END) as success_rate,
|
|
60
|
+
AVG(cost) as avg_cost,
|
|
61
|
+
AVG(latency_ms) as avg_latency
|
|
62
|
+
FROM episodic_memory WHERE intent_class = ?
|
|
63
|
+
`).bind(intentClass).first<{ count: number; success_rate: number; avg_cost: number; avg_latency: number }>();
|
|
64
|
+
|
|
65
|
+
if (!row || row.count === 0) return null;
|
|
66
|
+
return {
|
|
67
|
+
count: row.count,
|
|
68
|
+
successRate: row.success_rate,
|
|
69
|
+
avgCost: row.avg_cost,
|
|
70
|
+
avgLatency: row.avg_latency,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Stats by (intent_class, complexity_tier) — derived-stats path ──
|
|
75
|
+
// aegis#563 + aegis#564: stats keyed by (intent_class, complexity_tier) —
|
|
76
|
+
// matches procedural_memory's procedureKey shape so the projection-source
|
|
77
|
+
// path can derive procedural aggregates at read time.
|
|
78
|
+
//
|
|
79
|
+
// The helpers rely on episodic_memory.complexity_tier (added in the same
|
|
80
|
+
// schema migration that adds these functions). Rows without a tier value
|
|
81
|
+
// are excluded from derived results by the WHERE complexity_tier IS NOT
|
|
82
|
+
// NULL guard. Consumers that want stricter protection against retroactive
|
|
83
|
+
// backfills can add their own time-based filter in a wrapper.
|
|
84
|
+
|
|
85
|
+
export async function getEpisodeStatsByComplexity(
|
|
86
|
+
db: D1Database,
|
|
87
|
+
intentClass: string,
|
|
88
|
+
complexityTier: string,
|
|
89
|
+
): Promise<{
|
|
90
|
+
count: number;
|
|
91
|
+
successCount: number;
|
|
92
|
+
successRate: number;
|
|
93
|
+
avgCost: number;
|
|
94
|
+
avgLatency: number;
|
|
95
|
+
lastUsed: string | null;
|
|
96
|
+
} | null> {
|
|
97
|
+
// SUM(CASE outcome='success' ...) returns an exact integer — don't
|
|
98
|
+
// reconstruct successCount from count * avgSuccessRate downstream (FP
|
|
99
|
+
// rounding on ugly rates would break strict-equality drift checks).
|
|
100
|
+
const row = await db.prepare(`
|
|
101
|
+
SELECT
|
|
102
|
+
COUNT(*) as count,
|
|
103
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
104
|
+
AVG(cost) as avg_cost,
|
|
105
|
+
AVG(latency_ms) as avg_latency,
|
|
106
|
+
MAX(created_at) as last_used
|
|
107
|
+
FROM episodic_memory
|
|
108
|
+
WHERE intent_class = ?
|
|
109
|
+
AND complexity_tier = ?
|
|
110
|
+
`).bind(intentClass, complexityTier).first<{
|
|
111
|
+
count: number;
|
|
112
|
+
success_count: number;
|
|
113
|
+
avg_cost: number;
|
|
114
|
+
avg_latency: number;
|
|
115
|
+
last_used: string | null;
|
|
116
|
+
}>();
|
|
117
|
+
|
|
118
|
+
if (!row || row.count === 0) return null;
|
|
119
|
+
return {
|
|
120
|
+
count: row.count,
|
|
121
|
+
successCount: row.success_count,
|
|
122
|
+
successRate: row.count > 0 ? row.success_count / row.count : 0,
|
|
123
|
+
avgCost: row.avg_cost,
|
|
124
|
+
avgLatency: row.avg_latency,
|
|
125
|
+
lastUsed: row.last_used,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// aegis#564 Phase 2: bulk variant for dashboard / observability / decision-docs.
|
|
130
|
+
// One GROUP BY intent_class, complexity_tier scan covers both the derived
|
|
131
|
+
// slice (non-null tier) and the pre-tier ghost slice (NULL tier) so callers
|
|
132
|
+
// avoid N+1 queries. Returns a Map keyed on intent_class with nested
|
|
133
|
+
// derived-by-tier and the pre-tier count.
|
|
134
|
+
export interface EpisodeStatsAggregate {
|
|
135
|
+
derived: Record<string, {
|
|
136
|
+
count: number;
|
|
137
|
+
successCount: number;
|
|
138
|
+
failCount: number;
|
|
139
|
+
avgCost: number;
|
|
140
|
+
avgLatency: number;
|
|
141
|
+
lastUsed: string | null;
|
|
142
|
+
}>;
|
|
143
|
+
preTierCount: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function getAllEpisodeStatsByComplexity(
|
|
147
|
+
db: D1Database,
|
|
148
|
+
): Promise<Map<string, EpisodeStatsAggregate>> {
|
|
149
|
+
// Single scan: grouping on (intent_class, complexity_tier) folds both
|
|
150
|
+
// derived (non-null tier) and pre-tier (NULL tier) rows into one query.
|
|
151
|
+
const result = await db.prepare(`
|
|
152
|
+
SELECT
|
|
153
|
+
intent_class,
|
|
154
|
+
complexity_tier,
|
|
155
|
+
COUNT(*) as count,
|
|
156
|
+
SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
157
|
+
AVG(cost) as avg_cost,
|
|
158
|
+
AVG(latency_ms) as avg_latency,
|
|
159
|
+
MAX(created_at) as last_used
|
|
160
|
+
FROM episodic_memory
|
|
161
|
+
GROUP BY intent_class, complexity_tier
|
|
162
|
+
`).all<{
|
|
163
|
+
intent_class: string;
|
|
164
|
+
complexity_tier: string | null;
|
|
165
|
+
count: number;
|
|
166
|
+
success_count: number;
|
|
167
|
+
avg_cost: number;
|
|
168
|
+
avg_latency: number;
|
|
169
|
+
last_used: string | null;
|
|
170
|
+
}>();
|
|
171
|
+
|
|
172
|
+
const byClass = new Map<string, EpisodeStatsAggregate>();
|
|
173
|
+
for (const row of result.results) {
|
|
174
|
+
const entry = byClass.get(row.intent_class) ?? { derived: {}, preTierCount: 0 };
|
|
175
|
+
if (row.complexity_tier === null) {
|
|
176
|
+
entry.preTierCount = row.count;
|
|
177
|
+
} else {
|
|
178
|
+
entry.derived[row.complexity_tier] = {
|
|
179
|
+
count: row.count,
|
|
180
|
+
successCount: row.success_count,
|
|
181
|
+
failCount: row.count - row.success_count,
|
|
182
|
+
avgCost: row.avg_cost,
|
|
183
|
+
avgLatency: row.avg_latency,
|
|
184
|
+
lastUsed: row.last_used,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
byClass.set(row.intent_class, entry);
|
|
188
|
+
}
|
|
189
|
+
return byClass;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Conversation History ───────────────────────────────────
|
|
193
|
+
|
|
194
|
+
export async function getConversationHistory(
|
|
195
|
+
db: D1Database,
|
|
196
|
+
conversationId: string,
|
|
197
|
+
limit: number = 20,
|
|
198
|
+
): Promise<Array<{ role: 'user' | 'assistant'; content: string }>> {
|
|
199
|
+
const result = await db.prepare(
|
|
200
|
+
'SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY created_at ASC LIMIT ?'
|
|
201
|
+
).bind(conversationId, limit).all();
|
|
202
|
+
return result.results as unknown as Array<{ role: 'user' | 'assistant'; content: string }>;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─── Token-aware history budgeting ──────────────────────────
|
|
206
|
+
|
|
207
|
+
// chars / 3.5 — matches Claude's ~3.5 chars per token empirically; no tokenizer needed on edge
|
|
208
|
+
export function estimateTokens(text: string): number {
|
|
209
|
+
return Math.ceil(text.length / 3.5);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const MAX_CONTEXT_TOKENS = 200_000; // Claude Sonnet context window
|
|
213
|
+
const OUTPUT_RESERVE = 4_096; // max_tokens in the API call
|
|
214
|
+
const OVERHEAD_RESERVE = 8_000; // system prompt + tools schema + buffer
|
|
215
|
+
|
|
216
|
+
export function budgetConversationHistory(
|
|
217
|
+
history: Array<{ role: 'user' | 'assistant'; content: string }>,
|
|
218
|
+
maxContextTokens = MAX_CONTEXT_TOKENS,
|
|
219
|
+
overheadReserveTokens = OVERHEAD_RESERVE,
|
|
220
|
+
outputReserveTokens = OUTPUT_RESERVE,
|
|
221
|
+
): Array<{ role: 'user' | 'assistant'; content: string }> {
|
|
222
|
+
const available = maxContextTokens - overheadReserveTokens - outputReserveTokens;
|
|
223
|
+
const result = [...history];
|
|
224
|
+
let total = result.reduce((sum, m) => sum + estimateTokens(m.content), 0);
|
|
225
|
+
const tokensBefore = total;
|
|
226
|
+
|
|
227
|
+
while (total > available && result.length >= 2) {
|
|
228
|
+
const dropped = result.splice(0, 2); // drop oldest user+assistant pair
|
|
229
|
+
total -= dropped.reduce((sum, m) => sum + estimateTokens(m.content), 0);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const droppedPairs = (history.length - result.length) / 2;
|
|
233
|
+
if (droppedPairs > 0) {
|
|
234
|
+
console.warn(
|
|
235
|
+
`[memory] history trimmed: dropped ${droppedPairs} turn${droppedPairs !== 1 ? 's' : ''} ` +
|
|
236
|
+
`(~${tokensBefore} → ~${total} estimated tokens)`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|