agent-working-memory 0.5.4 → 0.5.6

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.
Files changed (71) hide show
  1. package/README.md +87 -46
  2. package/dist/api/routes.d.ts.map +1 -1
  3. package/dist/api/routes.js +21 -5
  4. package/dist/api/routes.js.map +1 -1
  5. package/dist/cli.js +67 -67
  6. package/dist/coordination/index.d.ts +11 -0
  7. package/dist/coordination/index.d.ts.map +1 -0
  8. package/dist/coordination/index.js +39 -0
  9. package/dist/coordination/index.js.map +1 -0
  10. package/dist/coordination/mcp-tools.d.ts +8 -0
  11. package/dist/coordination/mcp-tools.d.ts.map +1 -0
  12. package/dist/coordination/mcp-tools.js +216 -0
  13. package/dist/coordination/mcp-tools.js.map +1 -0
  14. package/dist/coordination/routes.d.ts +9 -0
  15. package/dist/coordination/routes.d.ts.map +1 -0
  16. package/dist/coordination/routes.js +434 -0
  17. package/dist/coordination/routes.js.map +1 -0
  18. package/dist/coordination/schema.d.ts +12 -0
  19. package/dist/coordination/schema.d.ts.map +1 -0
  20. package/dist/coordination/schema.js +91 -0
  21. package/dist/coordination/schema.js.map +1 -0
  22. package/dist/coordination/schemas.d.ts +208 -0
  23. package/dist/coordination/schemas.d.ts.map +1 -0
  24. package/dist/coordination/schemas.js +109 -0
  25. package/dist/coordination/schemas.js.map +1 -0
  26. package/dist/coordination/stale.d.ts +25 -0
  27. package/dist/coordination/stale.d.ts.map +1 -0
  28. package/dist/coordination/stale.js +53 -0
  29. package/dist/coordination/stale.js.map +1 -0
  30. package/dist/index.js +21 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/mcp.js +90 -79
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/storage/sqlite.d.ts +3 -0
  35. package/dist/storage/sqlite.d.ts.map +1 -1
  36. package/dist/storage/sqlite.js +285 -281
  37. package/dist/storage/sqlite.js.map +1 -1
  38. package/package.json +55 -55
  39. package/src/api/index.ts +3 -3
  40. package/src/api/routes.ts +551 -536
  41. package/src/cli.ts +397 -397
  42. package/src/coordination/index.ts +47 -0
  43. package/src/coordination/mcp-tools.ts +313 -0
  44. package/src/coordination/routes.ts +656 -0
  45. package/src/coordination/schema.ts +94 -0
  46. package/src/coordination/schemas.ts +136 -0
  47. package/src/coordination/stale.ts +89 -0
  48. package/src/core/decay.ts +63 -63
  49. package/src/core/embeddings.ts +88 -88
  50. package/src/core/hebbian.ts +93 -93
  51. package/src/core/index.ts +5 -5
  52. package/src/core/logger.ts +36 -36
  53. package/src/core/query-expander.ts +66 -66
  54. package/src/core/reranker.ts +101 -101
  55. package/src/engine/activation.ts +656 -656
  56. package/src/engine/connections.ts +103 -103
  57. package/src/engine/consolidation-scheduler.ts +125 -125
  58. package/src/engine/eval.ts +102 -102
  59. package/src/engine/eviction.ts +101 -101
  60. package/src/engine/index.ts +8 -8
  61. package/src/engine/retraction.ts +100 -100
  62. package/src/engine/staging.ts +74 -74
  63. package/src/index.ts +137 -121
  64. package/src/mcp.ts +1024 -1013
  65. package/src/storage/index.ts +3 -3
  66. package/src/storage/sqlite.ts +968 -963
  67. package/src/types/agent.ts +67 -67
  68. package/src/types/checkpoint.ts +46 -46
  69. package/src/types/engram.ts +217 -217
  70. package/src/types/eval.ts +100 -100
  71. package/src/types/index.ts +6 -6
package/src/api/routes.ts CHANGED
@@ -1,536 +1,551 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * API Routes — the black box interface agents interact with.
5
- *
6
- * Core (agent-facing):
7
- * POST /memory/write — write a memory (salience filter decides disposition)
8
- * POST /memory/activate — retrieve by context activation
9
- * POST /memory/feedback — report whether a memory was useful
10
- * POST /memory/retract — invalidate a wrong memory
11
- *
12
- * Checkpointing:
13
- * POST /memory/checkpoint — save explicit execution state
14
- * GET /memory/restore/:agentId — restore state + targeted recall + async mini-consolidation
15
- *
16
- * Task management:
17
- * POST /task/create — create a prioritized task
18
- * POST /task/update — update status, priority, or blocking
19
- * GET /task/list/:agentId — list tasks (filtered by status)
20
- * GET /task/next/:agentId — get highest-priority actionable task
21
- *
22
- * Diagnostic (debugging/eval):
23
- * POST /memory/search — deterministic search (not cognitive)
24
- * GET /memory/:id — get a specific engram
25
- * GET /agent/:id/stats — memory stats for an agent
26
- * GET /agent/:id/metrics — eval metrics
27
- * POST /agent/register — register a new agent
28
- *
29
- * System:
30
- * POST /system/evict — trigger eviction check
31
- * POST /system/decay — trigger edge decay
32
- * POST /system/consolidate — run sleep cycle (strengthen, decay, sweep)
33
- * GET /health — health check
34
- */
35
-
36
- import type { FastifyInstance } from 'fastify';
37
- import type { EngramStore } from '../storage/sqlite.js';
38
- import type { ActivationEngine } from '../engine/activation.js';
39
- import type { ConnectionEngine } from '../engine/connections.js';
40
- import type { EvictionEngine } from '../engine/eviction.js';
41
- import type { RetractionEngine } from '../engine/retraction.js';
42
- import type { EvalEngine } from '../engine/eval.js';
43
- import type { ConsolidationEngine } from '../engine/consolidation.js';
44
- import type { ConsolidationScheduler } from '../engine/consolidation-scheduler.js';
45
- import { evaluateSalience, computeNovelty } from '../core/salience.js';
46
- import type { SalienceEventType } from '../core/salience.js';
47
- import type { TaskStatus, TaskPriority } from '../types/engram.js';
48
- import type { ConsciousState } from '../types/checkpoint.js';
49
- import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
50
- import { embed } from '../core/embeddings.js';
51
-
52
- export interface MemoryDeps {
53
- store: EngramStore;
54
- activationEngine: ActivationEngine;
55
- connectionEngine: ConnectionEngine;
56
- evictionEngine: EvictionEngine;
57
- retractionEngine: RetractionEngine;
58
- evalEngine: EvalEngine;
59
- consolidationEngine: ConsolidationEngine;
60
- consolidationScheduler: ConsolidationScheduler;
61
- }
62
-
63
- export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
64
- const { store, activationEngine, connectionEngine, evictionEngine, retractionEngine, evalEngine, consolidationEngine, consolidationScheduler } = deps;
65
-
66
- // ============================================================
67
- // CORE — Agent-facing endpoints
68
- // ============================================================
69
-
70
- app.post('/memory/write', async (req, reply) => {
71
- const body = req.body as {
72
- agentId: string;
73
- concept: string;
74
- content: string;
75
- tags?: string[];
76
- eventType?: SalienceEventType;
77
- surprise?: number;
78
- decisionMade?: boolean;
79
- causalDepth?: number;
80
- resolutionEffort?: number;
81
- confidence?: number;
82
- };
83
-
84
- if (!body.agentId || typeof body.agentId !== 'string' ||
85
- !body.concept || typeof body.concept !== 'string' ||
86
- !body.content || typeof body.content !== 'string') {
87
- return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
88
- }
89
-
90
- const novelty = computeNovelty(store, body.agentId, body.concept, body.content);
91
-
92
- const salience = evaluateSalience({
93
- content: body.content,
94
- eventType: body.eventType,
95
- surprise: body.surprise,
96
- decisionMade: body.decisionMade,
97
- causalDepth: body.causalDepth,
98
- resolutionEffort: body.resolutionEffort,
99
- novelty,
100
- });
101
-
102
- // v0.5.4: No longer discard — store with low confidence for ranking.
103
- const isLowSalience = salience.disposition === 'discard';
104
- const confidence = isLowSalience
105
- ? 0.25
106
- : body.confidence ?? (salience.disposition === 'staging' ? 0.40 : 0.50);
107
-
108
- const engram = store.createEngram({
109
- agentId: body.agentId,
110
- concept: body.concept,
111
- content: body.content,
112
- tags: isLowSalience ? [...(body.tags ?? []), 'low-salience'] : body.tags,
113
- salience: salience.score,
114
- confidence,
115
- salienceFeatures: salience.features,
116
- reasonCodes: salience.reasonCodes,
117
- ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
118
- });
119
-
120
- if (salience.disposition === 'staging') {
121
- store.updateStage(engram.id, 'staging');
122
- }
123
-
124
- // Create temporal adjacency edge to previous memory (conversation thread graph)
125
- // This enables multi-hop graph walk through conversation sequences
126
- try {
127
- const prev = store.getLatestEngram(body.agentId, engram.id);
128
- if (prev) {
129
- store.upsertAssociation(prev.id, engram.id, 0.3, 'temporal', 0.8);
130
- }
131
- } catch { /* Temporal edge creation is non-fatal */ }
132
-
133
- if (salience.disposition === 'active' || isLowSalience) {
134
- connectionEngine.enqueue(engram.id);
135
-
136
- // Auto-assign to episode (1-hour window per agent)
137
- try {
138
- let episode = store.getActiveEpisode(body.agentId, 3600_000);
139
- if (!episode) {
140
- episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
141
- }
142
- store.addEngramToEpisode(engram.id, episode.id);
143
- } catch { /* Episode assignment is non-fatal */ }
144
- }
145
-
146
- // Generate embedding asynchronously (don't block response)
147
- embed(`${body.concept} ${body.content}`).then(vec => {
148
- store.updateEmbedding(engram.id, vec);
149
- }).catch(() => {}); // Embedding failure is non-fatal
150
-
151
- // Auto-checkpoint: track write for consolidation scheduling
152
- try { store.updateAutoCheckpointWrite(body.agentId, engram.id); } catch { /* non-fatal */ }
153
-
154
- return reply.code(201).send({
155
- stored: true,
156
- disposition: isLowSalience ? 'low-salience' : salience.disposition,
157
- salience: salience.score,
158
- reasonCodes: salience.reasonCodes,
159
- engram,
160
- });
161
- });
162
-
163
- app.post('/memory/activate', async (req, reply) => {
164
- const body = req.body as {
165
- agentId: string;
166
- context: string;
167
- limit?: number;
168
- minScore?: number;
169
- includeStaging?: boolean;
170
- useReranker?: boolean;
171
- useExpansion?: boolean;
172
- abstentionThreshold?: number;
173
- };
174
-
175
- const results = await activationEngine.activate({
176
- agentId: body.agentId,
177
- context: body.context,
178
- limit: body.limit,
179
- minScore: body.minScore,
180
- includeStaging: body.includeStaging,
181
- useReranker: body.useReranker,
182
- useExpansion: body.useExpansion,
183
- abstentionThreshold: body.abstentionThreshold,
184
- });
185
-
186
- // Auto-checkpoint: track recall for consolidation scheduling
187
- try {
188
- const ids = results.map(r => r.engram.id);
189
- store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
190
- } catch { /* non-fatal */ }
191
-
192
- return reply.send({ results });
193
- });
194
-
195
- app.post('/memory/feedback', async (req, reply) => {
196
- const body = req.body as {
197
- activationEventId?: string;
198
- engramId: string;
199
- useful: boolean;
200
- context?: string;
201
- };
202
-
203
- store.logRetrievalFeedback(
204
- body.activationEventId ?? null,
205
- body.engramId,
206
- body.useful,
207
- body.context ?? ''
208
- );
209
-
210
- // Update engram confidence based on feedback
211
- const engram = store.getEngram(body.engramId);
212
- if (engram) {
213
- const config = DEFAULT_AGENT_CONFIG;
214
- const delta = body.useful
215
- ? config.feedbackPositiveBoost
216
- : -config.feedbackNegativePenalty;
217
- store.updateConfidence(engram.id, engram.confidence + delta);
218
- }
219
-
220
- // Touch activity for consolidation scheduling
221
- if (engram) {
222
- try { store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
223
- }
224
-
225
- return reply.send({ recorded: true });
226
- });
227
-
228
- app.post('/memory/retract', async (req, reply) => {
229
- const body = req.body as {
230
- agentId: string;
231
- targetEngramId: string;
232
- reason: string;
233
- counterContent?: string;
234
- };
235
-
236
- const result = retractionEngine.retract({
237
- agentId: body.agentId,
238
- targetEngramId: body.targetEngramId,
239
- reason: body.reason,
240
- counterContent: body.counterContent,
241
- });
242
-
243
- // Touch activity for consolidation scheduling
244
- try { store.touchActivity(body.agentId); } catch { /* non-fatal */ }
245
-
246
- return reply.send(result);
247
- });
248
-
249
- // ============================================================
250
- // DIAGNOSTIC — Debugging and inspection
251
- // ============================================================
252
-
253
- app.post('/memory/search', async (req, reply) => {
254
- const body = req.body as {
255
- agentId: string;
256
- text?: string;
257
- concept?: string;
258
- tags?: string[];
259
- stage?: string;
260
- retracted?: boolean;
261
- limit?: number;
262
- offset?: number;
263
- };
264
-
265
- const results = store.search({
266
- agentId: body.agentId,
267
- text: body.text,
268
- concept: body.concept,
269
- tags: body.tags,
270
- stage: body.stage as any,
271
- retracted: body.retracted,
272
- limit: body.limit,
273
- offset: body.offset,
274
- });
275
-
276
- return reply.send({ results, count: results.length });
277
- });
278
-
279
- app.get('/memory/:id', async (req, reply) => {
280
- const { id } = req.params as { id: string };
281
- const engram = store.getEngram(id);
282
- if (!engram) return reply.code(404).send({ error: 'Not found' });
283
-
284
- const associations = store.getAssociationsFor(id);
285
- return reply.send({ engram, associations });
286
- });
287
-
288
- app.get('/agent/:id/stats', async (req, reply) => {
289
- const { id } = req.params as { id: string };
290
- const active = store.getEngramsByAgent(id, 'active');
291
- const staging = store.getEngramsByAgent(id, 'staging');
292
- const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
293
- const associations = store.getAllAssociations(id);
294
-
295
- return reply.send({
296
- agentId: id,
297
- engrams: {
298
- active: active.length,
299
- staging: staging.length,
300
- retracted: retracted.length,
301
- total: active.length + staging.length + retracted.length,
302
- },
303
- associations: associations.length,
304
- avgConfidence: active.length > 0
305
- ? +(active.reduce((s, e) => s + e.confidence, 0) / active.length).toFixed(3)
306
- : 0,
307
- });
308
- });
309
-
310
- app.get('/agent/:id/metrics', async (req, reply) => {
311
- const { id } = req.params as { id: string };
312
- const windowHours = parseInt((req.query as any).window ?? '24', 10);
313
- const metrics = evalEngine.computeMetrics(id, windowHours);
314
- return reply.send({ metrics });
315
- });
316
-
317
- app.post('/agent/register', async (req, reply) => {
318
- const body = req.body as { name: string };
319
- const id = crypto.randomUUID();
320
- return reply.code(201).send({
321
- id,
322
- name: body.name,
323
- config: DEFAULT_AGENT_CONFIG,
324
- });
325
- });
326
-
327
- // ============================================================
328
- // SYSTEM — Maintenance operations
329
- // ============================================================
330
-
331
- app.post('/system/evict', async (req, reply) => {
332
- const body = req.body as { agentId: string };
333
- const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
334
- return reply.send(result);
335
- });
336
-
337
- app.post('/system/decay', async (req, reply) => {
338
- const body = req.body as { agentId: string; halfLifeDays?: number };
339
- const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
340
- return reply.send({ edgesDecayed: decayed });
341
- });
342
-
343
- app.post('/system/consolidate', async (req, reply) => {
344
- const body = req.body as { agentId: string };
345
- const result = await consolidationEngine.consolidate(body.agentId);
346
- return reply.send(result);
347
- });
348
-
349
- // ============================================================
350
- // CHECKPOINTING — Conscious state preservation
351
- // ============================================================
352
-
353
- app.post('/memory/checkpoint', async (req, reply) => {
354
- const body = req.body as {
355
- agentId: string;
356
- currentTask: string;
357
- decisions?: string[];
358
- activeFiles?: string[];
359
- nextSteps?: string[];
360
- relatedMemoryIds?: string[];
361
- notes?: string;
362
- episodeId?: string | null;
363
- };
364
-
365
- const state: ConsciousState = {
366
- currentTask: body.currentTask,
367
- decisions: body.decisions ?? [],
368
- activeFiles: body.activeFiles ?? [],
369
- nextSteps: body.nextSteps ?? [],
370
- relatedMemoryIds: body.relatedMemoryIds ?? [],
371
- notes: body.notes ?? '',
372
- episodeId: body.episodeId ?? null,
373
- };
374
-
375
- store.saveCheckpoint(body.agentId, state);
376
- return reply.send({ saved: true, agentId: body.agentId });
377
- });
378
-
379
- app.get('/memory/restore/:agentId', async (req, reply) => {
380
- const { agentId } = req.params as { agentId: string };
381
- const checkpoint = store.getCheckpoint(agentId);
382
-
383
- const now = Date.now();
384
- const idleMs = checkpoint
385
- ? now - checkpoint.auto.lastActivityAt.getTime()
386
- : 0;
387
-
388
- // Get last written engram for context
389
- let lastWrite: { id: string; concept: string; content: string } | null = null;
390
- if (checkpoint?.auto.lastWriteId) {
391
- const engram = store.getEngram(checkpoint.auto.lastWriteId);
392
- if (engram) {
393
- lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
394
- }
395
- }
396
-
397
- // Recall memories using last context (if available)
398
- let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
399
- const recallContext = checkpoint?.auto.lastRecallContext
400
- ?? checkpoint?.executionState?.currentTask
401
- ?? null;
402
-
403
- if (recallContext) {
404
- try {
405
- const results = await activationEngine.activate({
406
- agentId,
407
- context: recallContext,
408
- limit: 5,
409
- minScore: 0.05,
410
- useReranker: true,
411
- useExpansion: true,
412
- });
413
- recalledMemories = results.map(r => ({
414
- id: r.engram.id,
415
- concept: r.engram.concept,
416
- content: r.engram.content,
417
- score: r.score,
418
- }));
419
- } catch { /* recall failure is non-fatal */ }
420
- }
421
-
422
- // Trigger mini-consolidation if idle >5min (async, fire-and-forget)
423
- const MINI_CONSOLIDATION_IDLE_MS = 5 * 60_000;
424
- let miniConsolidationTriggered = false;
425
- if (idleMs > MINI_CONSOLIDATION_IDLE_MS) {
426
- miniConsolidationTriggered = true;
427
- consolidationScheduler.runMiniConsolidation(agentId).catch(() => {});
428
- }
429
-
430
- return reply.send({
431
- executionState: checkpoint?.executionState ?? null,
432
- checkpointAt: checkpoint?.checkpointAt ?? null,
433
- recalledMemories,
434
- lastWrite,
435
- idleMs,
436
- miniConsolidationTriggered,
437
- });
438
- });
439
-
440
- // ============================================================
441
- // TASK MANAGEMENT
442
- // ============================================================
443
-
444
- app.post('/task/create', async (req, reply) => {
445
- const body = req.body as {
446
- agentId: string;
447
- concept: string;
448
- content: string;
449
- tags?: string[];
450
- priority?: TaskPriority;
451
- blockedBy?: string;
452
- };
453
-
454
- const engram = store.createEngram({
455
- agentId: body.agentId,
456
- concept: body.concept,
457
- content: body.content,
458
- tags: [...(body.tags ?? []), 'task'],
459
- salience: 0.9,
460
- confidence: 0.8,
461
- salienceFeatures: {
462
- surprise: 0.5, decisionMade: true, causalDepth: 0.5,
463
- resolutionEffort: 0.5, eventType: 'decision',
464
- },
465
- reasonCodes: ['task-created'],
466
- taskStatus: body.blockedBy ? 'blocked' : 'open',
467
- taskPriority: body.priority ?? 'medium',
468
- blockedBy: body.blockedBy,
469
- });
470
-
471
- connectionEngine.enqueue(engram.id);
472
- embed(`${body.concept} ${body.content}`).then(vec => {
473
- store.updateEmbedding(engram.id, vec);
474
- }).catch(() => {});
475
-
476
- return reply.send(engram);
477
- });
478
-
479
- app.post('/task/update', async (req, reply) => {
480
- const body = req.body as {
481
- taskId: string;
482
- status?: TaskStatus;
483
- priority?: TaskPriority;
484
- blockedBy?: string | null;
485
- };
486
-
487
- const engram = store.getEngram(body.taskId);
488
- if (!engram || !engram.taskStatus) {
489
- return reply.code(404).send({ error: 'Task not found' });
490
- }
491
-
492
- if (body.blockedBy !== undefined) {
493
- store.updateBlockedBy(body.taskId, body.blockedBy);
494
- }
495
- if (body.status) {
496
- store.updateTaskStatus(body.taskId, body.status);
497
- }
498
- if (body.priority) {
499
- store.updateTaskPriority(body.taskId, body.priority);
500
- }
501
-
502
- return reply.send(store.getEngram(body.taskId));
503
- });
504
-
505
- app.get('/task/list/:agentId', async (req, reply) => {
506
- const { agentId } = req.params as { agentId: string };
507
- const { status, includeDone } = req.query as { status?: TaskStatus; includeDone?: string };
508
-
509
- let tasks = store.getTasks(agentId, status);
510
- if (includeDone !== 'true' && !status) {
511
- tasks = tasks.filter(t => t.taskStatus !== 'done');
512
- }
513
-
514
- return reply.send({ tasks, count: tasks.length });
515
- });
516
-
517
- app.get('/task/next/:agentId', async (req, reply) => {
518
- const { agentId } = req.params as { agentId: string };
519
- const next = store.getNextTask(agentId);
520
- return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
521
- });
522
-
523
- // Time warp — shift all timestamps backward by N days (for testing)
524
- app.post('/system/time-warp', async (req, reply) => {
525
- const body = req.body as { agentId: string; days: number };
526
- const ms = body.days * 24 * 60 * 60 * 1000;
527
- const shifted = store.timeWarp(body.agentId, ms);
528
- return reply.send({ shifted, days: body.days });
529
- });
530
-
531
- app.get('/health', async () => ({
532
- status: 'ok',
533
- timestamp: new Date().toISOString(),
534
- version: '0.5.4',
535
- }));
536
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * API Routes — the black box interface agents interact with.
5
+ *
6
+ * Core (agent-facing):
7
+ * POST /memory/write — write a memory (salience filter decides disposition)
8
+ * POST /memory/activate — retrieve by context activation
9
+ * POST /memory/feedback — report whether a memory was useful
10
+ * POST /memory/retract — invalidate a wrong memory
11
+ *
12
+ * Checkpointing:
13
+ * POST /memory/checkpoint — save explicit execution state
14
+ * GET /memory/restore/:agentId — restore state + targeted recall + async mini-consolidation
15
+ *
16
+ * Task management:
17
+ * POST /task/create — create a prioritized task
18
+ * POST /task/update — update status, priority, or blocking
19
+ * GET /task/list/:agentId — list tasks (filtered by status)
20
+ * GET /task/next/:agentId — get highest-priority actionable task
21
+ *
22
+ * Diagnostic (debugging/eval):
23
+ * POST /memory/search — deterministic search (not cognitive)
24
+ * GET /memory/:id — get a specific engram
25
+ * GET /agent/:id/stats — memory stats for an agent
26
+ * GET /agent/:id/metrics — eval metrics
27
+ * POST /agent/register — register a new agent
28
+ *
29
+ * System:
30
+ * POST /system/evict — trigger eviction check
31
+ * POST /system/decay — trigger edge decay
32
+ * POST /system/consolidate — run sleep cycle (strengthen, decay, sweep)
33
+ * GET /health — health check
34
+ */
35
+
36
+ import type { FastifyInstance } from 'fastify';
37
+ import type { EngramStore } from '../storage/sqlite.js';
38
+ import type { ActivationEngine } from '../engine/activation.js';
39
+ import type { ConnectionEngine } from '../engine/connections.js';
40
+ import type { EvictionEngine } from '../engine/eviction.js';
41
+ import type { RetractionEngine } from '../engine/retraction.js';
42
+ import type { EvalEngine } from '../engine/eval.js';
43
+ import type { ConsolidationEngine } from '../engine/consolidation.js';
44
+ import type { ConsolidationScheduler } from '../engine/consolidation-scheduler.js';
45
+ import { evaluateSalience, computeNovelty } from '../core/salience.js';
46
+ import type { SalienceEventType } from '../core/salience.js';
47
+ import type { TaskStatus, TaskPriority } from '../types/engram.js';
48
+ import type { ConsciousState } from '../types/checkpoint.js';
49
+ import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
50
+ import { embed } from '../core/embeddings.js';
51
+
52
+ export interface MemoryDeps {
53
+ store: EngramStore;
54
+ activationEngine: ActivationEngine;
55
+ connectionEngine: ConnectionEngine;
56
+ evictionEngine: EvictionEngine;
57
+ retractionEngine: RetractionEngine;
58
+ evalEngine: EvalEngine;
59
+ consolidationEngine: ConsolidationEngine;
60
+ consolidationScheduler: ConsolidationScheduler;
61
+ }
62
+
63
+ export function registerRoutes(app: FastifyInstance, deps: MemoryDeps): void {
64
+ const { store, activationEngine, connectionEngine, evictionEngine, retractionEngine, evalEngine, consolidationEngine, consolidationScheduler } = deps;
65
+
66
+ // ============================================================
67
+ // CORE — Agent-facing endpoints
68
+ // ============================================================
69
+
70
+ app.post('/memory/write', async (req, reply) => {
71
+ const body = req.body as {
72
+ agentId: string;
73
+ concept: string;
74
+ content: string;
75
+ tags?: string[];
76
+ eventType?: SalienceEventType;
77
+ surprise?: number;
78
+ decisionMade?: boolean;
79
+ causalDepth?: number;
80
+ resolutionEffort?: number;
81
+ confidence?: number;
82
+ };
83
+
84
+ if (!body.agentId || typeof body.agentId !== 'string' ||
85
+ !body.concept || typeof body.concept !== 'string' ||
86
+ !body.content || typeof body.content !== 'string') {
87
+ return reply.status(400).send({ error: 'agentId, concept, and content are required strings' });
88
+ }
89
+
90
+ const novelty = computeNovelty(store, body.agentId, body.concept, body.content);
91
+
92
+ const salience = evaluateSalience({
93
+ content: body.content,
94
+ eventType: body.eventType,
95
+ surprise: body.surprise,
96
+ decisionMade: body.decisionMade,
97
+ causalDepth: body.causalDepth,
98
+ resolutionEffort: body.resolutionEffort,
99
+ novelty,
100
+ });
101
+
102
+ // v0.5.4: No longer discard — store with low confidence for ranking.
103
+ const isLowSalience = salience.disposition === 'discard';
104
+ const confidence = isLowSalience
105
+ ? 0.25
106
+ : body.confidence ?? (salience.disposition === 'staging' ? 0.40 : 0.50);
107
+
108
+ const engram = store.createEngram({
109
+ agentId: body.agentId,
110
+ concept: body.concept,
111
+ content: body.content,
112
+ tags: isLowSalience ? [...(body.tags ?? []), 'low-salience'] : body.tags,
113
+ salience: salience.score,
114
+ confidence,
115
+ salienceFeatures: salience.features,
116
+ reasonCodes: salience.reasonCodes,
117
+ ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
118
+ });
119
+
120
+ if (salience.disposition === 'staging') {
121
+ store.updateStage(engram.id, 'staging');
122
+ }
123
+
124
+ // Create temporal adjacency edge to previous memory (conversation thread graph)
125
+ // This enables multi-hop graph walk through conversation sequences
126
+ try {
127
+ const prev = store.getLatestEngram(body.agentId, engram.id);
128
+ if (prev) {
129
+ store.upsertAssociation(prev.id, engram.id, 0.3, 'temporal', 0.8);
130
+ }
131
+ } catch { /* Temporal edge creation is non-fatal */ }
132
+
133
+ if (salience.disposition === 'active' || isLowSalience) {
134
+ connectionEngine.enqueue(engram.id);
135
+
136
+ // Auto-assign to episode (1-hour window per agent)
137
+ try {
138
+ let episode = store.getActiveEpisode(body.agentId, 3600_000);
139
+ if (!episode) {
140
+ episode = store.createEpisode({ agentId: body.agentId, label: body.concept });
141
+ }
142
+ store.addEngramToEpisode(engram.id, episode.id);
143
+ } catch { /* Episode assignment is non-fatal */ }
144
+ }
145
+
146
+ // Generate embedding asynchronously (don't block response)
147
+ embed(`${body.concept} ${body.content}`).then(vec => {
148
+ store.updateEmbedding(engram.id, vec);
149
+ }).catch(() => {}); // Embedding failure is non-fatal
150
+
151
+ // Auto-checkpoint: track write for consolidation scheduling
152
+ try { store.updateAutoCheckpointWrite(body.agentId, engram.id); } catch { /* non-fatal */ }
153
+
154
+ return reply.code(201).send({
155
+ stored: true,
156
+ disposition: isLowSalience ? 'low-salience' : salience.disposition,
157
+ salience: salience.score,
158
+ reasonCodes: salience.reasonCodes,
159
+ engram,
160
+ });
161
+ });
162
+
163
+ app.post('/memory/activate', async (req, reply) => {
164
+ const body = req.body as {
165
+ agentId: string;
166
+ context: string;
167
+ limit?: number;
168
+ minScore?: number;
169
+ includeStaging?: boolean;
170
+ useReranker?: boolean;
171
+ useExpansion?: boolean;
172
+ abstentionThreshold?: number;
173
+ };
174
+
175
+ const results = await activationEngine.activate({
176
+ agentId: body.agentId,
177
+ context: body.context,
178
+ limit: body.limit,
179
+ minScore: body.minScore,
180
+ includeStaging: body.includeStaging,
181
+ useReranker: body.useReranker,
182
+ useExpansion: body.useExpansion,
183
+ abstentionThreshold: body.abstentionThreshold,
184
+ });
185
+
186
+ // Auto-checkpoint: track recall for consolidation scheduling
187
+ try {
188
+ const ids = results.map(r => r.engram.id);
189
+ store.updateAutoCheckpointRecall(body.agentId, body.context, ids);
190
+ } catch { /* non-fatal */ }
191
+
192
+ return reply.send({ results });
193
+ });
194
+
195
+ app.post('/memory/feedback', async (req, reply) => {
196
+ const body = req.body as {
197
+ activationEventId?: string;
198
+ engramId: string;
199
+ useful: boolean;
200
+ context?: string;
201
+ };
202
+
203
+ store.logRetrievalFeedback(
204
+ body.activationEventId ?? null,
205
+ body.engramId,
206
+ body.useful,
207
+ body.context ?? ''
208
+ );
209
+
210
+ // Update engram confidence based on feedback
211
+ const engram = store.getEngram(body.engramId);
212
+ if (engram) {
213
+ const config = DEFAULT_AGENT_CONFIG;
214
+ const delta = body.useful
215
+ ? config.feedbackPositiveBoost
216
+ : -config.feedbackNegativePenalty;
217
+ store.updateConfidence(engram.id, engram.confidence + delta);
218
+ }
219
+
220
+ // Touch activity for consolidation scheduling
221
+ if (engram) {
222
+ try { store.touchActivity(engram.agentId); } catch { /* non-fatal */ }
223
+ }
224
+
225
+ return reply.send({ recorded: true });
226
+ });
227
+
228
+ app.post('/memory/retract', async (req, reply) => {
229
+ const body = req.body as {
230
+ agentId: string;
231
+ targetEngramId: string;
232
+ reason: string;
233
+ counterContent?: string;
234
+ };
235
+
236
+ const result = retractionEngine.retract({
237
+ agentId: body.agentId,
238
+ targetEngramId: body.targetEngramId,
239
+ reason: body.reason,
240
+ counterContent: body.counterContent,
241
+ });
242
+
243
+ // Touch activity for consolidation scheduling
244
+ try { store.touchActivity(body.agentId); } catch { /* non-fatal */ }
245
+
246
+ return reply.send(result);
247
+ });
248
+
249
+ // ============================================================
250
+ // DIAGNOSTIC — Debugging and inspection
251
+ // ============================================================
252
+
253
+ app.post('/memory/search', async (req, reply) => {
254
+ const body = req.body as {
255
+ agentId: string;
256
+ text?: string;
257
+ concept?: string;
258
+ tags?: string[];
259
+ stage?: string;
260
+ retracted?: boolean;
261
+ limit?: number;
262
+ offset?: number;
263
+ };
264
+
265
+ const results = store.search({
266
+ agentId: body.agentId,
267
+ text: body.text,
268
+ concept: body.concept,
269
+ tags: body.tags,
270
+ stage: body.stage as any,
271
+ retracted: body.retracted,
272
+ limit: body.limit,
273
+ offset: body.offset,
274
+ });
275
+
276
+ return reply.send({ results, count: results.length });
277
+ });
278
+
279
+ app.get('/memory/:id', async (req, reply) => {
280
+ const { id } = req.params as { id: string };
281
+ const engram = store.getEngram(id);
282
+ if (!engram) return reply.code(404).send({ error: 'Not found' });
283
+
284
+ const associations = store.getAssociationsFor(id);
285
+ return reply.send({ engram, associations });
286
+ });
287
+
288
+ app.get('/agent/:id/stats', async (req, reply) => {
289
+ const { id } = req.params as { id: string };
290
+ const active = store.getEngramsByAgent(id, 'active');
291
+ const staging = store.getEngramsByAgent(id, 'staging');
292
+ const retracted = store.getEngramsByAgent(id, undefined, true).filter(e => e.retracted);
293
+ const associations = store.getAllAssociations(id);
294
+
295
+ return reply.send({
296
+ agentId: id,
297
+ engrams: {
298
+ active: active.length,
299
+ staging: staging.length,
300
+ retracted: retracted.length,
301
+ total: active.length + staging.length + retracted.length,
302
+ },
303
+ associations: associations.length,
304
+ avgConfidence: active.length > 0
305
+ ? +(active.reduce((s, e) => s + e.confidence, 0) / active.length).toFixed(3)
306
+ : 0,
307
+ });
308
+ });
309
+
310
+ app.get('/agent/:id/metrics', async (req, reply) => {
311
+ const { id } = req.params as { id: string };
312
+ const windowHours = parseInt((req.query as any).window ?? '24', 10);
313
+ const metrics = evalEngine.computeMetrics(id, windowHours);
314
+ return reply.send({ metrics });
315
+ });
316
+
317
+ app.post('/agent/register', async (req, reply) => {
318
+ const body = req.body as { name: string };
319
+ const id = crypto.randomUUID();
320
+ return reply.code(201).send({
321
+ id,
322
+ name: body.name,
323
+ config: DEFAULT_AGENT_CONFIG,
324
+ });
325
+ });
326
+
327
+ // ============================================================
328
+ // SYSTEM — Maintenance operations
329
+ // ============================================================
330
+
331
+ app.post('/system/evict', async (req, reply) => {
332
+ const body = req.body as { agentId: string };
333
+ const result = evictionEngine.enforceCapacity(body.agentId, DEFAULT_AGENT_CONFIG);
334
+ return reply.send(result);
335
+ });
336
+
337
+ app.post('/system/decay', async (req, reply) => {
338
+ const body = req.body as { agentId: string; halfLifeDays?: number };
339
+ const decayed = evictionEngine.decayEdges(body.agentId, body.halfLifeDays);
340
+ return reply.send({ edgesDecayed: decayed });
341
+ });
342
+
343
+ app.post('/system/consolidate', async (req, reply) => {
344
+ const body = req.body as { agentId: string };
345
+ const result = await consolidationEngine.consolidate(body.agentId);
346
+ return reply.send(result);
347
+ });
348
+
349
+ // ============================================================
350
+ // CHECKPOINTING — Conscious state preservation
351
+ // ============================================================
352
+
353
+ app.post('/memory/checkpoint', async (req, reply) => {
354
+ const body = req.body as {
355
+ agentId: string;
356
+ currentTask: string;
357
+ decisions?: string[];
358
+ activeFiles?: string[];
359
+ nextSteps?: string[];
360
+ relatedMemoryIds?: string[];
361
+ notes?: string;
362
+ episodeId?: string | null;
363
+ };
364
+
365
+ const state: ConsciousState = {
366
+ currentTask: body.currentTask,
367
+ decisions: body.decisions ?? [],
368
+ activeFiles: body.activeFiles ?? [],
369
+ nextSteps: body.nextSteps ?? [],
370
+ relatedMemoryIds: body.relatedMemoryIds ?? [],
371
+ notes: body.notes ?? '',
372
+ episodeId: body.episodeId ?? null,
373
+ };
374
+
375
+ store.saveCheckpoint(body.agentId, state);
376
+ return reply.send({ saved: true, agentId: body.agentId });
377
+ });
378
+
379
+ app.get('/memory/restore/:agentId', async (req, reply) => {
380
+ const { agentId } = req.params as { agentId: string };
381
+ const checkpoint = store.getCheckpoint(agentId);
382
+
383
+ const now = Date.now();
384
+ const idleMs = checkpoint
385
+ ? now - checkpoint.auto.lastActivityAt.getTime()
386
+ : 0;
387
+
388
+ // Get last written engram for context
389
+ let lastWrite: { id: string; concept: string; content: string } | null = null;
390
+ if (checkpoint?.auto.lastWriteId) {
391
+ const engram = store.getEngram(checkpoint.auto.lastWriteId);
392
+ if (engram) {
393
+ lastWrite = { id: engram.id, concept: engram.concept, content: engram.content };
394
+ }
395
+ }
396
+
397
+ // Recall memories using last context (if available)
398
+ let recalledMemories: Array<{ id: string; concept: string; content: string; score: number }> = [];
399
+ const recallContext = checkpoint?.auto.lastRecallContext
400
+ ?? checkpoint?.executionState?.currentTask
401
+ ?? null;
402
+
403
+ if (recallContext) {
404
+ try {
405
+ const results = await activationEngine.activate({
406
+ agentId,
407
+ context: recallContext,
408
+ limit: 5,
409
+ minScore: 0.05,
410
+ useReranker: true,
411
+ useExpansion: true,
412
+ });
413
+ recalledMemories = results.map(r => ({
414
+ id: r.engram.id,
415
+ concept: r.engram.concept,
416
+ content: r.engram.content,
417
+ score: r.score,
418
+ }));
419
+ } catch { /* recall failure is non-fatal */ }
420
+ }
421
+
422
+ // Trigger mini-consolidation if idle >5min (async, fire-and-forget)
423
+ const MINI_CONSOLIDATION_IDLE_MS = 5 * 60_000;
424
+ let miniConsolidationTriggered = false;
425
+ if (idleMs > MINI_CONSOLIDATION_IDLE_MS) {
426
+ miniConsolidationTriggered = true;
427
+ consolidationScheduler.runMiniConsolidation(agentId).catch(() => {});
428
+ }
429
+
430
+ return reply.send({
431
+ executionState: checkpoint?.executionState ?? null,
432
+ checkpointAt: checkpoint?.checkpointAt ?? null,
433
+ recalledMemories,
434
+ lastWrite,
435
+ idleMs,
436
+ miniConsolidationTriggered,
437
+ });
438
+ });
439
+
440
+ // ============================================================
441
+ // TASK MANAGEMENT
442
+ // ============================================================
443
+
444
+ app.post('/task/create', async (req, reply) => {
445
+ const body = req.body as {
446
+ agentId: string;
447
+ concept: string;
448
+ content: string;
449
+ tags?: string[];
450
+ priority?: TaskPriority;
451
+ blockedBy?: string;
452
+ };
453
+
454
+ const engram = store.createEngram({
455
+ agentId: body.agentId,
456
+ concept: body.concept,
457
+ content: body.content,
458
+ tags: [...(body.tags ?? []), 'task'],
459
+ salience: 0.9,
460
+ confidence: 0.8,
461
+ salienceFeatures: {
462
+ surprise: 0.5, decisionMade: true, causalDepth: 0.5,
463
+ resolutionEffort: 0.5, eventType: 'decision',
464
+ },
465
+ reasonCodes: ['task-created'],
466
+ taskStatus: body.blockedBy ? 'blocked' : 'open',
467
+ taskPriority: body.priority ?? 'medium',
468
+ blockedBy: body.blockedBy,
469
+ });
470
+
471
+ connectionEngine.enqueue(engram.id);
472
+ embed(`${body.concept} ${body.content}`).then(vec => {
473
+ store.updateEmbedding(engram.id, vec);
474
+ }).catch(() => {});
475
+
476
+ return reply.send(engram);
477
+ });
478
+
479
+ app.post('/task/update', async (req, reply) => {
480
+ const body = req.body as {
481
+ taskId: string;
482
+ status?: TaskStatus;
483
+ priority?: TaskPriority;
484
+ blockedBy?: string | null;
485
+ };
486
+
487
+ const engram = store.getEngram(body.taskId);
488
+ if (!engram || !engram.taskStatus) {
489
+ return reply.code(404).send({ error: 'Task not found' });
490
+ }
491
+
492
+ if (body.blockedBy !== undefined) {
493
+ store.updateBlockedBy(body.taskId, body.blockedBy);
494
+ }
495
+ if (body.status) {
496
+ store.updateTaskStatus(body.taskId, body.status);
497
+ }
498
+ if (body.priority) {
499
+ store.updateTaskPriority(body.taskId, body.priority);
500
+ }
501
+
502
+ return reply.send(store.getEngram(body.taskId));
503
+ });
504
+
505
+ app.get('/task/list/:agentId', async (req, reply) => {
506
+ const { agentId } = req.params as { agentId: string };
507
+ const { status, includeDone } = req.query as { status?: TaskStatus; includeDone?: string };
508
+
509
+ let tasks = store.getTasks(agentId, status);
510
+ if (includeDone !== 'true' && !status) {
511
+ tasks = tasks.filter(t => t.taskStatus !== 'done');
512
+ }
513
+
514
+ return reply.send({ tasks, count: tasks.length });
515
+ });
516
+
517
+ app.get('/task/next/:agentId', async (req, reply) => {
518
+ const { agentId } = req.params as { agentId: string };
519
+ const next = store.getNextTask(agentId);
520
+ return reply.send(next ? { task: next } : { task: null, message: 'No actionable tasks' });
521
+ });
522
+
523
+ // Time warp — shift all timestamps backward by N days (for testing)
524
+ app.post('/system/time-warp', async (req, reply) => {
525
+ const body = req.body as { agentId: string; days: number };
526
+ const ms = body.days * 24 * 60 * 60 * 1000;
527
+ const shifted = store.timeWarp(body.agentId, ms);
528
+ return reply.send({ shifted, days: body.days });
529
+ });
530
+
531
+ app.get('/health', async () => {
532
+ const coordEnabled = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
533
+ const base: Record<string, unknown> = {
534
+ status: 'ok',
535
+ timestamp: new Date().toISOString(),
536
+ version: '0.5.5',
537
+ coordination: coordEnabled,
538
+ };
539
+ if (coordEnabled) {
540
+ try {
541
+ const db = deps.store.getDb();
542
+ const stats = db.prepare(`SELECT
543
+ (SELECT COUNT(*) FROM coord_agents WHERE status != 'dead') AS agents_alive,
544
+ (SELECT COUNT(*) FROM coord_assignments WHERE status = 'pending') AS pending_tasks,
545
+ (SELECT COUNT(*) FROM coord_locks) AS active_locks`).get() as { agents_alive: number; pending_tasks: number; active_locks: number };
546
+ Object.assign(base, stats);
547
+ } catch { /* tables may not exist yet */ }
548
+ }
549
+ return base;
550
+ });
551
+ }