agent-working-memory 0.7.16 → 0.8.5
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 +156 -15
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +2 -16
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +2 -11
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/common.d.ts +18 -0
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +254 -141
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/cursor.js +2 -15
- package/dist/adapters/cursor.js.map +1 -1
- package/dist/adapters/http.d.ts.map +1 -1
- package/dist/adapters/http.js +28 -34
- package/dist/adapters/http.js.map +1 -1
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +280 -113
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.d.ts +42 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +268 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli.js +45 -1
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.d.ts +24 -0
- package/dist/coordination/circuit-breaker.d.ts.map +1 -0
- package/dist/coordination/circuit-breaker.js +70 -0
- package/dist/coordination/circuit-breaker.js.map +1 -0
- package/dist/coordination/failure-modes.d.ts +21 -0
- package/dist/coordination/failure-modes.d.ts.map +1 -0
- package/dist/coordination/failure-modes.js +49 -0
- package/dist/coordination/failure-modes.js.map +1 -0
- package/dist/coordination/peer-decisions.js +7 -7
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +44 -1
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +18 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +9 -9
- package/dist/coordination/stale.d.ts +9 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +53 -9
- package/dist/coordination/stale.js.map +1 -1
- package/dist/core/embeddings.d.ts +16 -10
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +54 -38
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/ml-worker-entry.d.ts +17 -0
- package/dist/core/ml-worker-entry.d.ts.map +1 -0
- package/dist/core/ml-worker-entry.js +193 -0
- package/dist/core/ml-worker-entry.js.map +1 -0
- package/dist/core/ml-worker.d.ts +59 -0
- package/dist/core/ml-worker.d.ts.map +1 -0
- package/dist/core/ml-worker.js +253 -0
- package/dist/core/ml-worker.js.map +1 -0
- package/dist/core/query-expander.d.ts +13 -15
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +48 -54
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts +9 -19
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +33 -35
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.d.ts +32 -5
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +201 -28
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts +128 -0
- package/dist/core/write-pipeline.d.ts.map +1 -0
- package/dist/core/write-pipeline.js +405 -0
- package/dist/core/write-pipeline.js.map +1 -0
- package/dist/engine/activation.d.ts +2 -2
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +238 -115
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/confidence.d.ts +62 -0
- package/dist/engine/confidence.d.ts.map +1 -0
- package/dist/engine/confidence.js +100 -0
- package/dist/engine/confidence.js.map +1 -0
- package/dist/engine/connections.d.ts +47 -8
- package/dist/engine/connections.d.ts.map +1 -1
- package/dist/engine/connections.js +83 -22
- package/dist/engine/connections.js.map +1 -1
- package/dist/engine/consolidation-scheduler.d.ts +46 -10
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
- package/dist/engine/consolidation-scheduler.js +170 -50
- package/dist/engine/consolidation-scheduler.js.map +1 -1
- package/dist/engine/consolidation.d.ts +16 -2
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +143 -44
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/eval.d.ts +2 -2
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +9 -9
- package/dist/engine/eval.js.map +1 -1
- package/dist/engine/eviction.d.ts +4 -4
- package/dist/engine/eviction.d.ts.map +1 -1
- package/dist/engine/eviction.js +15 -15
- package/dist/engine/eviction.js.map +1 -1
- package/dist/engine/retraction.d.ts +93 -5
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +238 -19
- package/dist/engine/retraction.js.map +1 -1
- package/dist/engine/staging.d.ts +1 -1
- package/dist/engine/staging.d.ts.map +1 -1
- package/dist/engine/staging.js +3 -3
- package/dist/engine/staging.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +1 -1
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +4 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +48 -31
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +110 -155
- package/dist/mcp.js.map +1 -1
- package/dist/storage/factory.d.ts +55 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +135 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/pglite-schema.d.ts +20 -0
- package/dist/storage/pglite-schema.d.ts.map +1 -0
- package/dist/storage/pglite-schema.js +165 -0
- package/dist/storage/pglite-schema.js.map +1 -0
- package/dist/storage/pglite.d.ts +187 -0
- package/dist/storage/pglite.d.ts.map +1 -0
- package/dist/storage/pglite.js +1114 -0
- package/dist/storage/pglite.js.map +1 -0
- package/dist/storage/sqlite.d.ts +117 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +357 -8
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.d.ts +63 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +27 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/types/engram.d.ts +92 -2
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +3 -1
- package/src/adapters/claude-code.ts +218 -234
- package/src/adapters/codex.ts +252 -262
- package/src/adapters/common.ts +411 -284
- package/src/adapters/cursor.ts +135 -150
- package/src/adapters/http.ts +93 -100
- package/src/adapters/index.ts +31 -31
- package/src/adapters/types.ts +75 -75
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +367 -120
- package/src/cli/migrate.ts +307 -0
- package/src/cli.ts +37 -1
- package/src/coordination/circuit-breaker.ts +83 -0
- package/src/coordination/events.ts +90 -90
- package/src/coordination/failure-modes.ts +50 -0
- package/src/coordination/peer-decisions.ts +105 -105
- package/src/coordination/plugin-loader.ts +60 -60
- package/src/coordination/plugin.ts +44 -44
- package/src/coordination/routes.ts +56 -1
- package/src/coordination/schema.ts +13 -0
- package/src/coordination/stale.ts +80 -11
- package/src/coordination/types.ts +311 -311
- package/src/coordination/write-mutex.ts +69 -69
- package/src/core/auto-tagger.ts +168 -168
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -0
- package/src/core/ml-worker.ts +281 -0
- package/src/core/query-expander.ts +122 -128
- package/src/core/reranker.ts +119 -125
- package/src/core/salience.ts +200 -33
- package/src/core/write-pipeline.ts +521 -0
- package/src/engine/activation.ts +235 -109
- package/src/engine/confidence.ts +120 -0
- package/src/engine/connections.ts +162 -103
- package/src/engine/consolidation-scheduler.ts +242 -125
- package/src/engine/consolidation.ts +138 -45
- 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 +366 -116
- package/src/engine/staging.ts +74 -74
- package/src/hooks/sidecar.ts +5 -5
- package/src/index.ts +42 -28
- package/src/mcp.ts +105 -152
- package/src/storage/factory.ts +147 -0
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -0
- package/src/storage/pglite.ts +1363 -0
- package/src/storage/sqlite.ts +402 -7
- package/src/storage/store.ts +80 -0
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +101 -3
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
|
@@ -1,105 +1,105 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Peer-decision injection for memory_recall.
|
|
5
|
-
*
|
|
6
|
-
* When coordination is enabled, memory_recall appends recent decisions
|
|
7
|
-
* made by OTHER agents that are relevant to the current recall query.
|
|
8
|
-
* This enables passive cross-agent knowledge sharing without explicit
|
|
9
|
-
* communication.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import type Database from 'better-sqlite3';
|
|
13
|
-
|
|
14
|
-
export interface PeerDecision {
|
|
15
|
-
id: number;
|
|
16
|
-
author_name: string;
|
|
17
|
-
assignment_id: string | null;
|
|
18
|
-
tags: string | null;
|
|
19
|
-
summary: string;
|
|
20
|
-
created_at: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Stop words filtered out before keyword matching. */
|
|
24
|
-
const STOP_WORDS = new Set([
|
|
25
|
-
'that', 'this', 'with', 'from', 'have', 'been', 'will', 'when', 'what',
|
|
26
|
-
'which', 'then', 'than', 'into', 'over', 'after', 'some', 'more', 'also',
|
|
27
|
-
'most', 'other', 'each', 'such', 'only', 'just', 'about', 'there', 'their',
|
|
28
|
-
'where', 'would', 'could', 'should', 'these', 'those', 'make', 'made',
|
|
29
|
-
'using', 'used', 'call', 'calls', 'does', 'done', 'were', 'they',
|
|
30
|
-
]);
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Extract significant keywords from a recall query for relevance matching.
|
|
34
|
-
* Returns up to maxKeywords words of length >= 4 that are not stop words.
|
|
35
|
-
*/
|
|
36
|
-
export function extractKeywords(query: string, maxKeywords = 8): string[] {
|
|
37
|
-
return query
|
|
38
|
-
.toLowerCase()
|
|
39
|
-
.replace(/[^a-z0-9\s]/g, ' ')
|
|
40
|
-
.split(/\s+/)
|
|
41
|
-
.filter(w => w.length >= 4 && !STOP_WORDS.has(w))
|
|
42
|
-
.slice(0, maxKeywords);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Query coord_decisions for recent decisions by agents OTHER than the current one
|
|
47
|
-
* that are relevant to the given query string.
|
|
48
|
-
*
|
|
49
|
-
* @param db The shared coordination database handle.
|
|
50
|
-
* @param selfName The current agent's name (AWM_AGENT_ID / WORKER_NAME). Excluded from results.
|
|
51
|
-
* @param query The recall query text used for keyword relevance matching.
|
|
52
|
-
* @param windowHours How far back to look (default 1 hour).
|
|
53
|
-
* @param limit Maximum number of decisions to return (default 5).
|
|
54
|
-
* @returns Array of peer decisions, ordered newest-first. Empty array on any error.
|
|
55
|
-
*/
|
|
56
|
-
export function queryPeerDecisions(
|
|
57
|
-
db: Database.Database,
|
|
58
|
-
selfName: string,
|
|
59
|
-
query: string,
|
|
60
|
-
windowHours = 1,
|
|
61
|
-
limit = 5,
|
|
62
|
-
): PeerDecision[] {
|
|
63
|
-
try {
|
|
64
|
-
const safeHours = Math.max(0.1, Math.min(windowHours, 168)); // 6min – 1 week
|
|
65
|
-
const safeLimit = Math.max(1, Math.min(limit, 50));
|
|
66
|
-
const keywords = extractKeywords(query);
|
|
67
|
-
|
|
68
|
-
const baseWhere = `
|
|
69
|
-
d.created_at >= datetime('now', '-${safeHours} hours')
|
|
70
|
-
AND a.name != ?
|
|
71
|
-
`;
|
|
72
|
-
|
|
73
|
-
const select = `
|
|
74
|
-
SELECT d.id, a.name AS author_name, d.assignment_id, d.tags, d.summary, d.created_at
|
|
75
|
-
FROM coord_decisions d
|
|
76
|
-
JOIN coord_agents a ON d.author_id = a.id
|
|
77
|
-
`;
|
|
78
|
-
|
|
79
|
-
if (keywords.length === 0) {
|
|
80
|
-
// No useful keywords — return the N most recent decisions from other agents
|
|
81
|
-
const sql = `${select} WHERE ${baseWhere} ORDER BY d.created_at DESC LIMIT ?`;
|
|
82
|
-
return db.prepare(sql).all(selfName, safeLimit) as PeerDecision[];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Build keyword filter: match any keyword in summary or tags
|
|
86
|
-
const kwClauses = keywords.map(() => `(d.summary LIKE ? OR d.tags LIKE ?)`).join(' OR ');
|
|
87
|
-
const sql = `${select} WHERE ${baseWhere} AND (${kwClauses}) ORDER BY d.created_at DESC LIMIT ?`;
|
|
88
|
-
const kwParams = keywords.flatMap(kw => [`%${kw}%`, `%${kw}%`]);
|
|
89
|
-
|
|
90
|
-
return db.prepare(sql).all(selfName, ...kwParams, safeLimit) as PeerDecision[];
|
|
91
|
-
} catch {
|
|
92
|
-
return []; // non-fatal — peer decisions are best-effort
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Format peer decisions as a text section to append to memory_recall output.
|
|
98
|
-
* Returns an empty string when there are no peer decisions.
|
|
99
|
-
*/
|
|
100
|
-
export function formatPeerDecisions(decisions: PeerDecision[], windowHours = 1): string {
|
|
101
|
-
if (decisions.length === 0) return '';
|
|
102
|
-
const label = windowHours === 1 ? '1h' : `${windowHours}h`;
|
|
103
|
-
const lines = decisions.map(d => `[${d.author_name}] ${d.summary}`);
|
|
104
|
-
return `\n--- Peer Decisions (last ${label}) ---\n${lines.join('\n')}`;
|
|
105
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Peer-decision injection for memory_recall.
|
|
5
|
+
*
|
|
6
|
+
* When coordination is enabled, memory_recall appends recent decisions
|
|
7
|
+
* made by OTHER agents that are relevant to the current recall query.
|
|
8
|
+
* This enables passive cross-agent knowledge sharing without explicit
|
|
9
|
+
* communication.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type Database from 'better-sqlite3';
|
|
13
|
+
|
|
14
|
+
export interface PeerDecision {
|
|
15
|
+
id: number;
|
|
16
|
+
author_name: string;
|
|
17
|
+
assignment_id: string | null;
|
|
18
|
+
tags: string | null;
|
|
19
|
+
summary: string;
|
|
20
|
+
created_at: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Stop words filtered out before keyword matching. */
|
|
24
|
+
const STOP_WORDS = new Set([
|
|
25
|
+
'that', 'this', 'with', 'from', 'have', 'been', 'will', 'when', 'what',
|
|
26
|
+
'which', 'then', 'than', 'into', 'over', 'after', 'some', 'more', 'also',
|
|
27
|
+
'most', 'other', 'each', 'such', 'only', 'just', 'about', 'there', 'their',
|
|
28
|
+
'where', 'would', 'could', 'should', 'these', 'those', 'make', 'made',
|
|
29
|
+
'using', 'used', 'call', 'calls', 'does', 'done', 'were', 'they',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract significant keywords from a recall query for relevance matching.
|
|
34
|
+
* Returns up to maxKeywords words of length >= 4 that are not stop words.
|
|
35
|
+
*/
|
|
36
|
+
export function extractKeywords(query: string, maxKeywords = 8): string[] {
|
|
37
|
+
return query
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
40
|
+
.split(/\s+/)
|
|
41
|
+
.filter(w => w.length >= 4 && !STOP_WORDS.has(w))
|
|
42
|
+
.slice(0, maxKeywords);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Query coord_decisions for recent decisions by agents OTHER than the current one
|
|
47
|
+
* that are relevant to the given query string.
|
|
48
|
+
*
|
|
49
|
+
* @param db The shared coordination database handle.
|
|
50
|
+
* @param selfName The current agent's name (AWM_AGENT_ID / WORKER_NAME). Excluded from results.
|
|
51
|
+
* @param query The recall query text used for keyword relevance matching.
|
|
52
|
+
* @param windowHours How far back to look (default 1 hour).
|
|
53
|
+
* @param limit Maximum number of decisions to return (default 5).
|
|
54
|
+
* @returns Array of peer decisions, ordered newest-first. Empty array on any error.
|
|
55
|
+
*/
|
|
56
|
+
export function queryPeerDecisions(
|
|
57
|
+
db: Database.Database,
|
|
58
|
+
selfName: string,
|
|
59
|
+
query: string,
|
|
60
|
+
windowHours = 1,
|
|
61
|
+
limit = 5,
|
|
62
|
+
): PeerDecision[] {
|
|
63
|
+
try {
|
|
64
|
+
const safeHours = Math.max(0.1, Math.min(windowHours, 168)); // 6min – 1 week
|
|
65
|
+
const safeLimit = Math.max(1, Math.min(limit, 50));
|
|
66
|
+
const keywords = extractKeywords(query);
|
|
67
|
+
|
|
68
|
+
const baseWhere = `
|
|
69
|
+
d.created_at >= datetime('now', '-${safeHours} hours')
|
|
70
|
+
AND a.name != ?
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
const select = `
|
|
74
|
+
SELECT d.id, a.name AS author_name, d.assignment_id, d.tags, d.summary, d.created_at
|
|
75
|
+
FROM coord_decisions d
|
|
76
|
+
JOIN coord_agents a ON d.author_id = a.id
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
if (keywords.length === 0) {
|
|
80
|
+
// No useful keywords — return the N most recent decisions from other agents
|
|
81
|
+
const sql = `${select} WHERE ${baseWhere} ORDER BY d.created_at DESC LIMIT ?`;
|
|
82
|
+
return db.prepare(sql).all(selfName, safeLimit) as PeerDecision[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build keyword filter: match any keyword in summary or tags
|
|
86
|
+
const kwClauses = keywords.map(() => `(d.summary LIKE ? OR d.tags LIKE ?)`).join(' OR ');
|
|
87
|
+
const sql = `${select} WHERE ${baseWhere} AND (${kwClauses}) ORDER BY d.created_at DESC LIMIT ?`;
|
|
88
|
+
const kwParams = keywords.flatMap(kw => [`%${kw}%`, `%${kw}%`]);
|
|
89
|
+
|
|
90
|
+
return db.prepare(sql).all(selfName, ...kwParams, safeLimit) as PeerDecision[];
|
|
91
|
+
} catch {
|
|
92
|
+
return []; // non-fatal — peer decisions are best-effort
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Format peer decisions as a text section to append to memory_recall output.
|
|
98
|
+
* Returns an empty string when there are no peer decisions.
|
|
99
|
+
*/
|
|
100
|
+
export function formatPeerDecisions(decisions: PeerDecision[], windowHours = 1): string {
|
|
101
|
+
if (decisions.length === 0) return '';
|
|
102
|
+
const label = windowHours === 1 ? '1h' : `${windowHours}h`;
|
|
103
|
+
const lines = decisions.map(d => `[${d.author_name}] ${d.summary}`);
|
|
104
|
+
return `\n--- Peer Decisions (last ${label}) ---\n${lines.join('\n')}`;
|
|
105
|
+
}
|
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Plugin loader for AWM coordination.
|
|
5
|
-
*
|
|
6
|
-
* Reads AWM_PLUGINS env var (comma-separated module paths or package names),
|
|
7
|
-
* dynamically imports each, and calls register() with the plugin context.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* AWM_PLUGINS=./plugins/slack-notify.js,my-awm-plugin npm start
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { AWMPlugin, AWMPluginContext } from './plugin.js';
|
|
14
|
-
|
|
15
|
-
/** Loaded plugin instances (for teardown). */
|
|
16
|
-
const loadedPlugins: AWMPlugin[] = [];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Load and register all plugins specified in AWM_PLUGINS env var.
|
|
20
|
-
* Errors in individual plugins are logged but don't prevent other plugins from loading.
|
|
21
|
-
*/
|
|
22
|
-
export async function loadPlugins(ctx: AWMPluginContext): Promise<void> {
|
|
23
|
-
const pluginList = process.env.AWM_PLUGINS;
|
|
24
|
-
if (!pluginList) return;
|
|
25
|
-
|
|
26
|
-
const paths = pluginList
|
|
27
|
-
.split(',')
|
|
28
|
-
.map((p) => p.trim())
|
|
29
|
-
.filter(Boolean);
|
|
30
|
-
|
|
31
|
-
for (const modulePath of paths) {
|
|
32
|
-
try {
|
|
33
|
-
const mod = await import(modulePath);
|
|
34
|
-
const plugin: AWMPlugin = mod.default ?? mod;
|
|
35
|
-
|
|
36
|
-
if (!plugin.name || typeof plugin.register !== 'function') {
|
|
37
|
-
console.warn(` [plugin] Skipping ${modulePath}: missing name or register()`);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await plugin.register(ctx);
|
|
42
|
-
loadedPlugins.push(plugin);
|
|
43
|
-
console.log(` [plugin] Loaded: ${plugin.name}`);
|
|
44
|
-
} catch (err) {
|
|
45
|
-
console.error(` [plugin] Failed to load ${modulePath}:`, (err as Error).message);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Teardown all loaded plugins (call on shutdown). */
|
|
51
|
-
export async function teardownPlugins(): Promise<void> {
|
|
52
|
-
for (const plugin of loadedPlugins) {
|
|
53
|
-
try {
|
|
54
|
-
await plugin.teardown?.();
|
|
55
|
-
} catch (err) {
|
|
56
|
-
console.error(` [plugin] Teardown error (${plugin.name}):`, (err as Error).message);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
loadedPlugins.length = 0;
|
|
60
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Plugin loader for AWM coordination.
|
|
5
|
+
*
|
|
6
|
+
* Reads AWM_PLUGINS env var (comma-separated module paths or package names),
|
|
7
|
+
* dynamically imports each, and calls register() with the plugin context.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* AWM_PLUGINS=./plugins/slack-notify.js,my-awm-plugin npm start
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { AWMPlugin, AWMPluginContext } from './plugin.js';
|
|
14
|
+
|
|
15
|
+
/** Loaded plugin instances (for teardown). */
|
|
16
|
+
const loadedPlugins: AWMPlugin[] = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load and register all plugins specified in AWM_PLUGINS env var.
|
|
20
|
+
* Errors in individual plugins are logged but don't prevent other plugins from loading.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadPlugins(ctx: AWMPluginContext): Promise<void> {
|
|
23
|
+
const pluginList = process.env.AWM_PLUGINS;
|
|
24
|
+
if (!pluginList) return;
|
|
25
|
+
|
|
26
|
+
const paths = pluginList
|
|
27
|
+
.split(',')
|
|
28
|
+
.map((p) => p.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
|
|
31
|
+
for (const modulePath of paths) {
|
|
32
|
+
try {
|
|
33
|
+
const mod = await import(modulePath);
|
|
34
|
+
const plugin: AWMPlugin = mod.default ?? mod;
|
|
35
|
+
|
|
36
|
+
if (!plugin.name || typeof plugin.register !== 'function') {
|
|
37
|
+
console.warn(` [plugin] Skipping ${modulePath}: missing name or register()`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await plugin.register(ctx);
|
|
42
|
+
loadedPlugins.push(plugin);
|
|
43
|
+
console.log(` [plugin] Loaded: ${plugin.name}`);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(` [plugin] Failed to load ${modulePath}:`, (err as Error).message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Teardown all loaded plugins (call on shutdown). */
|
|
51
|
+
export async function teardownPlugins(): Promise<void> {
|
|
52
|
+
for (const plugin of loadedPlugins) {
|
|
53
|
+
try {
|
|
54
|
+
await plugin.teardown?.();
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error(` [plugin] Teardown error (${plugin.name}):`, (err as Error).message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
loadedPlugins.length = 0;
|
|
60
|
+
}
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* AWM Plugin contract.
|
|
5
|
-
*
|
|
6
|
-
* Plugins extend coordination with custom behavior by subscribing to
|
|
7
|
-
* events, adding routes, or querying the DB. They are loaded at startup
|
|
8
|
-
* via AWM_PLUGINS env var (comma-separated module paths).
|
|
9
|
-
*
|
|
10
|
-
* Example plugin:
|
|
11
|
-
* import type { AWMPlugin } from 'agent-working-memory';
|
|
12
|
-
* export default {
|
|
13
|
-
* name: 'my-plugin',
|
|
14
|
-
* register(ctx) {
|
|
15
|
-
* ctx.events.on('assignment.completed', (evt) => {
|
|
16
|
-
* console.log(`Task done: ${evt.assignmentId}`);
|
|
17
|
-
* });
|
|
18
|
-
* },
|
|
19
|
-
* } satisfies AWMPlugin;
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import type { FastifyInstance } from 'fastify';
|
|
23
|
-
import type Database from 'better-sqlite3';
|
|
24
|
-
import type { CoordinationEventBus } from './events.js';
|
|
25
|
-
|
|
26
|
-
/** Context passed to plugin register(). */
|
|
27
|
-
export interface AWMPluginContext {
|
|
28
|
-
/** Typed event bus — subscribe to coordination events. */
|
|
29
|
-
events: CoordinationEventBus;
|
|
30
|
-
/** Raw better-sqlite3 database handle (coordination tables). */
|
|
31
|
-
db: Database.Database;
|
|
32
|
-
/** Fastify instance — add custom routes if needed. */
|
|
33
|
-
fastify: FastifyInstance;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Plugin contract. Every AWM plugin must export this shape. */
|
|
37
|
-
export interface AWMPlugin {
|
|
38
|
-
/** Unique plugin name (for logging and dedup). */
|
|
39
|
-
name: string;
|
|
40
|
-
/** Called once at startup with the plugin context. */
|
|
41
|
-
register(ctx: AWMPluginContext): void | Promise<void>;
|
|
42
|
-
/** Optional cleanup on shutdown. */
|
|
43
|
-
teardown?(): void | Promise<void>;
|
|
44
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* AWM Plugin contract.
|
|
5
|
+
*
|
|
6
|
+
* Plugins extend coordination with custom behavior by subscribing to
|
|
7
|
+
* events, adding routes, or querying the DB. They are loaded at startup
|
|
8
|
+
* via AWM_PLUGINS env var (comma-separated module paths).
|
|
9
|
+
*
|
|
10
|
+
* Example plugin:
|
|
11
|
+
* import type { AWMPlugin } from 'agent-working-memory';
|
|
12
|
+
* export default {
|
|
13
|
+
* name: 'my-plugin',
|
|
14
|
+
* register(ctx) {
|
|
15
|
+
* ctx.events.on('assignment.completed', (evt) => {
|
|
16
|
+
* console.log(`Task done: ${evt.assignmentId}`);
|
|
17
|
+
* });
|
|
18
|
+
* },
|
|
19
|
+
* } satisfies AWMPlugin;
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { FastifyInstance } from 'fastify';
|
|
23
|
+
import type Database from 'better-sqlite3';
|
|
24
|
+
import type { CoordinationEventBus } from './events.js';
|
|
25
|
+
|
|
26
|
+
/** Context passed to plugin register(). */
|
|
27
|
+
export interface AWMPluginContext {
|
|
28
|
+
/** Typed event bus — subscribe to coordination events. */
|
|
29
|
+
events: CoordinationEventBus;
|
|
30
|
+
/** Raw better-sqlite3 database handle (coordination tables). */
|
|
31
|
+
db: Database.Database;
|
|
32
|
+
/** Fastify instance — add custom routes if needed. */
|
|
33
|
+
fastify: FastifyInstance;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Plugin contract. Every AWM plugin must export this shape. */
|
|
37
|
+
export interface AWMPlugin {
|
|
38
|
+
/** Unique plugin name (for logging and dedup). */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Called once at startup with the plugin context. */
|
|
41
|
+
register(ctx: AWMPluginContext): void | Promise<void>;
|
|
42
|
+
/** Optional cleanup on shutdown. */
|
|
43
|
+
teardown?(): void | Promise<void>;
|
|
44
|
+
}
|
|
@@ -21,7 +21,9 @@ import {
|
|
|
21
21
|
agentIdParamSchema, timelineQuerySchema,
|
|
22
22
|
channelRegisterSchema, channelDeregisterSchema, channelPushSchema,
|
|
23
23
|
} from './schemas.js';
|
|
24
|
-
import { detectStale, cleanupStale } from './stale.js';
|
|
24
|
+
import { detectStale, cleanupStale, retryOrFailAssignment } from './stale.js';
|
|
25
|
+
import { classifyFailure, FailureMode } from './failure-modes.js';
|
|
26
|
+
import { recordSuccess, recordFailure as circuitRecordFailure, isAvailable } from './circuit-breaker.js';
|
|
25
27
|
|
|
26
28
|
/** Pretty timestamp for coordination logs. */
|
|
27
29
|
function ts(): string {
|
|
@@ -444,6 +446,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
|
|
|
444
446
|
`SELECT * FROM coord_assignments WHERE status = 'pending' AND agent_id IS NULL ${blockedFilter} ORDER BY priority DESC, created_at ASC LIMIT 1`
|
|
445
447
|
).get() as { id: string } | undefined);
|
|
446
448
|
|
|
449
|
+
// Circuit breaker: refuse assignment if worker is in open state
|
|
450
|
+
if (pending && !isAvailable(db, agentId)) {
|
|
451
|
+
return reply.code(423).send({ status: 'idle', assignment: null, circuit_open: true, reason: 'circuit_open' });
|
|
452
|
+
}
|
|
453
|
+
|
|
447
454
|
if (pending) {
|
|
448
455
|
const claimed = db.prepare(
|
|
449
456
|
`UPDATE coord_assignments SET agent_id = ?, status = 'assigned', started_at = datetime('now') WHERE id = ? AND status = 'pending'`
|
|
@@ -724,6 +731,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
|
|
|
724
731
|
`SELECT * FROM coord_assignments WHERE status = 'pending' AND agent_id IS NULL ${blockedFilter} ORDER BY priority DESC, created_at ASC LIMIT 1`
|
|
725
732
|
).get() as { id: string } | undefined);
|
|
726
733
|
|
|
734
|
+
// Circuit breaker: refuse assignment if worker is in open state
|
|
735
|
+
if (pending && !isAvailable(db, agentId)) {
|
|
736
|
+
return reply.code(423).send({ assignment: null, circuit_open: true, reason: 'circuit_open' });
|
|
737
|
+
}
|
|
738
|
+
|
|
727
739
|
if (pending) {
|
|
728
740
|
const claimed = db.prepare(
|
|
729
741
|
`UPDATE coord_assignments SET agent_id = ?, status = 'assigned', started_at = datetime('now') WHERE id = ? AND status = 'pending'`
|
|
@@ -847,6 +859,12 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
|
|
|
847
859
|
coordLog(`${assignInfo?.agent_name ?? 'unknown'} ${status}: ${assignInfo?.task?.slice(0, 80) ?? id}`);
|
|
848
860
|
}
|
|
849
861
|
|
|
862
|
+
// Circuit breaker: track success/failure per worker
|
|
863
|
+
if (assignInfo?.agent_id) {
|
|
864
|
+
if (status === 'completed') recordSuccess(db, assignInfo.agent_id);
|
|
865
|
+
else if (status === 'failed') circuitRecordFailure(db, assignInfo.agent_id);
|
|
866
|
+
}
|
|
867
|
+
|
|
850
868
|
// Emit events
|
|
851
869
|
eventBus?.emit('assignment.updated', { assignmentId: id, agentId: assignInfo?.agent_id ?? null, status, result });
|
|
852
870
|
if (status === 'completed') {
|
|
@@ -1011,6 +1029,38 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
|
|
|
1011
1029
|
return reply.send({ ok: true });
|
|
1012
1030
|
});
|
|
1013
1031
|
|
|
1032
|
+
/** POST /assignment/:id/fail — Worker voluntarily fails an assignment with retry logic.
|
|
1033
|
+
* Body: { result: string, mode?: FailureMode }
|
|
1034
|
+
* Returns: { outcome: 'retried' | 'failed', attempt_count, last_failure_mode }
|
|
1035
|
+
*/
|
|
1036
|
+
app.post('/assignment/:id/fail', async (req, reply) => {
|
|
1037
|
+
const { id } = assignmentIdParamSchema.parse(req.params);
|
|
1038
|
+
const body = req.body as { result?: string; mode?: string } | undefined;
|
|
1039
|
+
const result = body?.result ?? 'worker-initiated failure';
|
|
1040
|
+
const mode = body?.mode as FailureMode | undefined;
|
|
1041
|
+
|
|
1042
|
+
const row = db.prepare(
|
|
1043
|
+
`SELECT id, agent_id, status FROM coord_assignments WHERE id = ?`
|
|
1044
|
+
).get(id) as { id: string; agent_id: string | null; status: string } | undefined;
|
|
1045
|
+
|
|
1046
|
+
if (!row) return reply.code(404).send({ error: 'assignment not found' });
|
|
1047
|
+
if (['completed', 'failed'].includes(row.status)) {
|
|
1048
|
+
return reply.code(400).send({ error: `cannot fail a ${row.status} assignment` });
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const agentId = row.agent_id ?? 'unknown';
|
|
1052
|
+
const outcome = retryOrFailAssignment(db, id, agentId, result, mode);
|
|
1053
|
+
|
|
1054
|
+
// Circuit breaker: record failure on voluntary fail
|
|
1055
|
+
if (agentId !== 'unknown') circuitRecordFailure(db, agentId);
|
|
1056
|
+
|
|
1057
|
+
const updated = db.prepare(
|
|
1058
|
+
`SELECT attempt_count, last_failure_mode FROM coord_assignments WHERE id = ?`
|
|
1059
|
+
).get(id) as { attempt_count: number; last_failure_mode: string | null } | undefined;
|
|
1060
|
+
|
|
1061
|
+
return reply.send({ ok: true, outcome, attempt_count: updated?.attempt_count ?? 0, last_failure_mode: updated?.last_failure_mode ?? null });
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1014
1064
|
app.put('/assignment/:id', async (req, reply) => {
|
|
1015
1065
|
const { id } = assignmentIdParamSchema.parse(req.params);
|
|
1016
1066
|
const parsed = assignmentUpdateSchema.safeParse(req.body);
|
|
@@ -1961,6 +2011,11 @@ export function registerCoordinationRoutes(app: FastifyInstance, db: Database.Da
|
|
|
1961
2011
|
const agent = db.prepare(`SELECT name, workspace FROM coord_agents WHERE id = ?`).get(agentId) as { name: string; workspace: string | null } | undefined;
|
|
1962
2012
|
if (!agent) return reply.status(404).send({ error: 'Agent not found' });
|
|
1963
2013
|
|
|
2014
|
+
// Circuit breaker: refuse push to open-circuit workers
|
|
2015
|
+
if (!isAvailable(db, agentId)) {
|
|
2016
|
+
return reply.status(423).send({ error: 'circuit_open', reason: 'Worker circuit is open — too many consecutive failures. Try again after 30s or after a successful assignment.' });
|
|
2017
|
+
}
|
|
2018
|
+
|
|
1964
2019
|
// Try live channel delivery first
|
|
1965
2020
|
const session = db.prepare(
|
|
1966
2021
|
`SELECT agent_id, channel_id FROM coord_channel_sessions WHERE agent_id = ? AND status = 'connected'`
|
|
@@ -130,6 +130,15 @@ CREATE TABLE IF NOT EXISTS coord_events (
|
|
|
130
130
|
detail TEXT,
|
|
131
131
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
132
132
|
);
|
|
133
|
+
|
|
134
|
+
-- Coordination: per-worker circuit breaker state (AWM 0.8.1)
|
|
135
|
+
CREATE TABLE IF NOT EXISTS coord_circuit_state (
|
|
136
|
+
agent_id TEXT PRIMARY KEY REFERENCES coord_agents(id),
|
|
137
|
+
state TEXT NOT NULL DEFAULT 'closed',
|
|
138
|
+
consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
|
139
|
+
opened_at TEXT NULL,
|
|
140
|
+
last_transition_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
141
|
+
);
|
|
133
142
|
`;
|
|
134
143
|
|
|
135
144
|
/**
|
|
@@ -170,4 +179,8 @@ export function initCoordinationTables(db: Database.Database): void {
|
|
|
170
179
|
|
|
171
180
|
// Migration: session token for hijack prevention (added 2026-03-26)
|
|
172
181
|
try { db.exec(`ALTER TABLE coord_agents ADD COLUMN session_token TEXT`); } catch { /* exists */ }
|
|
182
|
+
|
|
183
|
+
// Migrations: control-layer columns for retry tracking (AWM 0.8.1)
|
|
184
|
+
try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN attempt_count INTEGER NOT NULL DEFAULT 0`); } catch { /* exists */ }
|
|
185
|
+
try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN last_failure_mode TEXT`); } catch { /* exists */ }
|
|
173
186
|
}
|