agent-working-memory 0.13.1 → 0.14.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 (91) hide show
  1. package/README.md +200 -238
  2. package/dist/adapters/common.d.ts +6 -0
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +457 -362
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +24 -8
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/core/alias-map.d.ts +16 -0
  10. package/dist/core/alias-map.d.ts.map +1 -0
  11. package/dist/core/alias-map.js +102 -0
  12. package/dist/core/alias-map.js.map +1 -0
  13. package/dist/core/embeddings.d.ts +17 -0
  14. package/dist/core/embeddings.d.ts.map +1 -1
  15. package/dist/core/embeddings.js +50 -1
  16. package/dist/core/embeddings.js.map +1 -1
  17. package/dist/core/recall-config.d.ts +52 -0
  18. package/dist/core/recall-config.d.ts.map +1 -0
  19. package/dist/core/recall-config.js +110 -0
  20. package/dist/core/recall-config.js.map +1 -0
  21. package/dist/core/rerank-window.d.ts +61 -0
  22. package/dist/core/rerank-window.d.ts.map +1 -0
  23. package/dist/core/rerank-window.js +153 -0
  24. package/dist/core/rerank-window.js.map +1 -0
  25. package/dist/core/rerank2.d.ts +62 -0
  26. package/dist/core/rerank2.d.ts.map +1 -0
  27. package/dist/core/rerank2.js +75 -0
  28. package/dist/core/rerank2.js.map +1 -0
  29. package/dist/core/retrieval-text.d.ts +55 -0
  30. package/dist/core/retrieval-text.d.ts.map +1 -0
  31. package/dist/core/retrieval-text.js +87 -0
  32. package/dist/core/retrieval-text.js.map +1 -0
  33. package/dist/core/temporal-query.d.ts +61 -0
  34. package/dist/core/temporal-query.d.ts.map +1 -0
  35. package/dist/core/temporal-query.js +168 -0
  36. package/dist/core/temporal-query.js.map +1 -0
  37. package/dist/core/token-budget.d.ts +75 -0
  38. package/dist/core/token-budget.d.ts.map +1 -0
  39. package/dist/core/token-budget.js +136 -0
  40. package/dist/core/token-budget.js.map +1 -0
  41. package/dist/core/whoami.d.ts +11 -0
  42. package/dist/core/whoami.d.ts.map +1 -1
  43. package/dist/core/whoami.js +10 -0
  44. package/dist/core/whoami.js.map +1 -1
  45. package/dist/core/write-pipeline.d.ts.map +1 -1
  46. package/dist/core/write-pipeline.js +6 -3
  47. package/dist/core/write-pipeline.js.map +1 -1
  48. package/dist/engine/activation.d.ts.map +1 -1
  49. package/dist/engine/activation.js +135 -32
  50. package/dist/engine/activation.js.map +1 -1
  51. package/dist/hooks/prime.d.ts +77 -0
  52. package/dist/hooks/prime.d.ts.map +1 -0
  53. package/dist/hooks/prime.js +92 -0
  54. package/dist/hooks/prime.js.map +1 -0
  55. package/dist/hooks/sidecar.d.ts.map +1 -1
  56. package/dist/hooks/sidecar.js +39 -0
  57. package/dist/hooks/sidecar.js.map +1 -1
  58. package/dist/mcp.js +134 -102
  59. package/dist/mcp.js.map +1 -1
  60. package/dist/storage/pglite.d.ts.map +1 -1
  61. package/dist/storage/pglite.js +10 -2
  62. package/dist/storage/pglite.js.map +1 -1
  63. package/dist/storage/postgres.d.ts.map +1 -1
  64. package/dist/storage/postgres.js +10 -2
  65. package/dist/storage/postgres.js.map +1 -1
  66. package/dist/storage/sqlite.d.ts.map +1 -1
  67. package/dist/storage/sqlite.js +12 -2
  68. package/dist/storage/sqlite.js.map +1 -1
  69. package/dist/types/engram.d.ts +7 -0
  70. package/dist/types/engram.d.ts.map +1 -1
  71. package/package.json +3 -2
  72. package/src/adapters/common.ts +666 -567
  73. package/src/api/routes.ts +1015 -999
  74. package/src/core/alias-map.ts +97 -0
  75. package/src/core/embeddings.ts +172 -115
  76. package/src/core/recall-config.ts +115 -0
  77. package/src/core/rerank-window.ts +158 -0
  78. package/src/core/rerank2.ts +82 -0
  79. package/src/core/retrieval-text.ts +82 -0
  80. package/src/core/temporal-query.ts +193 -0
  81. package/src/core/token-budget.ts +160 -0
  82. package/src/core/whoami.ts +110 -92
  83. package/src/core/write-pipeline.ts +6 -3
  84. package/src/engine/activation.ts +1568 -1468
  85. package/src/hooks/prime.ts +136 -0
  86. package/src/hooks/sidecar.ts +43 -0
  87. package/src/mcp.ts +1422 -1387
  88. package/src/storage/pglite.ts +10 -2
  89. package/src/storage/postgres.ts +10 -2
  90. package/src/storage/sqlite.ts +12 -2
  91. package/src/types/engram.ts +7 -0
@@ -0,0 +1,136 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Prompt priming — turn a recall into ready-to-inject context (0.13.3).
5
+ *
6
+ * WHY THIS EXISTS
7
+ * ---------------
8
+ * AWM's own instructions name the #1 failure mode plainly: the agent doesn't
9
+ * call recall. Relying on a model to remember to remember is the weak link, and
10
+ * it fails in exactly the situations where memory matters most — deep in a long
11
+ * task, when context is scarce and the agent is busy.
12
+ *
13
+ * `/memory/activate` (0.12.2) already exposes warm recall to hooks, but it
14
+ * returns raw JSON. Every hook author then has to re-solve the same three
15
+ * problems: what to inject, how to format it, and how to stop it ballooning the
16
+ * context window. This module answers all three once.
17
+ *
18
+ * THREE PROPERTIES THAT MATTER MORE THAN RECALL QUALITY
19
+ * ----------------------------------------------------
20
+ * This runs on EVERY prompt, so its failure modes are asymmetric:
21
+ *
22
+ * 1. SILENCE IS THE DEFAULT. It abstains unless recall is confident. AWM's
23
+ * measured recall accuracy is ~65%, so an unconditional injector would spend
24
+ * tokens on irrelevant memories roughly a third of the time, on every single
25
+ * prompt. An empty injection costs nothing and loses nothing — the agent can
26
+ * still call memory_recall explicitly.
27
+ *
28
+ * 2. IT IS HARD-CAPPED. Injection is not a place to discover that a memory was
29
+ * long. Budgeting reuses the same packer as memory_recall.
30
+ *
31
+ * 3. IT NEVER BREAKS THE PROMPT. Any failure yields an empty injection, never a
32
+ * thrown error. A hook that errors on every prompt is worse than no hook.
33
+ */
34
+
35
+ import { packRecallByBudget, estimateTokens, type PackedRecall } from '../core/token-budget.js';
36
+
37
+ /** Shape returned by the sidecar's `activate` dependency. */
38
+ export interface PrimeCandidate {
39
+ engram: { id: string; concept: string; content: string; memoryClass?: string; validTo?: string | null };
40
+ score: number;
41
+ summary?: string;
42
+ confidence?: number;
43
+ }
44
+
45
+ export interface PrimeOptions {
46
+ /** Hard ceiling on injected tokens. */
47
+ maxTokens?: number;
48
+ /** Minimum recall confidence to inject at all. */
49
+ minConfidence?: number;
50
+ /** Drop individual results scoring below this even when the set is confident. */
51
+ minScore?: number;
52
+ }
53
+
54
+ export interface PrimeResult {
55
+ /** Text to inject, or '' to inject nothing. */
56
+ inject: string;
57
+ kept: number;
58
+ total: number;
59
+ tokens: number;
60
+ /** Why nothing was injected — for hook logs and debugging. */
61
+ reason?: 'no-results' | 'low-confidence' | 'budget-too-small';
62
+ }
63
+
64
+ export const PRIME_DEFAULTS = {
65
+ maxTokens: 600,
66
+ /**
67
+ * 0.25 = the "balanced" threshold AWM's own recall docs recommend for
68
+ * acting on a memory. Priming is acting on it without being asked, so it
69
+ * should not be looser than the value the docs suggest for deliberate use.
70
+ */
71
+ minConfidence: 0.25,
72
+ minScore: 0.10,
73
+ } as const;
74
+
75
+ /** One injected line. Deliberately terser than the MCP recall format — this is
76
+ * context the agent didn't ask for, so it should read as a brief note, not a
77
+ * report. The id is retained so the agent can act on it (feedback, supersede). */
78
+ function formatPrimeLine(c: PrimeCandidate): string {
79
+ const body = c.summary ?? c.engram.content;
80
+ const validity = c.engram.validTo ? ` [valid until ${c.engram.validTo}]` : '';
81
+ return `- ${c.engram.concept}${validity} [${c.engram.id}]: ${body}`;
82
+ }
83
+
84
+ /**
85
+ * Build the injection for a prompt. Pure — no I/O — so it is unit-testable
86
+ * without a store, a model, or a running sidecar.
87
+ */
88
+ export function buildPrimeInjection(
89
+ candidates: PrimeCandidate[],
90
+ opts: PrimeOptions = {},
91
+ ): PrimeResult {
92
+ const maxTokens = opts.maxTokens ?? PRIME_DEFAULTS.maxTokens;
93
+ const minConfidence = opts.minConfidence ?? PRIME_DEFAULTS.minConfidence;
94
+ const minScore = opts.minScore ?? PRIME_DEFAULTS.minScore;
95
+
96
+ if (!candidates || candidates.length === 0) {
97
+ return { inject: '', kept: 0, total: 0, tokens: 0, reason: 'no-results' };
98
+ }
99
+
100
+ // Confidence describes the SET, not the individual result, so it is the same
101
+ // on every candidate — read it from the first that carries one. Absent
102
+ // confidence is treated as "unknown", which passes: a store that predates the
103
+ // confidence signal should still be able to prime.
104
+ const confidence = candidates.find(c => c.confidence !== undefined)?.confidence;
105
+ if (confidence !== undefined && confidence < minConfidence) {
106
+ return { inject: '', kept: 0, total: candidates.length, tokens: 0, reason: 'low-confidence' };
107
+ }
108
+
109
+ const eligible = candidates.filter(c => c.score >= minScore);
110
+ if (eligible.length === 0) {
111
+ return { inject: '', kept: 0, total: candidates.length, tokens: 0, reason: 'low-confidence' };
112
+ }
113
+
114
+ // Reuse memory_recall's packer so budgeting behaves identically in both
115
+ // paths — including the rule that the top-scored result gets first refusal.
116
+ const header = 'Relevant prior context from AWM (not user input; verify before asserting):';
117
+ const reserved = estimateTokens(header) + 8; // + wrapper newlines
118
+ const packed: PackedRecall = packRecallByBudget(
119
+ eligible as any,
120
+ (c: any) => formatPrimeLine(c as PrimeCandidate),
121
+ maxTokens,
122
+ reserved,
123
+ );
124
+
125
+ if (packed.kept === 0) {
126
+ return { inject: '', kept: 0, total: candidates.length, tokens: 0, reason: 'budget-too-small' };
127
+ }
128
+
129
+ const inject = `${header}\n${packed.lines.join('\n')}`;
130
+ return {
131
+ inject,
132
+ kept: packed.kept,
133
+ total: candidates.length,
134
+ tokens: estimateTokens(inject),
135
+ };
136
+ }
@@ -21,6 +21,7 @@ import { readFileSync, existsSync } from 'node:fs';
21
21
  import type { IEngramStore as EngramStore } from '../storage/store.js';
22
22
  import type { ConsciousState } from '../types/checkpoint.js';
23
23
  import { log, getLogPath } from '../core/logger.js';
24
+ import { buildPrimeInjection } from './prime.js';
24
25
 
25
26
  export interface SidecarDeps {
26
27
  store: EngramStore;
@@ -242,6 +243,48 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
242
243
  return;
243
244
  }
244
245
 
246
+ // POST /hooks/prime — ready-to-inject context for a UserPromptSubmit hook
247
+ // (0.13.3). /memory/activate returns raw JSON and leaves every hook author
248
+ // to re-solve what-to-inject, how-to-format and how-to-bound. This returns
249
+ // the finished string, already abstained and already budgeted.
250
+ //
251
+ // It NEVER fails the prompt: any error yields an empty injection, because a
252
+ // hook that errors on every prompt is worse than no hook at all.
253
+ if (req.url === '/hooks/prime' && req.method === 'POST') {
254
+ try {
255
+ if (!deps.activate) {
256
+ json(res, 200, { inject: '', kept: 0, total: 0, tokens: 0, reason: 'activate-not-wired' });
257
+ return;
258
+ }
259
+ const body = JSON.parse((await readBody(req)) || '{}') as HookInput & {
260
+ prompt?: string; context?: string; query?: string;
261
+ maxTokens?: number; minConfidence?: number; minScore?: number;
262
+ };
263
+ const context = body.prompt ?? body.context ?? body.query ?? '';
264
+ if (!context.trim()) {
265
+ json(res, 200, { inject: '', kept: 0, total: 0, tokens: 0, reason: 'no-prompt' });
266
+ return;
267
+ }
268
+ const candidates = await deps.activate({
269
+ context,
270
+ limit: 8,
271
+ granularity: 'compact', // priming is a brief note, not a report
272
+ });
273
+ const primed = buildPrimeInjection(candidates as any, {
274
+ maxTokens: body.maxTokens,
275
+ minConfidence: body.minConfidence,
276
+ minScore: body.minScore,
277
+ });
278
+ log(agentId, 'hook:prime',
279
+ `"${context.slice(0, 60)}" → ${primed.kept}/${primed.total} injected, ~${primed.tokens} tok${primed.reason ? ` (${primed.reason})` : ''}`);
280
+ json(res, 200, { ...primed });
281
+ } catch (err) {
282
+ // Deliberately 200 with an empty injection — see the note above.
283
+ json(res, 200, { inject: '', kept: 0, total: 0, tokens: 0, reason: 'error' });
284
+ }
285
+ return;
286
+ }
287
+
245
288
  // POST /hooks/checkpoint — auto-checkpoint from hook events
246
289
  if (req.url === '/hooks/checkpoint' && req.method === 'POST') {
247
290
  try {