agent-working-memory 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -19
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +5 -1
- package/dist/adapters/common.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +2 -1
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.js +29 -29
- package/dist/cli.js +405 -224
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.js +23 -23
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +10 -1
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +5 -1
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +50 -3
- package/dist/mcp.js.map +1 -1
- package/dist/onboard/index.d.ts +68 -0
- package/dist/onboard/index.d.ts.map +1 -0
- package/dist/onboard/index.js +265 -0
- package/dist/onboard/index.js.map +1 -0
- package/dist/storage/factory.d.ts +1 -1
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +16 -2
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/pglite-schema.js +143 -143
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +8 -0
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +228 -0
- package/dist/storage/postgres.d.ts.map +1 -0
- package/dist/storage/postgres.js +1221 -0
- package/dist/storage/postgres.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +27 -0
- package/dist/version.js.map +1 -0
- package/package.json +11 -1
- package/src/adapters/common.ts +5 -1
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +2 -1
- package/src/cli/migrate.ts +307 -307
- package/src/cli.ts +342 -273
- package/src/coordination/circuit-breaker.ts +83 -83
- package/src/coordination/failure-modes.ts +50 -50
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -110
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -194
- package/src/core/ml-worker.ts +281 -281
- package/src/core/query-expander.ts +122 -122
- package/src/core/reranker.ts +119 -119
- package/src/core/salience.ts +10 -1
- package/src/core/write-pipeline.ts +5 -1
- package/src/engine/confidence.ts +120 -120
- package/src/engine/consolidation-scheduler.ts +242 -242
- 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 -366
- package/src/engine/staging.ts +74 -74
- package/src/index.ts +2 -1
- package/src/mcp.ts +62 -3
- package/src/onboard/index.ts +298 -0
- package/src/storage/factory.ts +15 -3
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -166
- package/src/storage/pglite.ts +9 -0
- package/src/storage/postgres.ts +1475 -0
- package/src/storage/store.ts +80 -80
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
- package/src/version.ts +26 -0
package/src/engine/staging.ts
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Staging Buffer — weak signal handler.
|
|
5
|
-
*
|
|
6
|
-
* Observations that don't meet the salience threshold for active memory
|
|
7
|
-
* go to staging. The staging buffer periodically:
|
|
8
|
-
* 1. Checks staged engrams against active memory for resonance
|
|
9
|
-
* 2. Promotes resonant engrams to active
|
|
10
|
-
* 3. Discards expired engrams that never resonated
|
|
11
|
-
*
|
|
12
|
-
* Modeled on hippocampal consolidation — provisional encoding
|
|
13
|
-
* that only persists if reactivated.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { IEngramStore as EngramStore } from '../storage/store.js';
|
|
17
|
-
import type { ActivationEngine } from './activation.js';
|
|
18
|
-
|
|
19
|
-
export class StagingBuffer {
|
|
20
|
-
private store: EngramStore;
|
|
21
|
-
private engine: ActivationEngine;
|
|
22
|
-
private checkInterval: ReturnType<typeof setInterval> | null = null;
|
|
23
|
-
|
|
24
|
-
constructor(store: EngramStore, engine: ActivationEngine) {
|
|
25
|
-
this.store = store;
|
|
26
|
-
this.engine = engine;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Start the periodic staging check.
|
|
31
|
-
*/
|
|
32
|
-
start(intervalMs: number = 60_000): void {
|
|
33
|
-
this.checkInterval = setInterval(() => this.sweep(), intervalMs);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
stop(): void {
|
|
37
|
-
if (this.checkInterval) {
|
|
38
|
-
clearInterval(this.checkInterval);
|
|
39
|
-
this.checkInterval = null;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Sweep staged engrams: promote or discard.
|
|
45
|
-
*/
|
|
46
|
-
async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
|
|
47
|
-
const promoted: string[] = [];
|
|
48
|
-
const discarded: string[] = [];
|
|
49
|
-
|
|
50
|
-
const expired = await this.store.getExpiredStaging();
|
|
51
|
-
for (const engram of expired) {
|
|
52
|
-
// Check if this engram resonates with active memory
|
|
53
|
-
const results = await this.engine.activate({
|
|
54
|
-
agentId: engram.agentId,
|
|
55
|
-
context: `${engram.concept} ${engram.content}`,
|
|
56
|
-
limit: 3,
|
|
57
|
-
minScore: 0.3,
|
|
58
|
-
internal: true,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
if (results.length > 0) {
|
|
62
|
-
// Resonance found — promote to active
|
|
63
|
-
await this.store.updateStage(engram.id, 'active');
|
|
64
|
-
promoted.push(engram.id);
|
|
65
|
-
} else {
|
|
66
|
-
// No resonance — discard
|
|
67
|
-
await this.store.deleteEngram(engram.id);
|
|
68
|
-
discarded.push(engram.id);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return { promoted, discarded };
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Staging Buffer — weak signal handler.
|
|
5
|
+
*
|
|
6
|
+
* Observations that don't meet the salience threshold for active memory
|
|
7
|
+
* go to staging. The staging buffer periodically:
|
|
8
|
+
* 1. Checks staged engrams against active memory for resonance
|
|
9
|
+
* 2. Promotes resonant engrams to active
|
|
10
|
+
* 3. Discards expired engrams that never resonated
|
|
11
|
+
*
|
|
12
|
+
* Modeled on hippocampal consolidation — provisional encoding
|
|
13
|
+
* that only persists if reactivated.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { IEngramStore as EngramStore } from '../storage/store.js';
|
|
17
|
+
import type { ActivationEngine } from './activation.js';
|
|
18
|
+
|
|
19
|
+
export class StagingBuffer {
|
|
20
|
+
private store: EngramStore;
|
|
21
|
+
private engine: ActivationEngine;
|
|
22
|
+
private checkInterval: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
|
|
24
|
+
constructor(store: EngramStore, engine: ActivationEngine) {
|
|
25
|
+
this.store = store;
|
|
26
|
+
this.engine = engine;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start the periodic staging check.
|
|
31
|
+
*/
|
|
32
|
+
start(intervalMs: number = 60_000): void {
|
|
33
|
+
this.checkInterval = setInterval(() => this.sweep(), intervalMs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
stop(): void {
|
|
37
|
+
if (this.checkInterval) {
|
|
38
|
+
clearInterval(this.checkInterval);
|
|
39
|
+
this.checkInterval = null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Sweep staged engrams: promote or discard.
|
|
45
|
+
*/
|
|
46
|
+
async sweep(): Promise<{ promoted: string[]; discarded: string[] }> {
|
|
47
|
+
const promoted: string[] = [];
|
|
48
|
+
const discarded: string[] = [];
|
|
49
|
+
|
|
50
|
+
const expired = await this.store.getExpiredStaging();
|
|
51
|
+
for (const engram of expired) {
|
|
52
|
+
// Check if this engram resonates with active memory
|
|
53
|
+
const results = await this.engine.activate({
|
|
54
|
+
agentId: engram.agentId,
|
|
55
|
+
context: `${engram.concept} ${engram.content}`,
|
|
56
|
+
limit: 3,
|
|
57
|
+
minScore: 0.3,
|
|
58
|
+
internal: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (results.length > 0) {
|
|
62
|
+
// Resonance found — promote to active
|
|
63
|
+
await this.store.updateStage(engram.id, 'active');
|
|
64
|
+
promoted.push(engram.id);
|
|
65
|
+
} else {
|
|
66
|
+
// No resonance — discard
|
|
67
|
+
await this.store.deleteEngram(engram.id);
|
|
68
|
+
discarded.push(engram.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { promoted, discarded };
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { readFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
|
|
4
4
|
import { resolve, dirname, basename } from 'node:path';
|
|
5
5
|
import Fastify from 'fastify';
|
|
6
|
+
import { VERSION } from './version.js';
|
|
6
7
|
|
|
7
8
|
// Load .env file if present (no external dependency)
|
|
8
9
|
try {
|
|
@@ -202,7 +203,7 @@ async function main() {
|
|
|
202
203
|
|
|
203
204
|
// Start server
|
|
204
205
|
await app.listen({ port: PORT, host: '0.0.0.0' });
|
|
205
|
-
console.log(`AgentWorkingMemory
|
|
206
|
+
console.log(`AgentWorkingMemory v${VERSION} listening on port ${PORT}`);
|
|
206
207
|
|
|
207
208
|
// Graceful shutdown
|
|
208
209
|
const shutdown = async () => {
|
package/src/mcp.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readFileSync } from 'node:fs';
|
|
32
|
-
import { resolve } from 'node:path';
|
|
32
|
+
import { resolve, basename } from 'node:path';
|
|
33
33
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
34
34
|
|
|
35
35
|
// Load .env file if present (no external dependency)
|
|
@@ -75,6 +75,8 @@ import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
|
|
|
75
75
|
import { embed } from './core/embeddings.js';
|
|
76
76
|
import { startSidecar } from './hooks/sidecar.js';
|
|
77
77
|
import { initLogger, log, getLogPath } from './core/logger.js';
|
|
78
|
+
import { VERSION } from './version.js';
|
|
79
|
+
import { buildPack, INTERVIEW_QUESTIONS } from './onboard/index.js';
|
|
78
80
|
import { liteCompress, retrieveOriginal } from './core/lite-compress.js';
|
|
79
81
|
import { queryPeerDecisions, formatPeerDecisions } from './coordination/peer-decisions.js';
|
|
80
82
|
|
|
@@ -86,7 +88,7 @@ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO
|
|
|
86
88
|
|
|
87
89
|
if (INCOGNITO) {
|
|
88
90
|
console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
|
|
89
|
-
const server = new McpServer({ name: 'agent-working-memory', version:
|
|
91
|
+
const server = new McpServer({ name: 'agent-working-memory', version: VERSION });
|
|
90
92
|
const transport = new StdioServerTransport();
|
|
91
93
|
server.connect(transport).catch(err => {
|
|
92
94
|
console.error('MCP server failed:', err);
|
|
@@ -141,7 +143,7 @@ let coordDb: import('better-sqlite3').Database | null = null;
|
|
|
141
143
|
|
|
142
144
|
const server = new McpServer({
|
|
143
145
|
name: 'agent-working-memory',
|
|
144
|
-
version:
|
|
146
|
+
version: VERSION,
|
|
145
147
|
});
|
|
146
148
|
|
|
147
149
|
server.registerResource(
|
|
@@ -639,6 +641,15 @@ Use this at the start of every session or after compaction to pick up where you
|
|
|
639
641
|
async () => {
|
|
640
642
|
const checkpoint = await store.getCheckpoint(AGENT_ID);
|
|
641
643
|
|
|
644
|
+
// Cold-store nudge: an empty store means the agent has nothing to recall — offer to warm-start.
|
|
645
|
+
let coldStoreNudge = '';
|
|
646
|
+
try {
|
|
647
|
+
const activeCount = (await store.getEngramsByAgent(AGENT_ID)).length;
|
|
648
|
+
if (activeCount < 3) {
|
|
649
|
+
coldStoreNudge = `🌱 **This memory store is nearly empty (${activeCount} ${activeCount === 1 ? 'memory' : 'memories'}).** Warm-start it before other work: recall the "onboard a new project" skill and follow it — or call \`onboard_scan\` on this project's docs/repo, refine the results, and save them with \`memory_write\` (canonical). Recall becomes useful immediately.`;
|
|
650
|
+
}
|
|
651
|
+
} catch { /* count is best-effort */ }
|
|
652
|
+
|
|
642
653
|
const now = Date.now();
|
|
643
654
|
const idleMs = checkpoint
|
|
644
655
|
? now - checkpoint.auto.lastActivityAt.getTime()
|
|
@@ -717,6 +728,7 @@ Use this at the start of every session or after compaction to pick up where you
|
|
|
717
728
|
: '';
|
|
718
729
|
log(AGENT_ID, 'restore', `idle=${idleMin}min checkpoint=${!!checkpoint?.executionState} recalled=${recalledMemories.length} lastWrite=${lastWrite?.concept ?? 'none'}${fullConsolidationTriggered ? ' FULL_CONSOLIDATION' : ''}`);
|
|
719
730
|
parts.push(`Idle: ${idleMin}min${consolidationNote}`);
|
|
731
|
+
if (coldStoreNudge) parts.push(`\n${coldStoreNudge}`);
|
|
720
732
|
|
|
721
733
|
if (checkpoint?.executionState) {
|
|
722
734
|
const s = checkpoint.executionState;
|
|
@@ -774,6 +786,53 @@ Use this at the start of every session or after compaction to pick up where you
|
|
|
774
786
|
}
|
|
775
787
|
);
|
|
776
788
|
|
|
789
|
+
// --- Onboarding Tools (warm-start a cold store) ---
|
|
790
|
+
|
|
791
|
+
server.tool(
|
|
792
|
+
'onboard_scan',
|
|
793
|
+
`Scan a project's documentation + repository and return CANDIDATE memories to seed a cold store.
|
|
794
|
+
|
|
795
|
+
Use this when the store is empty / you're new to a project. The scan is deterministic
|
|
796
|
+
(real file contents, not guesses) — YOUR job is to refine the candidates into atomic,
|
|
797
|
+
recall-shaped memories, run the interview (onboard_questions), confirm with the user, then
|
|
798
|
+
save the good ones with memory_write (memory_class="canonical"). Nothing is saved by this tool.`,
|
|
799
|
+
{
|
|
800
|
+
docs: z.array(z.string()).optional()
|
|
801
|
+
.describe('Doc files/dirs to scan (Markdown/text). Defaults to the repo (or cwd).'),
|
|
802
|
+
repo: z.string().optional()
|
|
803
|
+
.describe('Repo root — also derives stack (package.json) + layout memories.'),
|
|
804
|
+
project: z.string().optional()
|
|
805
|
+
.describe('Project name (becomes a tag). Defaults to the repo/dir name.'),
|
|
806
|
+
purpose: z.string().optional()
|
|
807
|
+
.describe('The goal of this memory system, if known — becomes the anchor memory.'),
|
|
808
|
+
},
|
|
809
|
+
async (params) => {
|
|
810
|
+
const repo = params.repo;
|
|
811
|
+
const docs = params.docs && params.docs.length ? params.docs : [repo ?? process.cwd()];
|
|
812
|
+
const project = params.project ?? basename(resolve(repo ?? docs[0] ?? process.cwd()));
|
|
813
|
+
const pack = buildPack({ docs, repo, project, agentId: AGENT_ID, purpose: params.purpose });
|
|
814
|
+
log(AGENT_ID, 'onboard', `scan ${docs.join(',')}${repo ? ' +repo' : ''} → ${pack.memories.length} candidates`);
|
|
815
|
+
const text = [
|
|
816
|
+
`Scanned ${docs.join(', ')}${repo ? ` (+repo ${repo})` : ''} → ${pack.memories.length} CANDIDATE memories (NOT saved).`,
|
|
817
|
+
`Next: refine each into an atomic memory (lead with the fact + identifiers), run onboard_questions,`,
|
|
818
|
+
`confirm with the user, then save the good ones with memory_write (memory_class="canonical").`,
|
|
819
|
+
``,
|
|
820
|
+
JSON.stringify({ project, candidates: pack.memories, questions: pack.questions }, null, 2),
|
|
821
|
+
].join('\n');
|
|
822
|
+
return { content: [{ type: 'text' as const, text }] };
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
server.tool(
|
|
827
|
+
'onboard_questions',
|
|
828
|
+
`Return the onboarding interview questions. Ask the user ONE at a time, starting with the
|
|
829
|
+
goal of the memory system, and ask follow-ups for clarity. Turn each answer into a canonical memory.`,
|
|
830
|
+
{},
|
|
831
|
+
async () => ({
|
|
832
|
+
content: [{ type: 'text' as const, text: INTERVIEW_QUESTIONS.map((q, i) => `${i + 1}. ${q}`).join('\n') }],
|
|
833
|
+
})
|
|
834
|
+
);
|
|
835
|
+
|
|
777
836
|
// --- Task Management Tools ---
|
|
778
837
|
|
|
779
838
|
server.tool(
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* `awm onboard` — warm-start a cold memory store from a project's own knowledge.
|
|
5
|
+
*
|
|
6
|
+
* The cold-start problem: a fresh AWM store knows nothing, so recall returns
|
|
7
|
+
* nothing until enough interactions accumulate. Onboarding derives a seed set of
|
|
8
|
+
* memories up front — from documentation, the repository, and a short interview —
|
|
9
|
+
* so an agent can be useful on a project from the first turn.
|
|
10
|
+
*
|
|
11
|
+
* Design decisions (what makes this good vs. a vector-DB doc-dump):
|
|
12
|
+
* - It emits an **`awm import`-compatible file**, not direct writes — so it reuses
|
|
13
|
+
* the importer, and the file is the human review/edit surface ("make changes
|
|
14
|
+
* based on what's needed"). Flow: onboard -> review/edit -> `awm import`.
|
|
15
|
+
* - It extracts **atomic, recall-shaped memories** (concept = the fact/heading,
|
|
16
|
+
* content = the supporting text, tags = proj/topic/origin), not raw chunks.
|
|
17
|
+
* - Seed facts from the owner's own docs are **canonical** — they bypass the
|
|
18
|
+
* salience filter (which is designed to reject low-novelty observations and
|
|
19
|
+
* would otherwise silently drop half the seed).
|
|
20
|
+
* - It is **model-free** (this tier): deterministic Markdown/section + repo
|
|
21
|
+
* structure extraction, no API keys — preserving AWM's "everything local"
|
|
22
|
+
* property. An LLM-assisted extractor + live interview layer on top of this.
|
|
23
|
+
* - Ids are content-hashed, so re-running on changed docs is **idempotent**
|
|
24
|
+
* (import `--dedupe` drops unchanged rows; edited sections become new rows).
|
|
25
|
+
*
|
|
26
|
+
* The interview is emitted as questions in the review file (a model-free tier
|
|
27
|
+
* can't converse); answering them and re-running folds the answers into the seed.
|
|
28
|
+
* The anchor question is deliberately "What is the goal of this memory system?" —
|
|
29
|
+
* the answer shapes what knowledge is worth keeping.
|
|
30
|
+
*/
|
|
31
|
+
import { readFileSync, readdirSync, existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
32
|
+
import { resolve, join, extname, basename, relative, dirname } from 'node:path';
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
|
|
35
|
+
export interface OnboardMemory {
|
|
36
|
+
id: string;
|
|
37
|
+
agent_id: string;
|
|
38
|
+
concept: string;
|
|
39
|
+
content: string;
|
|
40
|
+
tags: string[];
|
|
41
|
+
confidence: number;
|
|
42
|
+
salience: number;
|
|
43
|
+
memory_class: 'canonical' | 'working';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface OnboardPack {
|
|
47
|
+
version: string;
|
|
48
|
+
kind: 'awm-onboard-pack';
|
|
49
|
+
project: string;
|
|
50
|
+
generated_for: string; // agent id
|
|
51
|
+
purpose: string | null; // answer to "what is the goal of this memory system?"
|
|
52
|
+
memories: OnboardMemory[];
|
|
53
|
+
questions: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OnboardOptions {
|
|
57
|
+
/** Files/dirs to scan for documentation (Markdown/text). */
|
|
58
|
+
docs: string[];
|
|
59
|
+
/** Repo root to derive structural memories from (package.json, README, layout). */
|
|
60
|
+
repo?: string;
|
|
61
|
+
project: string;
|
|
62
|
+
/** Target agent id stamped on every seed memory. */
|
|
63
|
+
agentId: string;
|
|
64
|
+
/** The project/memory-system goal (the anchor interview answer), if provided. */
|
|
65
|
+
purpose?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const DOC_EXT = new Set(['.md', '.mdx', '.markdown', '.txt', '.rst']);
|
|
69
|
+
const MAX_CONTENT = 1200; // cap a section; the lead usually carries the fact
|
|
70
|
+
|
|
71
|
+
/** The high-value intake — what a domain expert would want captured that docs miss. */
|
|
72
|
+
export const INTERVIEW_QUESTIONS: string[] = [
|
|
73
|
+
'What is the goal of this memory system — what should the agent get better at over time?',
|
|
74
|
+
'What is the one-sentence description of this project and who it is for?',
|
|
75
|
+
'What is the tech stack and the non-obvious tools/services it depends on?',
|
|
76
|
+
'What naming conventions, patterns, or house style must the agent follow?',
|
|
77
|
+
'What decisions are settled (and should NOT be re-litigated)?',
|
|
78
|
+
'Who are the key people/systems, and how are they referred to?',
|
|
79
|
+
'What are the known gotchas, footguns, or "here be dragons" areas?',
|
|
80
|
+
'What does "done right" look like here — the definition of quality?',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The onboarding skill — a procedure stored AS a canonical memory so a host agent
|
|
85
|
+
* (Codex, Claude Code, MWA) can *recall* it and run the interview itself. This is
|
|
86
|
+
* how the "LLM-assisted" tier works without AWM ever calling a model: the agent
|
|
87
|
+
* that's already there is the brain; AWM provides the procedure + the tools.
|
|
88
|
+
* Seeded by `awm setup`; recalled on a cold store (see the restore nudge).
|
|
89
|
+
*/
|
|
90
|
+
export const ONBOARD_SKILL = {
|
|
91
|
+
concept: 'Skill: onboard a new project (warm-start protocol)',
|
|
92
|
+
content: [
|
|
93
|
+
'When the memory store is empty or you are new to this project, warm-start it before doing other work:',
|
|
94
|
+
'1. Call the `onboard_scan` tool with the docs dir + repo path to get candidate memories (a deterministic scan — real file contents, not guesses).',
|
|
95
|
+
'2. Refine each candidate into an ATOMIC, recall-shaped memory: lead with the fact, keep it to one idea, and include concrete identifiers (file paths, table columns, function names, ticket IDs). Split fat sections into 2-3 crisp facts; drop noise.',
|
|
96
|
+
'3. Run the interview: call `onboard_questions`, then ask the user ONE question at a time starting with "What is the goal of this memory system?". Ask follow-ups for clarity when an answer is vague.',
|
|
97
|
+
'4. Propose the memory set you intend to save and get the user\'s confirmation (edit/drop as they direct).',
|
|
98
|
+
'5. Save each with `memory_write`, memory_class="canonical", tagged with project + topic + source. Stamp facts from the owner\'s own docs as verified/observed; mark your own inferences lower.',
|
|
99
|
+
'Result: recall is useful from the next turn on. Re-run when the docs change to keep the seed fresh (supersede, don\'t duplicate).',
|
|
100
|
+
].join('\n'),
|
|
101
|
+
tags: ['topic=skill', 'name=onboard', 'src=onboarding', 'intent=context'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function slugify(s: string): string {
|
|
105
|
+
return s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Deterministic short id from content → idempotent re-runs. */
|
|
109
|
+
function mkId(seed: string): string {
|
|
110
|
+
return 'onb-' + createHash('sha1').update(seed).digest('hex').slice(0, 16);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function clean(text: string): string {
|
|
114
|
+
const t = text
|
|
115
|
+
.replace(/```[\s\S]*?```/g, (m) => m.length > 300 ? '[code block]' : m) // drop huge code fences
|
|
116
|
+
.replace(/\r/g, '')
|
|
117
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
118
|
+
.trim();
|
|
119
|
+
return t.length > MAX_CONTENT ? t.slice(0, MAX_CONTENT).trimEnd() + ' …' : t;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function mkMemory(
|
|
123
|
+
concept: string, content: string, topic: string, origin: string, opts: OnboardOptions,
|
|
124
|
+
): OnboardMemory | null {
|
|
125
|
+
const c = clean(content);
|
|
126
|
+
const title = concept.trim();
|
|
127
|
+
// Skip empties and thin headings with no supporting prose.
|
|
128
|
+
if (!title || c.length < 24) return null;
|
|
129
|
+
return {
|
|
130
|
+
id: mkId(`${opts.agentId}::${title}::${c}`),
|
|
131
|
+
agent_id: opts.agentId,
|
|
132
|
+
concept: title.slice(0, 120),
|
|
133
|
+
content: c,
|
|
134
|
+
tags: [`proj=${opts.project}`, `topic=${topic}`, 'src=onboarding', `origin=${origin}`, 'intent=context'],
|
|
135
|
+
confidence: 0.7, // observed — derived from the owner's own docs
|
|
136
|
+
salience: 0.7, // canonical floor
|
|
137
|
+
memory_class: 'canonical',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Split a Markdown/text doc into atomic memories, one per heading section.
|
|
143
|
+
* A section's memory is (heading, the prose under it up to the next heading).
|
|
144
|
+
* Untitled preamble before the first heading is captured as an "Overview".
|
|
145
|
+
*/
|
|
146
|
+
export function scanMarkdown(text: string, origin: string, opts: OnboardOptions): OnboardMemory[] {
|
|
147
|
+
const out: OnboardMemory[] = [];
|
|
148
|
+
const lines = text.replace(/\r/g, '').split('\n');
|
|
149
|
+
let heading = '';
|
|
150
|
+
let buf: string[] = [];
|
|
151
|
+
const flush = () => {
|
|
152
|
+
const body = buf.join('\n').trim();
|
|
153
|
+
const concept = heading || `${basename(origin)} — overview`;
|
|
154
|
+
const topic = slugify(heading || basename(origin, extname(origin)));
|
|
155
|
+
const mem = mkMemory(concept, body, topic || 'doc', origin, opts);
|
|
156
|
+
if (mem) out.push(mem);
|
|
157
|
+
buf = [];
|
|
158
|
+
};
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
const h = /^(#{1,3})\s+(.*)$/.exec(line);
|
|
161
|
+
if (h) { flush(); heading = h[2].trim(); }
|
|
162
|
+
else buf.push(line);
|
|
163
|
+
}
|
|
164
|
+
flush();
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Recursively collect documentation files under the given paths. */
|
|
169
|
+
function collectDocs(paths: string[]): string[] {
|
|
170
|
+
const files: string[] = [];
|
|
171
|
+
const walk = (p: string) => {
|
|
172
|
+
if (!existsSync(p)) return;
|
|
173
|
+
const st = statSync(p);
|
|
174
|
+
if (st.isDirectory()) {
|
|
175
|
+
if (/node_modules|\.git|dist|build/.test(p)) return;
|
|
176
|
+
for (const e of readdirSync(p)) walk(join(p, e));
|
|
177
|
+
} else if (DOC_EXT.has(extname(p).toLowerCase())) {
|
|
178
|
+
files.push(p);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
for (const p of paths) walk(resolve(p));
|
|
182
|
+
return files;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Derive structural memories from a repository (package.json, README, layout). */
|
|
186
|
+
export function scanRepo(root: string, opts: OnboardOptions): OnboardMemory[] {
|
|
187
|
+
const out: OnboardMemory[] = [];
|
|
188
|
+
const r = resolve(root);
|
|
189
|
+
// package.json → stack + scripts
|
|
190
|
+
const pkgPath = join(r, 'package.json');
|
|
191
|
+
if (existsSync(pkgPath)) {
|
|
192
|
+
try {
|
|
193
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
194
|
+
const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).slice(0, 25);
|
|
195
|
+
const scripts = Object.keys(pkg.scripts ?? {});
|
|
196
|
+
const parts = [
|
|
197
|
+
pkg.description ? `${pkg.name}: ${pkg.description}.` : `Project package: ${pkg.name} (v${pkg.version ?? '?'}).`,
|
|
198
|
+
deps.length ? `Key dependencies: ${deps.join(', ')}.` : '',
|
|
199
|
+
scripts.length ? `npm scripts: ${scripts.join(', ')}.` : '',
|
|
200
|
+
].filter(Boolean).join(' ');
|
|
201
|
+
const m = mkMemory(`Project stack — ${pkg.name}`, parts, 'stack', 'package.json', opts);
|
|
202
|
+
if (m) out.push(m);
|
|
203
|
+
} catch { /* malformed package.json — skip */ }
|
|
204
|
+
}
|
|
205
|
+
// Top-level layout → a structure memory
|
|
206
|
+
try {
|
|
207
|
+
const entries = readdirSync(r)
|
|
208
|
+
.filter((e) => !/^\.|node_modules|dist|build/.test(e))
|
|
209
|
+
.filter((e) => { try { return statSync(join(r, e)).isDirectory(); } catch { return false; } });
|
|
210
|
+
if (entries.length) {
|
|
211
|
+
const m = mkMemory(
|
|
212
|
+
`Repository layout — ${opts.project}`,
|
|
213
|
+
`Top-level directories: ${entries.map((e) => `${e}/`).join(', ')}.`,
|
|
214
|
+
'layout', 'repo-structure', opts,
|
|
215
|
+
);
|
|
216
|
+
if (m) out.push(m);
|
|
217
|
+
}
|
|
218
|
+
} catch { /* unreadable root — skip */ }
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Assemble a reviewable, `awm import`-compatible pack from the sources. */
|
|
223
|
+
export function buildPack(opts: OnboardOptions): OnboardPack {
|
|
224
|
+
const memories: OnboardMemory[] = [];
|
|
225
|
+
|
|
226
|
+
// The anchor: the goal of the memory system, if the owner supplied it.
|
|
227
|
+
if (opts.purpose && opts.purpose.trim()) {
|
|
228
|
+
const g = mkMemory(
|
|
229
|
+
`Goal of this memory system — ${opts.project}`, opts.purpose.trim(), 'goal', 'interview', opts,
|
|
230
|
+
);
|
|
231
|
+
if (g) memories.push(g);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const file of collectDocs(opts.docs)) {
|
|
235
|
+
const rel = opts.repo ? relative(resolve(opts.repo), file) : basename(file);
|
|
236
|
+
try {
|
|
237
|
+
memories.push(...scanMarkdown(readFileSync(file, 'utf-8'), rel.replace(/\\/g, '/'), opts));
|
|
238
|
+
} catch { /* unreadable file — skip */ }
|
|
239
|
+
}
|
|
240
|
+
if (opts.repo) memories.push(...scanRepo(opts.repo, opts));
|
|
241
|
+
|
|
242
|
+
// Dedup by content-hash id (idempotent across re-runs and overlapping sources).
|
|
243
|
+
const seen = new Set<string>();
|
|
244
|
+
const deduped = memories.filter((m) => (seen.has(m.id) ? false : (seen.add(m.id), true)));
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
version: '1',
|
|
248
|
+
kind: 'awm-onboard-pack',
|
|
249
|
+
project: opts.project,
|
|
250
|
+
generated_for: opts.agentId,
|
|
251
|
+
purpose: opts.purpose?.trim() || null,
|
|
252
|
+
memories: deduped,
|
|
253
|
+
questions: INTERVIEW_QUESTIONS,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** A human-readable review surface: the owner edits this understanding, then imports the JSON. */
|
|
258
|
+
export function renderReview(pack: OnboardPack): string {
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
lines.push(`# Onboarding review — ${pack.project}`);
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push(`Generated ${pack.memories.length} candidate memories for agent \`${pack.generated_for}\`.`);
|
|
263
|
+
lines.push('Edit/delete below as needed, then import the JSON pack:');
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('```');
|
|
266
|
+
lines.push(`awm import <pack>.json --db <path> --dedupe`);
|
|
267
|
+
lines.push('```');
|
|
268
|
+
lines.push('');
|
|
269
|
+
lines.push('## Interview — answer these and re-run to enrich the seed');
|
|
270
|
+
lines.push('');
|
|
271
|
+
for (const q of pack.questions) lines.push(`- [ ] ${q}`);
|
|
272
|
+
lines.push('');
|
|
273
|
+
lines.push('## Candidate memories');
|
|
274
|
+
lines.push('');
|
|
275
|
+
for (const m of pack.memories) {
|
|
276
|
+
lines.push(`### ${m.concept}`);
|
|
277
|
+
lines.push(`*${m.tags.join(' · ')}* — class=${m.memory_class}`);
|
|
278
|
+
lines.push('');
|
|
279
|
+
lines.push(m.content);
|
|
280
|
+
lines.push('');
|
|
281
|
+
}
|
|
282
|
+
return lines.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* CLI entry: scan → write the import pack (JSON) + a review file (Markdown).
|
|
287
|
+
* Does not touch a store — the produced JSON is fed to `awm import`.
|
|
288
|
+
*/
|
|
289
|
+
export function runOnboard(opts: OnboardOptions & { outDir: string }): { packPath: string; reviewPath: string; count: number } {
|
|
290
|
+
const pack = buildPack(opts);
|
|
291
|
+
mkdirSync(opts.outDir, { recursive: true });
|
|
292
|
+
const base = `onboard-${slugify(opts.project) || 'project'}`;
|
|
293
|
+
const packPath = join(opts.outDir, `${base}.pack.json`);
|
|
294
|
+
const reviewPath = join(opts.outDir, `${base}.review.md`);
|
|
295
|
+
writeFileSync(packPath, JSON.stringify(pack, null, 2), 'utf-8');
|
|
296
|
+
writeFileSync(reviewPath, renderReview(pack), 'utf-8');
|
|
297
|
+
return { packPath, reviewPath, count: pack.memories.length };
|
|
298
|
+
}
|
package/src/storage/factory.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
import { existsSync, statSync } from 'node:fs';
|
|
40
40
|
import type { IEngramStore } from './store.js';
|
|
41
41
|
|
|
42
|
-
export type StoreBackend = 'sqlite' | 'pglite';
|
|
42
|
+
export type StoreBackend = 'sqlite' | 'pglite' | 'postgres';
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Auto-detect the backend from on-disk state. Used when `AWM_STORE_BACKEND`
|
|
@@ -71,6 +71,7 @@ export function getConfiguredBackend(): StoreBackend {
|
|
|
71
71
|
const normalized = raw.toLowerCase();
|
|
72
72
|
if (normalized === 'pglite') return 'pglite';
|
|
73
73
|
if (normalized === 'sqlite') return 'sqlite';
|
|
74
|
+
if (normalized === 'postgres') return 'postgres';
|
|
74
75
|
console.warn(`Unknown AWM_STORE_BACKEND=${raw}; falling back to sqlite`);
|
|
75
76
|
return 'sqlite';
|
|
76
77
|
}
|
|
@@ -80,8 +81,11 @@ export function getConfiguredBackend(): StoreBackend {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function getConfiguredPath(): string {
|
|
84
|
+
const backend = getConfiguredBackend();
|
|
85
|
+
// Postgres uses a connection URL, not an on-disk path.
|
|
86
|
+
if (backend === 'postgres') return process.env.AWM_DATABASE_URL ?? 'postgres://localhost:5432/awm';
|
|
83
87
|
if (process.env.AWM_DB_PATH) return process.env.AWM_DB_PATH;
|
|
84
|
-
return
|
|
88
|
+
return backend === 'pglite' ? 'memory-pglite' : 'memory.db';
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
@@ -132,7 +136,15 @@ export async function openStore(): Promise<{
|
|
|
132
136
|
const backend = getConfiguredBackend();
|
|
133
137
|
const path = getConfiguredPath();
|
|
134
138
|
|
|
135
|
-
|
|
139
|
+
// The on-disk mismatch warning only applies to file/dir backends.
|
|
140
|
+
if (backend !== 'postgres') warnIfBackendDisagreesWithDisk(backend, path);
|
|
141
|
+
|
|
142
|
+
if (backend === 'postgres') {
|
|
143
|
+
const { PostgresEngramStore } = await import('./postgres.js');
|
|
144
|
+
const store = new PostgresEngramStore(path);
|
|
145
|
+
await store.ready();
|
|
146
|
+
return { store: store as unknown as IEngramStore, backend, path };
|
|
147
|
+
}
|
|
136
148
|
|
|
137
149
|
if (backend === 'pglite') {
|
|
138
150
|
const { PGliteEngramStore } = await import('./pglite.js');
|
package/src/storage/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
export * from './sqlite.js';
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
export * from './sqlite.js';
|