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.
- package/README.md +87 -46
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +21 -5
- package/dist/api/routes.js.map +1 -1
- package/dist/cli.js +67 -67
- package/dist/coordination/index.d.ts +11 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +39 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/mcp-tools.d.ts +8 -0
- package/dist/coordination/mcp-tools.d.ts.map +1 -0
- package/dist/coordination/mcp-tools.js +216 -0
- package/dist/coordination/mcp-tools.js.map +1 -0
- package/dist/coordination/routes.d.ts +9 -0
- package/dist/coordination/routes.d.ts.map +1 -0
- package/dist/coordination/routes.js +434 -0
- package/dist/coordination/routes.js.map +1 -0
- package/dist/coordination/schema.d.ts +12 -0
- package/dist/coordination/schema.d.ts.map +1 -0
- package/dist/coordination/schema.js +91 -0
- package/dist/coordination/schema.js.map +1 -0
- package/dist/coordination/schemas.d.ts +208 -0
- package/dist/coordination/schemas.d.ts.map +1 -0
- package/dist/coordination/schemas.js +109 -0
- package/dist/coordination/schemas.js.map +1 -0
- package/dist/coordination/stale.d.ts +25 -0
- package/dist/coordination/stale.d.ts.map +1 -0
- package/dist/coordination/stale.js +53 -0
- package/dist/coordination/stale.js.map +1 -0
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +90 -79
- package/dist/mcp.js.map +1 -1
- package/dist/storage/sqlite.d.ts +3 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +285 -281
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +55 -55
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +551 -536
- package/src/cli.ts +397 -397
- package/src/coordination/index.ts +47 -0
- package/src/coordination/mcp-tools.ts +313 -0
- package/src/coordination/routes.ts +656 -0
- package/src/coordination/schema.ts +94 -0
- package/src/coordination/schemas.ts +136 -0
- package/src/coordination/stale.ts +89 -0
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +88 -88
- package/src/core/hebbian.ts +93 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/query-expander.ts +66 -66
- package/src/core/reranker.ts +101 -101
- package/src/engine/activation.ts +656 -656
- package/src/engine/connections.ts +103 -103
- package/src/engine/consolidation-scheduler.ts +125 -125
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +100 -100
- package/src/engine/staging.ts +74 -74
- package/src/index.ts +137 -121
- package/src/mcp.ts +1024 -1013
- package/src/storage/index.ts +3 -3
- package/src/storage/sqlite.ts +968 -963
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +217 -217
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
-- Coordination: assignments
|
|
28
|
+
CREATE TABLE IF NOT EXISTS coord_assignments (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
agent_id TEXT,
|
|
31
|
+
task TEXT NOT NULL,
|
|
32
|
+
description TEXT,
|
|
33
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
34
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
started_at TEXT,
|
|
36
|
+
completed_at TEXT,
|
|
37
|
+
result TEXT,
|
|
38
|
+
workspace TEXT,
|
|
39
|
+
FOREIGN KEY (agent_id) REFERENCES coord_agents(id)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
-- Coordination: file locks
|
|
43
|
+
CREATE TABLE IF NOT EXISTS coord_locks (
|
|
44
|
+
file_path TEXT PRIMARY KEY,
|
|
45
|
+
agent_id TEXT NOT NULL,
|
|
46
|
+
locked_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
47
|
+
reason TEXT,
|
|
48
|
+
FOREIGN KEY (agent_id) REFERENCES coord_agents(id)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- Coordination: orchestrator broadcast commands
|
|
52
|
+
CREATE TABLE IF NOT EXISTS coord_commands (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
command TEXT NOT NULL,
|
|
55
|
+
reason TEXT,
|
|
56
|
+
issued_by TEXT,
|
|
57
|
+
issued_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
58
|
+
cleared_at TEXT,
|
|
59
|
+
workspace TEXT
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
-- Coordination: findings reported by agents
|
|
63
|
+
CREATE TABLE IF NOT EXISTS coord_findings (
|
|
64
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
65
|
+
agent_id TEXT NOT NULL,
|
|
66
|
+
category TEXT NOT NULL,
|
|
67
|
+
severity TEXT NOT NULL DEFAULT 'info',
|
|
68
|
+
file_path TEXT,
|
|
69
|
+
line_number INTEGER,
|
|
70
|
+
description TEXT NOT NULL,
|
|
71
|
+
suggestion TEXT,
|
|
72
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
73
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
74
|
+
resolved_at TEXT,
|
|
75
|
+
FOREIGN KEY (agent_id) REFERENCES coord_agents(id)
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
-- Coordination: event audit trail
|
|
79
|
+
CREATE TABLE IF NOT EXISTS coord_events (
|
|
80
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
|
+
agent_id TEXT,
|
|
82
|
+
event_type TEXT NOT NULL,
|
|
83
|
+
detail TEXT,
|
|
84
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
85
|
+
);
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Create all coordination tables in the given database.
|
|
90
|
+
* Safe to call multiple times (CREATE IF NOT EXISTS).
|
|
91
|
+
*/
|
|
92
|
+
export function initCoordinationTables(db: Database.Database): void {
|
|
93
|
+
db.exec(COORDINATION_TABLES);
|
|
94
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
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
|
+
});
|
|
47
|
+
|
|
48
|
+
export const assignmentQuerySchema = z.object({
|
|
49
|
+
agentId: z.string().uuid().optional(),
|
|
50
|
+
workspace: z.string().max(50).optional(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const assignmentClaimSchema = z.object({
|
|
54
|
+
agentId: z.string().uuid(),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export const assignmentUpdateSchema = z.object({
|
|
58
|
+
status: assignmentStatusEnum,
|
|
59
|
+
result: z.string().max(10000).optional(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Locks ──────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
export const lockAcquireSchema = z.object({
|
|
65
|
+
agentId: z.string().uuid(),
|
|
66
|
+
filePath: z.string().min(1).max(500),
|
|
67
|
+
reason: z.string().max(500).optional(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const lockReleaseSchema = z.object({
|
|
71
|
+
agentId: z.string().uuid(),
|
|
72
|
+
filePath: z.string().min(1).max(500),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── Commands ───────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export const commandCreateSchema = z.object({
|
|
78
|
+
command: commandEnum,
|
|
79
|
+
reason: z.string().max(1000).optional(),
|
|
80
|
+
issuedBy: z.string().max(50).optional(),
|
|
81
|
+
workspace: z.string().max(50).optional(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const commandWaitQuerySchema = z.object({
|
|
85
|
+
status: z.string().max(20).default('idle'),
|
|
86
|
+
timeout: z.coerce.number().int().min(0).max(30).optional(),
|
|
87
|
+
agentId: z.string().optional(),
|
|
88
|
+
workspace: z.string().max(50).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─── Findings ───────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export const findingCreateSchema = z.object({
|
|
94
|
+
agentId: z.string().uuid(),
|
|
95
|
+
category: findingCategoryEnum,
|
|
96
|
+
severity: findingSeverityEnum.default('info'),
|
|
97
|
+
filePath: z.string().max(500).optional(),
|
|
98
|
+
lineNumber: z.number().int().positive().optional(),
|
|
99
|
+
description: z.string().min(1).max(5000),
|
|
100
|
+
suggestion: z.string().max(5000).optional(),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const findingsQuerySchema = z.object({
|
|
104
|
+
category: findingCategoryEnum.optional(),
|
|
105
|
+
severity: findingSeverityEnum.optional(),
|
|
106
|
+
status: findingStatusEnum.optional(),
|
|
107
|
+
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ─── Param Schemas ─────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
export const assignmentIdParamSchema = z.object({ id: z.string().uuid() });
|
|
113
|
+
export const findingIdParamSchema = z.object({ id: z.coerce.number().int().positive() });
|
|
114
|
+
|
|
115
|
+
// ─── Pulse ─────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
export const pulseSchema = z.object({
|
|
118
|
+
agentId: z.string().uuid(),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─── Status / Events ────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
export const eventsQuerySchema = z.object({
|
|
124
|
+
limit: z.coerce.number().int().min(1).max(200).default(50),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const staleQuerySchema = z.object({
|
|
128
|
+
seconds: z.coerce.number().int().min(1).max(86400).default(120),
|
|
129
|
+
cleanup: z.enum(['0', '1', 'true', 'false']).optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const workersQuerySchema = z.object({
|
|
133
|
+
capability: z.string().max(50).optional(),
|
|
134
|
+
status: agentStatusEnum.optional(),
|
|
135
|
+
workspace: z.string().max(50).optional(),
|
|
136
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
/** Clean slate on startup: mark all live agents dead, release locks, clear commands. */
|
|
74
|
+
export function cleanSlate(db: Database.Database): void {
|
|
75
|
+
const alive = db.prepare(
|
|
76
|
+
`SELECT id, name FROM coord_agents WHERE status != 'dead'`
|
|
77
|
+
).all() as Array<{ id: string; name: string }>;
|
|
78
|
+
|
|
79
|
+
if (alive.length === 0) return;
|
|
80
|
+
|
|
81
|
+
for (const agent of alive) {
|
|
82
|
+
db.prepare(`UPDATE coord_agents SET status = 'dead', current_task = NULL WHERE id = ?`).run(agent.id);
|
|
83
|
+
db.prepare(`DELETE FROM coord_locks WHERE agent_id = ?`).run(agent.id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
db.prepare(`UPDATE coord_commands SET cleared_at = datetime('now') WHERE cleared_at IS NULL`).run();
|
|
87
|
+
|
|
88
|
+
console.log(` Coordination clean slate: marked ${alive.length} agent(s) from previous session as dead`);
|
|
89
|
+
}
|
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
|
+
}
|
package/src/core/embeddings.ts
CHANGED
|
@@ -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;
|