@stackbilt/aegis-core 0.4.0 → 0.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackbilt/aegis-core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Persistent AI agent framework for Cloudflare Workers. Multi-tier memory, autonomous goals, dreaming cycles, MCP native.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -61,7 +61,8 @@
61
61
  "./contracts/cc-task": "./src/contracts/cc-task.contract.ts",
62
62
  "./contracts/memory-entry": "./src/contracts/memory-entry.contract.ts",
63
63
  "./wiki/client": "./src/wiki/client.ts",
64
- "./wiki/types": "./src/wiki/types.ts"
64
+ "./wiki/types": "./src/wiki/types.ts",
65
+ "./kernel/memory-service": "./src/kernel/memory-service.ts"
65
66
  },
66
67
  "scripts": {
67
68
  "dev": "wrangler dev",
package/schema.sql CHANGED
@@ -65,7 +65,9 @@ CREATE TABLE IF NOT EXISTS episodic_memory (
65
65
  reclassified INTEGER NOT NULL DEFAULT 0,
66
66
  thread_id TEXT, -- conversation thread for dreaming cycle
67
67
  executor TEXT, -- which executor handled this
68
+ court_card TEXT, -- composite court card (king/queen/knight/page)
68
69
  complexity_tier TEXT, -- aegis#563: procedureKey complement (low|mid|high); NULL for non-dispatcher producers
70
+ executor_config TEXT, -- aegis#563: config snapshot at emit time (evaluator-replay fidelity)
69
71
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
70
72
  );
71
73
 
@@ -111,6 +113,8 @@ CREATE TABLE IF NOT EXISTS procedural_memory (
111
113
  last_used TEXT,
112
114
  candidate_executor TEXT, -- probation: untrusted executor being tested
113
115
  candidate_successes INTEGER NOT NULL DEFAULT 0, -- consecutive successes of candidate
116
+ gap_signal_count INTEGER NOT NULL DEFAULT 0, -- aegis#497: unresolved grounding-gap signals; >0 triggers tier escalation
117
+ gap_last_seen TEXT,
114
118
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
115
119
  );
116
120
 
@@ -204,6 +208,7 @@ CREATE INDEX IF NOT EXISTS idx_memory_validation_stage ON memory_entries(validat
204
208
  CREATE INDEX IF NOT EXISTS idx_episodic_class ON episodic_memory(intent_class);
205
209
  CREATE INDEX IF NOT EXISTS idx_episodic_created ON episodic_memory(created_at);
206
210
  CREATE INDEX IF NOT EXISTS idx_episodic_thread ON episodic_memory(thread_id);
211
+ CREATE INDEX IF NOT EXISTS idx_episodic_class_complexity ON episodic_memory(intent_class, complexity_tier);
207
212
  CREATE INDEX IF NOT EXISTS idx_procedural_pattern ON procedural_memory(task_pattern);
208
213
  CREATE INDEX IF NOT EXISTS idx_procedural_status ON procedural_memory(status);
209
214
  CREATE INDEX IF NOT EXISTS idx_heartbeat_created ON heartbeat_results(created_at);
@@ -1,5 +1,5 @@
1
1
  export { recordEpisode, retrogradeEpisode, sanitizeEpisodicOutcome, getRecentEpisodes, getEpisodeStats, getEpisodeStatsByComplexity, getAllEpisodeStatsByComplexity, type EpisodeStatsAggregate, getConversationHistory, estimateTokens, budgetConversationHistory } from './episodic.js';
2
- export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, getProcedureWithDerivedStats, getAllProceduresWithDerivedStats, type DriftLogOpts, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, maintainProcedures } from './procedural.js';
2
+ export { PROCEDURE_MIN_SUCCESSES, PROCEDURE_MIN_SUCCESS_RATE, complexityTier, procedureKey, getProcedure, getAllProcedures, getProcedureWithDerivedStats, getAllProceduresWithDerivedStats, type DriftLogOpts, findNearMiss, upsertProcedure, addRefinement, degradeProcedure, recordGapSignal, clearGapSignal, maintainProcedures } from './procedural.js';
3
3
  export { normalizeTopic, tokenize, jaccardSimilarity, recordMemory, searchMemoryByKeywords, getMemoryEntries, recallMemory, computeEwaScore, getAllMemoryForContext } from './semantic.js';
4
4
  export { pruneMemory } from './pruning.js';
5
5
  export { consolidateEpisodicToSemantic } from './consolidation.js';
@@ -465,3 +465,37 @@ export async function maintainProcedures(db: D1Database): Promise<void> {
465
465
  console.log(`[procedures] Utility-pruned: ${proc.task_pattern} (${(rate * 100).toFixed(0)}% success over ${proc.success_count + proc.fail_count} invocations)`);
466
466
  }
467
467
  }
468
+
469
+ /** Increment gap signal for a procedure. Called when the response carried
470
+ * unverified_claims[] or unknowns[] on a grounding-gated classification. */
471
+ export async function recordGapSignal(db: D1Database, taskPattern: string): Promise<void> {
472
+ const procedure = await getProcedure(db, taskPattern);
473
+ if (!procedure) return;
474
+
475
+ const newCount = (procedure.gap_signal_count ?? 0) + 1;
476
+ await db.prepare(
477
+ "UPDATE procedural_memory SET gap_signal_count = ?, gap_last_seen = datetime('now') WHERE task_pattern = ?"
478
+ ).bind(newCount, taskPattern).run();
479
+
480
+ console.log(`[procedures] Gap signal recorded: ${taskPattern} (count=${newCount})`);
481
+ }
482
+
483
+ /** Decrement gap signal for a procedure (min 0). Called on clean grounded
484
+ * response. When the counter returns to 0, gap_last_seen is cleared. */
485
+ export async function clearGapSignal(db: D1Database, taskPattern: string): Promise<void> {
486
+ const procedure = await getProcedure(db, taskPattern);
487
+ if (!procedure) return;
488
+
489
+ const current = procedure.gap_signal_count ?? 0;
490
+ if (current <= 0) return;
491
+
492
+ const newCount = current - 1;
493
+ const newLastSeen = newCount === 0 ? null : procedure.gap_last_seen ?? null;
494
+ await db.prepare(
495
+ 'UPDATE procedural_memory SET gap_signal_count = ?, gap_last_seen = ? WHERE task_pattern = ?'
496
+ ).bind(newCount, newLastSeen, taskPattern).run();
497
+
498
+ if (newCount === 0) {
499
+ console.log(`[procedures] Gap signal cleared: ${taskPattern}`);
500
+ }
501
+ }
@@ -0,0 +1,252 @@
1
+ /**
2
+ * MemoryService — unified facade over the memory tiers (episodic,
3
+ * procedural, semantic, insights) + goals. Phase 1B of the god-object
4
+ * decomposition epic (aegis#532 / aegis#534).
5
+ *
6
+ * Why: the audit flagged that dispatch, self-improvement, ambient-capture
7
+ * each reach into memory/*.ts and memory-adapter.ts directly, repeating
8
+ * the `{db, memoryBinding}` threading and obscuring which tier each call
9
+ * actually hits. This service hands callers one object and hides the
10
+ * backing-store distinction.
11
+ *
12
+ * Tier → backing store:
13
+ * - episodic, procedural, insights, goals → D1 (env.db)
14
+ * - semantic, context-recall → memory-worker (env.memoryBinding) via
15
+ * memory-adapter (aegis-core re-export)
16
+ *
17
+ * Prefer this over the raw tier functions in new code. Existing callers
18
+ * migrate incrementally — Phase 2/3 decomps will naturally fold the rest
19
+ * as those files get broken up.
20
+ */
21
+ import {
22
+ recordEpisode as _recordEpisode,
23
+ retrogradeEpisode as _retrogradeEpisode,
24
+ getRecentEpisodes as _getRecentEpisodes,
25
+ getEpisodeStatsByComplexity as _getEpisodeStatsByComplexity,
26
+ upsertProcedure as _upsertProcedure,
27
+ getProcedure as _getProcedure,
28
+ getProcedureWithDerivedStats as _getProcedureWithDerivedStats,
29
+ getAllProceduresWithDerivedStats as _getAllProceduresWithDerivedStats,
30
+ type DriftLogOpts,
31
+ addRefinement as _addRefinement,
32
+ degradeProcedure as _degradeProcedure,
33
+ findNearMiss as _findNearMiss,
34
+ recordGapSignal as _recordGapSignal,
35
+ clearGapSignal as _clearGapSignal,
36
+ publishInsight as _publishInsight,
37
+ validateInsight as _validateInsight,
38
+ recordGoalAction as _recordGoalAction,
39
+ type InsightPayload,
40
+ type ValidationStage,
41
+ type AgentAction,
42
+ } from './memory/index.js';
43
+ import {
44
+ recordMemory as _recordMemoryViaAdapter,
45
+ searchMemoryByKeywords as _searchMemoryByKeywordsViaAdapter,
46
+ getAllMemoryForContext as _getAllMemoryForContextViaAdapter,
47
+ } from './memory-adapter.js';
48
+ import type { MemoryServiceBinding } from '../types.js';
49
+ import type { EpisodicEntry, ProceduralEntry, Refinement } from './types.js';
50
+
51
+ export type { ValidationStage, InsightPayload };
52
+
53
+ export interface DispatchOutcomeArgs {
54
+ episode: Omit<EpisodicEntry, 'id' | 'created_at'>;
55
+ procedureKey: string;
56
+ executor: string;
57
+ executorConfig: string;
58
+ outcome: 'success' | 'failure' | 'partial_failure';
59
+ latencyMs: number;
60
+ cost: number;
61
+ /** Optional refinement log — added when a replan promotes a degraded procedure. */
62
+ refinement?: Refinement;
63
+ }
64
+
65
+ export interface GoalActionOpts {
66
+ autoExecuted?: boolean;
67
+ authorityLevel?: string;
68
+ toolCalled?: string;
69
+ toolArgsJson?: string;
70
+ toolResultJson?: string;
71
+ }
72
+
73
+ export class MemoryService {
74
+ constructor(
75
+ private readonly db: D1Database,
76
+ private readonly memoryBinding?: MemoryServiceBinding,
77
+ ) {}
78
+
79
+ // ─── Episodic ──────────────────────────────────────────────
80
+
81
+ recordEpisode(entry: Omit<EpisodicEntry, 'id' | 'created_at'>): Promise<void> {
82
+ return _recordEpisode(this.db, entry);
83
+ }
84
+
85
+ retrogradeEpisode(threadId: string): Promise<EpisodicEntry | null> {
86
+ return _retrogradeEpisode(this.db, threadId);
87
+ }
88
+
89
+ getRecentEpisodes(intentClass: string, limit = 5): Promise<EpisodicEntry[]> {
90
+ return _getRecentEpisodes(this.db, intentClass, limit);
91
+ }
92
+
93
+ // aegis#563: aggregates keyed by (intent_class, complexity_tier), matching
94
+ // procedural_memory's procedureKey shape. Foundation for the #564
95
+ // projection-vs-cache decomposition.
96
+ getEpisodeStatsByComplexity(intentClass: string, complexityTier: string) {
97
+ return _getEpisodeStatsByComplexity(this.db, intentClass, complexityTier);
98
+ }
99
+
100
+ // ─── Procedural ────────────────────────────────────────────
101
+
102
+ upsertProcedure(
103
+ taskPattern: string,
104
+ executor: string,
105
+ executorConfig: string,
106
+ outcome: 'success' | 'failure' | 'partial_failure',
107
+ latencyMs: number,
108
+ cost: number,
109
+ ): Promise<void> {
110
+ return _upsertProcedure(this.db, taskPattern, executor, executorConfig, outcome, latencyMs, cost);
111
+ }
112
+
113
+ getProcedure(taskPattern: string): Promise<ProceduralEntry | null> {
114
+ return _getProcedure(this.db, taskPattern);
115
+ }
116
+
117
+ // aegis#564 Phase 1 — callers that read the five cached aggregate
118
+ // columns (success_count, fail_count, avg_latency_ms, avg_cost,
119
+ // last_used) should route through this helper. Phase 1 is a pass-
120
+ // through; Phase 2 adds shadow-read logging; Phase 3 flips to derived.
121
+ getProcedureWithDerivedStats(taskPattern: string, opts?: DriftLogOpts): Promise<ProceduralEntry | null> {
122
+ return _getProcedureWithDerivedStats(this.db, taskPattern, opts);
123
+ }
124
+
125
+ getAllProceduresWithDerivedStats(opts?: DriftLogOpts): Promise<ProceduralEntry[]> {
126
+ return _getAllProceduresWithDerivedStats(this.db, opts);
127
+ }
128
+
129
+ addRefinement(taskPattern: string, refinement: Refinement): Promise<void> {
130
+ return _addRefinement(this.db, taskPattern, refinement);
131
+ }
132
+
133
+ degradeProcedure(taskPattern: string, executor: string): Promise<void> {
134
+ return _degradeProcedure(this.db, taskPattern, executor);
135
+ }
136
+
137
+ findNearMiss(taskPattern: string) {
138
+ return _findNearMiss(this.db, taskPattern);
139
+ }
140
+
141
+ recordGapSignal(taskPattern: string): Promise<void> {
142
+ return _recordGapSignal(this.db, taskPattern);
143
+ }
144
+
145
+ clearGapSignal(taskPattern: string): Promise<void> {
146
+ return _clearGapSignal(this.db, taskPattern);
147
+ }
148
+
149
+ // ─── Semantic (memory-worker via adapter) ──────────────────
150
+
151
+ async recordMemory(
152
+ topic: string,
153
+ fact: string,
154
+ confidence: number,
155
+ source: string,
156
+ metadata?: Record<string, unknown>,
157
+ ) {
158
+ this.requireBinding('recordMemory');
159
+ return _recordMemoryViaAdapter(this.memoryBinding!, topic, fact, confidence, source, metadata);
160
+ }
161
+
162
+ async searchMemory(query: string, limit?: number) {
163
+ this.requireBinding('searchMemory');
164
+ return _searchMemoryByKeywordsViaAdapter(this.memoryBinding!, query, limit);
165
+ }
166
+
167
+ async getMemoryForContext(query?: string) {
168
+ this.requireBinding('getMemoryForContext');
169
+ return _getAllMemoryForContextViaAdapter(this.memoryBinding!, query);
170
+ }
171
+
172
+ // ─── Insights ──────────────────────────────────────────────
173
+
174
+ publishInsight(payload: InsightPayload) {
175
+ return _publishInsight(this.db, payload, this.memoryBinding);
176
+ }
177
+
178
+ validateInsight(factHash: string, validatingRepo: string, confirmed: boolean) {
179
+ return _validateInsight(this.db, factHash, validatingRepo, confirmed);
180
+ }
181
+
182
+ // ─── Goals ─────────────────────────────────────────────────
183
+
184
+ recordGoalAction(
185
+ goalId: string | null,
186
+ actionType: AgentAction['action_type'],
187
+ description: string,
188
+ outcome: AgentAction['outcome'] = 'success',
189
+ opts?: GoalActionOpts,
190
+ ): Promise<void> {
191
+ return _recordGoalAction(this.db, goalId, actionType, description, outcome, opts);
192
+ }
193
+
194
+ // ─── Domain: dispatch outcome bookkeeping ──────────────────
195
+
196
+ /**
197
+ * Record a dispatch outcome: episode + procedural upsert, plus an
198
+ * optional refinement log when a replan happens on a degraded procedure.
199
+ * Collapses the three-call pattern that dispatch.recordOutcome carries
200
+ * inline.
201
+ */
202
+ async recordDispatchOutcome(args: DispatchOutcomeArgs): Promise<void> {
203
+ // aegis#563: enrich episode with complexity_tier (derived from
204
+ // procedureKey = `classification:complexity_tier`) and executor_config
205
+ // snapshot. Caller-supplied values win; otherwise derive from args so
206
+ // dispatcher call sites don't need to pass these explicitly.
207
+ const tierFromKey = args.procedureKey.includes(':')
208
+ ? args.procedureKey.slice(args.procedureKey.indexOf(':') + 1)
209
+ : null;
210
+ const enrichedEpisode: Omit<EpisodicEntry, 'id' | 'created_at'> = {
211
+ ...args.episode,
212
+ complexity_tier: args.episode.complexity_tier ?? tierFromKey,
213
+ executor_config: args.episode.executor_config ?? args.executorConfig,
214
+ };
215
+
216
+ await _recordEpisode(this.db, enrichedEpisode);
217
+ await _upsertProcedure(
218
+ this.db,
219
+ args.procedureKey,
220
+ args.executor,
221
+ args.executorConfig,
222
+ args.outcome,
223
+ args.latencyMs,
224
+ args.cost,
225
+ );
226
+ if (args.refinement) {
227
+ await _addRefinement(this.db, args.procedureKey, args.refinement);
228
+ }
229
+ }
230
+
231
+ // ─── Internal ──────────────────────────────────────────────
232
+
233
+ private requireBinding(method: string): void {
234
+ if (!this.memoryBinding) {
235
+ throw new Error(
236
+ `[memory-service] ${method}() requires memoryBinding — constructor received none`,
237
+ );
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Factory: construct a MemoryService from any env-shape carrying
244
+ * `db` and (optionally) `memoryBinding`. The daemon's EdgeEnv satisfies
245
+ * this shape; scheduled tasks and tests can pass a narrower object.
246
+ */
247
+ export function memoryServiceFor(env: {
248
+ db: D1Database;
249
+ memoryBinding?: MemoryServiceBinding;
250
+ }): MemoryService {
251
+ return new MemoryService(env.db, env.memoryBinding);
252
+ }
@@ -38,6 +38,9 @@ export interface EpisodicEntry {
38
38
  reclassified?: boolean;
39
39
  thread_id?: string | null;
40
40
  executor?: string | null;
41
+ court_card?: string | null;
42
+ complexity_tier?: string | null;
43
+ executor_config?: string | null;
41
44
  created_at?: string;
42
45
  }
43
46
 
@@ -65,6 +68,9 @@ export interface ProceduralEntry {
65
68
  last_used?: string;
66
69
  candidate_executor?: string | null;
67
70
  candidate_successes?: number;
71
+ // aegis#497 — gap-driven tier escalation
72
+ gap_signal_count?: number;
73
+ gap_last_seen?: string | null;
68
74
  created_at?: string;
69
75
  }
70
76