melchizedek-agents 0.9.6 → 0.16.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 (246) hide show
  1. package/.env.example +272 -0
  2. package/AGENT_SETUP.md +39 -23
  3. package/CHANGELOG.md +623 -0
  4. package/DOCUMENTATION.md +429 -109
  5. package/QUICKSTART.md +92 -18
  6. package/README.md +45 -5
  7. package/config/agents/README.md +46 -1
  8. package/config/agents/examples/ares.yaml +117 -40
  9. package/config/agents/examples/assistant.yaml +159 -0
  10. package/config/agents/examples/augustin.yaml +154 -51
  11. package/config/agents/examples/cartographers.yaml +91 -23
  12. package/config/agents/examples/claude.yaml +45 -6
  13. package/config/agents/examples/council.yaml +23 -8
  14. package/config/agents/examples/critic.yaml +57 -21
  15. package/config/agents/examples/delegation.yaml +76 -9
  16. package/config/agents/examples/hierarchical.yaml +55 -8
  17. package/config/agents/examples/image_production.yaml +54 -16
  18. package/config/agents/examples/librarian.yaml +70 -14
  19. package/config/agents/examples/model_zoo.yaml +105 -29
  20. package/config/agents/examples/patient_advocate.yaml +102 -34
  21. package/config/agents/examples/research.yaml +250 -0
  22. package/config/agents/examples/scribe.yaml +242 -0
  23. package/config/agents/examples/scriptorium.yaml +87 -24
  24. package/config/agents/examples/style_council.yaml +31 -9
  25. package/config/agents/examples/syndicate.yaml +29 -10
  26. package/config/agents/examples/tutor.yaml +10 -3
  27. package/config/agents/syndicateSchema.yaml +44 -9
  28. package/config/agents/templates/README.md +58 -0
  29. package/config/agents/templates/account_memory.yaml +153 -0
  30. package/config/agents/templates/case_desk.yaml +188 -0
  31. package/config/agents/templates/conversational.yaml +110 -0
  32. package/config/agents/templates/draft_review.yaml +130 -0
  33. package/config/agents/templates/intake_extractor.yaml +152 -0
  34. package/config/agents/templates/research_brief.yaml +169 -0
  35. package/config/agents/templates/research_desk.yaml +152 -0
  36. package/config/agents/templates/review_panel.yaml +188 -0
  37. package/config/agents/templates/support_triage.yaml +273 -0
  38. package/config/agents/templates/systems_operator.yaml +157 -0
  39. package/db/hardening.sql +202 -0
  40. package/db/memory_v2.sql +77 -0
  41. package/db/migrations/0001_base.sql +167 -0
  42. package/db/migrations/0002_erase_scope.sql +172 -0
  43. package/db/migrations/0003_postgres_storage.sql +58 -0
  44. package/db/migrations/0004_usage.sql +94 -0
  45. package/db/migrations/0005_agent_registry.sql +200 -0
  46. package/db/migrations/0006_task_leases.sql +25 -0
  47. package/db/migrations/0007_memory_commit.sql +134 -0
  48. package/db/migrations/0008_memory_retention.sql +37 -0
  49. package/db/migrations/0009_task_queue.sql +51 -0
  50. package/db/migrations/0010_erase_complete.sql +180 -0
  51. package/db/telemetry.sql +401 -0
  52. package/demo/README.md +77 -0
  53. package/demo/a2a_demo.mjs +95 -0
  54. package/dist/lib/a2a/app.d.ts +253 -0
  55. package/dist/lib/a2a/app.js +751 -0
  56. package/dist/lib/a2a/executor.d.ts +167 -0
  57. package/dist/lib/a2a/executor.js +548 -0
  58. package/dist/lib/a2a/identity.d.ts +122 -0
  59. package/dist/lib/a2a/identity.js +254 -0
  60. package/dist/lib/a2a/policy.d.ts +110 -0
  61. package/dist/lib/a2a/policy.js +178 -0
  62. package/dist/lib/a2a/redisLimits.d.ts +26 -0
  63. package/dist/lib/a2a/redisLimits.js +31 -0
  64. package/dist/lib/a2a/remoteAgent.d.ts +91 -0
  65. package/dist/lib/a2a/remoteAgent.js +268 -0
  66. package/dist/lib/a2a/turnLock.d.ts +28 -0
  67. package/dist/lib/a2a/turnLock.js +76 -0
  68. package/dist/lib/compile.d.ts +11 -1
  69. package/dist/lib/compile.js +109 -8
  70. package/dist/lib/config.d.ts +7 -7
  71. package/dist/lib/config.js +7 -7
  72. package/dist/lib/doctor.d.ts +145 -0
  73. package/dist/lib/doctor.js +517 -0
  74. package/dist/lib/grounding.d.ts +2 -2
  75. package/dist/lib/grounding.js +2 -2
  76. package/dist/lib/guards/index.d.ts +37 -0
  77. package/dist/lib/guards/index.js +66 -0
  78. package/dist/lib/guards/science.d.ts +117 -0
  79. package/dist/lib/guards/science.js +444 -0
  80. package/dist/lib/index.d.ts +37 -2
  81. package/dist/lib/index.js +35 -4
  82. package/dist/lib/loadEnv.d.ts +23 -11
  83. package/dist/lib/loadEnv.js +77 -29
  84. package/dist/lib/loadSyndicate.d.ts +97 -5
  85. package/dist/lib/loadSyndicate.js +176 -76
  86. package/dist/lib/memory/erase.d.ts +24 -0
  87. package/dist/lib/memory/erase.js +47 -0
  88. package/dist/lib/memory/namespace.d.ts +37 -0
  89. package/dist/lib/memory/namespace.js +74 -0
  90. package/dist/lib/memory/providers.d.ts +99 -0
  91. package/dist/lib/memory/providers.js +240 -0
  92. package/dist/lib/memory/store.d.ts +74 -0
  93. package/dist/lib/memory/store.js +108 -0
  94. package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
  95. package/dist/lib/memory/supabaseMemoryService.js +239 -125
  96. package/dist/lib/models/capabilities.d.ts +103 -0
  97. package/dist/lib/models/capabilities.js +291 -0
  98. package/dist/lib/models/claudeLlm.d.ts +10 -1
  99. package/dist/lib/models/claudeLlm.js +57 -27
  100. package/dist/lib/models/endpoints.d.ts +100 -0
  101. package/dist/lib/models/endpoints.js +282 -0
  102. package/dist/lib/models/gateway.d.ts +107 -0
  103. package/dist/lib/models/gateway.js +157 -0
  104. package/dist/lib/models/gatewayLlm.d.ts +57 -0
  105. package/dist/lib/models/gatewayLlm.js +105 -0
  106. package/dist/lib/models/gptLlm.d.ts +51 -2
  107. package/dist/lib/models/gptLlm.js +169 -26
  108. package/dist/lib/models/grokLlm.d.ts +19 -10
  109. package/dist/lib/models/grokLlm.js +29 -14
  110. package/dist/lib/models/ollamaLlm.d.ts +26 -0
  111. package/dist/lib/models/ollamaLlm.js +43 -1
  112. package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
  113. package/dist/lib/models/openAiCompatibleLlm.js +200 -37
  114. package/dist/lib/models/providerMap.js +9 -2
  115. package/dist/lib/models/registry.d.ts +42 -5
  116. package/dist/lib/models/registry.js +140 -29
  117. package/dist/lib/models/retry.d.ts +146 -0
  118. package/dist/lib/models/retry.js +305 -0
  119. package/dist/lib/models/schemaNormalize.d.ts +20 -0
  120. package/dist/lib/models/schemaNormalize.js +43 -0
  121. package/dist/lib/net/addressGuard.d.ts +60 -0
  122. package/dist/lib/net/addressGuard.js +216 -0
  123. package/dist/lib/observability/embeddings.d.ts +11 -2
  124. package/dist/lib/observability/embeddings.js +18 -14
  125. package/dist/lib/observability/metrics.d.ts +36 -0
  126. package/dist/lib/observability/metrics.js +86 -0
  127. package/dist/lib/observability/redact.d.ts +29 -0
  128. package/dist/lib/observability/redact.js +102 -0
  129. package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
  130. package/dist/lib/observability/supabaseSpanExporter.js +22 -6
  131. package/dist/lib/observability/tracer.d.ts +10 -0
  132. package/dist/lib/observability/tracer.js +70 -3
  133. package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
  134. package/dist/lib/persistence/supabaseProvider.js +32 -6
  135. package/dist/lib/registry.d.ts +87 -0
  136. package/dist/lib/registry.js +146 -0
  137. package/dist/lib/runtime/approvals.d.ts +44 -0
  138. package/dist/lib/runtime/approvals.js +67 -0
  139. package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
  140. package/dist/lib/runtime/syndicateTurn.js +597 -0
  141. package/dist/lib/runtime/turnControl.d.ts +90 -0
  142. package/dist/lib/runtime/turnControl.js +131 -0
  143. package/dist/lib/session/supabaseSessionService.js +1 -1
  144. package/dist/lib/session/transcript.d.ts +9 -1
  145. package/dist/lib/session/transcript.js +15 -7
  146. package/dist/lib/skills.d.ts +50 -0
  147. package/dist/lib/skills.js +138 -0
  148. package/dist/lib/storage/postgres/index.d.ts +98 -0
  149. package/dist/lib/storage/postgres/index.js +146 -0
  150. package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
  151. package/dist/lib/storage/postgres/memoryStore.js +82 -0
  152. package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
  153. package/dist/lib/storage/postgres/sessionService.js +225 -0
  154. package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
  155. package/dist/lib/storage/postgres/taskQueue.js +101 -0
  156. package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
  157. package/dist/lib/storage/postgres/taskStore.js +168 -0
  158. package/dist/lib/storage/schema.d.ts +27 -0
  159. package/dist/lib/storage/schema.js +50 -0
  160. package/dist/lib/storage/schemaVersion.d.ts +25 -0
  161. package/dist/lib/storage/schemaVersion.js +46 -0
  162. package/dist/lib/syndicateSchema.d.ts +275 -0
  163. package/dist/lib/syndicateSchema.js +534 -0
  164. package/dist/lib/toolRegistry.d.ts +15 -0
  165. package/dist/lib/toolRegistry.js +59 -10
  166. package/dist/lib/tools/collectionsSearchTool.js +1 -1
  167. package/dist/lib/tools/mcpServe.d.ts +45 -0
  168. package/dist/lib/tools/mcpServe.js +88 -0
  169. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  170. package/dist/lib/tools/mcpToolFactory.js +41 -21
  171. package/dist/lib/tools/science/corrections.d.ts +42 -0
  172. package/dist/lib/tools/science/corrections.js +115 -0
  173. package/dist/lib/tools/science/europepmc.d.ts +51 -0
  174. package/dist/lib/tools/science/europepmc.js +127 -0
  175. package/dist/lib/tools/science/http.d.ts +31 -0
  176. package/dist/lib/tools/science/http.js +104 -0
  177. package/dist/lib/tools/science/openalex.d.ts +30 -0
  178. package/dist/lib/tools/science/openalex.js +81 -0
  179. package/dist/lib/tools/science/trials.d.ts +58 -0
  180. package/dist/lib/tools/science/trials.js +203 -0
  181. package/dist/lib/tools/science/types.d.ts +69 -0
  182. package/dist/lib/tools/science/types.js +48 -0
  183. package/dist/lib/tools/scienceTools.d.ts +100 -0
  184. package/dist/lib/tools/scienceTools.js +277 -0
  185. package/dist/lib/tools/taskTools.d.ts +204 -0
  186. package/dist/lib/tools/taskTools.js +374 -0
  187. package/dist/lib/tools/toolContract.d.ts +17 -4
  188. package/dist/lib/tools/toolContract.js +13 -5
  189. package/dist/lib/tools/webExtractTool.d.ts +13 -10
  190. package/dist/lib/tools/webExtractTool.js +23 -55
  191. package/dist/lib/tools/webSearchTool.d.ts +2 -1
  192. package/dist/lib/tools/webSearchTool.js +4 -3
  193. package/dist/lib/tools/wikiTools.d.ts +2 -2
  194. package/dist/lib/tools/wikiTools.js +12 -12
  195. package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
  196. package/dist/lib/tools/xApiSearchTool.js +583 -0
  197. package/dist/lib/tools/xSearchTool.js +1 -1
  198. package/dist/lib/wiki/agentRun.js +1 -1
  199. package/dist/lib/wiki/builder.js +21 -6
  200. package/dist/lib/wiki/dive.js +2 -2
  201. package/dist/lib/wiki/entities.d.ts +2 -2
  202. package/dist/lib/wiki/entities.js +7 -7
  203. package/dist/lib/wiki/extract.js +5 -1
  204. package/dist/lib/wiki/fill.js +5 -5
  205. package/dist/lib/wiki/format.d.ts +7 -5
  206. package/dist/lib/wiki/format.js +2 -2
  207. package/dist/lib/wiki/graph.js +2 -2
  208. package/dist/lib/wiki/lint.js +2 -2
  209. package/dist/lib/wiki/markdown.d.ts +1 -1
  210. package/dist/lib/wiki/markdown.js +6 -1
  211. package/dist/lib/wiki/pipeline.js +2 -2
  212. package/dist/lib/wiki/vault.js +2 -2
  213. package/dist/scripts/a2a_server.d.ts +17 -11
  214. package/dist/scripts/a2a_server.js +405 -1028
  215. package/dist/scripts/assistant_worker.d.ts +33 -0
  216. package/dist/scripts/assistant_worker.js +193 -0
  217. package/dist/scripts/db.d.ts +26 -0
  218. package/dist/scripts/db.js +179 -0
  219. package/dist/scripts/doctor.d.ts +23 -0
  220. package/dist/scripts/doctor.js +90 -0
  221. package/dist/scripts/init.d.ts +43 -0
  222. package/dist/scripts/init.js +152 -0
  223. package/dist/scripts/registry.d.ts +25 -0
  224. package/dist/scripts/registry.js +182 -0
  225. package/dist/scripts/skills_install.d.ts +7 -0
  226. package/dist/scripts/skills_install.js +137 -0
  227. package/dist/scripts/syndicate_chat.js +159 -276
  228. package/package.json +90 -16
  229. package/skills/README.md +74 -0
  230. package/skills/briefs/README.md +16 -0
  231. package/skills/briefs/_shared.md +16 -0
  232. package/skills/briefs/melchizedek-author.md +60 -0
  233. package/skills/briefs/melchizedek-memory.md +47 -0
  234. package/skills/briefs/melchizedek-models.md +47 -0
  235. package/skills/briefs/melchizedek-scribe.md +37 -0
  236. package/skills/briefs/melchizedek-serve.md +56 -0
  237. package/skills/briefs/melchizedek.md +69 -0
  238. package/skills/briefs/skills-readme.md +33 -0
  239. package/skills/melchizedek/SKILL.md +159 -0
  240. package/skills/melchizedek-author/SKILL.md +99 -0
  241. package/skills/melchizedek-author/templates/minimal.yaml +60 -0
  242. package/skills/melchizedek-memory/SKILL.md +138 -0
  243. package/skills/melchizedek-models/SKILL.md +137 -0
  244. package/skills/melchizedek-scribe/SKILL.md +72 -0
  245. package/skills/melchizedek-scribe/templates/brief.md +27 -0
  246. package/skills/melchizedek-serve/SKILL.md +119 -0
@@ -1,8 +1,10 @@
1
- import { GoogleGenAI } from '@google/genai';
2
- import { EMBEDDING_MODEL, EMBEDDING_DIMENSIONS, MEMORY_EXTRACTION_MODEL } from "../config.js";
1
+ import { memoryProvidersFromEnv, modelExtractor } from './providers.js';
2
+ import { providerForModel } from '../models/providerMap.js';
3
+ import { isSupabaseClient, supabaseMemoryStore } from './store.js';
3
4
  /**
4
- * Harness-injected blocks the Discord surface prefixes to a user message
5
- * (nihilistic-penguin stocks.py): the date marker and the portfolio payload.
5
+ * Harness-injected blocks a calling surface prefixes to a user message: the
6
+ * `[System Context: …]` date marker and a labelled data block (`[… Payload]`
7
+ * or `[… Sheet]` followed by a fenced block).
6
8
  * Neither is something the USER said. Stripped from the memory SEARCH query
7
9
  * (a portfolio JSON would dominate the embedding and drown the question) and
8
10
  * from the extraction transcript (a snapshot that changes daily would be
@@ -12,7 +14,9 @@ import { EMBEDDING_MODEL, EMBEDDING_DIMENSIONS, MEMORY_EXTRACTION_MODEL } from "
12
14
  export function stripHarnessBlocks(text) {
13
15
  return text
14
16
  .replace(/\[System Context:[^\]]*\]\s*/g, '')
15
- .replace(/\[(?:Portfolio Performance Payload|Stock Trade Sheet)\]\s*```[\s\S]*?```\s*/g, '')
17
+ // A data block a calling surface attaches: a bracketed label ending in
18
+ // "Payload" or "Sheet" (e.g. `[Account Payload]`), then a fenced block.
19
+ .replace(/\[[A-Za-z][A-Za-z ]{0,60}(?:Payload|Sheet)\]\s*```[\s\S]*?```\s*/g, '')
16
20
  .trim();
17
21
  }
18
22
  const FACT_EXTRACTION_PROMPT = `You are a precise record-keeping engine. Your job is to analyze a conversation transcript and distill it into structured, self-contained memory records that can be trusted and evolved in future conversations.
@@ -100,21 +104,47 @@ const MONTH_NAMES = [
100
104
  'july', 'august', 'september', 'october', 'november', 'december',
101
105
  ];
102
106
  export class SupabaseVectorMemoryService {
103
- genai;
104
- supabase;
107
+ extractor;
108
+ embedder;
109
+ store;
105
110
  /**
106
111
  * How many of each session's events have already been distilled.
107
112
  * Keyed `{userKey}::{sessionId}`.
108
113
  *
109
- * In-process on purpose: a restart resets it, so the next turn re-reads
110
- * that session once. Semantic dedup absorbs that batch, which is why this
111
- * needs no table of its own — the two fixes cover each other's edges.
114
+ * The durable marker lives in the store when it has one
115
+ * (`melchizedek_memory_ingest`, migration 0007), committed with the
116
+ * facts; this map is the fallback for a store without it, where a restart
117
+ * re-reads a session once and the semantic dedup absorbs that batch.
112
118
  */
113
119
  ingestedEventCount = new Map();
114
- constructor(config, supabaseClient) {
115
- this.genai = new GoogleGenAI({ apiKey: config.apiKey });
116
- this.supabase = supabaseClient;
117
- console.log(`[MemoryService] Initialized — backend: Supabase Vector Search (pgvector), structured records`);
120
+ /** The server's Gemini key, for a per-syndicate Gemini extraction model. */
121
+ geminiKey;
122
+ /** Extractors for per-syndicate models (memory_extraction_model), built once. */
123
+ extractors = new Map();
124
+ buildExtractor;
125
+ /**
126
+ * @param config.apiKey The server's Gemini key, used only when the
127
+ * configured extraction model or embedder is Gemini (the default).
128
+ * @param config.extractor / config.embedder What computes memory
129
+ * (lib/memory/providers.ts, ADR 0020). Default: from the environment
130
+ * (MEMORY_EXTRACTION_MODEL, MEMORY_EMBEDDING_*), which is Gemini when
131
+ * nothing is set.
132
+ */
133
+ /**
134
+ * @param backend Where facts live: a Supabase client (REST), or any
135
+ * MemoryStore, e.g. the direct-Postgres one (lib/storage/postgres).
136
+ */
137
+ constructor(config, backend) {
138
+ const fromEnv = config.extractor && config.embedder
139
+ ? undefined
140
+ : memoryProvidersFromEnv(process.env, config.apiKey);
141
+ this.extractor = config.extractor ?? fromEnv.extractor;
142
+ this.geminiKey = config.apiKey;
143
+ this.buildExtractor = config.extractorFor;
144
+ this.embedder = config.embedder ?? fromEnv.embedder;
145
+ this.store = isSupabaseClient(backend) ? supabaseMemoryStore(backend) : backend;
146
+ console.log(`[MemoryService] Initialized — backend: Supabase Vector Search (pgvector), structured records; `
147
+ + `extraction ${this.extractor.model}, embeddings ${this.embedder.provider}/${this.embedder.model} (${this.embedder.dimensions}d)`);
118
148
  }
119
149
  /**
120
150
  * @param extractionRules Optional domain rules appended to the shared
@@ -123,10 +153,20 @@ export class SupabaseVectorMemoryService {
123
153
  * "never store a market quote" — arrives here rather than being edited
124
154
  * into it. Declared as `memory_extraction_rules` on the served syndicate.
125
155
  */
126
- async addSessionToMemory(session, extractionRules) {
156
+ async addSessionToMemory(session, extractionRules, options = {}) {
127
157
  const userKey = `${session.appName}/${session.userId}`;
128
158
  const watermarkKey = `${userKey}::${session.id}`;
129
- const alreadyIngested = this.ingestedEventCount.get(watermarkKey) ?? 0;
159
+ let alreadyIngested = this.ingestedEventCount.get(watermarkKey) ?? 0;
160
+ // The durable marker (migration 0007), when the store has one: it
161
+ // survives a restart, so a turn is never distilled twice.
162
+ if (this.store.ingestedEvents) {
163
+ try {
164
+ alreadyIngested = Math.max(alreadyIngested, await this.store.ingestedEvents(userKey, session.id));
165
+ }
166
+ catch (err) {
167
+ console.error(`[MemoryService] Processed marker unreadable (using this process's):`, err instanceof Error ? err.message : err);
168
+ }
169
+ }
130
170
  const fresh = eventsToIngest(session.events ?? [], alreadyIngested);
131
171
  if (fresh.length === 0) {
132
172
  console.log(`[MemoryService] Session ${session.id}: no new turns since last ingestion — skipping.`);
@@ -139,18 +179,43 @@ export class SupabaseVectorMemoryService {
139
179
  }
140
180
  console.log(`[MemoryService] Extracting records from session: ${session.id} `
141
181
  + `(${fresh.length} new turn(s) of ${(session.events ?? []).length})`);
142
- const records = await this.extractRecords(transcript, extractionRules);
143
- // Advance only after extraction SUCCEEDS — a throw leaves the turns
144
- // pending so the next task retries them rather than losing them.
145
- this.ingestedEventCount.set(watermarkKey, (session.events ?? []).length);
182
+ // Every step below THROWS on failure, and the watermark advances only
183
+ // after the records are stored. A provider 429 during extraction, a
184
+ // failed embedding or a rejected insert therefore leaves these turns
185
+ // pending, and the next task in this session retries them. (Before,
186
+ // each failure was swallowed and the watermark moved past turns that
187
+ // were never distilled — a lost fact costs the user something they
188
+ // said.) A retried batch that partly landed is absorbed by the
189
+ // semantic dedup below.
190
+ const total = (session.events ?? []).length;
191
+ const marker = { sessionId: session.id, events: total };
192
+ const advance = () => this.ingestedEventCount.set(watermarkKey, total);
193
+ const records = await this.extractRecords(transcript, extractionRules, this.extractorFor(options.extractionModel));
146
194
  if (records.length === 0) {
195
+ // "No facts" is an answer: record that these turns are done.
196
+ if (this.store.commit)
197
+ await this.commitOrThrow(userKey, marker, [], []);
198
+ advance();
147
199
  console.log(`[MemoryService] No records extracted from session ${session.id}.`);
148
200
  return;
149
201
  }
150
202
  console.log(`[MemoryService] Extracted ${records.length} record(s) from session ${session.id}.`);
151
203
  const embeddings = await this.embedTexts(records.map(r => r.line));
204
+ if (this.store.commit) {
205
+ // One transaction: the new facts, the rows they supersede, and the
206
+ // marker. A failure leaves all three as they were, and the turns
207
+ // pending for the next task to retry (ADR 0020 item 6).
208
+ const payload = await this.prepareFacts(userKey, records, embeddings);
209
+ const kept = new Set(payload.map(p => p.fact));
210
+ const retire = await this.findRetirements(userKey, records.filter(r => r.supersedes && kept.has(r.line)), new Set());
211
+ const inserted = await this.commitOrThrow(userKey, marker, payload, retire);
212
+ advance();
213
+ console.log(`[MemoryService] Stored ${inserted.length} record(s), superseded ${retire.length}, for user key: ${userKey}`);
214
+ return;
215
+ }
152
216
  const inserted = await this.upsertToSupabase(userKey, records, embeddings);
153
217
  await this.applySupersessions(userKey, records, inserted);
218
+ advance();
154
219
  console.log(`[MemoryService] Stored ${inserted.size} record(s) for user key: ${userKey}`);
155
220
  }
156
221
  /**
@@ -163,15 +228,15 @@ export class SupabaseVectorMemoryService {
163
228
  * `adk_sessions` and must be cleared separately if full erasure is needed.
164
229
  */
165
230
  async deleteUserMemory(userKey) {
166
- const { count, error } = await this.supabase
167
- .from('adk_memory_facts')
168
- .delete({ count: 'exact' })
169
- .eq('user_key', userKey);
170
- if (error) {
171
- throw new Error(`Memory deletion failed for ${userKey}: ${error.message}`);
231
+ let count;
232
+ try {
233
+ count = await this.store.deleteUser(userKey);
172
234
  }
173
- console.log(`[MemoryService] Deleted ${count ?? 0} fact(s) for user key: ${userKey}`);
174
- return count ?? 0;
235
+ catch (err) {
236
+ throw new Error(`Memory deletion failed for ${userKey}: ${err instanceof Error ? err.message : String(err)}`);
237
+ }
238
+ console.log(`[MemoryService] Deleted ${count} fact(s) for user key: ${userKey}`);
239
+ return count;
175
240
  }
176
241
  async searchMemory(request) {
177
242
  const query = stripHarnessBlocks(request.query) || request.query;
@@ -179,7 +244,27 @@ export class SupabaseVectorMemoryService {
179
244
  const userKey = `${request.appName}/${request.userId}`;
180
245
  return this.searchSupabase(userKey, query);
181
246
  }
182
- async extractRecords(transcript, extractionRules) {
247
+ /**
248
+ * The extractor for a syndicate's `memory_extraction_model` (ADR 0020
249
+ * item 4), or the deployment's. The server's Gemini key reaches only a
250
+ * Gemini model; another provider reads its own key from the environment.
251
+ */
252
+ extractorFor(model) {
253
+ if (!model || model === this.extractor.model)
254
+ return this.extractor;
255
+ let extractor = this.extractors.get(model);
256
+ if (!extractor) {
257
+ extractor = this.buildExtractor
258
+ ? this.buildExtractor(model)
259
+ : modelExtractor({
260
+ model,
261
+ ...(this.geminiKey && providerForModel(model) === 'gemini' ? { apiKey: this.geminiKey } : {}),
262
+ });
263
+ this.extractors.set(model, extractor);
264
+ }
265
+ return extractor;
266
+ }
267
+ async extractRecords(transcript, extractionRules, extractor = this.extractor) {
183
268
  const sessionDate = new Date().toISOString().slice(0, 10);
184
269
  // Empty by default, so a consumer that declares no rules gets exactly
185
270
  // the prompt it got before this slot existed.
@@ -191,15 +276,7 @@ export class SupabaseVectorMemoryService {
191
276
  .replace('{domain_rules}', domainRules)
192
277
  .replace('{transcript}', transcript);
193
278
  try {
194
- const response = await this.genai.models.generateContent({
195
- model: MEMORY_EXTRACTION_MODEL,
196
- contents: [{ role: 'user', parts: [{ text: prompt }] }],
197
- config: {
198
- temperature: 0.1,
199
- maxOutputTokens: 4096,
200
- }
201
- });
202
- const text = response.text?.trim() ?? '';
279
+ const text = (await extractor.extract(prompt)).trim();
203
280
  if (!text || text === 'NO_FACTS_EXTRACTED')
204
281
  return [];
205
282
  return text
@@ -209,8 +286,7 @@ export class SupabaseVectorMemoryService {
209
286
  }
210
287
  catch (err) {
211
288
  const msg = err instanceof Error ? err.message : String(err);
212
- console.error(`[MemoryService] Record extraction failed: ${msg}`);
213
- return [];
289
+ throw new Error(`Record extraction failed (turns stay pending for retry): ${msg}`);
214
290
  }
215
291
  }
216
292
  /**
@@ -262,31 +338,13 @@ export class SupabaseVectorMemoryService {
262
338
  return { line, tag, factDate, source, status, keys, supersedes };
263
339
  }
264
340
  async embedTexts(texts) {
265
- const embeddings = [];
266
- for (const text of texts) {
267
- try {
268
- const response = await this.genai.models.embedContent({
269
- model: EMBEDDING_MODEL,
270
- contents: text,
271
- config: {
272
- outputDimensionality: EMBEDDING_DIMENSIONS
273
- }
274
- });
275
- if (response.embeddings && response.embeddings.length > 0) {
276
- embeddings.push(response.embeddings[0].values ?? []);
277
- }
278
- else {
279
- console.warn(`[MemoryService] Empty embedding returned for: "${text.slice(0, 50)}..."`);
280
- embeddings.push([]);
281
- }
282
- }
283
- catch (err) {
284
- const msg = err instanceof Error ? err.message : String(err);
285
- console.error(`[MemoryService] Embedding failed: ${msg}`);
286
- embeddings.push([]);
287
- }
341
+ try {
342
+ return await this.embedder.embed(texts);
343
+ }
344
+ catch (err) {
345
+ const msg = err instanceof Error ? err.message : String(err);
346
+ throw new Error(`Embedding failed (turns stay pending for retry): ${msg}`);
288
347
  }
289
- return embeddings;
290
348
  }
291
349
  serializeEvents(events) {
292
350
  const lines = [];
@@ -314,17 +372,41 @@ export class SupabaseVectorMemoryService {
314
372
  */
315
373
  async upsertToSupabase(userKey, records, embeddings) {
316
374
  const insertedIds = new Map();
375
+ const payload = await this.prepareFacts(userKey, records, embeddings);
376
+ if (payload.length === 0)
377
+ return insertedIds;
378
+ let data;
379
+ try {
380
+ data = await this.store.insert(payload);
381
+ }
382
+ catch (err) {
383
+ throw new Error(`Failed to store memory records (turns stay pending for retry): ${err instanceof Error ? err.message : String(err)}`);
384
+ }
385
+ for (const row of data) {
386
+ insertedIds.set(row.fact, row.id);
387
+ }
388
+ return insertedIds;
389
+ }
390
+ /** The atomic commit, with a failure worded for the operator. */
391
+ async commitOrThrow(userKey, marker, rows, retire) {
392
+ try {
393
+ return await this.store.commit(userKey, marker, rows, retire);
394
+ }
395
+ catch (err) {
396
+ throw new Error(`Failed to store memory records (turns stay pending for retry): ${err instanceof Error ? err.message : String(err)}`);
397
+ }
398
+ }
399
+ /**
400
+ * The rows worth storing: records not already stored byte for byte and
401
+ * not a semantic restatement of an active row of the same tag.
402
+ */
403
+ async prepareFacts(userKey, records, embeddings) {
317
404
  // First pass: byte-identical lines already stored. Cheap, and catches
318
405
  // a genuinely repeated extraction. It is NOT sufficient on its own —
319
406
  // the extraction model rephrases, so the semantic pass below is what
320
407
  // actually stops "the user has $831 in a forgotten Roth IRA" being
321
408
  // stored six ways.
322
- const { data: existing } = await this.supabase
323
- .from('adk_memory_facts')
324
- .select('fact')
325
- .eq('user_key', userKey)
326
- .in('fact', records.map(r => r.line));
327
- const known = new Set((existing ?? []).map((row) => row.fact));
409
+ const known = await this.store.existingFacts(userKey, records.map(r => r.line));
328
410
  // Second pass: semantic. One similarity probe per surviving candidate —
329
411
  // the embedding is already computed, and the same RPC the supersession
330
412
  // path uses. A restatement is dropped; the stored row keeps its
@@ -334,17 +416,16 @@ export class SupabaseVectorMemoryService {
334
416
  const record = records[i];
335
417
  if (embeddings[i].length === 0 || known.has(record.line))
336
418
  continue;
337
- const { data: near, error: nearErr } = await this.supabase.rpc('match_memory_facts', {
338
- query_embedding: embeddings[i],
339
- match_count: 5,
340
- filter_user_key: userKey,
341
- });
342
- if (nearErr) {
419
+ let near;
420
+ try {
421
+ near = await this.store.match(userKey, embeddings[i], 5);
422
+ }
423
+ catch (err) {
343
424
  // Fail OPEN: a dedup probe that errors must not silently drop a
344
425
  // record. A duplicate is recoverable; a lost fact is not.
345
- console.error(`[MemoryService] Dedup probe failed (storing anyway):`, nearErr.message);
426
+ console.error(`[MemoryService] Dedup probe failed (storing anyway):`, err instanceof Error ? err.message : err);
346
427
  }
347
- else if (isSemanticDuplicate(record, (near ?? []))) {
428
+ if (near && isSemanticDuplicate(record, near)) {
348
429
  console.log(`[MemoryService] Duplicate skipped [${record.tag}]: "${record.line.slice(0, 70)}..."`);
349
430
  continue;
350
431
  }
@@ -359,20 +440,7 @@ export class SupabaseVectorMemoryService {
359
440
  keys: record.keys,
360
441
  });
361
442
  }
362
- if (payload.length === 0)
363
- return insertedIds;
364
- const { data, error } = await this.supabase
365
- .from('adk_memory_facts')
366
- .insert(payload)
367
- .select('id, fact');
368
- if (error) {
369
- console.error(`[MemoryService] Failed to upsert to Supabase:`, error.message);
370
- return insertedIds;
371
- }
372
- for (const row of data ?? []) {
373
- insertedIds.set(row.fact, row.id);
374
- }
375
- return insertedIds;
443
+ return payload;
376
444
  }
377
445
  /**
378
446
  * The record evolves: a stored record carrying a `supersedes` header
@@ -385,47 +453,93 @@ export class SupabaseVectorMemoryService {
385
453
  */
386
454
  async applySupersessions(userKey, records, insertedIds) {
387
455
  const corrections = records.filter(r => r.supersedes && insertedIds.has(r.line));
456
+ const retire = await this.findRetirements(userKey, corrections, new Set(insertedIds.values()));
457
+ for (const r of retire) {
458
+ const correctionId = insertedIds.get(r.byFact);
459
+ try {
460
+ await this.store.retire(userKey, r.id, correctionId);
461
+ console.log(`[MemoryService] Superseded ${r.id} → ${correctionId}`);
462
+ }
463
+ catch (err) {
464
+ console.error(`[MemoryService] Failed to retire superseded record:`, err instanceof Error ? err.message : err);
465
+ }
466
+ }
467
+ }
468
+ /**
469
+ * Which active rows each correction retires: rows found by embedding the
470
+ * superseded quote, retired when semantically close and sharing an index
471
+ * key with the correction (or a near-exact match). `exclude` holds ids
472
+ * from this same ingestion, which are never retired.
473
+ */
474
+ async findRetirements(userKey, corrections, exclude) {
388
475
  if (corrections.length === 0)
389
- return;
390
- const newIds = new Set(insertedIds.values());
476
+ return [];
391
477
  const targetEmbeddings = await this.embedTexts(corrections.map(r => r.supersedes));
478
+ const out = [];
479
+ const taken = new Set();
392
480
  for (let i = 0; i < corrections.length; i++) {
393
481
  const correction = corrections[i];
394
482
  const vec = targetEmbeddings[i];
395
483
  if (!vec || vec.length === 0)
396
484
  continue;
397
- const { data, error } = await this.supabase.rpc('match_memory_facts', {
398
- query_embedding: vec,
399
- match_count: 5,
400
- filter_user_key: userKey,
401
- });
402
- if (error) {
403
- console.error(`[MemoryService] Supersession lookup failed:`, error.message);
485
+ let data;
486
+ try {
487
+ data = await this.store.match(userKey, vec, 5);
488
+ }
489
+ catch (err) {
490
+ console.error(`[MemoryService] Supersession lookup failed:`, err instanceof Error ? err.message : err);
404
491
  continue;
405
492
  }
406
- const correctionId = insertedIds.get(correction.line);
407
- for (const row of (data ?? [])) {
408
- if (newIds.has(row.id))
409
- continue; // never retire a record from this same ingestion
493
+ for (const row of data) {
494
+ if (exclude.has(row.id) || taken.has(row.id))
495
+ continue;
410
496
  if ((row.status ?? 'active') !== 'active')
411
497
  continue;
412
498
  const sharesKey = (row.keys ?? []).some(k => correction.keys.includes(k));
413
499
  const retire = row.similarity >= 0.85 || (row.similarity >= 0.6 && sharesKey);
414
500
  if (!retire)
415
501
  continue;
416
- const { error: updateError } = await this.supabase
417
- .from('adk_memory_facts')
418
- .update({ status: 'superseded', superseded_by: correctionId })
419
- .eq('id', row.id)
420
- .eq('user_key', userKey);
421
- if (updateError) {
422
- console.error(`[MemoryService] Failed to retire superseded record:`, updateError.message);
423
- }
424
- else {
425
- console.log(`[MemoryService] Superseded: "${row.fact.slice(0, 60)}..." → ${correctionId}`);
426
- }
502
+ taken.add(row.id);
503
+ out.push({ id: row.id, byFact: correction.line });
504
+ console.log(`[MemoryService] Supersedes: "${row.fact.slice(0, 60)}..."`);
427
505
  }
428
506
  }
507
+ return out;
508
+ }
509
+ /**
510
+ * Deletes facts in `namespace` older than `days` (a syndicate's
511
+ * memory_retention_days, ADR 0020 item 7). Returns how many; null when
512
+ * the store cannot prune.
513
+ */
514
+ async pruneExpired(namespace, days) {
515
+ if (!this.store.pruneNamespace)
516
+ return null;
517
+ const n = await this.store.pruneNamespace(namespace, days);
518
+ if (n)
519
+ console.log(`[MemoryService] Retention: deleted ${n} fact(s) older than ${days} day(s) in ${namespace}.`);
520
+ return n;
521
+ }
522
+ /**
523
+ * Refuses an embedder whose vectors would not fit the stored column
524
+ * (ADR 0020 item 5): every insert would fail, and the fix is a re-embed
525
+ * job, not dropping the table. Silent when the store cannot say.
526
+ */
527
+ async verifyEmbeddingDimensions() {
528
+ if (!this.store.embeddingDimensions)
529
+ return;
530
+ let stored;
531
+ try {
532
+ stored = await this.store.embeddingDimensions();
533
+ }
534
+ catch {
535
+ return; // migration 0007 not applied: the schema check reports that
536
+ }
537
+ if (stored != null && stored !== this.embedder.dimensions) {
538
+ throw new Error(`The memory table stores ${stored}-dimension embeddings, but the embedder `
539
+ + `${this.embedder.provider}/${this.embedder.model} produces ${this.embedder.dimensions}. `
540
+ + 'Set MEMORY_EMBEDDING_* to a model of that size, or re-embed the stored facts into a column of the new size; '
541
+ + 'never drop the table.');
542
+ }
429
543
  }
430
544
  /**
431
545
  * Hybrid recall: candidates come from pgvector cosine similarity, then
@@ -441,20 +555,20 @@ export class SupabaseVectorMemoryService {
441
555
  return { memories: [] };
442
556
  }
443
557
  try {
444
- // Calls the Postgres RPC function `match_memory_facts` created in Supabase
445
- const { data, error } = await this.supabase.rpc('match_memory_facts', {
446
- query_embedding: queryVec,
447
- match_count: 24,
448
- filter_user_key: userKey,
449
- });
450
- if (error) {
451
- console.error(`[MemoryService] Failed to query Supabase Vector Search:`, error.message);
558
+ // The `match_memory_facts` function (db/migrations/0001_base.sql),
559
+ // through whichever store this service was given.
560
+ let data;
561
+ try {
562
+ data = await this.store.match(userKey, queryVec, 24);
563
+ }
564
+ catch (err) {
565
+ console.error(`[MemoryService] Failed to query memory:`, err instanceof Error ? err.message : err);
452
566
  return { memories: [] };
453
567
  }
454
568
  const q = query.toLowerCase();
455
569
  const queryYears = q.match(/\b20\d{2}\b/g) ?? [];
456
570
  const queryMonths = MONTH_NAMES.filter(mn => q.includes(mn));
457
- const scored = (data ?? []).map(row => {
571
+ const scored = data.map(row => {
458
572
  let score = row.similarity;
459
573
  const status = row.status ?? 'active';
460
574
  // Index-key channel: the query names an entity the record is filed under.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * lib/models/capabilities.ts — what an agent keeps and loses on the path
3
+ * its model will actually take.
4
+ *
5
+ * WHY this file exists:
6
+ * A model id is the whole routing decision, and some of what an agent
7
+ * declares only exists on one provider: Gemini grounding, xAI's x_search
8
+ * and Collections, the server-side web_search sentinel that every cloud
9
+ * adapter turns into its provider's native search. Until now the only
10
+ * defence against losing one was a one-time console.warn inside an
11
+ * adapter. This module states the loss BEFORE a request is made, per
12
+ * agent, on the RESOLVED transport (direct or gateway), so the doctor
13
+ * (lib/doctor.ts), the A2A startup log (lib/compile.ts) and the ledger
14
+ * (llm.capability.dropped) all say the same thing.
15
+ *
16
+ * It knows nothing about gateways beyond planTransport(); a future
17
+ * transport that restores a native feature changes the table here, not
18
+ * the callers.
19
+ */
20
+ import type { ProviderId } from './providerMap.ts';
21
+ import type { TransportPlan } from './gateway.ts';
22
+ import type { Platform } from './endpoints.ts';
23
+ /**
24
+ * Server-side tool sentinels and the providers whose DIRECT adapter
25
+ * honours them natively. Anything not listed is a client-side function
26
+ * tool and travels on every path.
27
+ */
28
+ export declare const SERVER_SIDE_TOOLS: Record<string, ProviderId[]>;
29
+ export interface CapabilityReport {
30
+ model: string;
31
+ provider: ProviderId;
32
+ providerLabel: string;
33
+ transport: TransportPlan['transport'];
34
+ /** Gateway id when transport is 'gateway'. */
35
+ gateway?: string;
36
+ /** The cloud platform of a direct path (ADR 0023); absent on Ollama and the gateway. */
37
+ platform?: Platform;
38
+ /** False when neither a direct key nor a gateway can serve this id. */
39
+ funded: boolean;
40
+ /** The env var that would fund the direct path. */
41
+ keyEnv: string | null;
42
+ /** Declared server-side tools this path runs natively. */
43
+ native: string[];
44
+ /** Declared server-side tools this path cannot run — they are omitted. */
45
+ dropped: string[];
46
+ /** Declared client-side tools; portable across every path. */
47
+ portable: string[];
48
+ }
49
+ export declare function describeCapabilities(model: string, tools?: readonly string[], opts?: {
50
+ callerKey?: boolean;
51
+ }): CapabilityReport;
52
+ /**
53
+ * One line for a startup log, or undefined when there is nothing to say —
54
+ * a funded direct path with no dropped tool is the quiet default.
55
+ */
56
+ export declare function capabilitySummary(agentName: string, r: CapabilityReport): string | undefined;
57
+ export declare const CAPABILITIES: readonly ['delegation', 'memory_tools', 'structured_output', 'thinking_with_tools', 'streaming', 'vision', 'native_search'];
58
+ export type Capability = (typeof CAPABILITIES)[number];
59
+ export declare const CAPABILITY_LABELS: Record<Capability, string>;
60
+ export type Support = 'supported' | 'degraded' | 'unsupported';
61
+ export interface CapabilityCell {
62
+ support: Support;
63
+ /** What is lost, or what happens instead. Present unless `supported` is the whole story. */
64
+ note?: string;
65
+ evidence: 'test' | 'adk';
66
+ }
67
+ /** Matrix rows: each direct provider, plus the gateway transport. */
68
+ export type MatrixRow = ProviderId | 'gateway';
69
+ export declare const CAPABILITY_MATRIX: Record<MatrixRow, Record<Capability, CapabilityCell>>;
70
+ /** The matrix cell for a model on the path it will actually take. */
71
+ export declare function capabilityOf(model: string, capability: Capability, opts?: {
72
+ callerKey?: boolean;
73
+ }): CapabilityCell & {
74
+ row: MatrixRow;
75
+ };
76
+ /** What one agent's YAML asks of its model. */
77
+ export interface AgentNeedsInput {
78
+ tools?: readonly string[];
79
+ outputSchema?: unknown;
80
+ generateContentConfig?: {
81
+ thinkingConfig?: {
82
+ thinkingBudget?: number;
83
+ includeThoughts?: boolean;
84
+ };
85
+ };
86
+ /** True when this agent delegates to subagents through tools (DELEGATE mode). */
87
+ delegates?: boolean;
88
+ }
89
+ export declare function requiredCapabilities(agent: AgentNeedsInput): Capability[];
90
+ export interface CapabilityGap {
91
+ capability: Capability;
92
+ support: Exclude<Support, 'supported'>;
93
+ row: MatrixRow;
94
+ note?: string;
95
+ }
96
+ /** The capabilities an agent needs that its resolved path does not fully give it. */
97
+ export declare function capabilityGaps(model: string, agent: AgentNeedsInput, opts?: {
98
+ callerKey?: boolean;
99
+ }): CapabilityGap[];
100
+ /** The cell that differs from the provider's row on a platform, or undefined. */
101
+ export declare function platformCell(provider: ProviderId, platform: Platform, capability: Capability): CapabilityCell | undefined;
102
+ /** The matrix as a Markdown table plus notes, for documentation and the doctor. */
103
+ export declare function renderCapabilityMatrix(): string;