agent-working-memory 0.7.17 → 0.8.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 +156 -15
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +289 -202
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/http.js +22 -22
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +258 -79
- 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 +147 -103
- 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 +10 -2
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +218 -49
- package/dist/core/write-pipeline.js.map +1 -1
- 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 +167 -141
- 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 -218
- package/src/adapters/codex.ts +252 -252
- package/src/adapters/common.ts +498 -411
- package/src/adapters/cursor.ts +135 -135
- package/src/adapters/http.ts +93 -93
- 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 +970 -714
- package/src/cli/migrate.ts +307 -0
- package/src/cli.ts +755 -719
- 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 -343
- 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 +226 -212
- package/src/mcp.ts +1145 -1121
- 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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Per-worker CircuitBreaker for the coordination control layer.
|
|
5
|
+
* Prevents chronically-stale workers from poisoning the assignment queue.
|
|
6
|
+
* Part of AWM 0.8.1 — additive, no breaking changes.
|
|
7
|
+
*
|
|
8
|
+
* States:
|
|
9
|
+
* closed — normal operation
|
|
10
|
+
* open — worker blocked after FAILURE_THRESHOLD consecutive failures
|
|
11
|
+
* half_open — probe window (30s after open), allows one assignment attempt
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type Database from 'better-sqlite3';
|
|
15
|
+
|
|
16
|
+
export type CircuitState = 'closed' | 'open' | 'half_open';
|
|
17
|
+
|
|
18
|
+
const FAILURE_THRESHOLD = 5;
|
|
19
|
+
const HALF_OPEN_DELAY_MS = 30_000;
|
|
20
|
+
|
|
21
|
+
/** Record a worker failure. Opens the circuit when consecutive failures hit the threshold. */
|
|
22
|
+
export function recordFailure(db: Database.Database, agentId: string): void {
|
|
23
|
+
db.prepare(`
|
|
24
|
+
INSERT INTO coord_circuit_state (agent_id, consecutive_failures, last_transition_at)
|
|
25
|
+
VALUES (?, 1, datetime('now'))
|
|
26
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
27
|
+
consecutive_failures = consecutive_failures + 1,
|
|
28
|
+
state = CASE
|
|
29
|
+
WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} THEN 'open'
|
|
30
|
+
ELSE state
|
|
31
|
+
END,
|
|
32
|
+
opened_at = CASE
|
|
33
|
+
WHEN consecutive_failures + 1 >= ${FAILURE_THRESHOLD} AND (state != 'open' OR opened_at IS NULL)
|
|
34
|
+
THEN datetime('now')
|
|
35
|
+
ELSE opened_at
|
|
36
|
+
END,
|
|
37
|
+
last_transition_at = datetime('now')
|
|
38
|
+
`).run(agentId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Record a worker success. Resets to closed regardless of prior state. */
|
|
42
|
+
export function recordSuccess(db: Database.Database, agentId: string): void {
|
|
43
|
+
db.prepare(`
|
|
44
|
+
INSERT INTO coord_circuit_state (agent_id, state, consecutive_failures, last_transition_at)
|
|
45
|
+
VALUES (?, 'closed', 0, datetime('now'))
|
|
46
|
+
ON CONFLICT(agent_id) DO UPDATE SET
|
|
47
|
+
state = 'closed',
|
|
48
|
+
consecutive_failures = 0,
|
|
49
|
+
opened_at = NULL,
|
|
50
|
+
last_transition_at = datetime('now')
|
|
51
|
+
`).run(agentId);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get current circuit state for a worker.
|
|
56
|
+
* If the circuit has been open for >30s, auto-transitions to half_open.
|
|
57
|
+
*/
|
|
58
|
+
export function getState(db: Database.Database, agentId: string): CircuitState {
|
|
59
|
+
const row = db.prepare(
|
|
60
|
+
`SELECT state, opened_at FROM coord_circuit_state WHERE agent_id = ?`
|
|
61
|
+
).get(agentId) as { state: string; opened_at: string | null } | undefined;
|
|
62
|
+
|
|
63
|
+
if (!row || row.state === 'closed') return 'closed';
|
|
64
|
+
if (row.state === 'half_open') return 'half_open';
|
|
65
|
+
|
|
66
|
+
// open — check if half-open window has elapsed
|
|
67
|
+
if (row.state === 'open' && row.opened_at) {
|
|
68
|
+
const openedAt = new Date(row.opened_at.endsWith('Z') ? row.opened_at : row.opened_at + 'Z').getTime();
|
|
69
|
+
if (Date.now() - openedAt > HALF_OPEN_DELAY_MS) {
|
|
70
|
+
db.prepare(
|
|
71
|
+
`UPDATE coord_circuit_state SET state = 'half_open', last_transition_at = datetime('now') WHERE agent_id = ?`
|
|
72
|
+
).run(agentId);
|
|
73
|
+
return 'half_open';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return 'open';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Returns true when the worker is eligible to receive an assignment. */
|
|
81
|
+
export function isAvailable(db: Database.Database, agentId: string): boolean {
|
|
82
|
+
return getState(db, agentId) !== 'open';
|
|
83
|
+
}
|
|
@@ -1,90 +1,90 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Typed internal event emitter for coordination.
|
|
5
|
-
* Lightweight Node EventEmitter wrapper with typed events.
|
|
6
|
-
* Used to decouple route handlers from side-effects (channel push, logging, etc).
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { EventEmitter } from 'events';
|
|
10
|
-
|
|
11
|
-
// ─── Event Payloads ──────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
-
export interface AssignmentCreatedEvent {
|
|
14
|
-
assignmentId: string;
|
|
15
|
-
agentId: string;
|
|
16
|
-
task: string;
|
|
17
|
-
workspace?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface AssignmentUpdatedEvent {
|
|
21
|
-
assignmentId: string;
|
|
22
|
-
agentId: string | null;
|
|
23
|
-
status: string;
|
|
24
|
-
result?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface AssignmentCompletedEvent {
|
|
28
|
-
assignmentId: string;
|
|
29
|
-
agentId: string | null;
|
|
30
|
-
result: string | null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface AgentCheckinEvent {
|
|
34
|
-
agentId: string;
|
|
35
|
-
name: string;
|
|
36
|
-
role: string;
|
|
37
|
-
workspace?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface AgentCheckoutEvent {
|
|
41
|
-
agentId: string;
|
|
42
|
-
name: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SessionStartedEvent {
|
|
46
|
-
agentId: string;
|
|
47
|
-
channelId: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SessionClosedEvent {
|
|
51
|
-
agentId: string;
|
|
52
|
-
channelId: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─── Event Map ───────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
export interface CoordinationEvents {
|
|
58
|
-
'assignment.created': [AssignmentCreatedEvent];
|
|
59
|
-
'assignment.updated': [AssignmentUpdatedEvent];
|
|
60
|
-
'assignment.completed': [AssignmentCompletedEvent];
|
|
61
|
-
'agent.checkin': [AgentCheckinEvent];
|
|
62
|
-
'agent.checkout': [AgentCheckoutEvent];
|
|
63
|
-
'session.started': [SessionStartedEvent];
|
|
64
|
-
'session.closed': [SessionClosedEvent];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── Typed Event Bus ─────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
export class CoordinationEventBus extends EventEmitter {
|
|
70
|
-
emit<K extends keyof CoordinationEvents>(event: K, ...args: CoordinationEvents[K]): boolean {
|
|
71
|
-
return super.emit(event, ...args);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
on<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
75
|
-
return super.on(event, listener as (...args: unknown[]) => void);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
once<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
79
|
-
return super.once(event, listener as (...args: unknown[]) => void);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
off<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
83
|
-
return super.off(event, listener as (...args: unknown[]) => void);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Create a new coordination event bus. */
|
|
88
|
-
export function createEventBus(): CoordinationEventBus {
|
|
89
|
-
return new CoordinationEventBus();
|
|
90
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Typed internal event emitter for coordination.
|
|
5
|
+
* Lightweight Node EventEmitter wrapper with typed events.
|
|
6
|
+
* Used to decouple route handlers from side-effects (channel push, logging, etc).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from 'events';
|
|
10
|
+
|
|
11
|
+
// ─── Event Payloads ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface AssignmentCreatedEvent {
|
|
14
|
+
assignmentId: string;
|
|
15
|
+
agentId: string;
|
|
16
|
+
task: string;
|
|
17
|
+
workspace?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AssignmentUpdatedEvent {
|
|
21
|
+
assignmentId: string;
|
|
22
|
+
agentId: string | null;
|
|
23
|
+
status: string;
|
|
24
|
+
result?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AssignmentCompletedEvent {
|
|
28
|
+
assignmentId: string;
|
|
29
|
+
agentId: string | null;
|
|
30
|
+
result: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface AgentCheckinEvent {
|
|
34
|
+
agentId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
role: string;
|
|
37
|
+
workspace?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface AgentCheckoutEvent {
|
|
41
|
+
agentId: string;
|
|
42
|
+
name: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SessionStartedEvent {
|
|
46
|
+
agentId: string;
|
|
47
|
+
channelId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SessionClosedEvent {
|
|
51
|
+
agentId: string;
|
|
52
|
+
channelId: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Event Map ───────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface CoordinationEvents {
|
|
58
|
+
'assignment.created': [AssignmentCreatedEvent];
|
|
59
|
+
'assignment.updated': [AssignmentUpdatedEvent];
|
|
60
|
+
'assignment.completed': [AssignmentCompletedEvent];
|
|
61
|
+
'agent.checkin': [AgentCheckinEvent];
|
|
62
|
+
'agent.checkout': [AgentCheckoutEvent];
|
|
63
|
+
'session.started': [SessionStartedEvent];
|
|
64
|
+
'session.closed': [SessionClosedEvent];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Typed Event Bus ─────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export class CoordinationEventBus extends EventEmitter {
|
|
70
|
+
emit<K extends keyof CoordinationEvents>(event: K, ...args: CoordinationEvents[K]): boolean {
|
|
71
|
+
return super.emit(event, ...args);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
on<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
75
|
+
return super.on(event, listener as (...args: unknown[]) => void);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
once<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
79
|
+
return super.once(event, listener as (...args: unknown[]) => void);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
off<K extends keyof CoordinationEvents>(event: K, listener: (...args: CoordinationEvents[K]) => void): this {
|
|
83
|
+
return super.off(event, listener as (...args: unknown[]) => void);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Create a new coordination event bus. */
|
|
88
|
+
export function createEventBus(): CoordinationEventBus {
|
|
89
|
+
return new CoordinationEventBus();
|
|
90
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* FailureMode taxonomy and mutation-hint map for the coordination control layer.
|
|
5
|
+
* Part of AWM 0.8.1 — additive, no breaking changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export enum FailureMode {
|
|
9
|
+
AGENT_STALE = 'agent_stale',
|
|
10
|
+
TIMEOUT = 'timeout',
|
|
11
|
+
OUTPUT_INVALID = 'output_invalid',
|
|
12
|
+
TEST_FAIL = 'test_fail',
|
|
13
|
+
LINT_FAIL = 'lint_fail',
|
|
14
|
+
MERGE_CONFLICT = 'merge_conflict',
|
|
15
|
+
UNKNOWN = 'unknown',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Classify a failure result string into one of the known modes. */
|
|
19
|
+
export function classifyFailure(result: string | null): FailureMode {
|
|
20
|
+
if (!result) return FailureMode.UNKNOWN;
|
|
21
|
+
const r = result.toLowerCase();
|
|
22
|
+
if (r.includes('stale') || r.includes('disconnected')) return FailureMode.AGENT_STALE;
|
|
23
|
+
if (r.includes('timeout') || r.includes('timed out')) return FailureMode.TIMEOUT;
|
|
24
|
+
if (r.includes('json') || r.includes('schema') || r.includes('parse')) return FailureMode.OUTPUT_INVALID;
|
|
25
|
+
if (r.includes('test fail') || r.includes('vitest') || r.includes('jest')) return FailureMode.TEST_FAIL;
|
|
26
|
+
if (r.includes('lint') || r.includes('eslint') || r.includes('typecheck')) return FailureMode.LINT_FAIL;
|
|
27
|
+
if (r.includes('conflict')) return FailureMode.MERGE_CONFLICT;
|
|
28
|
+
return FailureMode.UNKNOWN;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Corrective guidance injected into the task description on retry.
|
|
33
|
+
* Each hint is written in the vocabulary the next worker will read.
|
|
34
|
+
*/
|
|
35
|
+
export const MUTATION_HINTS: Record<FailureMode, string> = {
|
|
36
|
+
[FailureMode.AGENT_STALE]:
|
|
37
|
+
'Previous worker disconnected before completion. Resume from last known state; check git status before re-running destructive commands.',
|
|
38
|
+
[FailureMode.TIMEOUT]:
|
|
39
|
+
'Previous attempt timed out. Break work into smaller commits; report progress every 5 minutes.',
|
|
40
|
+
[FailureMode.OUTPUT_INVALID]:
|
|
41
|
+
'Previous output failed validation. Return a single fenced code block; verify JSON parses before submitting.',
|
|
42
|
+
[FailureMode.TEST_FAIL]:
|
|
43
|
+
'Previous attempt left tests failing. Run vitest before completion; do NOT mark complete if any test fails.',
|
|
44
|
+
[FailureMode.LINT_FAIL]:
|
|
45
|
+
'Previous attempt had lint/typecheck errors. Run pnpm typecheck and pnpm lint before completion.',
|
|
46
|
+
[FailureMode.MERGE_CONFLICT]:
|
|
47
|
+
'Previous attempt left merge conflicts unresolved. git pull --rebase, resolve, then re-attempt.',
|
|
48
|
+
[FailureMode.UNKNOWN]:
|
|
49
|
+
'Previous attempt failed for an unclassified reason. Investigate the prior result before re-running.',
|
|
50
|
+
};
|
|
@@ -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
|
+
}
|