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.
- package/.env.example +272 -0
- package/AGENT_SETUP.md +39 -23
- package/CHANGELOG.md +623 -0
- package/DOCUMENTATION.md +429 -109
- package/QUICKSTART.md +92 -18
- package/README.md +45 -5
- package/config/agents/README.md +46 -1
- package/config/agents/examples/ares.yaml +117 -40
- package/config/agents/examples/assistant.yaml +159 -0
- package/config/agents/examples/augustin.yaml +154 -51
- package/config/agents/examples/cartographers.yaml +91 -23
- package/config/agents/examples/claude.yaml +45 -6
- package/config/agents/examples/council.yaml +23 -8
- package/config/agents/examples/critic.yaml +57 -21
- package/config/agents/examples/delegation.yaml +76 -9
- package/config/agents/examples/hierarchical.yaml +55 -8
- package/config/agents/examples/image_production.yaml +54 -16
- package/config/agents/examples/librarian.yaml +70 -14
- package/config/agents/examples/model_zoo.yaml +105 -29
- package/config/agents/examples/patient_advocate.yaml +102 -34
- package/config/agents/examples/research.yaml +250 -0
- package/config/agents/examples/scribe.yaml +242 -0
- package/config/agents/examples/scriptorium.yaml +87 -24
- package/config/agents/examples/style_council.yaml +31 -9
- package/config/agents/examples/syndicate.yaml +29 -10
- package/config/agents/examples/tutor.yaml +10 -3
- package/config/agents/syndicateSchema.yaml +44 -9
- package/config/agents/templates/README.md +58 -0
- package/config/agents/templates/account_memory.yaml +153 -0
- package/config/agents/templates/case_desk.yaml +188 -0
- package/config/agents/templates/conversational.yaml +110 -0
- package/config/agents/templates/draft_review.yaml +130 -0
- package/config/agents/templates/intake_extractor.yaml +152 -0
- package/config/agents/templates/research_brief.yaml +169 -0
- package/config/agents/templates/research_desk.yaml +152 -0
- package/config/agents/templates/review_panel.yaml +188 -0
- package/config/agents/templates/support_triage.yaml +273 -0
- package/config/agents/templates/systems_operator.yaml +157 -0
- package/db/hardening.sql +202 -0
- package/db/memory_v2.sql +77 -0
- package/db/migrations/0001_base.sql +167 -0
- package/db/migrations/0002_erase_scope.sql +172 -0
- package/db/migrations/0003_postgres_storage.sql +58 -0
- package/db/migrations/0004_usage.sql +94 -0
- package/db/migrations/0005_agent_registry.sql +200 -0
- package/db/migrations/0006_task_leases.sql +25 -0
- package/db/migrations/0007_memory_commit.sql +134 -0
- package/db/migrations/0008_memory_retention.sql +37 -0
- package/db/migrations/0009_task_queue.sql +51 -0
- package/db/migrations/0010_erase_complete.sql +180 -0
- package/db/telemetry.sql +401 -0
- package/demo/README.md +77 -0
- package/demo/a2a_demo.mjs +95 -0
- package/dist/lib/a2a/app.d.ts +253 -0
- package/dist/lib/a2a/app.js +751 -0
- package/dist/lib/a2a/executor.d.ts +167 -0
- package/dist/lib/a2a/executor.js +548 -0
- package/dist/lib/a2a/identity.d.ts +122 -0
- package/dist/lib/a2a/identity.js +254 -0
- package/dist/lib/a2a/policy.d.ts +110 -0
- package/dist/lib/a2a/policy.js +178 -0
- package/dist/lib/a2a/redisLimits.d.ts +26 -0
- package/dist/lib/a2a/redisLimits.js +31 -0
- package/dist/lib/a2a/remoteAgent.d.ts +91 -0
- package/dist/lib/a2a/remoteAgent.js +268 -0
- package/dist/lib/a2a/turnLock.d.ts +28 -0
- package/dist/lib/a2a/turnLock.js +76 -0
- package/dist/lib/compile.d.ts +11 -1
- package/dist/lib/compile.js +109 -8
- package/dist/lib/config.d.ts +7 -7
- package/dist/lib/config.js +7 -7
- package/dist/lib/doctor.d.ts +145 -0
- package/dist/lib/doctor.js +517 -0
- package/dist/lib/grounding.d.ts +2 -2
- package/dist/lib/grounding.js +2 -2
- package/dist/lib/guards/index.d.ts +37 -0
- package/dist/lib/guards/index.js +66 -0
- package/dist/lib/guards/science.d.ts +117 -0
- package/dist/lib/guards/science.js +444 -0
- package/dist/lib/index.d.ts +37 -2
- package/dist/lib/index.js +35 -4
- package/dist/lib/loadEnv.d.ts +23 -11
- package/dist/lib/loadEnv.js +77 -29
- package/dist/lib/loadSyndicate.d.ts +97 -5
- package/dist/lib/loadSyndicate.js +176 -76
- package/dist/lib/memory/erase.d.ts +24 -0
- package/dist/lib/memory/erase.js +47 -0
- package/dist/lib/memory/namespace.d.ts +37 -0
- package/dist/lib/memory/namespace.js +74 -0
- package/dist/lib/memory/providers.d.ts +99 -0
- package/dist/lib/memory/providers.js +240 -0
- package/dist/lib/memory/store.d.ts +74 -0
- package/dist/lib/memory/store.js +108 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +69 -9
- package/dist/lib/memory/supabaseMemoryService.js +239 -125
- package/dist/lib/models/capabilities.d.ts +103 -0
- package/dist/lib/models/capabilities.js +291 -0
- package/dist/lib/models/claudeLlm.d.ts +10 -1
- package/dist/lib/models/claudeLlm.js +57 -27
- package/dist/lib/models/endpoints.d.ts +100 -0
- package/dist/lib/models/endpoints.js +282 -0
- package/dist/lib/models/gateway.d.ts +107 -0
- package/dist/lib/models/gateway.js +157 -0
- package/dist/lib/models/gatewayLlm.d.ts +57 -0
- package/dist/lib/models/gatewayLlm.js +105 -0
- package/dist/lib/models/gptLlm.d.ts +51 -2
- package/dist/lib/models/gptLlm.js +169 -26
- package/dist/lib/models/grokLlm.d.ts +19 -10
- package/dist/lib/models/grokLlm.js +29 -14
- package/dist/lib/models/ollamaLlm.d.ts +26 -0
- package/dist/lib/models/ollamaLlm.js +43 -1
- package/dist/lib/models/openAiCompatibleLlm.d.ts +67 -2
- package/dist/lib/models/openAiCompatibleLlm.js +200 -37
- package/dist/lib/models/providerMap.js +9 -2
- package/dist/lib/models/registry.d.ts +42 -5
- package/dist/lib/models/registry.js +140 -29
- package/dist/lib/models/retry.d.ts +146 -0
- package/dist/lib/models/retry.js +305 -0
- package/dist/lib/models/schemaNormalize.d.ts +20 -0
- package/dist/lib/models/schemaNormalize.js +43 -0
- package/dist/lib/net/addressGuard.d.ts +60 -0
- package/dist/lib/net/addressGuard.js +216 -0
- package/dist/lib/observability/embeddings.d.ts +11 -2
- package/dist/lib/observability/embeddings.js +18 -14
- package/dist/lib/observability/metrics.d.ts +36 -0
- package/dist/lib/observability/metrics.js +86 -0
- package/dist/lib/observability/redact.d.ts +29 -0
- package/dist/lib/observability/redact.js +102 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +2 -0
- package/dist/lib/observability/supabaseSpanExporter.js +22 -6
- package/dist/lib/observability/tracer.d.ts +10 -0
- package/dist/lib/observability/tracer.js +70 -3
- package/dist/lib/persistence/supabaseProvider.d.ts +11 -0
- package/dist/lib/persistence/supabaseProvider.js +32 -6
- package/dist/lib/registry.d.ts +87 -0
- package/dist/lib/registry.js +146 -0
- package/dist/lib/runtime/approvals.d.ts +44 -0
- package/dist/lib/runtime/approvals.js +67 -0
- package/dist/lib/runtime/syndicateTurn.d.ts +233 -0
- package/dist/lib/runtime/syndicateTurn.js +597 -0
- package/dist/lib/runtime/turnControl.d.ts +90 -0
- package/dist/lib/runtime/turnControl.js +131 -0
- package/dist/lib/session/supabaseSessionService.js +1 -1
- package/dist/lib/session/transcript.d.ts +9 -1
- package/dist/lib/session/transcript.js +15 -7
- package/dist/lib/skills.d.ts +50 -0
- package/dist/lib/skills.js +138 -0
- package/dist/lib/storage/postgres/index.d.ts +98 -0
- package/dist/lib/storage/postgres/index.js +146 -0
- package/dist/lib/storage/postgres/memoryStore.d.ts +11 -0
- package/dist/lib/storage/postgres/memoryStore.js +82 -0
- package/dist/lib/storage/postgres/sessionService.d.ts +51 -0
- package/dist/lib/storage/postgres/sessionService.js +225 -0
- package/dist/lib/storage/postgres/taskQueue.d.ts +22 -0
- package/dist/lib/storage/postgres/taskQueue.js +101 -0
- package/dist/lib/storage/postgres/taskStore.d.ts +55 -0
- package/dist/lib/storage/postgres/taskStore.js +168 -0
- package/dist/lib/storage/schema.d.ts +27 -0
- package/dist/lib/storage/schema.js +50 -0
- package/dist/lib/storage/schemaVersion.d.ts +25 -0
- package/dist/lib/storage/schemaVersion.js +46 -0
- package/dist/lib/syndicateSchema.d.ts +275 -0
- package/dist/lib/syndicateSchema.js +534 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +59 -10
- package/dist/lib/tools/collectionsSearchTool.js +1 -1
- package/dist/lib/tools/mcpServe.d.ts +45 -0
- package/dist/lib/tools/mcpServe.js +88 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +41 -21
- package/dist/lib/tools/science/corrections.d.ts +42 -0
- package/dist/lib/tools/science/corrections.js +115 -0
- package/dist/lib/tools/science/europepmc.d.ts +51 -0
- package/dist/lib/tools/science/europepmc.js +127 -0
- package/dist/lib/tools/science/http.d.ts +31 -0
- package/dist/lib/tools/science/http.js +104 -0
- package/dist/lib/tools/science/openalex.d.ts +30 -0
- package/dist/lib/tools/science/openalex.js +81 -0
- package/dist/lib/tools/science/trials.d.ts +58 -0
- package/dist/lib/tools/science/trials.js +203 -0
- package/dist/lib/tools/science/types.d.ts +69 -0
- package/dist/lib/tools/science/types.js +48 -0
- package/dist/lib/tools/scienceTools.d.ts +100 -0
- package/dist/lib/tools/scienceTools.js +277 -0
- package/dist/lib/tools/taskTools.d.ts +204 -0
- package/dist/lib/tools/taskTools.js +374 -0
- package/dist/lib/tools/toolContract.d.ts +17 -4
- package/dist/lib/tools/toolContract.js +13 -5
- package/dist/lib/tools/webExtractTool.d.ts +13 -10
- package/dist/lib/tools/webExtractTool.js +23 -55
- package/dist/lib/tools/webSearchTool.d.ts +2 -1
- package/dist/lib/tools/webSearchTool.js +4 -3
- package/dist/lib/tools/wikiTools.d.ts +2 -2
- package/dist/lib/tools/wikiTools.js +12 -12
- package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
- package/dist/lib/tools/xApiSearchTool.js +583 -0
- package/dist/lib/tools/xSearchTool.js +1 -1
- package/dist/lib/wiki/agentRun.js +1 -1
- package/dist/lib/wiki/builder.js +21 -6
- package/dist/lib/wiki/dive.js +2 -2
- package/dist/lib/wiki/entities.d.ts +2 -2
- package/dist/lib/wiki/entities.js +7 -7
- package/dist/lib/wiki/extract.js +5 -1
- package/dist/lib/wiki/fill.js +5 -5
- package/dist/lib/wiki/format.d.ts +7 -5
- package/dist/lib/wiki/format.js +2 -2
- package/dist/lib/wiki/graph.js +2 -2
- package/dist/lib/wiki/lint.js +2 -2
- package/dist/lib/wiki/markdown.d.ts +1 -1
- package/dist/lib/wiki/markdown.js +6 -1
- package/dist/lib/wiki/pipeline.js +2 -2
- package/dist/lib/wiki/vault.js +2 -2
- package/dist/scripts/a2a_server.d.ts +17 -11
- package/dist/scripts/a2a_server.js +405 -1028
- package/dist/scripts/assistant_worker.d.ts +33 -0
- package/dist/scripts/assistant_worker.js +193 -0
- package/dist/scripts/db.d.ts +26 -0
- package/dist/scripts/db.js +179 -0
- package/dist/scripts/doctor.d.ts +23 -0
- package/dist/scripts/doctor.js +90 -0
- package/dist/scripts/init.d.ts +43 -0
- package/dist/scripts/init.js +152 -0
- package/dist/scripts/registry.d.ts +25 -0
- package/dist/scripts/registry.js +182 -0
- package/dist/scripts/skills_install.d.ts +7 -0
- package/dist/scripts/skills_install.js +137 -0
- package/dist/scripts/syndicate_chat.js +159 -276
- package/package.json +90 -16
- package/skills/README.md +74 -0
- package/skills/briefs/README.md +16 -0
- package/skills/briefs/_shared.md +16 -0
- package/skills/briefs/melchizedek-author.md +60 -0
- package/skills/briefs/melchizedek-memory.md +47 -0
- package/skills/briefs/melchizedek-models.md +47 -0
- package/skills/briefs/melchizedek-scribe.md +37 -0
- package/skills/briefs/melchizedek-serve.md +56 -0
- package/skills/briefs/melchizedek.md +69 -0
- package/skills/briefs/skills-readme.md +33 -0
- package/skills/melchizedek/SKILL.md +159 -0
- package/skills/melchizedek-author/SKILL.md +99 -0
- package/skills/melchizedek-author/templates/minimal.yaml +60 -0
- package/skills/melchizedek-memory/SKILL.md +138 -0
- package/skills/melchizedek-models/SKILL.md +137 -0
- package/skills/melchizedek-scribe/SKILL.md +72 -0
- package/skills/melchizedek-scribe/templates/brief.md +27 -0
- 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
|
|
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
|
-
/**
|
|
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
|
|
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 {
|
|
12
|
-
import {
|
|
11
|
+
import { EMBEDDING_DIMENSIONS, EMBEDDING_MODEL } from '../config.js';
|
|
12
|
+
import { memoryProvidersFromEnv } from '../memory/providers.js';
|
|
13
13
|
const MAX_CHARS = 8_000;
|
|
14
|
-
/**
|
|
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
|
-
|
|
17
|
-
if (!
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
42
|
-
import { TELEMETRY_SCHEMA_VERSION } from
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
}
|