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,7 +1,7 @@
1
1
  /**
2
2
  * lib/observability/embeddings.ts — embeddings for ledger search.
3
3
  *
4
- * The same model and dimensionality as long-term memory (lib/config.ts), so
4
+ * The same embedder as long-term memory (lib/memory/providers.ts), so
5
5
  * a query embedded here is comparable to a turn embedded by
6
6
  * `npm run telemetry:embed`, and the `match_turns` RPC (db/telemetry.sql)
7
7
  * compares apples to apples. Kept out of the exporter on purpose: an API
@@ -9,12 +9,21 @@
9
9
  * batches after the fact.
10
10
  */
11
11
  import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL } from '../config.ts';
12
+ import type { Embedder } from '../memory/providers.ts';
12
13
  export interface EmbedOptions {
14
+ /** The Gemini key, when the configured embedder is Gemini (the default). */
13
15
  apiKey?: string;
14
16
  /** Characters kept per text (long answers are truncated, not skipped). */
15
17
  maxChars?: number;
18
+ /** Test seam; default the deployment's memory embedder (lib/memory/providers.ts). */
19
+ embedder?: Embedder;
16
20
  }
17
- /** Embeds each text; a failed text yields an empty vector rather than a throw. */
21
+ /**
22
+ * Embeds each text with the SAME embedder long-term memory uses, so the two
23
+ * vector columns stay comparable and a deployment that moved memory off
24
+ * Google does not keep sending ledger text there. A failed text yields an
25
+ * empty vector rather than a throw: this is a batch job over stored turns.
26
+ */
18
27
  export declare function embedTexts(texts: string[], options?: EmbedOptions): Promise<number[][]>;
19
28
  /** The text a turn is embedded by: the question and the answer, in that order. */
20
29
  export declare function turnEmbeddingText(input: string | null | undefined, output: string | null | undefined): string;
@@ -1,22 +1,31 @@
1
1
  /**
2
2
  * lib/observability/embeddings.ts — embeddings for ledger search.
3
3
  *
4
- * The same model and dimensionality as long-term memory (lib/config.ts), so
4
+ * The same embedder as long-term memory (lib/memory/providers.ts), so
5
5
  * a query embedded here is comparable to a turn embedded by
6
6
  * `npm run telemetry:embed`, and the `match_turns` RPC (db/telemetry.sql)
7
7
  * compares apples to apples. Kept out of the exporter on purpose: an API
8
8
  * call per turn would put inference on the export path; a job embeds in
9
9
  * batches after the fact.
10
10
  */
11
- import { GoogleGenAI } from '@google/genai';
12
- import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL } from "../config.js";
11
+ import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL } from '../config.js';
12
+ import { memoryProvidersFromEnv } from '../memory/providers.js';
13
13
  const MAX_CHARS = 8_000;
14
- /** Embeds each text; a failed text yields an empty vector rather than a throw. */
14
+ /**
15
+ * Embeds each text with the SAME embedder long-term memory uses, so the two
16
+ * vector columns stay comparable and a deployment that moved memory off
17
+ * Google does not keep sending ledger text there. A failed text yields an
18
+ * empty vector rather than a throw: this is a batch job over stored turns.
19
+ */
15
20
  export async function embedTexts(texts, options = {}) {
16
- const apiKey = options.apiKey ?? process.env.GOOGLE_GENAI_API_KEY ?? process.env.GEMINI_API_KEY;
17
- if (!apiKey)
18
- throw new Error('GOOGLE_GENAI_API_KEY is required for embeddings');
19
- const genai = new GoogleGenAI({ apiKey });
21
+ let embedder = options.embedder;
22
+ if (!embedder) {
23
+ const apiKey = options.apiKey ?? process.env.GOOGLE_GENAI_API_KEY ?? process.env.GEMINI_API_KEY;
24
+ const provider = (process.env.MEMORY_EMBEDDING_PROVIDER?.trim() || 'gemini').toLowerCase();
25
+ if (provider === 'gemini' && !apiKey)
26
+ throw new Error('GOOGLE_GENAI_API_KEY is required for Gemini embeddings');
27
+ embedder = memoryProvidersFromEnv(process.env, apiKey).embedder;
28
+ }
20
29
  const limit = options.maxChars ?? MAX_CHARS;
21
30
  const out = [];
22
31
  for (const text of texts) {
@@ -26,12 +35,7 @@ export async function embedTexts(texts, options = {}) {
26
35
  continue;
27
36
  }
28
37
  try {
29
- const response = await genai.models.embedContent({
30
- model: EMBEDDING_MODEL,
31
- contents: clipped,
32
- config: { outputDimensionality: EMBEDDING_DIMENSIONS },
33
- });
34
- out.push(response.embeddings?.[0]?.values ?? []);
38
+ out.push((await embedder.embed([clipped]))[0] ?? []);
35
39
  }
36
40
  catch (err) {
37
41
  console.warn(`[embeddings] failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * lib/observability/metrics.ts — Prometheus metrics for the A2A server
3
+ * (ADR 0026), from one record per finished task. No dependency: the text
4
+ * exposition format is a few lines.
5
+ *
6
+ * Labels are the agent, the outcome and the caller name. A caller name is
7
+ * an operator-chosen identifier (a caller token's name, 'shared-secret',
8
+ * 'jwt' or 'jwt:<tenant>'), never a user id, so the label set stays small
9
+ * and holds no personal data. Scopes are never a label.
10
+ */
11
+ /** One finished (or refused) task, as the executor reports it. */
12
+ export interface TaskRecord {
13
+ /** The agent id the task was sent to; '' is the default syndicate. */
14
+ agentId: string;
15
+ syndicate: string;
16
+ caller?: string;
17
+ /** SHA-256 prefix of the scope key: joins log lines without naming a user. */
18
+ scopeHash: string;
19
+ /** input-required: the turn paused for an approval (ADR 0028); the answer is a new task. */
20
+ status: 'completed' | 'failed' | 'canceled' | 'rejected' | 'input-required';
21
+ /** Why a task was rejected or failed: 'policy', 'capacity', 'input', or the turn's error code. */
22
+ reason?: string;
23
+ durationMs: number;
24
+ usage: {
25
+ llmCalls: number;
26
+ inputTokens: number;
27
+ outputTokens: number;
28
+ thinkingTokens: number;
29
+ };
30
+ }
31
+ export interface Metrics {
32
+ observeTask(record: TaskRecord): void;
33
+ /** Prometheus text exposition. `inFlight` is read at scrape time. */
34
+ render(inFlight?: number): string;
35
+ }
36
+ export declare function createMetrics(): Metrics;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * lib/observability/metrics.ts — Prometheus metrics for the A2A server
3
+ * (ADR 0026), from one record per finished task. No dependency: the text
4
+ * exposition format is a few lines.
5
+ *
6
+ * Labels are the agent, the outcome and the caller name. A caller name is
7
+ * an operator-chosen identifier (a caller token's name, 'shared-secret',
8
+ * 'jwt' or 'jwt:<tenant>'), never a user id, so the label set stays small
9
+ * and holds no personal data. Scopes are never a label.
10
+ */
11
+ const BUCKETS = [1, 2.5, 5, 10, 20, 40, 60, 120, 300, 900];
12
+ const key = (l) => JSON.stringify(Object.entries(l).sort(([a], [b]) => a.localeCompare(b)));
13
+ const fmt = (l) => Object.keys(l).length
14
+ ? `{${Object.entries(l)
15
+ .sort(([a], [b]) => a.localeCompare(b))
16
+ .map(([k, v]) => `${k}="${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`)
17
+ .join(',')}}`
18
+ : '';
19
+ export function createMetrics() {
20
+ const counters = new Map();
21
+ const hist = new Map();
22
+ const inc = (name, labels, by = 1) => {
23
+ if (!by)
24
+ return;
25
+ const series = counters.get(name) ?? new Map();
26
+ counters.set(name, series);
27
+ const k = key(labels);
28
+ const cur = series.get(k) ?? { labels, value: 0 };
29
+ cur.value += by;
30
+ series.set(k, cur);
31
+ };
32
+ const HELP = {
33
+ melchizedek_a2a_tasks_total: ['counter', 'Tasks finished or refused, by agent, outcome and caller.'],
34
+ melchizedek_a2a_llm_calls_total: ['counter', 'Model calls made by tasks.'],
35
+ melchizedek_a2a_tokens_total: ['counter', 'Tokens reported by the providers, by kind (input, output, thinking).'],
36
+ melchizedek_a2a_task_duration_seconds: ['histogram', 'Wall-clock duration of tasks that ran.'],
37
+ melchizedek_a2a_tasks_in_flight: ['gauge', 'Tasks running now.'],
38
+ };
39
+ return {
40
+ observeTask(r) {
41
+ const base = { agent: r.agentId || 'default', caller: r.caller ?? 'none' };
42
+ inc('melchizedek_a2a_tasks_total', { ...base, status: r.status, reason: r.reason ?? '' });
43
+ inc('melchizedek_a2a_llm_calls_total', base, r.usage.llmCalls);
44
+ inc('melchizedek_a2a_tokens_total', { ...base, kind: 'input' }, r.usage.inputTokens);
45
+ inc('melchizedek_a2a_tokens_total', { ...base, kind: 'output' }, r.usage.outputTokens);
46
+ inc('melchizedek_a2a_tokens_total', { ...base, kind: 'thinking' }, r.usage.thinkingTokens);
47
+ if (r.status === 'rejected')
48
+ return;
49
+ const labels = { agent: base.agent };
50
+ const k = key(labels);
51
+ const h = hist.get(k) ?? { labels, buckets: BUCKETS.map(() => 0), sum: 0, count: 0 };
52
+ const secs = r.durationMs / 1000;
53
+ BUCKETS.forEach((b, i) => {
54
+ if (secs <= b)
55
+ h.buckets[i] += 1;
56
+ });
57
+ h.sum += secs;
58
+ h.count += 1;
59
+ hist.set(k, h);
60
+ },
61
+ render(inFlight) {
62
+ const out = [];
63
+ const head = (name) => {
64
+ const [type, help] = HELP[name];
65
+ out.push(`# HELP ${name} ${help}`, `# TYPE ${name} ${type}`);
66
+ };
67
+ for (const name of ['melchizedek_a2a_tasks_total', 'melchizedek_a2a_llm_calls_total', 'melchizedek_a2a_tokens_total']) {
68
+ head(name);
69
+ for (const { labels, value } of counters.get(name)?.values() ?? [])
70
+ out.push(`${name}${fmt(labels)} ${value}`);
71
+ }
72
+ const hn = 'melchizedek_a2a_task_duration_seconds';
73
+ head(hn);
74
+ for (const h of hist.values()) {
75
+ BUCKETS.forEach((b, i) => out.push(`${hn}_bucket${fmt({ ...h.labels, le: String(b) })} ${h.buckets[i]}`));
76
+ out.push(`${hn}_bucket${fmt({ ...h.labels, le: '+Inf' })} ${h.count}`);
77
+ out.push(`${hn}_sum${fmt(h.labels)} ${h.sum}`, `${hn}_count${fmt(h.labels)} ${h.count}`);
78
+ }
79
+ if (inFlight !== undefined) {
80
+ head('melchizedek_a2a_tasks_in_flight');
81
+ out.push(`melchizedek_a2a_tasks_in_flight ${inFlight}`);
82
+ }
83
+ return `${out.join('\n')}\n`;
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * lib/observability/redact.ts — scrubs user text before it reaches the
3
+ * telemetry ledger (adk_turns, adk_telemetry, adk_payloads; ADR 0026).
4
+ *
5
+ * The ledger keeps what a conversation said so a turn can be debugged and
6
+ * judged; it should not keep the credentials or personal identifiers that
7
+ * people paste into a chat. One redactor runs on every row the exporter
8
+ * writes, over every text value except the identifier columns joins depend
9
+ * on (ids, timestamps, model and agent names).
10
+ *
11
+ * TELEMETRY_REDACT=secret (default) key-shaped credentials
12
+ * TELEMETRY_REDACT=secret,email,phone,card,ssn
13
+ * TELEMETRY_REDACT=off
14
+ * setTelemetryRedactor((text) => myDlp(text)) your own, in code
15
+ *
16
+ * Sessions and memory are NOT redacted: they must hold the conversation for
17
+ * the agent to work. Erasure (`DELETE /memory`) is the answer there.
18
+ */
19
+ export type Redactor = (text: string) => string;
20
+ /** Built-in patterns. Each match becomes `[redacted:<kind>]`. */
21
+ export declare const REDACTION_PATTERNS: Record<string, RegExp>;
22
+ /** A redactor applying the named built-in patterns, in a fixed order. */
23
+ export declare function patternRedactor(kinds: string[]): Redactor;
24
+ /** Plug in your own redactor (a DLP service, say); `null` turns redaction off. */
25
+ export declare function setTelemetryRedactor(redactor: Redactor | null): void;
26
+ /** The redactor in force: the one set in code, else TELEMETRY_REDACT (default `secret`). */
27
+ export declare function telemetryRedactor(env?: NodeJS.ProcessEnv): Redactor | undefined;
28
+ /** Every text value in a row, redacted, except identifier columns. */
29
+ export declare function redactRow<T>(row: T, redactor: Redactor): T;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * lib/observability/redact.ts — scrubs user text before it reaches the
3
+ * telemetry ledger (adk_turns, adk_telemetry, adk_payloads; ADR 0026).
4
+ *
5
+ * The ledger keeps what a conversation said so a turn can be debugged and
6
+ * judged; it should not keep the credentials or personal identifiers that
7
+ * people paste into a chat. One redactor runs on every row the exporter
8
+ * writes, over every text value except the identifier columns joins depend
9
+ * on (ids, timestamps, model and agent names).
10
+ *
11
+ * TELEMETRY_REDACT=secret (default) key-shaped credentials
12
+ * TELEMETRY_REDACT=secret,email,phone,card,ssn
13
+ * TELEMETRY_REDACT=off
14
+ * setTelemetryRedactor((text) => myDlp(text)) your own, in code
15
+ *
16
+ * Sessions and memory are NOT redacted: they must hold the conversation for
17
+ * the agent to work. Erasure (`DELETE /memory`) is the answer there.
18
+ */
19
+ /** Built-in patterns. Each match becomes `[redacted:<kind>]`. */
20
+ export const REDACTION_PATTERNS = {
21
+ secret: new RegExp([
22
+ 'sk-(?:ant-|proj-)?[A-Za-z0-9_-]{20,}', // Anthropic, OpenAI
23
+ 'AIza[0-9A-Za-z_-]{35}', // Google
24
+ 'xai-[A-Za-z0-9]{20,}', // xAI
25
+ 'gh[pousr]_[A-Za-z0-9]{30,}', // GitHub
26
+ 'xox[abprs]-[A-Za-z0-9-]{10,}', // Slack
27
+ 'AKIA[0-9A-Z]{16}', // AWS access key id
28
+ 'eyJ[A-Za-z0-9_-]{8,}\\.eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}', // a JWT
29
+ '(?<=Bearer\\s)[A-Za-z0-9._~+/-]{20,}=*', // a bearer token in pasted headers
30
+ ].join('|'), 'g'),
31
+ email: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
32
+ // Card and SSN run before phone, which would otherwise take their digits.
33
+ card: /(?<!\d)(?:\d[ -]?){12,18}\d(?!\d)/g,
34
+ ssn: /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g,
35
+ // Never starts or ends inside a longer run of digit groups (an order number).
36
+ phone: /(?<![\w.+]|\d[\s.-])\+?\d{1,3}[\s.-]?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{3,4}(?![\w.]|[\s.-]\d)/g,
37
+ };
38
+ /** Passes the Luhn check: what separates a card number from any long number. */
39
+ function luhn(digits) {
40
+ let sum = 0;
41
+ let double = false;
42
+ for (let i = digits.length - 1; i >= 0; i--) {
43
+ let d = digits.charCodeAt(i) - 48;
44
+ if (double) {
45
+ d *= 2;
46
+ if (d > 9)
47
+ d -= 9;
48
+ }
49
+ sum += d;
50
+ double = !double;
51
+ }
52
+ return sum % 10 === 0;
53
+ }
54
+ /** A redactor applying the named built-in patterns, in a fixed order. */
55
+ export function patternRedactor(kinds) {
56
+ const unknown = kinds.filter((k) => !REDACTION_PATTERNS[k]);
57
+ if (unknown.length)
58
+ throw new Error(`TELEMETRY_REDACT: unknown kind(s) ${unknown.join(', ')} (use ${Object.keys(REDACTION_PATTERNS).join(', ')}, or off)`);
59
+ const order = Object.keys(REDACTION_PATTERNS).filter((k) => kinds.includes(k));
60
+ return (text) => {
61
+ let out = text;
62
+ for (const kind of order) {
63
+ const re = new RegExp(REDACTION_PATTERNS[kind].source, REDACTION_PATTERNS[kind].flags);
64
+ out = out.replace(re, (m) => (kind === 'card' && !luhn(m.replace(/\D/g, '')) ? m : `[redacted:${kind}]`));
65
+ }
66
+ return out;
67
+ };
68
+ }
69
+ let custom;
70
+ /** Plug in your own redactor (a DLP service, say); `null` turns redaction off. */
71
+ export function setTelemetryRedactor(redactor) {
72
+ custom = redactor;
73
+ }
74
+ /** The redactor in force: the one set in code, else TELEMETRY_REDACT (default `secret`). */
75
+ export function telemetryRedactor(env = process.env) {
76
+ if (custom !== undefined)
77
+ return custom ?? undefined;
78
+ const raw = (env.TELEMETRY_REDACT ?? 'secret').trim().toLowerCase();
79
+ if (raw === 'off' || raw === 'none' || raw === 'false')
80
+ return undefined;
81
+ const kinds = raw === 'all' ? Object.keys(REDACTION_PATTERNS) : raw.split(',').map((s) => s.trim()).filter(Boolean);
82
+ return patternRedactor(kinds);
83
+ }
84
+ /** Columns that identify and join rows: never rewritten. */
85
+ const IDENTIFIER_KEY = /(^|[_.])(id|ts|at|hash|version)$|[a-z](Id|ID)$|^(syndicate|agent|model|models|provider|route|stage|span_name|surface|surface_guild|surface_channel|surface_user|kind|reason|eval_[a-z]+|schema_version|error_code)$/;
86
+ /** Every text value in a row, redacted, except identifier columns. */
87
+ export function redactRow(row, redactor) {
88
+ const walk = (value, key) => {
89
+ if (typeof value === 'string')
90
+ return key && IDENTIFIER_KEY.test(key) ? value : redactor(value);
91
+ if (Array.isArray(value))
92
+ return value.map((v) => walk(v, key));
93
+ if (value && typeof value === 'object' && !(value instanceof Date)) {
94
+ const out = {};
95
+ for (const [k, v] of Object.entries(value))
96
+ out[k] = walk(v, k);
97
+ return out;
98
+ }
99
+ return value;
100
+ };
101
+ return walk(row, undefined);
102
+ }
@@ -146,6 +146,8 @@ export declare function isRootSpan(span: ReadableSpan): boolean;
146
146
  export declare function isPayloadSpan(span: ReadableSpan): boolean;
147
147
  /** `syndicate.input` is JSON.stringify of either a string or A2A parts. */
148
148
  export declare function parseInputText(raw: unknown): string;
149
+ /** Span attributes minus the payload capture (`llm.payload.*`). */
150
+ export declare function withoutPayloadAttributes(attrs: Record<string, unknown>): Record<string, unknown>;
149
151
  /** Flatten an llm.request or root span into one adk_telemetry row. */
150
152
  export declare function toTelemetryRow(span: ReadableSpan): TelemetryRow;
151
153
  /** Project a root span into the adk_turns system-of-record row. */
@@ -38,8 +38,9 @@ import { appendFileSync, mkdirSync } from 'node:fs';
38
38
  import { join } from 'node:path';
39
39
  import core from '@opentelemetry/core';
40
40
  const { ExportResultCode } = core;
41
- import { hasSupabaseCredentials } from "../persistence/supabaseProvider.js";
42
- import { TELEMETRY_SCHEMA_VERSION } from "./lineage.js";
41
+ import { hasSupabaseCredentials } from '../persistence/supabaseProvider.js';
42
+ import { TELEMETRY_SCHEMA_VERSION } from './lineage.js';
43
+ import { redactRow, telemetryRedactor } from './redact.js';
43
44
  /**
44
45
  * TELEMETRY_PAYLOADS: off | errors | sample (default) | all
45
46
  * TELEMETRY_PAYLOAD_SAMPLE: 0..1, default 0.10
@@ -154,11 +155,22 @@ function spanJson(span) {
154
155
  spanId: span.spanContext().spanId,
155
156
  name: span.name,
156
157
  durationMs: durationMs(span),
157
- attributes: attrsOf(span),
158
+ // The full request/response of a failed call (llm.payload.*) belongs only
159
+ // in adk_payloads, which honours TELEMETRY_PAYLOADS and expires; copying
160
+ // it into this indefinitely-kept row bypassed both.
161
+ attributes: withoutPayloadAttributes(attrsOf(span)),
158
162
  events: span.events.map((e) => ({ name: e.name, attributes: e.attributes ?? {} })),
159
163
  status: span.status,
160
164
  };
161
165
  }
166
+ /** Span attributes minus the payload capture (`llm.payload.*`). */
167
+ export function withoutPayloadAttributes(attrs) {
168
+ const out = {};
169
+ for (const [k, v] of Object.entries(attrs))
170
+ if (!k.startsWith('llm.payload.'))
171
+ out[k] = v;
172
+ return out;
173
+ }
162
174
  /** Flatten an llm.request or root span into one adk_telemetry row. */
163
175
  export function toTelemetryRow(span) {
164
176
  const attrs = attrsOf(span);
@@ -358,12 +370,16 @@ export class SupabaseSpanExporter {
358
370
  }
359
371
  }
360
372
  this.pruneBuffers();
373
+ // Credentials and (by choice) personal identifiers never reach the
374
+ // ledger or its dead-letter file (lib/observability/redact.ts).
375
+ const redactor = telemetryRedactor();
376
+ const clean = (rows) => (redactor ? rows.map((r) => redactRow(r, redactor)) : rows);
361
377
  if (telemetryRows.length)
362
- this.insert('adk_telemetry', telemetryRows);
378
+ this.insert('adk_telemetry', clean(telemetryRows));
363
379
  if (turnRows.length)
364
- this.insert('adk_turns', turnRows);
380
+ this.insert('adk_turns', clean(turnRows));
365
381
  if (payloadRows.length)
366
- this.insert('adk_payloads', payloadRows);
382
+ this.insert('adk_payloads', clean(payloadRows));
367
383
  resultCallback({ code: ExportResultCode.SUCCESS });
368
384
  }
369
385
  pruneBuffers() {
@@ -91,6 +91,16 @@ export interface TraceMetadata {
91
91
  * Wraps an ADK `Runner.runAsync` stream with a root OpenTelemetry span.
92
92
  * Accumulates token counts, records variables (bindings) and the raw input/output.
93
93
  */
94
+ /**
95
+ * ToolCall/ToolResponse span events for the server-side tool calls a
96
+ * Responses adapter (gptLlm.ts) carried on a final event's customMetadata.
97
+ * Pure, so the projection is testable without an OTEL provider. Partial
98
+ * (streamed) events carry none and repeat nothing.
99
+ */
100
+ export declare function serverToolEvents(ev: any): Array<{
101
+ name: 'ToolCall' | 'ToolResponse';
102
+ attributes: Record<string, string | boolean>;
103
+ }>;
94
104
  export declare function traceAgentRun(stream: AsyncIterableIterator<Event>, metadata: TraceMetadata): AsyncGenerator<Event, void, void>;
95
105
  export interface LlmCallMeta {
96
106
  /** Provider id, e.g. 'anthropic', 'openai', 'xai', 'ollama', 'gemini'. */
@@ -1,13 +1,14 @@
1
1
  import { trace, context } from '@opentelemetry/api';
2
2
  import { createRequire } from 'node:module';
3
- import { TELEMETRY_SCHEMA_VERSION, engineVersion } from "./lineage.js";
3
+ import { TELEMETRY_SCHEMA_VERSION, engineVersion } from './lineage.js';
4
+ import { chargeLlmCall, chargeTokens } from '../runtime/turnControl.js';
4
5
  import sdkNode from '@opentelemetry/sdk-trace-node';
5
6
  const { NodeTracerProvider } = sdkNode;
6
7
  import sdkBase from '@opentelemetry/sdk-trace-base';
7
8
  const { SimpleSpanProcessor } = sdkBase;
8
9
  import core from '@opentelemetry/core';
9
10
  const { ExportResultCode } = core;
10
- import { SupabaseSpanExporter, telemetrySinkEnabled, } from "./supabaseSpanExporter.js";
11
+ import { SupabaseSpanExporter, telemetrySinkEnabled, } from './supabaseSpanExporter.js';
11
12
  /**
12
13
  * Optional OTLP export for a live trace viewer (Phoenix, Langfuse, Grafana
13
14
  * Tempo, Jaeger — anything that speaks OTLP/HTTP). Set
@@ -102,7 +103,10 @@ class JsonConsoleExporter {
102
103
  startTime: span.startTime,
103
104
  endTime: span.endTime,
104
105
  durationMs: span.duration[0] * 1000 + span.duration[1] / 1000000,
105
- attributes: span.attributes,
106
+ // Never the payload capture: a failed call carries its whole prompt
107
+ // in llm.payload.request, and stdout is a log drain with its own
108
+ // retention, outside every prune and erasure path.
109
+ attributes: Object.fromEntries(Object.entries(span.attributes).filter(([k]) => !k.startsWith('llm.payload.'))),
106
110
  events: span.events.map(e => ({
107
111
  name: e.name,
108
112
  time: e.time,
@@ -278,9 +282,24 @@ export function initializeTracing() {
278
282
  }
279
283
  const provider = new NodeTracerProvider({ spanProcessors, ...(resource ? { resource } : {}) });
280
284
  provider.register();
285
+ // `register()` points the proxy of the @opentelemetry/api copy the SDK
286
+ // resolves. An installed package can resolve a DIFFERENT copy for this
287
+ // module (npm nests one under melchizedek-agents when versions differ), and
288
+ // the module-level `tracer` below is a proxy on THAT copy: unpointed, every
289
+ // span it starts is a no-op and the ledger silently stays empty. Point our
290
+ // own proxy too; with one shared copy this repeats what register() did.
291
+ trace._proxyTracerProvider?.setDelegate?.(provider);
281
292
  adoptAdkTracerApi(provider);
282
293
  tracerProvider = provider;
283
294
  isOtelInitialized = true;
295
+ // Fail loudly, not silently: a tracer that does not record means no
296
+ // ledger rows, no metrics from spans and no OTLP export.
297
+ const probe = trace.getTracer('melchizedek-tracer').startSpan('melchizedek.tracer.probe');
298
+ const recording = probe.isRecording();
299
+ probe.end();
300
+ if (!recording && (telemetryExporter || otlp)) {
301
+ console.warn('[TELEMETRY] ⚠ spans are not recording: telemetry is configured but nothing will be exported (two @opentelemetry/api copies?).');
302
+ }
284
303
  }
285
304
  const tracer = trace.getTracer('melchizedek-tracer');
286
305
  /** Turn identity by trace id, so llm.request children can carry it too. */
@@ -289,6 +308,36 @@ const turnContexts = new Map();
289
308
  * Wraps an ADK `Runner.runAsync` stream with a root OpenTelemetry span.
290
309
  * Accumulates token counts, records variables (bindings) and the raw input/output.
291
310
  */
311
+ /**
312
+ * ToolCall/ToolResponse span events for the server-side tool calls a
313
+ * Responses adapter (gptLlm.ts) carried on a final event's customMetadata.
314
+ * Pure, so the projection is testable without an OTEL provider. Partial
315
+ * (streamed) events carry none and repeat nothing.
316
+ */
317
+ export function serverToolEvents(ev) {
318
+ const calls = ev?.partial ? undefined : ev?.customMetadata?.['responses.server_tool_calls'];
319
+ if (!Array.isArray(calls))
320
+ return [];
321
+ const out = [];
322
+ for (const c of calls) {
323
+ const name = String(c?.name ?? 'server_tool');
324
+ out.push({
325
+ name: 'ToolCall',
326
+ attributes: { 'tool.name': name, 'tool.args': JSON.stringify(c?.args ?? {}), 'tool.server_side': true },
327
+ });
328
+ if (Array.isArray(c?.sources) && c.sources.length > 0) {
329
+ out.push({
330
+ name: 'ToolResponse',
331
+ attributes: {
332
+ 'tool.name': name,
333
+ 'tool.data_gathered': JSON.stringify({ sources: c.sources }),
334
+ 'tool.server_side': true,
335
+ },
336
+ });
337
+ }
338
+ }
339
+ return out;
340
+ }
292
341
  export async function* traceAgentRun(stream, metadata) {
293
342
  initializeTracing();
294
343
  const span = tracer.startSpan(`Syndicate Execution: ${metadata.syndicateName}`);
@@ -402,6 +451,13 @@ export async function* traceAgentRun(stream, metadata) {
402
451
  }
403
452
  }
404
453
  }
454
+ // Server-side tools (xAI web_search/x_search via the Responses
455
+ // adapters) run inside one model call and never surface as
456
+ // functionCall parts. gptLlm.ts carries them on the final response's
457
+ // customMetadata; recording them here is what makes adk_turns.tool_calls
458
+ // count a searched Grok answer instead of reporting zero.
459
+ for (const e of serverToolEvents(ev))
460
+ span.addEvent(e.name, e.attributes);
405
461
  yield event;
406
462
  }
407
463
  }
@@ -488,6 +544,15 @@ function refineErrorCode(code, message) {
488
544
  */
489
545
  export async function* traceLlmGeneration(meta, inner) {
490
546
  initializeTracing();
547
+ // Every model call on every provider passes here, so this is where the
548
+ // turn's step budget and cancellation are enforced (lib/runtime/
549
+ // turnControl.ts). A refused call never reaches the provider.
550
+ const charge = chargeLlmCall();
551
+ if (!charge.ok) {
552
+ await inner.return?.(undefined);
553
+ yield { errorCode: charge.code, errorMessage: charge.message };
554
+ return;
555
+ }
491
556
  const span = tracer.startSpan('llm.request');
492
557
  span.setAttribute('llm.provider', meta.provider);
493
558
  span.setAttribute('llm.model', meta.model);
@@ -555,6 +620,8 @@ export async function* traceLlmGeneration(meta, inner) {
555
620
  throw error;
556
621
  }
557
622
  finally {
623
+ // The turn's usage (budgets, metrics, the task log) counts every call.
624
+ chargeTokens(inputTokens, outputTokens, thinkingTokens);
558
625
  if (thinkingPreview) {
559
626
  span.addEvent('llm.thinking', { 'thinking.preview': thinkingPreview });
560
627
  }
@@ -8,6 +8,7 @@
8
8
  * - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
9
9
  */
10
10
  import type { BaseSessionService, BaseMemoryService } from '@google/adk';
11
+ import type { Embedder, MemoryExtractor } from '../memory/providers.ts';
11
12
  export interface RlsHardeningStatus {
12
13
  /** true only when db/hardening.sql has been applied and RLS is on for both tables. */
13
14
  applied: boolean;
@@ -24,12 +25,22 @@ export interface PersistenceServices {
24
25
  * throws — callers use this to warn operators at boot, not to gate.
25
26
  */
26
27
  checkRlsHardening: () => Promise<RlsHardeningStatus>;
28
+ /** RPC access for operations that span stores (lib/memory/erase.ts). */
29
+ rpcClient: {
30
+ rpc: (fn: string, args?: Record<string, unknown>) => any;
31
+ };
32
+ /** The highest migration recorded in melchizedek_schema_version; null if none. */
33
+ schemaVersion: () => Promise<number | null>;
27
34
  }
28
35
  export interface SupabaseProviderOptions {
29
36
  /** The Gemini API key passed to SupabaseVectorMemoryService for embeddings. */
30
37
  apiKey: string;
31
38
  /** Whether to construct the SupabaseVectorMemoryService (long-term memory). */
32
39
  withMemory: boolean;
40
+ /** Fact extractor and embedder for memory (lib/memory/providers.ts).
41
+ * Default: from the environment (MEMORY_* variables), Gemini when unset. */
42
+ extractor?: MemoryExtractor;
43
+ embedder?: Embedder;
33
44
  }
34
45
  /**
35
46
  * Returns true if all required Supabase credentials are present in the
@@ -7,14 +7,26 @@
7
7
  * - App initialization
8
8
  * - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
9
9
  */
10
+ import { isPlaceholderValue } from '../loadEnv.js';
10
11
  /**
11
12
  * Returns true if all required Supabase credentials are present in the
12
13
  * environment. Does NOT throw — callers use this to decide whether to
13
14
  * fall back to in-memory services.
14
15
  */
15
16
  export function hasSupabaseCredentials() {
16
- return !!(process.env.SUPABASE_URL &&
17
- process.env.SUPABASE_SERVICE_ROLE_KEY);
17
+ const url = process.env.SUPABASE_URL?.trim();
18
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();
19
+ if (!url || !key || isPlaceholderValue(url) || isPlaceholderValue(key))
20
+ return false;
21
+ // A value that is not an http(s) URL is a misconfiguration, not credentials:
22
+ // treating it as present crashed supabase-js before the first prompt.
23
+ try {
24
+ const parsed = new URL(url);
25
+ return parsed.protocol === 'https:' || parsed.protocol === 'http:';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
18
30
  }
19
31
  /**
20
32
  * Initializes the Supabase client and constructs the
@@ -26,13 +38,13 @@ export async function createSupabaseServices(options) {
26
38
  const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
27
39
  const supabase = createClient(supabaseUrl, supabaseKey);
28
40
  // ── Session service ────────────────────────────────────────────────────────
29
- const { SupabaseSessionService } = await import("../session/supabaseSessionService.js");
41
+ const { SupabaseSessionService } = await import('../session/supabaseSessionService.js');
30
42
  const sessionService = new SupabaseSessionService(supabase);
31
43
  // ── Memory service (optional) ──────────────────────────────────────────────
32
44
  let memoryService;
33
45
  if (options.withMemory) {
34
- const { SupabaseVectorMemoryService } = await import("../memory/supabaseMemoryService.js");
35
- memoryService = new SupabaseVectorMemoryService({ apiKey: options.apiKey }, supabase);
46
+ const { SupabaseVectorMemoryService } = await import('../memory/supabaseMemoryService.js');
47
+ memoryService = new SupabaseVectorMemoryService({ apiKey: options.apiKey, extractor: options.extractor, embedder: options.embedder }, supabase);
36
48
  }
37
49
  // ── Hardening probe ─────────────────────────────────────────────────────────
38
50
  // Calls the melchizedek_rls_status() function created by db/hardening.sql.
@@ -84,5 +96,19 @@ export async function createSupabaseServices(options) {
84
96
  return { applied: false, detail: `hardening check failed: ${msg}` };
85
97
  }
86
98
  };
87
- return { sessionService, memoryService, checkRlsHardening };
99
+ const schemaVersion = async () => {
100
+ const { data, error } = await supabase
101
+ .from('melchizedek_schema_version')
102
+ .select('version')
103
+ .order('version', { ascending: false })
104
+ .limit(1);
105
+ if (error) {
106
+ // No version table: the migrations were never applied.
107
+ if (/does not exist|PGRST205|42P01/i.test(`${error.message} ${error.code ?? ''}`))
108
+ return null;
109
+ throw new Error(`Reading melchizedek_schema_version: ${error.message}`);
110
+ }
111
+ return data?.[0]?.version == null ? null : Number(data[0].version);
112
+ };
113
+ return { sessionService, memoryService, checkRlsHardening, rpcClient: supabase, schemaVersion };
88
114
  }