agent-working-memory 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +118 -19
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +63 -3
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/common.d.ts.map +1 -1
  6. package/dist/adapters/common.js +329 -302
  7. package/dist/adapters/common.js.map +1 -1
  8. package/dist/api/routes.d.ts.map +1 -1
  9. package/dist/api/routes.js +31 -8
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/cli/migrate.js +29 -29
  12. package/dist/cli.js +82 -2
  13. package/dist/cli.js.map +1 -1
  14. package/dist/coordination/circuit-breaker.js +23 -23
  15. package/dist/coordination/routes.d.ts.map +1 -1
  16. package/dist/coordination/routes.js +174 -170
  17. package/dist/coordination/routes.js.map +1 -1
  18. package/dist/core/embeddings.d.ts.map +1 -1
  19. package/dist/core/embeddings.js +3 -0
  20. package/dist/core/embeddings.js.map +1 -1
  21. package/dist/core/entity-extract.d.ts +3 -0
  22. package/dist/core/entity-extract.d.ts.map +1 -0
  23. package/dist/core/entity-extract.js +47 -0
  24. package/dist/core/entity-extract.js.map +1 -0
  25. package/dist/core/salience.d.ts.map +1 -1
  26. package/dist/core/salience.js +14 -2
  27. package/dist/core/salience.js.map +1 -1
  28. package/dist/core/whoami.d.ts +24 -0
  29. package/dist/core/whoami.d.ts.map +1 -0
  30. package/dist/core/whoami.js +66 -0
  31. package/dist/core/whoami.js.map +1 -0
  32. package/dist/core/write-pipeline.d.ts +9 -0
  33. package/dist/core/write-pipeline.d.ts.map +1 -1
  34. package/dist/core/write-pipeline.js +109 -68
  35. package/dist/core/write-pipeline.js.map +1 -1
  36. package/dist/core/write-telemetry.d.ts +33 -0
  37. package/dist/core/write-telemetry.d.ts.map +1 -0
  38. package/dist/core/write-telemetry.js +110 -0
  39. package/dist/core/write-telemetry.js.map +1 -0
  40. package/dist/engine/activation.d.ts +22 -12
  41. package/dist/engine/activation.d.ts.map +1 -1
  42. package/dist/engine/activation.js +133 -17
  43. package/dist/engine/activation.js.map +1 -1
  44. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  45. package/dist/engine/consolidation-scheduler.js +1 -1
  46. package/dist/engine/consolidation.d.ts +1 -0
  47. package/dist/engine/consolidation.d.ts.map +1 -1
  48. package/dist/engine/consolidation.js +18 -0
  49. package/dist/engine/consolidation.js.map +1 -1
  50. package/dist/engine/eval.d.ts.map +1 -1
  51. package/dist/engine/eval.js +5 -1
  52. package/dist/engine/eval.js.map +1 -1
  53. package/dist/index.js +21 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp.d.ts +2 -1
  56. package/dist/mcp.d.ts.map +1 -1
  57. package/dist/mcp.js +212 -97
  58. package/dist/mcp.js.map +1 -1
  59. package/dist/onboard/index.d.ts +68 -0
  60. package/dist/onboard/index.d.ts.map +1 -0
  61. package/dist/onboard/index.js +265 -0
  62. package/dist/onboard/index.js.map +1 -0
  63. package/dist/recipes/index.d.ts +57 -0
  64. package/dist/recipes/index.d.ts.map +1 -0
  65. package/dist/recipes/index.js +81 -0
  66. package/dist/recipes/index.js.map +1 -0
  67. package/dist/storage/pglite-schema.d.ts.map +1 -1
  68. package/dist/storage/pglite-schema.js +170 -143
  69. package/dist/storage/pglite-schema.js.map +1 -1
  70. package/dist/storage/pglite.d.ts +5 -0
  71. package/dist/storage/pglite.d.ts.map +1 -1
  72. package/dist/storage/pglite.js +180 -138
  73. package/dist/storage/pglite.js.map +1 -1
  74. package/dist/storage/postgres.d.ts +5 -0
  75. package/dist/storage/postgres.d.ts.map +1 -1
  76. package/dist/storage/postgres.js +44 -2
  77. package/dist/storage/postgres.js.map +1 -1
  78. package/dist/storage/sqlite.d.ts +9 -0
  79. package/dist/storage/sqlite.d.ts.map +1 -1
  80. package/dist/storage/sqlite.js +394 -326
  81. package/dist/storage/sqlite.js.map +1 -1
  82. package/dist/types/engram.d.ts +14 -0
  83. package/dist/types/engram.d.ts.map +1 -1
  84. package/dist/types/engram.js.map +1 -1
  85. package/dist/version.d.ts +2 -0
  86. package/dist/version.d.ts.map +1 -0
  87. package/dist/version.js +27 -0
  88. package/dist/version.js.map +1 -0
  89. package/package.json +9 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +538 -511
  92. package/src/api/index.ts +3 -3
  93. package/src/api/routes.ts +999 -970
  94. package/src/cli/migrate.ts +307 -307
  95. package/src/cli.ts +77 -2
  96. package/src/coordination/circuit-breaker.ts +83 -83
  97. package/src/coordination/failure-modes.ts +50 -50
  98. package/src/coordination/routes.ts +2155 -2150
  99. package/src/core/decay.ts +63 -63
  100. package/src/core/embeddings.ts +113 -110
  101. package/src/core/entity-extract.ts +47 -0
  102. package/src/core/index.ts +5 -5
  103. package/src/core/logger.ts +36 -36
  104. package/src/core/ml-worker-entry.ts +194 -194
  105. package/src/core/ml-worker.ts +281 -281
  106. package/src/core/query-expander.ts +122 -122
  107. package/src/core/reranker.ts +119 -119
  108. package/src/core/salience.ts +529 -514
  109. package/src/core/whoami.ts +92 -0
  110. package/src/core/write-pipeline.ts +60 -8
  111. package/src/core/write-telemetry.ts +131 -0
  112. package/src/engine/activation.ts +1468 -1369
  113. package/src/engine/confidence.ts +120 -120
  114. package/src/engine/consolidation-scheduler.ts +242 -242
  115. package/src/engine/consolidation.ts +887 -869
  116. package/src/engine/eval.ts +107 -102
  117. package/src/engine/eviction.ts +101 -101
  118. package/src/engine/index.ts +8 -8
  119. package/src/engine/retraction.ts +366 -366
  120. package/src/engine/staging.ts +74 -74
  121. package/src/index.ts +248 -226
  122. package/src/mcp.ts +1341 -1211
  123. package/src/onboard/index.ts +298 -0
  124. package/src/recipes/index.ts +125 -0
  125. package/src/storage/index.ts +3 -3
  126. package/src/storage/pglite-schema.ts +193 -166
  127. package/src/storage/pglite.ts +1420 -1372
  128. package/src/storage/postgres.ts +50 -2
  129. package/src/storage/sqlite.ts +1936 -1861
  130. package/src/storage/store.ts +80 -80
  131. package/src/types/agent.ts +67 -67
  132. package/src/types/checkpoint.ts +46 -46
  133. package/src/types/engram.ts +22 -0
  134. package/src/types/eval.ts +100 -100
  135. package/src/types/index.ts +6 -6
  136. package/src/version.ts +26 -0
@@ -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
+ }
@@ -0,0 +1,125 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Cognition Recipes — the intelligence interface (D14, 2026-07-30).
6
+ *
7
+ * AWM is a memory space for an LLM, not containing an LLM. Anything that
8
+ * requires real thinking (distilling a procedure, reflecting on a failure)
9
+ * runs HOST-SIDE: AWM ships a versioned recipe — a prompt plus a strict
10
+ * output contract — the host agent executes it as a SEPARATE focused call,
11
+ * and writes the result back as an ordinary memory carrying provenance
12
+ * (origin_class='recipe', recipe_id). AWM validates the write-back shape.
13
+ *
14
+ * Ported from memory-working-agent (MWA #14, live since 2026-06-15), whose
15
+ * production lessons are baked in:
16
+ * - SEPARATE focused call: cheap models reliably omit a "skill" field when
17
+ * it is bundled with other questions. One recipe = one call.
18
+ * - Host-side gating: only the host knows whether a task was procedural
19
+ * (tool calls, dispatches). The invitation states the gate; the host
20
+ * decides.
21
+ * - Dedupe by concept: writing the same skill name reinforces the existing
22
+ * memory (the R1 write-pipeline rule) instead of piling up copies.
23
+ */
24
+
25
+ export interface RecipeValidation {
26
+ ok: boolean;
27
+ errors: string[];
28
+ }
29
+
30
+ export interface CognitionRecipe {
31
+ /** Stable id including version, e.g. 'skill-derivation@1'. */
32
+ id: string;
33
+ title: string;
34
+ /** When the HOST should run this (the host owns the gate). */
35
+ gate: string;
36
+ /** The system prompt for the host's separate focused call. */
37
+ prompt: string;
38
+ /** Human-readable description of the required JSON output. */
39
+ output: string;
40
+ /** How the host must write the result back to AWM. */
41
+ writeBack: string;
42
+ /** Validate a write-back's concept/content shape. */
43
+ validate(concept: string, content: string): RecipeValidation;
44
+ }
45
+
46
+ function slugify(name: string): string {
47
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40);
48
+ }
49
+
50
+ export const SKILL_DERIVATION: CognitionRecipe = {
51
+ id: 'skill-derivation@1',
52
+ title: 'Derive a reusable skill from a completed task',
53
+ gate: 'Run ONLY after a task that used tools/multiple steps (roughly: 3+ tool calls or a delegated sub-task). Skip trivial one-offs.',
54
+ prompt: 'A task was just completed using tools. If it followed a REPEATABLE procedure that would help on similar future requests, output ONLY JSON {"name":"short skill name (e.g. \'triage scouting inbox\')","steps":"numbered how-to a future run can follow"}. If it was a trivial one-off, output exactly {}.',
55
+ output: '{"name": string, "steps": string-with-numbered-lines} or {} when nothing repeatable was learned.',
56
+ writeBack: "memory_write with concept: 'skill: <name>', content: <steps>, origin_class: 'recipe', recipe_id: 'skill-derivation@1', memory_class: 'canonical', memory_type: 'procedural', tags: ['topic=skill', 'skill=<slug>'].",
57
+ validate(concept: string, content: string): RecipeValidation {
58
+ const errors: string[] = [];
59
+ if (!/^skill:\s*\S/.test(concept)) errors.push("concept must start with 'skill: <name>'");
60
+ if (!content || content.trim().length < 20) errors.push('steps content too short to be a usable procedure');
61
+ if (!/(^|\n)\s*(\d+[.)]|[-*])\s+/.test(content)) errors.push('content must contain numbered or bulleted steps');
62
+ return { ok: errors.length === 0, errors };
63
+ },
64
+ };
65
+
66
+ export const FRICTION_LESSON: CognitionRecipe = {
67
+ id: 'friction-lesson@1',
68
+ title: 'Record a failure lesson (Reflexion) from a task that went wrong',
69
+ gate: 'Run ONLY after a task that failed, required a retry/revert, or surfaced a wrong assumption.',
70
+ prompt: 'A task just hit a failure, retry, or wrong assumption. Output ONLY JSON {"topic":"short subject of the lesson","lesson":"what went wrong and the rule to apply next time (include the WHY)"}. If nothing generalizable was learned, output exactly {}.',
71
+ output: '{"topic": string, "lesson": string} or {} when nothing generalizable was learned.',
72
+ writeBack: "memory_write with concept: 'lesson: <topic>', content: <lesson>, origin_class: 'recipe', recipe_id: 'friction-lesson@1', memory_class: 'canonical', event_type: 'friction', tags: ['topic=friction', 'about=<slug>'].",
73
+ validate(concept: string, content: string): RecipeValidation {
74
+ const errors: string[] = [];
75
+ if (!/^lesson:\s*\S/.test(concept)) errors.push("concept must start with 'lesson: <topic>'");
76
+ if (!content || content.trim().length < 20) errors.push('lesson content too short to be applicable');
77
+ return { ok: errors.length === 0, errors };
78
+ },
79
+ };
80
+
81
+ const REGISTRY: Record<string, CognitionRecipe> = {
82
+ [SKILL_DERIVATION.id]: SKILL_DERIVATION,
83
+ [FRICTION_LESSON.id]: FRICTION_LESSON,
84
+ };
85
+
86
+ export function getRecipe(id: string): CognitionRecipe | null {
87
+ return REGISTRY[id] ?? null;
88
+ }
89
+
90
+ export function listRecipes(): CognitionRecipe[] {
91
+ return Object.values(REGISTRY);
92
+ }
93
+
94
+ /**
95
+ * Validate a recipe-attributed write. Unknown recipe ids are rejected —
96
+ * provenance must never claim a recipe that does not exist.
97
+ */
98
+ export function validateRecipeWrite(recipeId: string, concept: string, content: string): RecipeValidation {
99
+ const recipe = getRecipe(recipeId);
100
+ if (!recipe) return { ok: false, errors: [`unknown recipe id '${recipeId}' — known: ${Object.keys(REGISTRY).join(', ')}`] };
101
+ return recipe.validate(concept, content);
102
+ }
103
+
104
+ /** Derive the standard skill/lesson slug tag value from a concept. */
105
+ export function recipeSlug(concept: string): string {
106
+ return slugify(concept.replace(/^(skill|lesson):\s*/i, ''));
107
+ }
108
+
109
+ /**
110
+ * The invitation appended to memory_task_end responses. Kept compact — it is
111
+ * read by an LLM in-band. The host runs the recipe as its OWN next step
112
+ * (separate focused call) and writes back via ordinary memory_write.
113
+ */
114
+ export function renderTaskEndInvitation(): string {
115
+ return [
116
+ '',
117
+ 'COGNITION RECIPES (host-side — you do the thinking, one SEPARATE focused pass each):',
118
+ `1. ${SKILL_DERIVATION.title}. Gate: ${SKILL_DERIVATION.gate}`,
119
+ ` Think: ${SKILL_DERIVATION.prompt}`,
120
+ ` If non-empty, write back: ${SKILL_DERIVATION.writeBack}`,
121
+ `2. ${FRICTION_LESSON.title}. Gate: ${FRICTION_LESSON.gate}`,
122
+ ` Think: ${FRICTION_LESSON.prompt}`,
123
+ ` If non-empty, write back: ${FRICTION_LESSON.writeBack}`,
124
+ ].join('\n');
125
+ }
@@ -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';