agent-working-memory 0.5.5 → 0.6.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.
Files changed (82) hide show
  1. package/README.md +428 -399
  2. package/dist/api/routes.d.ts.map +1 -1
  3. package/dist/api/routes.js +60 -5
  4. package/dist/api/routes.js.map +1 -1
  5. package/dist/cli.js +468 -68
  6. package/dist/cli.js.map +1 -1
  7. package/dist/coordination/index.d.ts +11 -0
  8. package/dist/coordination/index.d.ts.map +1 -0
  9. package/dist/coordination/index.js +39 -0
  10. package/dist/coordination/index.js.map +1 -0
  11. package/dist/coordination/mcp-tools.d.ts +8 -0
  12. package/dist/coordination/mcp-tools.d.ts.map +1 -0
  13. package/dist/coordination/mcp-tools.js +221 -0
  14. package/dist/coordination/mcp-tools.js.map +1 -0
  15. package/dist/coordination/routes.d.ts +9 -0
  16. package/dist/coordination/routes.d.ts.map +1 -0
  17. package/dist/coordination/routes.js +573 -0
  18. package/dist/coordination/routes.js.map +1 -0
  19. package/dist/coordination/schema.d.ts +12 -0
  20. package/dist/coordination/schema.d.ts.map +1 -0
  21. package/dist/coordination/schema.js +125 -0
  22. package/dist/coordination/schema.js.map +1 -0
  23. package/dist/coordination/schemas.d.ts +227 -0
  24. package/dist/coordination/schemas.d.ts.map +1 -0
  25. package/dist/coordination/schemas.js +125 -0
  26. package/dist/coordination/schemas.js.map +1 -0
  27. package/dist/coordination/stale.d.ts +27 -0
  28. package/dist/coordination/stale.d.ts.map +1 -0
  29. package/dist/coordination/stale.js +58 -0
  30. package/dist/coordination/stale.js.map +1 -0
  31. package/dist/engine/activation.d.ts.map +1 -1
  32. package/dist/engine/activation.js +119 -23
  33. package/dist/engine/activation.js.map +1 -1
  34. package/dist/engine/consolidation.d.ts.map +1 -1
  35. package/dist/engine/consolidation.js +27 -6
  36. package/dist/engine/consolidation.js.map +1 -1
  37. package/dist/index.js +100 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/mcp.js +149 -80
  40. package/dist/mcp.js.map +1 -1
  41. package/dist/storage/sqlite.d.ts +21 -0
  42. package/dist/storage/sqlite.d.ts.map +1 -1
  43. package/dist/storage/sqlite.js +331 -282
  44. package/dist/storage/sqlite.js.map +1 -1
  45. package/dist/types/engram.d.ts +24 -0
  46. package/dist/types/engram.d.ts.map +1 -1
  47. package/dist/types/engram.js.map +1 -1
  48. package/package.json +57 -55
  49. package/src/api/index.ts +3 -3
  50. package/src/api/routes.ts +600 -536
  51. package/src/cli.ts +850 -397
  52. package/src/coordination/index.ts +47 -0
  53. package/src/coordination/mcp-tools.ts +318 -0
  54. package/src/coordination/routes.ts +846 -0
  55. package/src/coordination/schema.ts +120 -0
  56. package/src/coordination/schemas.ts +155 -0
  57. package/src/coordination/stale.ts +97 -0
  58. package/src/core/decay.ts +63 -63
  59. package/src/core/embeddings.ts +88 -88
  60. package/src/core/hebbian.ts +93 -93
  61. package/src/core/index.ts +5 -5
  62. package/src/core/logger.ts +36 -36
  63. package/src/core/query-expander.ts +66 -66
  64. package/src/core/reranker.ts +101 -101
  65. package/src/engine/activation.ts +758 -656
  66. package/src/engine/connections.ts +103 -103
  67. package/src/engine/consolidation-scheduler.ts +125 -125
  68. package/src/engine/consolidation.ts +29 -6
  69. package/src/engine/eval.ts +102 -102
  70. package/src/engine/eviction.ts +101 -101
  71. package/src/engine/index.ts +8 -8
  72. package/src/engine/retraction.ts +100 -100
  73. package/src/engine/staging.ts +74 -74
  74. package/src/index.ts +208 -121
  75. package/src/mcp.ts +1093 -1013
  76. package/src/storage/index.ts +3 -3
  77. package/src/storage/sqlite.ts +1017 -963
  78. package/src/types/agent.ts +67 -67
  79. package/src/types/checkpoint.ts +46 -46
  80. package/src/types/engram.ts +245 -217
  81. package/src/types/eval.ts +100 -100
  82. package/src/types/index.ts +6 -6
@@ -0,0 +1,120 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * SQL table definitions for the coordination module.
5
+ * Tables are created conditionally when AWM_COORDINATION=true.
6
+ * Uses the same memory.db — coordination events feed the activation engine.
7
+ */
8
+
9
+ import type Database from 'better-sqlite3';
10
+
11
+ const COORDINATION_TABLES = `
12
+ -- Coordination: agents in the hive
13
+ CREATE TABLE IF NOT EXISTS coord_agents (
14
+ id TEXT PRIMARY KEY,
15
+ name TEXT NOT NULL,
16
+ role TEXT NOT NULL DEFAULT 'worker',
17
+ status TEXT NOT NULL DEFAULT 'idle',
18
+ pid INTEGER,
19
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
20
+ last_seen TEXT NOT NULL DEFAULT (datetime('now')),
21
+ current_task TEXT,
22
+ metadata TEXT,
23
+ capabilities TEXT,
24
+ workspace TEXT
25
+ );
26
+
27
+ -- Prevent duplicate agent registrations for the same name+workspace
28
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_coord_agents_name_workspace
29
+ ON coord_agents (name, COALESCE(workspace, ''));
30
+
31
+ -- Coordination: assignments
32
+ CREATE TABLE IF NOT EXISTS coord_assignments (
33
+ id TEXT PRIMARY KEY,
34
+ agent_id TEXT,
35
+ task TEXT NOT NULL,
36
+ description TEXT,
37
+ status TEXT NOT NULL DEFAULT 'pending',
38
+ priority INTEGER NOT NULL DEFAULT 0,
39
+ blocked_by TEXT,
40
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
41
+ started_at TEXT,
42
+ completed_at TEXT,
43
+ result TEXT,
44
+ commit_sha TEXT,
45
+ workspace TEXT,
46
+ FOREIGN KEY (agent_id) REFERENCES coord_agents(id),
47
+ FOREIGN KEY (blocked_by) REFERENCES coord_assignments(id)
48
+ );
49
+
50
+ -- Coordination: file locks
51
+ CREATE TABLE IF NOT EXISTS coord_locks (
52
+ file_path TEXT PRIMARY KEY,
53
+ agent_id TEXT NOT NULL,
54
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
55
+ reason TEXT,
56
+ FOREIGN KEY (agent_id) REFERENCES coord_agents(id)
57
+ );
58
+
59
+ -- Coordination: orchestrator broadcast commands
60
+ CREATE TABLE IF NOT EXISTS coord_commands (
61
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
62
+ command TEXT NOT NULL,
63
+ reason TEXT,
64
+ issued_by TEXT,
65
+ issued_at TEXT NOT NULL DEFAULT (datetime('now')),
66
+ cleared_at TEXT,
67
+ workspace TEXT
68
+ );
69
+
70
+ -- Coordination: findings reported by agents
71
+ CREATE TABLE IF NOT EXISTS coord_findings (
72
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ agent_id TEXT NOT NULL,
74
+ category TEXT NOT NULL,
75
+ severity TEXT NOT NULL DEFAULT 'info',
76
+ file_path TEXT,
77
+ line_number INTEGER,
78
+ description TEXT NOT NULL,
79
+ suggestion TEXT,
80
+ status TEXT NOT NULL DEFAULT 'open',
81
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
82
+ resolved_at TEXT,
83
+ FOREIGN KEY (agent_id) REFERENCES coord_agents(id)
84
+ );
85
+
86
+ -- Coordination: cross-agent decision propagation
87
+ CREATE TABLE IF NOT EXISTS coord_decisions (
88
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
89
+ author_id TEXT NOT NULL,
90
+ assignment_id TEXT,
91
+ tags TEXT,
92
+ summary TEXT NOT NULL,
93
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
94
+ FOREIGN KEY (author_id) REFERENCES coord_agents(id)
95
+ );
96
+ CREATE INDEX IF NOT EXISTS idx_coord_decisions_assignment
97
+ ON coord_decisions (assignment_id, created_at);
98
+
99
+ -- Coordination: event audit trail
100
+ CREATE TABLE IF NOT EXISTS coord_events (
101
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
102
+ agent_id TEXT,
103
+ event_type TEXT NOT NULL,
104
+ detail TEXT,
105
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
106
+ );
107
+ `;
108
+
109
+ /**
110
+ * Create all coordination tables in the given database.
111
+ * Safe to call multiple times (CREATE IF NOT EXISTS).
112
+ */
113
+ export function initCoordinationTables(db: Database.Database): void {
114
+ db.exec(COORDINATION_TABLES);
115
+
116
+ // Migrations: add columns to existing coord_assignments tables
117
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN commit_sha TEXT`); } catch { /* exists */ }
118
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN priority INTEGER NOT NULL DEFAULT 0`); } catch { /* exists */ }
119
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN blocked_by TEXT`); } catch { /* exists */ }
120
+ }
@@ -0,0 +1,155 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Zod validation schemas for the coordination module.
5
+ * Ported from AgentSynapse packages/coordinator/src/schemas.ts.
6
+ */
7
+
8
+ import { z } from 'zod';
9
+
10
+ // ─── Enums ──────────────────────────────────────────────────────
11
+
12
+ export const agentRoleEnum = z.enum(['worker', 'orchestrator', 'dev-lead']);
13
+ export const agentStatusEnum = z.enum(['idle', 'working', 'dead']);
14
+ export const assignmentStatusEnum = z.enum(['in_progress', 'completed', 'failed', 'blocked']);
15
+ export const commandEnum = z.enum(['BUILD_FREEZE', 'PAUSE', 'RESUME', 'SHUTDOWN']);
16
+ export const findingSeverityEnum = z.enum(['critical', 'error', 'warn', 'info']);
17
+ export const findingCategoryEnum = z.enum([
18
+ 'typecheck', 'lint', 'test-failure', 'security', 'performance',
19
+ 'dead-code', 'todo', 'bug', 'ux', 'a11y', 'sql', 'convention',
20
+ 'freshdesk', 'data-quality', 'other',
21
+ ]);
22
+ export const findingStatusEnum = z.enum(['open', 'resolved']);
23
+
24
+ // ─── Checkin ────────────────────────────────────────────────────
25
+
26
+ export const checkinSchema = z.object({
27
+ name: z.string().min(1).max(50),
28
+ role: agentRoleEnum.default('worker'),
29
+ pid: z.number().int().positive().optional(),
30
+ metadata: z.record(z.string(), z.unknown()).optional(),
31
+ capabilities: z.array(z.string().max(50)).max(20).optional(),
32
+ workspace: z.string().max(50).optional(),
33
+ });
34
+
35
+ export const checkoutSchema = z.object({
36
+ agentId: z.string().uuid(),
37
+ });
38
+
39
+ // ─── Assignments ────────────────────────────────────────────────
40
+
41
+ export const assignCreateSchema = z.object({
42
+ agentId: z.string().uuid().optional(),
43
+ task: z.string().min(1).max(1000),
44
+ description: z.string().max(5000).optional(),
45
+ workspace: z.string().max(50).optional(),
46
+ priority: z.number().int().min(0).max(10).default(0),
47
+ blocked_by: z.string().uuid().optional(),
48
+ });
49
+
50
+ export const assignmentQuerySchema = z.object({
51
+ agentId: z.string().uuid().optional(),
52
+ name: z.string().min(1).max(50).optional(),
53
+ workspace: z.string().max(50).optional(),
54
+ });
55
+
56
+ export const nextSchema = z.object({
57
+ name: z.string().min(1).max(50),
58
+ workspace: z.string().max(50).optional(),
59
+ role: agentRoleEnum.default('worker'),
60
+ capabilities: z.array(z.string().max(50)).max(20).optional(),
61
+ });
62
+
63
+ export const assignmentClaimSchema = z.object({
64
+ agentId: z.string().uuid(),
65
+ });
66
+
67
+ export const assignmentUpdateSchema = z.object({
68
+ status: assignmentStatusEnum,
69
+ result: z.string().max(10000).optional(),
70
+ commit_sha: z.string().max(100).optional(),
71
+ });
72
+
73
+ // ─── Locks ──────────────────────────────────────────────────────
74
+
75
+ export const lockAcquireSchema = z.object({
76
+ agentId: z.string().uuid(),
77
+ filePath: z.string().min(1).max(500),
78
+ reason: z.string().max(500).optional(),
79
+ });
80
+
81
+ export const lockReleaseSchema = z.object({
82
+ agentId: z.string().uuid(),
83
+ filePath: z.string().min(1).max(500),
84
+ });
85
+
86
+ // ─── Commands ───────────────────────────────────────────────────
87
+
88
+ export const commandCreateSchema = z.object({
89
+ command: commandEnum,
90
+ reason: z.string().max(1000).optional(),
91
+ issuedBy: z.string().max(50).optional(),
92
+ workspace: z.string().max(50).optional(),
93
+ });
94
+
95
+ export const commandWaitQuerySchema = z.object({
96
+ status: z.string().max(20).default('idle'),
97
+ timeout: z.coerce.number().int().min(0).max(30).optional(),
98
+ agentId: z.string().optional(),
99
+ workspace: z.string().max(50).optional(),
100
+ });
101
+
102
+ // ─── Findings ───────────────────────────────────────────────────
103
+
104
+ export const findingCreateSchema = z.object({
105
+ agentId: z.string().uuid(),
106
+ category: findingCategoryEnum,
107
+ severity: findingSeverityEnum.default('info'),
108
+ filePath: z.string().max(500).optional(),
109
+ lineNumber: z.number().int().positive().optional(),
110
+ description: z.string().min(1).max(5000),
111
+ suggestion: z.string().max(5000).optional(),
112
+ });
113
+
114
+ export const findingsQuerySchema = z.object({
115
+ category: findingCategoryEnum.optional(),
116
+ severity: findingSeverityEnum.optional(),
117
+ status: findingStatusEnum.optional(),
118
+ limit: z.coerce.number().int().min(1).max(200).default(50),
119
+ });
120
+
121
+ // ─── Param Schemas ─────────────────────────────────────────────
122
+
123
+ export const assignmentIdParamSchema = z.object({ id: z.string().uuid() });
124
+ export const findingIdParamSchema = z.object({ id: z.coerce.number().int().positive() });
125
+
126
+ // ─── Pulse ─────────────────────────────────────────────────────
127
+
128
+ export const pulseSchema = z.object({
129
+ agentId: z.string().uuid(),
130
+ });
131
+
132
+ // ─── Decisions ─────────────────────────────────────────────────
133
+
134
+ export const decisionsQuerySchema = z.object({
135
+ since_id: z.coerce.number().int().min(0).default(0),
136
+ assignment_id: z.string().max(100).optional(),
137
+ limit: z.coerce.number().int().min(1).max(200).default(20),
138
+ });
139
+
140
+ // ─── Status / Events ────────────────────────────────────────────
141
+
142
+ export const eventsQuerySchema = z.object({
143
+ limit: z.coerce.number().int().min(1).max(200).default(50),
144
+ });
145
+
146
+ export const staleQuerySchema = z.object({
147
+ seconds: z.coerce.number().int().min(1).max(86400).default(120),
148
+ cleanup: z.enum(['0', '1', 'true', 'false']).optional(),
149
+ });
150
+
151
+ export const workersQuerySchema = z.object({
152
+ capability: z.string().max(50).optional(),
153
+ status: agentStatusEnum.optional(),
154
+ workspace: z.string().max(50).optional(),
155
+ });
@@ -0,0 +1,97 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Stale agent detection and cleanup for the coordination module.
5
+ */
6
+
7
+ import type Database from 'better-sqlite3';
8
+
9
+ interface StaleAgent {
10
+ id: string;
11
+ name: string;
12
+ role: string;
13
+ status: string;
14
+ last_seen: string;
15
+ seconds_since_seen: number;
16
+ }
17
+
18
+ /** Detect agents that haven't checked in within the threshold. */
19
+ export function detectStale(db: Database.Database, thresholdSeconds: number): StaleAgent[] {
20
+ return db.prepare(
21
+ `SELECT id, name, role, status, last_seen,
22
+ ROUND((julianday('now') - julianday(last_seen)) * 86400) AS seconds_since_seen
23
+ FROM coord_agents
24
+ WHERE status NOT IN ('dead')
25
+ AND (julianday('now') - julianday(last_seen)) * 86400 > ?`
26
+ ).all(thresholdSeconds) as StaleAgent[];
27
+ }
28
+
29
+ /** Clean up stale agents: fail assignments, release locks, mark dead. */
30
+ export function cleanupStale(db: Database.Database, thresholdSeconds: number): { stale: StaleAgent[]; cleaned: number } {
31
+ const stale = detectStale(db, thresholdSeconds);
32
+ let cleaned = 0;
33
+
34
+ for (const agent of stale) {
35
+ // Fail active assignments
36
+ const orphaned = db.prepare(
37
+ `UPDATE coord_assignments SET status = 'failed', result = 'agent disconnected (stale)', completed_at = datetime('now')
38
+ WHERE agent_id = ? AND status IN ('assigned', 'in_progress')`
39
+ ).run(agent.id);
40
+
41
+ if (orphaned.changes > 0) {
42
+ db.prepare(
43
+ `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'assignment_failed', ?)`
44
+ ).run(agent.id, `auto-failed ${orphaned.changes} orphaned assignment(s) — agent stale`);
45
+ }
46
+
47
+ // Release locks
48
+ const locks = db.prepare(`DELETE FROM coord_locks WHERE agent_id = ?`).run(agent.id);
49
+
50
+ // Mark dead
51
+ db.prepare(`UPDATE coord_agents SET status = 'dead', current_task = NULL WHERE id = ?`).run(agent.id);
52
+
53
+ cleaned += orphaned.changes + locks.changes;
54
+
55
+ if (orphaned.changes > 0 || locks.changes > 0) {
56
+ db.prepare(
57
+ `INSERT INTO coord_events (agent_id, event_type, detail) VALUES (?, 'stale_cleanup', ?)`
58
+ ).run(agent.id, `failed ${orphaned.changes} assignment(s), released ${locks.changes} lock(s)`);
59
+ }
60
+ }
61
+
62
+ return { stale, cleaned };
63
+ }
64
+
65
+ /** Prune heartbeat events older than 1 hour. Keeps assignment, registered, and command events permanently. */
66
+ export function pruneOldHeartbeats(db: Database.Database): number {
67
+ const result = db.prepare(
68
+ `DELETE FROM coord_events WHERE event_type = 'heartbeat' AND created_at < datetime('now', '-1 hour')`
69
+ ).run();
70
+ return result.changes;
71
+ }
72
+
73
+ /** Purge dead agents older than 24 hours to prevent table bloat. */
74
+ export function purgeDeadAgents(db: Database.Database, maxAgeHours = 24): number {
75
+ const result = db.prepare(
76
+ `DELETE FROM coord_agents WHERE status = 'dead' AND last_seen < datetime('now', '-' || ? || ' hours')`
77
+ ).run(maxAgeHours);
78
+ return result.changes;
79
+ }
80
+
81
+ /** Clean slate on startup: mark all live agents dead, release locks, clear commands. */
82
+ export function cleanSlate(db: Database.Database): void {
83
+ const alive = db.prepare(
84
+ `SELECT id, name FROM coord_agents WHERE status != 'dead'`
85
+ ).all() as Array<{ id: string; name: string }>;
86
+
87
+ if (alive.length === 0) return;
88
+
89
+ for (const agent of alive) {
90
+ db.prepare(`UPDATE coord_agents SET status = 'dead', current_task = NULL WHERE id = ?`).run(agent.id);
91
+ db.prepare(`DELETE FROM coord_locks WHERE agent_id = ?`).run(agent.id);
92
+ }
93
+
94
+ db.prepare(`UPDATE coord_commands SET cleared_at = datetime('now') WHERE cleared_at IS NULL`).run();
95
+
96
+ console.log(` Coordination clean slate: marked ${alive.length} agent(s) from previous session as dead`);
97
+ }
package/src/core/decay.ts CHANGED
@@ -1,63 +1,63 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * ACT-R Base-Level Activation
5
- *
6
- * Based on Anderson's ACT-R cognitive architecture (1993).
7
- * Memories that are accessed more recently and more frequently
8
- * have higher activation — a well-established model of human memory.
9
- *
10
- * Formula: B(M) = ln(n + 1) - d * ln(ageDays / (n + 1))
11
- *
12
- * Where:
13
- * n = access count
14
- * d = decay exponent (default 0.5)
15
- * ageDays = age of memory in days
16
- */
17
-
18
- export function baseLevelActivation(
19
- accessCount: number,
20
- ageDays: number,
21
- decayExponent: number = 0.5
22
- ): number {
23
- const n = Math.max(accessCount, 0);
24
- const age = Math.max(ageDays, 0.001); // Avoid log(0)
25
- return Math.log(n + 1) - decayExponent * Math.log(age / (n + 1));
26
- }
27
-
28
- /**
29
- * Softplus — smooth approximation of ReLU.
30
- * Used to keep activation scores positive without hard clipping.
31
- */
32
- export function softplus(x: number): number {
33
- return Math.log(1 + Math.exp(x));
34
- }
35
-
36
- /**
37
- * Composite activation score combining content match, temporal decay,
38
- * Hebbian boost, and confidence.
39
- *
40
- * Score = contentMatch * softplus(B(M) + scale * hebbianBoost) * confidence
41
- */
42
- export function compositeScore(params: {
43
- contentMatch: number;
44
- accessCount: number;
45
- ageDays: number;
46
- hebbianBoost: number;
47
- confidence: number;
48
- decayExponent?: number;
49
- hebbianScale?: number;
50
- }): number {
51
- const {
52
- contentMatch,
53
- accessCount,
54
- ageDays,
55
- hebbianBoost,
56
- confidence,
57
- decayExponent = 0.5,
58
- hebbianScale = 1.0,
59
- } = params;
60
-
61
- const bm = baseLevelActivation(accessCount, ageDays, decayExponent);
62
- return contentMatch * softplus(bm + hebbianScale * hebbianBoost) * confidence;
63
- }
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * ACT-R Base-Level Activation
5
+ *
6
+ * Based on Anderson's ACT-R cognitive architecture (1993).
7
+ * Memories that are accessed more recently and more frequently
8
+ * have higher activation — a well-established model of human memory.
9
+ *
10
+ * Formula: B(M) = ln(n + 1) - d * ln(ageDays / (n + 1))
11
+ *
12
+ * Where:
13
+ * n = access count
14
+ * d = decay exponent (default 0.5)
15
+ * ageDays = age of memory in days
16
+ */
17
+
18
+ export function baseLevelActivation(
19
+ accessCount: number,
20
+ ageDays: number,
21
+ decayExponent: number = 0.5
22
+ ): number {
23
+ const n = Math.max(accessCount, 0);
24
+ const age = Math.max(ageDays, 0.001); // Avoid log(0)
25
+ return Math.log(n + 1) - decayExponent * Math.log(age / (n + 1));
26
+ }
27
+
28
+ /**
29
+ * Softplus — smooth approximation of ReLU.
30
+ * Used to keep activation scores positive without hard clipping.
31
+ */
32
+ export function softplus(x: number): number {
33
+ return Math.log(1 + Math.exp(x));
34
+ }
35
+
36
+ /**
37
+ * Composite activation score combining content match, temporal decay,
38
+ * Hebbian boost, and confidence.
39
+ *
40
+ * Score = contentMatch * softplus(B(M) + scale * hebbianBoost) * confidence
41
+ */
42
+ export function compositeScore(params: {
43
+ contentMatch: number;
44
+ accessCount: number;
45
+ ageDays: number;
46
+ hebbianBoost: number;
47
+ confidence: number;
48
+ decayExponent?: number;
49
+ hebbianScale?: number;
50
+ }): number {
51
+ const {
52
+ contentMatch,
53
+ accessCount,
54
+ ageDays,
55
+ hebbianBoost,
56
+ confidence,
57
+ decayExponent = 0.5,
58
+ hebbianScale = 1.0,
59
+ } = params;
60
+
61
+ const bm = baseLevelActivation(accessCount, ageDays, decayExponent);
62
+ return contentMatch * softplus(bm + hebbianScale * hebbianBoost) * confidence;
63
+ }
@@ -1,88 +1,88 @@
1
- // Copyright 2026 Robert Winter / Complete Ideas
2
- // SPDX-License-Identifier: Apache-2.0
3
- /**
4
- * Embedding Engine — local vector embeddings via transformers.js
5
- *
6
- * Default: bge-small-en-v1.5 (384 dimensions, ~90MB, MTEB retrieval-optimized).
7
- * Better short-text similarity than MiniLM for agent memory concepts.
8
- * Configurable via AWM_EMBED_MODEL env var.
9
- * Model is downloaded once on first use and cached locally.
10
- *
11
- * Singleton pattern — call getEmbedder() to get the shared instance.
12
- *
13
- * NOTE: Changing the model invalidates existing embeddings.
14
- * Set AWM_EMBED_MODEL=Xenova/all-MiniLM-L6-v2 for backward compatibility.
15
- */
16
-
17
- import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
18
-
19
- const MODEL_ID = process.env.AWM_EMBED_MODEL ?? 'Xenova/bge-small-en-v1.5';
20
- const DIMENSIONS = parseInt(process.env.AWM_EMBED_DIMS ?? '384', 10);
21
- const POOLING = (process.env.AWM_EMBED_POOLING ?? 'mean') as 'cls' | 'mean';
22
-
23
- let instance: FeatureExtractionPipeline | null = null;
24
- let initPromise: Promise<FeatureExtractionPipeline> | null = null;
25
-
26
- /**
27
- * Get or initialize the embedding pipeline (singleton).
28
- * First call downloads the model (~22MB), subsequent calls are instant.
29
- */
30
- export async function getEmbedder(): Promise<FeatureExtractionPipeline> {
31
- if (instance) return instance;
32
- if (initPromise) return initPromise;
33
-
34
- initPromise = pipeline('feature-extraction', MODEL_ID, {
35
- dtype: 'fp32',
36
- }).then(pipe => {
37
- instance = pipe;
38
- console.log(`Embedding model loaded: ${MODEL_ID} (${DIMENSIONS}d)`);
39
- return pipe;
40
- });
41
-
42
- return initPromise;
43
- }
44
-
45
- /**
46
- * Generate an embedding vector for a text string.
47
- * Returns a normalized float32 array of length DIMENSIONS.
48
- */
49
- export async function embed(text: string): Promise<number[]> {
50
- const embedder = await getEmbedder();
51
- const result = await embedder(text, { pooling: POOLING, normalize: true });
52
- // result is a Tensor — extract the data
53
- return Array.from(result.data as Float32Array).slice(0, DIMENSIONS);
54
- }
55
-
56
- /**
57
- * Generate embeddings for multiple texts in a batch.
58
- * More efficient than calling embed() in a loop.
59
- */
60
- export async function embedBatch(texts: string[]): Promise<number[][]> {
61
- if (texts.length === 0) return [];
62
- const embedder = await getEmbedder();
63
- const result = await embedder(texts, { pooling: POOLING, normalize: true });
64
- const data = result.data as Float32Array;
65
-
66
- const vectors: number[][] = [];
67
- for (let i = 0; i < texts.length; i++) {
68
- vectors.push(Array.from(data.slice(i * DIMENSIONS, (i + 1) * DIMENSIONS)));
69
- }
70
- return vectors;
71
- }
72
-
73
- /**
74
- * Cosine similarity between two normalized vectors.
75
- * Since vectors are pre-normalized, this is just the dot product.
76
- */
77
- export function cosineSimilarity(a: number[], b: number[]): number {
78
- if (a.length !== b.length || a.length === 0) return 0;
79
- let dot = 0;
80
- for (let i = 0; i < a.length; i++) {
81
- dot += a[i] * b[i];
82
- }
83
- // Clamp to [-1, 1] to handle floating point drift
84
- return Math.max(-1, Math.min(1, dot));
85
- }
86
-
87
- /** Vector dimensions for this model */
88
- export const EMBEDDING_DIMENSIONS = DIMENSIONS;
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Embedding Engine — local vector embeddings via transformers.js
5
+ *
6
+ * Default: bge-small-en-v1.5 (384 dimensions, ~90MB, MTEB retrieval-optimized).
7
+ * Better short-text similarity than MiniLM for agent memory concepts.
8
+ * Configurable via AWM_EMBED_MODEL env var.
9
+ * Model is downloaded once on first use and cached locally.
10
+ *
11
+ * Singleton pattern — call getEmbedder() to get the shared instance.
12
+ *
13
+ * NOTE: Changing the model invalidates existing embeddings.
14
+ * Set AWM_EMBED_MODEL=Xenova/all-MiniLM-L6-v2 for backward compatibility.
15
+ */
16
+
17
+ import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
18
+
19
+ const MODEL_ID = process.env.AWM_EMBED_MODEL ?? 'Xenova/bge-small-en-v1.5';
20
+ const DIMENSIONS = parseInt(process.env.AWM_EMBED_DIMS ?? '384', 10);
21
+ const POOLING = (process.env.AWM_EMBED_POOLING ?? 'mean') as 'cls' | 'mean';
22
+
23
+ let instance: FeatureExtractionPipeline | null = null;
24
+ let initPromise: Promise<FeatureExtractionPipeline> | null = null;
25
+
26
+ /**
27
+ * Get or initialize the embedding pipeline (singleton).
28
+ * First call downloads the model (~22MB), subsequent calls are instant.
29
+ */
30
+ export async function getEmbedder(): Promise<FeatureExtractionPipeline> {
31
+ if (instance) return instance;
32
+ if (initPromise) return initPromise;
33
+
34
+ initPromise = pipeline('feature-extraction', MODEL_ID, {
35
+ dtype: 'fp32',
36
+ }).then(pipe => {
37
+ instance = pipe;
38
+ console.log(`Embedding model loaded: ${MODEL_ID} (${DIMENSIONS}d)`);
39
+ return pipe;
40
+ });
41
+
42
+ return initPromise;
43
+ }
44
+
45
+ /**
46
+ * Generate an embedding vector for a text string.
47
+ * Returns a normalized float32 array of length DIMENSIONS.
48
+ */
49
+ export async function embed(text: string): Promise<number[]> {
50
+ const embedder = await getEmbedder();
51
+ const result = await embedder(text, { pooling: POOLING, normalize: true });
52
+ // result is a Tensor — extract the data
53
+ return Array.from(result.data as Float32Array).slice(0, DIMENSIONS);
54
+ }
55
+
56
+ /**
57
+ * Generate embeddings for multiple texts in a batch.
58
+ * More efficient than calling embed() in a loop.
59
+ */
60
+ export async function embedBatch(texts: string[]): Promise<number[][]> {
61
+ if (texts.length === 0) return [];
62
+ const embedder = await getEmbedder();
63
+ const result = await embedder(texts, { pooling: POOLING, normalize: true });
64
+ const data = result.data as Float32Array;
65
+
66
+ const vectors: number[][] = [];
67
+ for (let i = 0; i < texts.length; i++) {
68
+ vectors.push(Array.from(data.slice(i * DIMENSIONS, (i + 1) * DIMENSIONS)));
69
+ }
70
+ return vectors;
71
+ }
72
+
73
+ /**
74
+ * Cosine similarity between two normalized vectors.
75
+ * Since vectors are pre-normalized, this is just the dot product.
76
+ */
77
+ export function cosineSimilarity(a: number[], b: number[]): number {
78
+ if (a.length !== b.length || a.length === 0) return 0;
79
+ let dot = 0;
80
+ for (let i = 0; i < a.length; i++) {
81
+ dot += a[i] * b[i];
82
+ }
83
+ // Clamp to [-1, 1] to handle floating point drift
84
+ return Math.max(-1, Math.min(1, dot));
85
+ }
86
+
87
+ /** Vector dimensions for this model */
88
+ export const EMBEDDING_DIMENSIONS = DIMENSIONS;