melchizedek-agents 0.9.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 (111) hide show
  1. package/AGENT_SETUP.md +182 -0
  2. package/DOCUMENTATION.md +614 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +164 -0
  5. package/README.md +151 -0
  6. package/config/agents/README.md +27 -0
  7. package/config/agents/examples/ares.yaml +74 -0
  8. package/config/agents/examples/cartographers.yaml +115 -0
  9. package/config/agents/examples/claude.yaml +17 -0
  10. package/config/agents/examples/council.yaml +65 -0
  11. package/config/agents/examples/critic.yaml +74 -0
  12. package/config/agents/examples/delegation.yaml +29 -0
  13. package/config/agents/examples/hierarchical.yaml +26 -0
  14. package/config/agents/examples/image_production.yaml +112 -0
  15. package/config/agents/examples/librarian.yaml +75 -0
  16. package/config/agents/examples/model_zoo.yaml +81 -0
  17. package/config/agents/examples/patient_advocate.yaml +107 -0
  18. package/config/agents/examples/scriptorium.yaml +96 -0
  19. package/config/agents/examples/style_council.yaml +83 -0
  20. package/config/agents/examples/syndicate.yaml +34 -0
  21. package/config/agents/examples/tutor.yaml +66 -0
  22. package/config/agents/syndicateSchema.yaml +291 -0
  23. package/dist/lib/config.d.ts +108 -0
  24. package/dist/lib/config.js +110 -0
  25. package/dist/lib/dispatch.d.ts +133 -0
  26. package/dist/lib/dispatch.js +182 -0
  27. package/dist/lib/grounding.d.ts +28 -0
  28. package/dist/lib/grounding.js +61 -0
  29. package/dist/lib/index.d.ts +14 -0
  30. package/dist/lib/index.js +13 -0
  31. package/dist/lib/loadEnv.d.ts +21 -0
  32. package/dist/lib/loadEnv.js +58 -0
  33. package/dist/lib/loadSyndicate.d.ts +177 -0
  34. package/dist/lib/loadSyndicate.js +264 -0
  35. package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
  36. package/dist/lib/memory/supabaseMemoryService.js +499 -0
  37. package/dist/lib/models/claudeLlm.d.ts +93 -0
  38. package/dist/lib/models/claudeLlm.js +332 -0
  39. package/dist/lib/models/gptLlm.d.ts +110 -0
  40. package/dist/lib/models/gptLlm.js +497 -0
  41. package/dist/lib/models/grokLlm.d.ts +64 -0
  42. package/dist/lib/models/grokLlm.js +86 -0
  43. package/dist/lib/models/ollamaLlm.d.ts +105 -0
  44. package/dist/lib/models/ollamaLlm.js +134 -0
  45. package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
  46. package/dist/lib/models/openAiCompatibleLlm.js +331 -0
  47. package/dist/lib/models/providerMap.d.ts +28 -0
  48. package/dist/lib/models/providerMap.js +45 -0
  49. package/dist/lib/models/registry.d.ts +71 -0
  50. package/dist/lib/models/registry.js +159 -0
  51. package/dist/lib/models/schemaNormalize.d.ts +22 -0
  52. package/dist/lib/models/schemaNormalize.js +58 -0
  53. package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
  54. package/dist/lib/observability/supabaseSpanExporter.js +115 -0
  55. package/dist/lib/observability/tracer.d.ts +44 -0
  56. package/dist/lib/observability/tracer.js +242 -0
  57. package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
  58. package/dist/lib/persistence/supabaseProvider.js +88 -0
  59. package/dist/lib/session/supabaseSessionService.d.ts +26 -0
  60. package/dist/lib/session/supabaseSessionService.js +193 -0
  61. package/dist/lib/session/transcript.d.ts +193 -0
  62. package/dist/lib/session/transcript.js +309 -0
  63. package/dist/lib/toolRegistry.d.ts +15 -0
  64. package/dist/lib/toolRegistry.js +58 -0
  65. package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
  66. package/dist/lib/tools/collectionsSearchTool.js +86 -0
  67. package/dist/lib/tools/generateImageTool.d.ts +16 -0
  68. package/dist/lib/tools/generateImageTool.js +119 -0
  69. package/dist/lib/tools/inspectImageTool.d.ts +21 -0
  70. package/dist/lib/tools/inspectImageTool.js +97 -0
  71. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  72. package/dist/lib/tools/mcpToolFactory.js +116 -0
  73. package/dist/lib/tools/toolContract.d.ts +78 -0
  74. package/dist/lib/tools/toolContract.js +129 -0
  75. package/dist/lib/tools/webExtractTool.d.ts +70 -0
  76. package/dist/lib/tools/webExtractTool.js +461 -0
  77. package/dist/lib/tools/webSearchTool.d.ts +53 -0
  78. package/dist/lib/tools/webSearchTool.js +126 -0
  79. package/dist/lib/tools/wikiTools.d.ts +112 -0
  80. package/dist/lib/tools/wikiTools.js +697 -0
  81. package/dist/lib/tools/xSearchTool.d.ts +61 -0
  82. package/dist/lib/tools/xSearchTool.js +125 -0
  83. package/dist/lib/wiki/agentRun.d.ts +39 -0
  84. package/dist/lib/wiki/agentRun.js +79 -0
  85. package/dist/lib/wiki/builder.d.ts +71 -0
  86. package/dist/lib/wiki/builder.js +165 -0
  87. package/dist/lib/wiki/dive.d.ts +56 -0
  88. package/dist/lib/wiki/dive.js +208 -0
  89. package/dist/lib/wiki/entities.d.ts +220 -0
  90. package/dist/lib/wiki/entities.js +661 -0
  91. package/dist/lib/wiki/extract.d.ts +97 -0
  92. package/dist/lib/wiki/extract.js +305 -0
  93. package/dist/lib/wiki/fill.d.ts +37 -0
  94. package/dist/lib/wiki/fill.js +175 -0
  95. package/dist/lib/wiki/format.d.ts +132 -0
  96. package/dist/lib/wiki/format.js +177 -0
  97. package/dist/lib/wiki/graph.d.ts +80 -0
  98. package/dist/lib/wiki/graph.js +147 -0
  99. package/dist/lib/wiki/lint.d.ts +58 -0
  100. package/dist/lib/wiki/lint.js +267 -0
  101. package/dist/lib/wiki/markdown.d.ts +127 -0
  102. package/dist/lib/wiki/markdown.js +336 -0
  103. package/dist/lib/wiki/pipeline.d.ts +69 -0
  104. package/dist/lib/wiki/pipeline.js +119 -0
  105. package/dist/lib/wiki/vault.d.ts +84 -0
  106. package/dist/lib/wiki/vault.js +194 -0
  107. package/dist/scripts/a2a_server.d.ts +2 -0
  108. package/dist/scripts/a2a_server.js +997 -0
  109. package/dist/scripts/syndicate_chat.d.ts +2 -0
  110. package/dist/scripts/syndicate_chat.js +485 -0
  111. package/package.json +96 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * lib/models/schemaNormalize.ts — JSON-Schema dialect bridge.
3
+ *
4
+ * WHY this file exists:
5
+ * The ADK is Gemini-native, and Gemini's Schema type spells JSON-Schema
6
+ * types in UPPERCASE ('OBJECT', 'STRING', …). Every other provider this
7
+ * framework routes to (Anthropic, OpenAI, xAI, Ollama's OpenAI-compatible
8
+ * endpoint) requires standard lowercase JSON-Schema types and rejects the
9
+ * Gemini spelling — Anthropic, for example, with:
10
+ * 400 invalid_request_error: tools.N.custom.input_schema.type:
11
+ * Input should be 'object'
12
+ * Tools are declared once (YAML / MCP discovery) in the Gemini dialect, so
13
+ * every non-Gemini adapter runs its tool schemas through this function at
14
+ * request-build time. See DOCUMENTATION.md §7.1.
15
+ */
16
+ /**
17
+ * Deep-clones a Gemini/ADK-style JSON schema, lowercasing every `type` value
18
+ * ('OBJECT' → 'object', ['STRING','NULL'] → ['string','null']) while leaving
19
+ * `description`, `enum`, `required`, `format`, and unknown keywords untouched.
20
+ * Never mutates the input — a Gemini agent may hold the same tool object.
21
+ */
22
+ export function toLowercaseJsonSchema(schema) {
23
+ const result = normalizeNode(schema);
24
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
25
+ return result;
26
+ }
27
+ return { type: 'object', properties: {} };
28
+ }
29
+ function normalizeNode(node) {
30
+ if (Array.isArray(node))
31
+ return node.map(normalizeNode);
32
+ if (!node || typeof node !== 'object')
33
+ return node;
34
+ const out = {};
35
+ for (const [key, value] of Object.entries(node)) {
36
+ if (key === 'type') {
37
+ // `type` may be a string or an array of strings (nullable unions).
38
+ if (typeof value === 'string') {
39
+ out[key] = value.toLowerCase();
40
+ }
41
+ else if (Array.isArray(value)) {
42
+ out[key] = value.map((v) => (typeof v === 'string' ? v.toLowerCase() : v));
43
+ }
44
+ else {
45
+ out[key] = value;
46
+ }
47
+ }
48
+ else if (key === 'enum' || key === 'required') {
49
+ // Value lists, not schema nodes — copy verbatim (enum values are data
50
+ // and must keep their original casing).
51
+ out[key] = Array.isArray(value) ? [...value] : value;
52
+ }
53
+ else {
54
+ out[key] = normalizeNode(value);
55
+ }
56
+ }
57
+ return out;
58
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * lib/observability/supabaseSpanExporter.ts — optional OpenTelemetry sink
3
+ * that persists model-request telemetry to Supabase.
4
+ *
5
+ * WHY this file exists:
6
+ * The console exporter in tracer.ts prints every span as an
7
+ * [OTEL_SPAN_JSON] line — good for a terminal, gone when it scrolls.
8
+ * Operators who want durable usage data (tokens per model, per provider,
9
+ * over time) can opt in with TELEMETRY_SUPABASE=true, and every
10
+ * `llm.request` and `Syndicate Execution:*` span is also inserted into
11
+ * the adk_telemetry table (schema: db/telemetry.sql, RLS: db/hardening.sql).
12
+ *
13
+ * DESIGN NOTES:
14
+ * - Fire-and-forget: an insert failure is logged once and never thrown
15
+ * into the tracing pipeline — telemetry must not break inference.
16
+ * - Only spans that carry usage are exported (name filter below); ADK's
17
+ * internal spans stay console-only to keep row volume sane.
18
+ */
19
+ import type { SpanExporter, ReadableSpan } from '@opentelemetry/sdk-trace-base';
20
+ /** True when the operator has opted into the Supabase telemetry sink. */
21
+ export declare function telemetrySinkEnabled(): boolean;
22
+ export declare class SupabaseSpanExporter implements SpanExporter {
23
+ /** Lazily created on first export so constructing the exporter is free
24
+ * and synchronous (NodeTracerProvider wants processors at construction). */
25
+ private clientPromise;
26
+ private insertFailureLogged;
27
+ private pendingInserts;
28
+ private getClient;
29
+ export(spans: ReadableSpan[], resultCallback: (result: any) => void): void;
30
+ /** Waits for in-flight inserts so short-lived scripts don't drop rows. */
31
+ shutdown(): Promise<void>;
32
+ forceFlush(): Promise<void>;
33
+ /** Flatten a span into one adk_telemetry row. */
34
+ private toRow;
35
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * lib/observability/supabaseSpanExporter.ts — optional OpenTelemetry sink
3
+ * that persists model-request telemetry to Supabase.
4
+ *
5
+ * WHY this file exists:
6
+ * The console exporter in tracer.ts prints every span as an
7
+ * [OTEL_SPAN_JSON] line — good for a terminal, gone when it scrolls.
8
+ * Operators who want durable usage data (tokens per model, per provider,
9
+ * over time) can opt in with TELEMETRY_SUPABASE=true, and every
10
+ * `llm.request` and `Syndicate Execution:*` span is also inserted into
11
+ * the adk_telemetry table (schema: db/telemetry.sql, RLS: db/hardening.sql).
12
+ *
13
+ * DESIGN NOTES:
14
+ * - Fire-and-forget: an insert failure is logged once and never thrown
15
+ * into the tracing pipeline — telemetry must not break inference.
16
+ * - Only spans that carry usage are exported (name filter below); ADK's
17
+ * internal spans stay console-only to keep row volume sane.
18
+ */
19
+ import core from '@opentelemetry/core';
20
+ const { ExportResultCode } = core;
21
+ import { hasSupabaseCredentials } from "../persistence/supabaseProvider.js";
22
+ /** True when the operator has opted into the Supabase telemetry sink. */
23
+ export function telemetrySinkEnabled() {
24
+ return process.env.TELEMETRY_SUPABASE === 'true' && hasSupabaseCredentials();
25
+ }
26
+ const EXPORTED_SPAN_NAMES = /^(llm\.request$|Syndicate Execution: )/;
27
+ export class SupabaseSpanExporter {
28
+ /** Lazily created on first export so constructing the exporter is free
29
+ * and synchronous (NodeTracerProvider wants processors at construction). */
30
+ clientPromise;
31
+ insertFailureLogged = false;
32
+ pendingInserts = new Set();
33
+ getClient() {
34
+ if (!this.clientPromise) {
35
+ this.clientPromise = import('@supabase/supabase-js').then(({ createClient }) => createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY));
36
+ }
37
+ return this.clientPromise;
38
+ }
39
+ export(spans, resultCallback) {
40
+ const rows = spans
41
+ .filter((span) => EXPORTED_SPAN_NAMES.test(span.name))
42
+ .map((span) => this.toRow(span));
43
+ if (rows.length === 0) {
44
+ resultCallback({ code: ExportResultCode.SUCCESS });
45
+ return;
46
+ }
47
+ // Fire-and-forget: report success to the pipeline immediately; log the
48
+ // first insert failure so a misconfigured table is visible without
49
+ // spamming every span batch.
50
+ const insert = this.getClient()
51
+ .then((client) => client.from('adk_telemetry').insert(rows))
52
+ .then(({ error }) => {
53
+ if (error && !this.insertFailureLogged) {
54
+ this.insertFailureLogged = true;
55
+ console.warn(`[TELEMETRY] Supabase insert failed (${error.message}). ` +
56
+ 'Has db/telemetry.sql been applied? Telemetry rows are being dropped; ' +
57
+ 'console [OTEL_SPAN_JSON] output is unaffected.');
58
+ }
59
+ })
60
+ .catch((err) => {
61
+ if (!this.insertFailureLogged) {
62
+ this.insertFailureLogged = true;
63
+ console.warn(`[TELEMETRY] Supabase sink error: ${err instanceof Error ? err.message : err}`);
64
+ }
65
+ })
66
+ .finally(() => this.pendingInserts.delete(insert));
67
+ this.pendingInserts.add(insert);
68
+ resultCallback({ code: ExportResultCode.SUCCESS });
69
+ }
70
+ /** Waits for in-flight inserts so short-lived scripts don't drop rows. */
71
+ async shutdown() {
72
+ await Promise.allSettled([...this.pendingInserts]);
73
+ }
74
+ async forceFlush() {
75
+ await Promise.allSettled([...this.pendingInserts]);
76
+ }
77
+ /** Flatten a span into one adk_telemetry row. */
78
+ toRow(span) {
79
+ const attrs = span.attributes;
80
+ const num = (key) => typeof attrs[key] === 'number' ? attrs[key] : null;
81
+ const str = (key) => typeof attrs[key] === 'string' ? attrs[key] : null;
82
+ // Root spans are named "Syndicate Execution: <name>"; llm.request spans
83
+ // carry syndicate context only if the caller set syndicate.name.
84
+ const syndicate = str('syndicate.name') ??
85
+ (span.name.startsWith('Syndicate Execution: ')
86
+ ? span.name.slice('Syndicate Execution: '.length)
87
+ : null);
88
+ return {
89
+ ts: new Date(span.startTime[0] * 1000 + span.startTime[1] / 1e6).toISOString(),
90
+ trace_id: span.spanContext().traceId,
91
+ span_id: span.spanContext().spanId,
92
+ span_name: span.name,
93
+ syndicate,
94
+ agent: str('llm.agent'),
95
+ provider: str('llm.provider'),
96
+ model: str('llm.model'),
97
+ input_tokens: num('llm.tokens.input') ?? num('syndicate.tokens.input'),
98
+ output_tokens: num('llm.tokens.output') ?? num('syndicate.tokens.output'),
99
+ thinking_tokens: num('llm.tokens.thinking') ?? num('syndicate.tokens.thinking'),
100
+ latency_ms: span.duration[0] * 1000 + span.duration[1] / 1e6,
101
+ span: {
102
+ traceId: span.spanContext().traceId,
103
+ spanId: span.spanContext().spanId,
104
+ name: span.name,
105
+ durationMs: span.duration[0] * 1000 + span.duration[1] / 1e6,
106
+ attributes: attrs,
107
+ events: span.events.map((e) => ({
108
+ name: e.name,
109
+ attributes: e.attributes ?? {},
110
+ })),
111
+ status: span.status,
112
+ },
113
+ };
114
+ }
115
+ }
@@ -0,0 +1,44 @@
1
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
2
+ import type { Event, LlmResponse } from '@google/adk';
3
+ /**
4
+ * Flushes buffered span processors (console is synchronous; the Supabase
5
+ * batch sink is not). Call before process exit in short-lived scripts so
6
+ * telemetry rows aren't dropped.
7
+ */
8
+ export declare function flushTracing(): Promise<void>;
9
+ /** Subscribe to finished spans. Returns an unsubscribe function. */
10
+ export declare function onSpanEnd(listener: (span: ReadableSpan) => void): () => void;
11
+ export declare function initializeTracing(): void;
12
+ export interface TraceMetadata {
13
+ syndicateName: string;
14
+ bindings?: Record<string, any>;
15
+ input?: any;
16
+ }
17
+ /**
18
+ * Wraps an ADK `Runner.runAsync` stream with a root OpenTelemetry span.
19
+ * Accumulates token counts, records variables (bindings) and the raw input/output.
20
+ */
21
+ export declare function traceAgentRun(stream: AsyncIterableIterator<Event>, metadata: TraceMetadata): AsyncGenerator<Event, void, void>;
22
+ export interface LlmCallMeta {
23
+ /** Provider id, e.g. 'anthropic', 'openai', 'xai', 'ollama', 'gemini'. */
24
+ provider: string;
25
+ /** The model id as declared in the agent YAML (e.g. 'ollama/qwen3:8b'). */
26
+ model: string;
27
+ }
28
+ /**
29
+ * Wraps one adapter `generateContentAsync` invocation in an `llm.request`
30
+ * span — one span per model call, for EVERY provider. Records provider,
31
+ * model, input/output/thinking token counts, latency, and a truncated
32
+ * thinking summary as a span event. Adapters call this around their own
33
+ * generator; TracedGemini (lib/models/registry.ts) does the same for Gemini,
34
+ * so per-request telemetry is uniform across the fleet.
35
+ *
36
+ * Adapters can decorate the active llm.request span with extra attributes
37
+ * (e.g. llm.web_search.native) via setLlmSpanAttribute below.
38
+ */
39
+ export declare function traceLlmGeneration(meta: LlmCallMeta, inner: AsyncGenerator<LlmResponse, void>): AsyncGenerator<LlmResponse, void>;
40
+ /**
41
+ * Sets an attribute on the currently active llm.request span, if any.
42
+ * Used by adapters for per-request flags like llm.web_search.omitted.
43
+ */
44
+ export declare function setLlmSpanAttribute(key: string, value: string | number | boolean): void;
@@ -0,0 +1,242 @@
1
+ import { trace, context } from '@opentelemetry/api';
2
+ import sdkNode from '@opentelemetry/sdk-trace-node';
3
+ const { NodeTracerProvider } = sdkNode;
4
+ import sdkBase from '@opentelemetry/sdk-trace-base';
5
+ const { SimpleSpanProcessor } = sdkBase;
6
+ import core from '@opentelemetry/core';
7
+ const { ExportResultCode } = core;
8
+ import { SupabaseSpanExporter, telemetrySinkEnabled, } from "./supabaseSpanExporter.js";
9
+ let isOtelInitialized = false;
10
+ let tracerProvider;
11
+ let telemetryExporter;
12
+ /**
13
+ * Flushes buffered span processors (console is synchronous; the Supabase
14
+ * batch sink is not). Call before process exit in short-lived scripts so
15
+ * telemetry rows aren't dropped.
16
+ */
17
+ export async function flushTracing() {
18
+ await tracerProvider?.forceFlush?.();
19
+ await telemetryExporter?.forceFlush?.();
20
+ }
21
+ // ── In-process span listeners ────────────────────────────────────────────────
22
+ // Lets a script (e.g. scripts/demo_model_optionality.ts) read finished spans
23
+ // without scraping its own stdout for [OTEL_SPAN_JSON] lines.
24
+ const spanEndListeners = new Set();
25
+ /** Subscribe to finished spans. Returns an unsubscribe function. */
26
+ export function onSpanEnd(listener) {
27
+ spanEndListeners.add(listener);
28
+ return () => spanEndListeners.delete(listener);
29
+ }
30
+ class JsonConsoleExporter {
31
+ export(spans, resultCallback) {
32
+ for (const span of spans) {
33
+ for (const listener of spanEndListeners) {
34
+ try {
35
+ listener(span);
36
+ }
37
+ catch {
38
+ /* a listener bug must not break the export pipeline */
39
+ }
40
+ }
41
+ // Create a clean JSON representation of the span
42
+ const jsonSpan = {
43
+ traceId: span.spanContext().traceId,
44
+ spanId: span.spanContext().spanId,
45
+ name: span.name,
46
+ startTime: span.startTime,
47
+ endTime: span.endTime,
48
+ durationMs: span.duration[0] * 1000 + span.duration[1] / 1000000,
49
+ attributes: span.attributes,
50
+ events: span.events.map(e => ({
51
+ name: e.name,
52
+ time: e.time,
53
+ attributes: e.attributes || {}
54
+ })),
55
+ status: span.status,
56
+ };
57
+ // Print to stdout with a specific prefix so test_local.py can easily extract it
58
+ console.log(`[OTEL_SPAN_JSON] ${JSON.stringify(jsonSpan)}`);
59
+ }
60
+ resultCallback({ code: ExportResultCode.SUCCESS });
61
+ }
62
+ shutdown() {
63
+ return Promise.resolve();
64
+ }
65
+ }
66
+ export function initializeTracing() {
67
+ if (isOtelInitialized)
68
+ return;
69
+ // Set up the global OpenTelemetry provider
70
+ // Since we register this globally, any internal ADK spans (e.g. tool calls, model calls)
71
+ // will also automatically route to this provider and show up in the console.
72
+ const spanProcessors = [
73
+ new SimpleSpanProcessor(new JsonConsoleExporter()),
74
+ ];
75
+ // Optional durable sink (TELEMETRY_SUPABASE=true + Supabase credentials):
76
+ // llm.request and Syndicate Execution spans also land in adk_telemetry.
77
+ // Batched so inserts never sit on the inference hot path; the exporter
78
+ // creates its Supabase client lazily on first export.
79
+ if (telemetrySinkEnabled()) {
80
+ const { BatchSpanProcessor } = sdkBase;
81
+ telemetryExporter = new SupabaseSpanExporter();
82
+ spanProcessors.push(new BatchSpanProcessor(telemetryExporter));
83
+ console.log('[TELEMETRY] Supabase sink enabled → adk_telemetry');
84
+ }
85
+ const provider = new NodeTracerProvider({ spanProcessors });
86
+ provider.register();
87
+ tracerProvider = provider;
88
+ isOtelInitialized = true;
89
+ }
90
+ const tracer = trace.getTracer('melchizedek-tracer');
91
+ /**
92
+ * Wraps an ADK `Runner.runAsync` stream with a root OpenTelemetry span.
93
+ * Accumulates token counts, records variables (bindings) and the raw input/output.
94
+ */
95
+ export async function* traceAgentRun(stream, metadata) {
96
+ initializeTracing();
97
+ const span = tracer.startSpan(`Syndicate Execution: ${metadata.syndicateName}`);
98
+ if (metadata.bindings) {
99
+ span.setAttribute('syndicate.bindings', JSON.stringify(metadata.bindings));
100
+ }
101
+ if (metadata.input) {
102
+ span.setAttribute('syndicate.input', JSON.stringify(metadata.input));
103
+ }
104
+ let totalInputTokens = 0;
105
+ let totalOutputTokens = 0;
106
+ let totalThinkingTokens = 0;
107
+ let outputText = '';
108
+ const ctx = trace.setSpan(context.active(), span);
109
+ try {
110
+ const wrappedStream = {
111
+ [Symbol.asyncIterator]() {
112
+ return {
113
+ async next() { return context.with(ctx, () => stream.next()); },
114
+ async return(value) { return stream.return ? context.with(ctx, () => stream.return(value)) : { done: true, value }; },
115
+ async throw(e) { return stream.throw ? context.with(ctx, () => stream.throw(e)) : Promise.reject(e); }
116
+ };
117
+ }
118
+ };
119
+ for await (const event of wrappedStream) {
120
+ if (event.usageMetadata) {
121
+ // Aggregation caveat: a multi-turn run repeats the growing prompt on
122
+ // every model call, so Math.max under-reports true billed input. The
123
+ // per-call llm.request spans (traceLlmGeneration) carry the honest
124
+ // per-request numbers; this root-span figure is the peak context size.
125
+ if (event.usageMetadata.promptTokenCount) {
126
+ totalInputTokens = Math.max(totalInputTokens, event.usageMetadata.promptTokenCount);
127
+ }
128
+ if (event.usageMetadata.candidatesTokenCount) {
129
+ totalOutputTokens += event.usageMetadata.candidatesTokenCount;
130
+ }
131
+ if (event.usageMetadata.thoughtsTokenCount) {
132
+ totalThinkingTokens += event.usageMetadata.thoughtsTokenCount;
133
+ }
134
+ }
135
+ if (event.content && event.content.parts) {
136
+ for (const part of event.content.parts) {
137
+ const p = part;
138
+ if (p.text && !p.thought) {
139
+ outputText += p.text;
140
+ }
141
+ if (p.functionCall) {
142
+ span.addEvent('ToolCall', {
143
+ 'tool.name': p.functionCall.name,
144
+ 'tool.args': JSON.stringify(p.functionCall.args || {})
145
+ });
146
+ }
147
+ if (p.functionResponse) {
148
+ let respData = p.functionResponse.response;
149
+ if (typeof respData !== 'string') {
150
+ respData = JSON.stringify(respData);
151
+ }
152
+ span.addEvent('ToolResponse', {
153
+ 'tool.name': p.functionResponse.name,
154
+ 'tool.data_gathered': respData
155
+ });
156
+ }
157
+ }
158
+ }
159
+ yield event;
160
+ }
161
+ }
162
+ catch (error) {
163
+ span.recordException(error);
164
+ throw error;
165
+ }
166
+ finally {
167
+ span.setAttribute('syndicate.name', metadata.syndicateName);
168
+ span.setAttribute('syndicate.tokens.input', totalInputTokens);
169
+ span.setAttribute('syndicate.tokens.output', totalOutputTokens);
170
+ span.setAttribute('syndicate.tokens.thinking', totalThinkingTokens);
171
+ span.setAttribute('syndicate.output', outputText);
172
+ span.end();
173
+ }
174
+ }
175
+ const THINKING_EVENT_MAX_CHARS = 600;
176
+ /**
177
+ * Wraps one adapter `generateContentAsync` invocation in an `llm.request`
178
+ * span — one span per model call, for EVERY provider. Records provider,
179
+ * model, input/output/thinking token counts, latency, and a truncated
180
+ * thinking summary as a span event. Adapters call this around their own
181
+ * generator; TracedGemini (lib/models/registry.ts) does the same for Gemini,
182
+ * so per-request telemetry is uniform across the fleet.
183
+ *
184
+ * Adapters can decorate the active llm.request span with extra attributes
185
+ * (e.g. llm.web_search.native) via setLlmSpanAttribute below.
186
+ */
187
+ export async function* traceLlmGeneration(meta, inner) {
188
+ initializeTracing();
189
+ const span = tracer.startSpan('llm.request');
190
+ span.setAttribute('llm.provider', meta.provider);
191
+ span.setAttribute('llm.model', meta.model);
192
+ let inputTokens = 0;
193
+ let outputTokens = 0;
194
+ let thinkingTokens = 0;
195
+ let thinkingPreview = '';
196
+ const ctx = trace.setSpan(context.active(), span);
197
+ try {
198
+ while (true) {
199
+ const { value: resp, done } = await context.with(ctx, () => inner.next());
200
+ if (done)
201
+ break;
202
+ if (resp.usageMetadata) {
203
+ // One llm.request span covers one API call; the last usage report
204
+ // for that call is authoritative (streaming may repeat partials).
205
+ inputTokens = resp.usageMetadata.promptTokenCount ?? inputTokens;
206
+ outputTokens = resp.usageMetadata.candidatesTokenCount ?? outputTokens;
207
+ thinkingTokens = resp.usageMetadata.thoughtsTokenCount ?? thinkingTokens;
208
+ }
209
+ if (resp.errorCode) {
210
+ span.setAttribute('llm.error_code', resp.errorCode);
211
+ }
212
+ for (const part of resp.content?.parts ?? []) {
213
+ const p = part;
214
+ if (p.thought && p.text && thinkingPreview.length < THINKING_EVENT_MAX_CHARS) {
215
+ thinkingPreview = (thinkingPreview + p.text).slice(0, THINKING_EVENT_MAX_CHARS);
216
+ }
217
+ }
218
+ yield resp;
219
+ }
220
+ }
221
+ catch (error) {
222
+ span.recordException(error);
223
+ throw error;
224
+ }
225
+ finally {
226
+ if (thinkingPreview) {
227
+ span.addEvent('llm.thinking', { 'thinking.preview': thinkingPreview });
228
+ }
229
+ span.setAttribute('llm.tokens.input', inputTokens);
230
+ span.setAttribute('llm.tokens.output', outputTokens);
231
+ span.setAttribute('llm.tokens.thinking', thinkingTokens);
232
+ // llm.latency_ms mirrors the span duration for the flat adk_telemetry row.
233
+ span.end();
234
+ }
235
+ }
236
+ /**
237
+ * Sets an attribute on the currently active llm.request span, if any.
238
+ * Used by adapters for per-request flags like llm.web_search.omitted.
239
+ */
240
+ export function setLlmSpanAttribute(key, value) {
241
+ trace.getActiveSpan()?.setAttribute(key, value);
242
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * lib/persistence/supabaseProvider.ts — Supabase initialization factory.
3
+ *
4
+ * WHY this file exists:
5
+ * This module encapsulates ALL Supabase-specific concerns:
6
+ * - Credential detection (reads env vars)
7
+ * - App initialization
8
+ * - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
9
+ */
10
+ import type { BaseSessionService, BaseMemoryService } from '@google/adk';
11
+ export interface RlsHardeningStatus {
12
+ /** true only when db/hardening.sql has been applied and RLS is on for both tables. */
13
+ applied: boolean;
14
+ /** Human-readable explanation for logs. */
15
+ detail: string;
16
+ }
17
+ export interface PersistenceServices {
18
+ sessionService: BaseSessionService;
19
+ memoryService: BaseMemoryService | undefined;
20
+ /**
21
+ * Checks whether the database hardening in db/hardening.sql has been
22
+ * applied (RLS enabled on adk_memory_facts, adk_sessions and — where it
23
+ * exists — adk_agent_registry). Never
24
+ * throws — callers use this to warn operators at boot, not to gate.
25
+ */
26
+ checkRlsHardening: () => Promise<RlsHardeningStatus>;
27
+ }
28
+ export interface SupabaseProviderOptions {
29
+ /** The Gemini API key passed to SupabaseVectorMemoryService for embeddings. */
30
+ apiKey: string;
31
+ /** Whether to construct the SupabaseVectorMemoryService (long-term memory). */
32
+ withMemory: boolean;
33
+ }
34
+ /**
35
+ * Returns true if all required Supabase credentials are present in the
36
+ * environment. Does NOT throw — callers use this to decide whether to
37
+ * fall back to in-memory services.
38
+ */
39
+ export declare function hasSupabaseCredentials(): boolean;
40
+ /**
41
+ * Initializes the Supabase client and constructs the
42
+ * SupabaseSessionService and optionally the SupabaseVectorMemoryService.
43
+ */
44
+ export declare function createSupabaseServices(options: SupabaseProviderOptions): Promise<PersistenceServices>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * lib/persistence/supabaseProvider.ts — Supabase initialization factory.
3
+ *
4
+ * WHY this file exists:
5
+ * This module encapsulates ALL Supabase-specific concerns:
6
+ * - Credential detection (reads env vars)
7
+ * - App initialization
8
+ * - Service construction (SupabaseSessionService, SupabaseVectorMemoryService)
9
+ */
10
+ /**
11
+ * Returns true if all required Supabase credentials are present in the
12
+ * environment. Does NOT throw — callers use this to decide whether to
13
+ * fall back to in-memory services.
14
+ */
15
+ export function hasSupabaseCredentials() {
16
+ return !!(process.env.SUPABASE_URL &&
17
+ process.env.SUPABASE_SERVICE_ROLE_KEY);
18
+ }
19
+ /**
20
+ * Initializes the Supabase client and constructs the
21
+ * SupabaseSessionService and optionally the SupabaseVectorMemoryService.
22
+ */
23
+ export async function createSupabaseServices(options) {
24
+ const { createClient } = await import('@supabase/supabase-js');
25
+ const supabaseUrl = process.env.SUPABASE_URL;
26
+ const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
27
+ const supabase = createClient(supabaseUrl, supabaseKey);
28
+ // ── Session service ────────────────────────────────────────────────────────
29
+ const { SupabaseSessionService } = await import("../session/supabaseSessionService.js");
30
+ const sessionService = new SupabaseSessionService(supabase);
31
+ // ── Memory service (optional) ──────────────────────────────────────────────
32
+ let memoryService;
33
+ if (options.withMemory) {
34
+ const { SupabaseVectorMemoryService } = await import("../memory/supabaseMemoryService.js");
35
+ memoryService = new SupabaseVectorMemoryService({ apiKey: options.apiKey }, supabase);
36
+ }
37
+ // ── Hardening probe ─────────────────────────────────────────────────────────
38
+ // Calls the melchizedek_rls_status() function created by db/hardening.sql.
39
+ // If the function is missing, hardening was never run — that is itself the
40
+ // answer, not an error.
41
+ const checkRlsHardening = async () => {
42
+ try {
43
+ const { data, error } = await supabase.rpc('melchizedek_rls_status');
44
+ if (error) {
45
+ return {
46
+ applied: false,
47
+ detail: 'db/hardening.sql has not been applied (status function missing)',
48
+ };
49
+ }
50
+ const rows = (data ?? []);
51
+ const unprotected = ['adk_memory_facts', 'adk_sessions'].filter((t) => !rows.some((r) => r.table_name === t && r.rls_enabled));
52
+ if (unprotected.length > 0) {
53
+ return {
54
+ applied: false,
55
+ detail: `RLS is disabled on: ${unprotected.join(', ')}`,
56
+ };
57
+ }
58
+ // adk_agent_registry is optional (only deployments that boot
59
+ // `registry:<id>` create it) but NOT advisory: an unprotected registry
60
+ // is agent takeover via the anon key, so a present-and-open table
61
+ // fails the check outright. Absent table => no row => nothing to say.
62
+ const registryRow = rows.find((r) => r.table_name === 'adk_agent_registry');
63
+ if (registryRow && !registryRow.rls_enabled) {
64
+ return {
65
+ applied: false,
66
+ detail: 'RLS is disabled on: adk_agent_registry (agent definitions are ' +
67
+ 'writable with the anon key) — re-run db/hardening.sql',
68
+ };
69
+ }
70
+ // Advisory only — adk_telemetry is optional (db/telemetry.sql). If it
71
+ // exists but hardening.sql wasn't re-run afterwards, say so without
72
+ // failing the check (the mandatory tables are protected).
73
+ const telemetryRow = rows.find((r) => r.table_name === 'adk_telemetry');
74
+ if (telemetryRow && !telemetryRow.rls_enabled) {
75
+ return {
76
+ applied: true,
77
+ detail: 'RLS enabled on adk_memory_facts and adk_sessions — but NOT on adk_telemetry; re-run db/hardening.sql',
78
+ };
79
+ }
80
+ return { applied: true, detail: 'RLS enabled on adk_memory_facts and adk_sessions' };
81
+ }
82
+ catch (err) {
83
+ const msg = err instanceof Error ? err.message : String(err);
84
+ return { applied: false, detail: `hardening check failed: ${msg}` };
85
+ }
86
+ };
87
+ return { sessionService, memoryService, checkRlsHardening };
88
+ }
@@ -0,0 +1,26 @@
1
+ import { BaseSessionService } from '@google/adk';
2
+ import type { CreateSessionRequest, GetSessionRequest, ListSessionsRequest, ListSessionsResponse, DeleteSessionRequest, Session, Event } from '@google/adk';
3
+ import type { SupabaseClient } from '@supabase/supabase-js';
4
+ export declare class SupabaseSessionService extends BaseSessionService {
5
+ private supabase;
6
+ constructor(supabaseClient: SupabaseClient);
7
+ private getDbId;
8
+ createSession(request: CreateSessionRequest): Promise<Session>;
9
+ getSession(request: GetSessionRequest): Promise<Session | undefined>;
10
+ /**
11
+ * ADK's ListSessions contract carries paging: `limit` with either `page`
12
+ * (1-based, wins) or `offset` (0-based), plus an optional sort by last
13
+ * update, and the response must report where in the set the caller is.
14
+ * The window is pushed down to Postgres via .range(), and `count:
15
+ * 'exact'` returns the TOTAL matching rows alongside the page — so
16
+ * totalItems is the real total, not the slice length. With no `limit`,
17
+ * the whole set is one page, which is what the ADK's own docs specify
18
+ * ("Page size used. Equals `totalItems` when no limit was requested").
19
+ */
20
+ listSessions(request: ListSessionsRequest): Promise<ListSessionsResponse>;
21
+ deleteSession(request: DeleteSessionRequest): Promise<void>;
22
+ appendEvent(request: {
23
+ session: Session;
24
+ event: Event;
25
+ }): Promise<Event>;
26
+ }