melchizedek-agents 0.16.0 → 0.16.2
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
CHANGED
|
@@ -229,7 +229,12 @@ WIKI_AGENT_MODEL=
|
|
|
229
229
|
# Send every span to any OTLP/HTTP backend (Phoenix, Langfuse, Jaeger, Tempo):
|
|
230
230
|
# a local Phoenix is `docker run -p 6006:6006 arizephoenix/phoenix`.
|
|
231
231
|
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:6006
|
|
232
|
+
# OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20<token> # a hosted backend's auth, if any
|
|
232
233
|
# OTEL_SERVICE_NAME=melchizedek
|
|
234
|
+
# How much conversation leaves with the spans (lib/observability/otlpFilter.ts):
|
|
235
|
+
# redacted (default: the TELEMETRY_REDACT rules below), off (no conversation
|
|
236
|
+
# text, user id hashed; timings, models, tokens, routes, errors stay), raw.
|
|
237
|
+
# OTEL_EXPORT_CONTENT=redacted
|
|
233
238
|
# Print [OTEL_SPAN_JSON] lines to stdout. Off by default: the root span carries
|
|
234
239
|
# the full user message and answer.
|
|
235
240
|
# OTEL_CONSOLE_SPANS=false
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ Consumers of the package read this file; it records changes to the
|
|
|
4
4
|
**published API surface** (the exports map in `package.json`, the bins,
|
|
5
5
|
the starter pack and the templates), not the repo's full history.
|
|
6
6
|
|
|
7
|
+
## 0.16.2 — 2026-10-02
|
|
8
|
+
|
|
9
|
+
- **`OTEL_EXPORT_CONTENT=off` also drops library content attributes.** In
|
|
10
|
+
0.16.1, `off` removed the engine's own conversation attributes but let
|
|
11
|
+
through ADK's: its `call_llm` span carries the full model request and
|
|
12
|
+
response as `gcp.vertex.agent.llm_request` / `_response`. `off` now drops
|
|
13
|
+
any text attribute whose name marks content (request, response, input,
|
|
14
|
+
output, args, prompt, messages, thinking, payload, …) unless the name
|
|
15
|
+
marks metadata (model, name, id, finish reasons); numbers always stay.
|
|
16
|
+
Verified end to end against Jaeger. **If you export OTLP with
|
|
17
|
+
`OTEL_EXPORT_CONTENT=off`, upgrade.** The default `redacted` mode
|
|
18
|
+
already ran the redactor on those attributes.
|
|
19
|
+
|
|
20
|
+
## 0.16.1 — 2026-10-02
|
|
21
|
+
|
|
22
|
+
- **OTLP export is filtered before it leaves** (`OTEL_EXPORT_CONTENT`).
|
|
23
|
+
Spans sent to `OTEL_EXPORTER_OTLP_ENDPOINT` carried the conversation
|
|
24
|
+
unredacted, while the ledger scrubs it (`TELEMETRY_REDACT`). The exporter
|
|
25
|
+
now applies the same redactor by default; `OTEL_EXPORT_CONTENT=off` drops
|
|
26
|
+
every conversation attribute (input, output, thinking, tool arguments and
|
|
27
|
+
results, payloads) and hashes `user.id`, keeping timings, models, token
|
|
28
|
+
counts, routes and errors; `raw` restores the old behaviour.
|
|
29
|
+
|
|
7
30
|
## 0.16.0 — 2026-10-02
|
|
8
31
|
|
|
9
32
|
### Breaking — read before upgrading
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
|
|
2
|
+
import type { Redactor } from './redact.ts';
|
|
3
|
+
export type OtlpContentMode = 'redacted' | 'off' | 'raw';
|
|
4
|
+
/** Span attributes that carry what a conversation said. */
|
|
5
|
+
export declare const CONTENT_ATTRIBUTES: Set<string>;
|
|
6
|
+
export declare function otlpContentMode(env?: NodeJS.ProcessEnv): OtlpContentMode;
|
|
7
|
+
/** One span's attributes as they may leave under `mode`. */
|
|
8
|
+
export declare function filterAttributes(attributes: Record<string, unknown>, mode: OtlpContentMode, redactor: Redactor | undefined): Record<string, unknown>;
|
|
9
|
+
/** Wraps the OTLP exporter so every span is filtered before it is sent. */
|
|
10
|
+
export declare class FilteringSpanExporter implements SpanExporter {
|
|
11
|
+
private readonly inner;
|
|
12
|
+
private readonly mode;
|
|
13
|
+
private readonly redactor;
|
|
14
|
+
constructor(inner: SpanExporter, mode: OtlpContentMode, redactor: Redactor | undefined);
|
|
15
|
+
export(spans: ReadableSpan[], resultCallback: Parameters<SpanExporter['export']>[1]): void;
|
|
16
|
+
shutdown(): Promise<void>;
|
|
17
|
+
forceFlush(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/observability/otlpFilter.ts — what leaves for an external trace backend.
|
|
3
|
+
*
|
|
4
|
+
* Spans carry the conversation: a turn's input and output, the model's
|
|
5
|
+
* thinking, tool arguments and results, the full request and response. The
|
|
6
|
+
* ledger scrubs those before it writes them (redact.ts, TELEMETRY_REDACT); an
|
|
7
|
+
* OTLP backend is a third party, so the same rule applies on the way out,
|
|
8
|
+
* and an adopter can choose to send no conversation at all.
|
|
9
|
+
*
|
|
10
|
+
* OTEL_EXPORT_CONTENT=redacted (default) the ledger's redactor runs on
|
|
11
|
+
* every text attribute and event
|
|
12
|
+
* OTEL_EXPORT_CONTENT=off conversation attributes are dropped (the
|
|
13
|
+
* engine's and any library's, by name) and
|
|
14
|
+
* the user id is hashed; timings, models,
|
|
15
|
+
* token counts, routes, error codes and ids
|
|
16
|
+
* remain
|
|
17
|
+
* OTEL_EXPORT_CONTENT=raw spans are sent as recorded
|
|
18
|
+
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
/** Span attributes that carry what a conversation said. */
|
|
21
|
+
export const CONTENT_ATTRIBUTES = new Set([
|
|
22
|
+
'syndicate.input',
|
|
23
|
+
'syndicate.output',
|
|
24
|
+
'syndicate.bindings',
|
|
25
|
+
'syndicate.route.reason',
|
|
26
|
+
'syndicate.error.message',
|
|
27
|
+
'llm.thinking',
|
|
28
|
+
'llm.error_message',
|
|
29
|
+
'tool.args',
|
|
30
|
+
'tool.data_gathered',
|
|
31
|
+
]);
|
|
32
|
+
const CONTENT_PREFIXES = ['llm.payload.'];
|
|
33
|
+
const PERSON_ATTRIBUTES = new Set(['user.id']);
|
|
34
|
+
/**
|
|
35
|
+
* Libraries add their own content attributes (ADK puts the whole model request
|
|
36
|
+
* on its call_llm span as `gcp.vertex.agent.llm_request`, and the response and
|
|
37
|
+
* tool calls beside it), and a later version can add more. So `off` also drops
|
|
38
|
+
* any text attribute whose name says it carries content, unless the name says
|
|
39
|
+
* it is metadata (a model, a name, an id, a finish reason).
|
|
40
|
+
*/
|
|
41
|
+
const CONTENT_KEY = /(^|[._])(request|response|input|output|args|arguments|prompt|completion|messages?|content|thinking|payload|data_gathered|bindings|reason|error_message)([._]|$)/i;
|
|
42
|
+
const METADATA_KEY = /([._])(model|name|id|system|description|finish_reasons|type|kind|version|stage)$/i;
|
|
43
|
+
export function otlpContentMode(env = process.env) {
|
|
44
|
+
const raw = (env.OTEL_EXPORT_CONTENT ?? 'redacted').trim().toLowerCase();
|
|
45
|
+
if (raw === 'off' || raw === 'none' || raw === 'false')
|
|
46
|
+
return 'off';
|
|
47
|
+
if (raw === 'raw')
|
|
48
|
+
return 'raw';
|
|
49
|
+
return 'redacted';
|
|
50
|
+
}
|
|
51
|
+
const isText = (v) => typeof v === 'string' || (Array.isArray(v) && v.some((x) => typeof x === 'string'));
|
|
52
|
+
const isContent = (key, value) => CONTENT_ATTRIBUTES.has(key) ||
|
|
53
|
+
CONTENT_PREFIXES.some((p) => key.startsWith(p)) ||
|
|
54
|
+
(isText(value) && CONTENT_KEY.test(key) && !METADATA_KEY.test(key));
|
|
55
|
+
const hashed = (v) => `sha256:${createHash('sha256').update(String(v)).digest('hex').slice(0, 16)}`;
|
|
56
|
+
/** One span's attributes as they may leave under `mode`. */
|
|
57
|
+
export function filterAttributes(attributes, mode, redactor) {
|
|
58
|
+
if (mode === 'raw')
|
|
59
|
+
return attributes;
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
62
|
+
if (mode === 'off') {
|
|
63
|
+
if (isContent(key, value))
|
|
64
|
+
continue;
|
|
65
|
+
out[key] = PERSON_ATTRIBUTES.has(key) ? hashed(value) : value;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
out[key] =
|
|
69
|
+
redactor && typeof value === 'string'
|
|
70
|
+
? redactor(value)
|
|
71
|
+
: redactor && Array.isArray(value)
|
|
72
|
+
? value.map((v) => (typeof v === 'string' ? redactor(v) : v))
|
|
73
|
+
: value;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** A span whose attributes and events are filtered; everything else reads through. */
|
|
78
|
+
function filtered(span, mode, redactor) {
|
|
79
|
+
const view = Object.create(span);
|
|
80
|
+
Object.defineProperty(view, 'attributes', { value: filterAttributes(span.attributes, mode, redactor) });
|
|
81
|
+
Object.defineProperty(view, 'events', {
|
|
82
|
+
value: span.events.map((e) => (e.attributes ? { ...e, attributes: filterAttributes(e.attributes, mode, redactor) } : e)),
|
|
83
|
+
});
|
|
84
|
+
return view;
|
|
85
|
+
}
|
|
86
|
+
/** Wraps the OTLP exporter so every span is filtered before it is sent. */
|
|
87
|
+
export class FilteringSpanExporter {
|
|
88
|
+
inner;
|
|
89
|
+
mode;
|
|
90
|
+
redactor;
|
|
91
|
+
constructor(inner, mode, redactor) {
|
|
92
|
+
this.inner = inner;
|
|
93
|
+
this.mode = mode;
|
|
94
|
+
this.redactor = redactor;
|
|
95
|
+
}
|
|
96
|
+
export(spans, resultCallback) {
|
|
97
|
+
this.inner.export(this.mode === 'raw' ? spans : spans.map((s) => filtered(s, this.mode, this.redactor)), resultCallback);
|
|
98
|
+
}
|
|
99
|
+
shutdown() {
|
|
100
|
+
return this.inner.shutdown();
|
|
101
|
+
}
|
|
102
|
+
forceFlush() {
|
|
103
|
+
return this.inner.forceFlush?.() ?? Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -7,6 +7,8 @@ import type { Event, LlmResponse } from '@google/adk';
|
|
|
7
7
|
* every span — ADK's included, so the viewer gets the waterfall — is also
|
|
8
8
|
* sent there. OTEL_EXPORTER_OTLP_HEADERS is honoured by the exporter itself
|
|
9
9
|
* (api keys); OTEL_SERVICE_NAME names the service (default "melchizedek").
|
|
10
|
+
* OTEL_EXPORT_CONTENT decides how much conversation leaves with the spans:
|
|
11
|
+
* redacted (default, the ledger's TELEMETRY_REDACT rules), off, or raw.
|
|
10
12
|
* Supabase stays the system of record; this is a viewer.
|
|
11
13
|
*/
|
|
12
14
|
export declare function otlpEndpoint(): string | undefined;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { trace, context } from '@opentelemetry/api';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { TELEMETRY_SCHEMA_VERSION, engineVersion } from './lineage.js';
|
|
4
|
+
import { FilteringSpanExporter, otlpContentMode } from './otlpFilter.js';
|
|
5
|
+
import { telemetryRedactor } from './redact.js';
|
|
4
6
|
import { chargeLlmCall, chargeTokens } from '../runtime/turnControl.js';
|
|
5
7
|
import sdkNode from '@opentelemetry/sdk-trace-node';
|
|
6
8
|
const { NodeTracerProvider } = sdkNode;
|
|
@@ -16,6 +18,8 @@ import { SupabaseSpanExporter, telemetrySinkEnabled, } from './supabaseSpanExpor
|
|
|
16
18
|
* every span — ADK's included, so the viewer gets the waterfall — is also
|
|
17
19
|
* sent there. OTEL_EXPORTER_OTLP_HEADERS is honoured by the exporter itself
|
|
18
20
|
* (api keys); OTEL_SERVICE_NAME names the service (default "melchizedek").
|
|
21
|
+
* OTEL_EXPORT_CONTENT decides how much conversation leaves with the spans:
|
|
22
|
+
* redacted (default, the ledger's TELEMETRY_REDACT rules), off, or raw.
|
|
19
23
|
* Supabase stays the system of record; this is a viewer.
|
|
20
24
|
*/
|
|
21
25
|
export function otlpEndpoint() {
|
|
@@ -33,8 +37,11 @@ function otlpProcessor() {
|
|
|
33
37
|
const mod = createRequire(import.meta.url)('@opentelemetry/exporter-trace-otlp-http');
|
|
34
38
|
const OTLPTraceExporter = mod.OTLPTraceExporter ?? mod.default?.OTLPTraceExporter;
|
|
35
39
|
const { BatchSpanProcessor } = sdkBase;
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
// What leaves for a third-party backend is filtered first (otlpFilter.ts).
|
|
41
|
+
const mode = otlpContentMode();
|
|
42
|
+
console.log(`[TELEMETRY] OTLP export enabled → ${url} (content: ${mode})`);
|
|
43
|
+
const exporter = new OTLPTraceExporter({ url });
|
|
44
|
+
return new BatchSpanProcessor(mode === 'raw' ? exporter : new FilteringSpanExporter(exporter, mode, telemetryRedactor()));
|
|
38
45
|
}
|
|
39
46
|
catch (err) {
|
|
40
47
|
console.warn(`[TELEMETRY] OTLP exporter unavailable (${err instanceof Error ? err.message : err}); spans not sent to ${url}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "melchizedek-agents",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "A multi-model, multi-agent orchestration framework on Google ADK — YAML-defined syndicates, Supabase session persistence, pgvector long-term memory. Companion repo to the lyceumagents.com curriculum.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|