@senad-d/observme 0.1.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 +57 -0
- package/CHANGELOG.md +52 -0
- package/LICENSE +21 -0
- package/ObservMe-Production-Docs/00-README.md +79 -0
- package/ObservMe-Production-Docs/01-requirements-and-scope.md +207 -0
- package/ObservMe-Production-Docs/02-reference-architecture.md +306 -0
- package/ObservMe-Production-Docs/03-pi-event-and-session-model.md +266 -0
- package/ObservMe-Production-Docs/04-telemetry-semantic-conventions.md +722 -0
- package/ObservMe-Production-Docs/05-otel-pipeline-and-collector.md +355 -0
- package/ObservMe-Production-Docs/06-security-privacy-redaction.md +294 -0
- package/ObservMe-Production-Docs/07-extension-implementation-blueprint.md +447 -0
- package/ObservMe-Production-Docs/08-query-grafana-integration.md +276 -0
- package/ObservMe-Production-Docs/09-dashboards-alerts-slos.md +640 -0
- package/ObservMe-Production-Docs/10-testing-release-operations.md +319 -0
- package/ObservMe-Production-Docs/11-deployment-runbooks.md +203 -0
- package/ObservMe-Production-Docs/12-configuration-reference.md +337 -0
- package/ObservMe-Production-Docs/13-source-notes.md +33 -0
- package/ObservMe-Production-Docs/pi-session-format.md +427 -0
- package/README.md +356 -0
- package/SECURITY.md +45 -0
- package/dashboards/observme-agent-node-graphs.json +250 -0
- package/dashboards/observme-agents.json +1880 -0
- package/dashboards/observme-alerts.yaml +113 -0
- package/dashboards/observme-branches-compactions.json +1042 -0
- package/dashboards/observme-cost.json +1254 -0
- package/dashboards/observme-errors.json +1659 -0
- package/dashboards/observme-export-health.json +1802 -0
- package/dashboards/observme-latency.json +1494 -0
- package/dashboards/observme-llm-conversations.json +730 -0
- package/dashboards/observme-logs-llm.json +644 -0
- package/dashboards/observme-models.json +933 -0
- package/dashboards/observme-overview.json +2129 -0
- package/dashboards/observme-slo-health.json +737 -0
- package/dashboards/observme-slos.yaml +56 -0
- package/dashboards/observme-tools.json +902 -0
- package/dashboards/observme-trace-journey.json +1793 -0
- package/docs/STRUCTURE.md +49 -0
- package/docs/agent-subagent-observability-requirements.md +997 -0
- package/docs/compatibility-matrix.md +39 -0
- package/docs/configuration-tui-design-standard.md +767 -0
- package/docs/configuration.md +38 -0
- package/docs/review-validation.md +115 -0
- package/docs/validation-flow.md +117 -0
- package/examples/collector.yaml +123 -0
- package/examples/observme.yaml +131 -0
- package/img/demo.gif +0 -0
- package/img/icon.svg +47 -0
- package/package.json +103 -0
- package/src/commands/obs-agents-runtime.ts +150 -0
- package/src/commands/obs-agents.ts +491 -0
- package/src/commands/obs-args.ts +63 -0
- package/src/commands/obs-backfill.ts +1334 -0
- package/src/commands/obs-command-support.ts +43 -0
- package/src/commands/obs-cost.ts +228 -0
- package/src/commands/obs-diagnostics.ts +22 -0
- package/src/commands/obs-errors.ts +156 -0
- package/src/commands/obs-health.ts +301 -0
- package/src/commands/obs-link.ts +90 -0
- package/src/commands/obs-logs.ts +194 -0
- package/src/commands/obs-loki-summary.ts +184 -0
- package/src/commands/obs-session.ts +259 -0
- package/src/commands/obs-status.ts +359 -0
- package/src/commands/obs-tools.ts +274 -0
- package/src/commands/obs-trace.ts +411 -0
- package/src/commands/obs.ts +211 -0
- package/src/config/bootstrap-project-config.ts +300 -0
- package/src/config/defaults.ts +143 -0
- package/src/config/load-config.ts +631 -0
- package/src/config/project-paths.ts +61 -0
- package/src/config/schema.ts +405 -0
- package/src/config/validate.ts +456 -0
- package/src/constants.ts +4 -0
- package/src/diagnostics/sanitize.ts +6 -0
- package/src/extension.ts +38 -0
- package/src/otel/logs.ts +160 -0
- package/src/otel/metrics.ts +165 -0
- package/src/otel/otlp-endpoint.ts +10 -0
- package/src/otel/sdk.ts +114 -0
- package/src/otel/shutdown.ts +102 -0
- package/src/otel/traces.ts +166 -0
- package/src/pi/agent-lineage.ts +378 -0
- package/src/pi/agent-tree-tracker.ts +258 -0
- package/src/pi/event-handlers/agent-turn.ts +155 -0
- package/src/pi/event-handlers/lifecycle.ts +642 -0
- package/src/pi/event-handlers/llm.ts +115 -0
- package/src/pi/event-handlers/session-events.ts +159 -0
- package/src/pi/event-handlers/tool-bash.ts +275 -0
- package/src/pi/handler-internals.ts +2154 -0
- package/src/pi/handler-runtime.ts +633 -0
- package/src/pi/handler-types.ts +261 -0
- package/src/pi/handlers.ts +75 -0
- package/src/pi/subagent-spawn.ts +975 -0
- package/src/pi/subagent-types.ts +29 -0
- package/src/privacy/content-capture.ts +104 -0
- package/src/privacy/hash.ts +93 -0
- package/src/privacy/redact.ts +619 -0
- package/src/privacy/secret-patterns.ts +185 -0
- package/src/privacy/truncate.ts +69 -0
- package/src/query/grafana-readiness.ts +164 -0
- package/src/query/grafana-transport.ts +481 -0
- package/src/query/grafana.ts +371 -0
- package/src/query/loki.ts +332 -0
- package/src/query/prometheus.ts +388 -0
- package/src/query/tempo.ts +332 -0
- package/src/safety/sensitive-input.ts +208 -0
- package/src/semconv/attributes.ts +279 -0
- package/src/semconv/metrics.ts +146 -0
- package/src/semconv/spans.ts +19 -0
- package/src/semconv/values.ts +13 -0
- package/src/util/bounded-map.ts +97 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
3
|
+
import type { ValidationIssue } from "../config/validate.ts";
|
|
4
|
+
import { validateObservMeConfig } from "../config/validate.ts";
|
|
5
|
+
import { AGENT_LINEAGE_ATTRIBUTES, COMMON_SPAN_ATTRIBUTES, RESOURCE_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
6
|
+
|
|
7
|
+
export type AgentRole = "root" | "subagent" | "orchestrator" | "worker" | "reviewer" | "unknown";
|
|
8
|
+
export type ParentPropagationFailureReason = "partial_envelope" | "malformed_envelope" | "stale_envelope";
|
|
9
|
+
|
|
10
|
+
export interface ValidatedParentTraceContext {
|
|
11
|
+
readonly traceId: string;
|
|
12
|
+
readonly spanId: string;
|
|
13
|
+
readonly traceFlags: number;
|
|
14
|
+
readonly tracestate?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentLineageContext {
|
|
18
|
+
readonly workflowId: string;
|
|
19
|
+
readonly workflowRootAgentId: string;
|
|
20
|
+
readonly agentId: string;
|
|
21
|
+
readonly parentAgentId?: string;
|
|
22
|
+
readonly rootAgentId: string;
|
|
23
|
+
readonly depth: number;
|
|
24
|
+
readonly role: AgentRole;
|
|
25
|
+
readonly capability?: string;
|
|
26
|
+
readonly parentSessionId?: string;
|
|
27
|
+
readonly parentTraceId?: string;
|
|
28
|
+
readonly parentSpanId?: string;
|
|
29
|
+
readonly spawnId?: string;
|
|
30
|
+
readonly propagatedTraceContext?: ValidatedParentTraceContext;
|
|
31
|
+
readonly propagationFailure?: ParentPropagationFailureReason;
|
|
32
|
+
readonly orphaned: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CreateAgentLineageContextOptions {
|
|
36
|
+
readonly config: ObservMeConfig;
|
|
37
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
38
|
+
readonly trustedParentContext?: boolean;
|
|
39
|
+
readonly role?: AgentRole;
|
|
40
|
+
readonly capability?: string;
|
|
41
|
+
readonly generateId?: () => string;
|
|
42
|
+
readonly requireCompletePropagationEnvelope?: boolean;
|
|
43
|
+
readonly failOpenInvalidPropagation?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface PropagatedLineageEnvironment {
|
|
47
|
+
readonly workflowId?: string;
|
|
48
|
+
readonly agentId?: string;
|
|
49
|
+
readonly parentAgentId?: string;
|
|
50
|
+
readonly rootAgentId?: string;
|
|
51
|
+
readonly parentSessionId?: string;
|
|
52
|
+
readonly parentTraceId?: string;
|
|
53
|
+
readonly parentSpanId?: string;
|
|
54
|
+
readonly depth?: number;
|
|
55
|
+
readonly spawnId?: string;
|
|
56
|
+
readonly capability?: string;
|
|
57
|
+
readonly traceContext?: ValidatedParentTraceContext;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type LineageAttributeValue = boolean | number | string;
|
|
61
|
+
export type LineageAttributes = Record<string, LineageAttributeValue>;
|
|
62
|
+
|
|
63
|
+
export class LineageValidationError extends Error {
|
|
64
|
+
readonly issues: ValidationIssue[];
|
|
65
|
+
readonly reason: ParentPropagationFailureReason;
|
|
66
|
+
|
|
67
|
+
constructor(issues: ValidationIssue[], reason: ParentPropagationFailureReason = "malformed_envelope") {
|
|
68
|
+
super(`Invalid propagated ObservMe lineage: ${issues.map(issue => issue.message).join("; ")}`);
|
|
69
|
+
this.name = "LineageValidationError";
|
|
70
|
+
this.issues = issues;
|
|
71
|
+
this.reason = reason;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createAgentLineageContext(options: CreateAgentLineageContextOptions): AgentLineageContext {
|
|
76
|
+
const env = options.env ?? process.env;
|
|
77
|
+
const generateId = options.generateId ?? generateSafeLineageId;
|
|
78
|
+
const propagation = resolveTrustedPropagation(options, env);
|
|
79
|
+
const propagated = propagation.lineage;
|
|
80
|
+
const parentAgentId = propagated.parentAgentId;
|
|
81
|
+
const agentId = propagated.agentId ?? `agent-${generateId()}`;
|
|
82
|
+
const workflowId = propagated.workflowId ?? `workflow-${generateId()}`;
|
|
83
|
+
const rootAgentId = resolveRootAgentId(agentId, propagated.rootAgentId, parentAgentId);
|
|
84
|
+
const depth = resolveDepth(propagated.depth, parentAgentId);
|
|
85
|
+
const role = options.role ?? resolveDefaultRole(parentAgentId);
|
|
86
|
+
const capability = options.capability ?? propagated.capability;
|
|
87
|
+
const orphaned = Boolean(parentAgentId && !propagated.rootAgentId) || propagation.failure !== undefined;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
workflowId,
|
|
91
|
+
workflowRootAgentId: rootAgentId,
|
|
92
|
+
agentId,
|
|
93
|
+
parentAgentId,
|
|
94
|
+
rootAgentId,
|
|
95
|
+
depth,
|
|
96
|
+
role,
|
|
97
|
+
capability,
|
|
98
|
+
parentSessionId: propagated.parentSessionId,
|
|
99
|
+
parentTraceId: propagated.parentTraceId,
|
|
100
|
+
parentSpanId: propagated.parentSpanId,
|
|
101
|
+
spawnId: propagated.spawnId,
|
|
102
|
+
propagatedTraceContext: propagated.traceContext,
|
|
103
|
+
propagationFailure: propagation.failure,
|
|
104
|
+
orphaned,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readTrustedPropagatedLineage(
|
|
109
|
+
config: ObservMeConfig,
|
|
110
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
111
|
+
trustedParentContext = false,
|
|
112
|
+
): PropagatedLineageEnvironment {
|
|
113
|
+
if (!trustedParentContext) return {};
|
|
114
|
+
|
|
115
|
+
assertValidPropagatedLineage(config, env);
|
|
116
|
+
const traceContext = readValidatedParentTraceContext(env);
|
|
117
|
+
assertConsistentParentTraceMetadata(config, env, traceContext);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
workflowId: env[config.workflow.idEnv],
|
|
121
|
+
agentId: env[config.agent.idEnv],
|
|
122
|
+
parentAgentId: env[config.agent.parentIdEnv],
|
|
123
|
+
rootAgentId: env[config.agent.rootIdEnv],
|
|
124
|
+
parentSessionId: env[config.agent.parentSessionIdEnv],
|
|
125
|
+
parentTraceId: traceContext?.traceId ?? env[config.agent.parentTraceIdEnv],
|
|
126
|
+
parentSpanId: traceContext?.spanId ?? env[config.agent.parentSpanIdEnv],
|
|
127
|
+
depth: parsePropagatedDepth(env[config.agent.depthEnv]),
|
|
128
|
+
spawnId: env[config.agent.spawnIdEnv],
|
|
129
|
+
capability: env[config.agent.capabilityEnv],
|
|
130
|
+
traceContext,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function resolveTrustedPropagation(
|
|
135
|
+
options: CreateAgentLineageContextOptions,
|
|
136
|
+
env: NodeJS.ProcessEnv,
|
|
137
|
+
): { readonly lineage: PropagatedLineageEnvironment; readonly failure?: ParentPropagationFailureReason } {
|
|
138
|
+
if (options.trustedParentContext !== true) return { lineage: {} };
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const lineage = readTrustedPropagatedLineage(options.config, env, true);
|
|
142
|
+
if (options.requireCompletePropagationEnvelope) assertCompletePropagationEnvelope(options.config, env);
|
|
143
|
+
return { lineage };
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (!options.failOpenInvalidPropagation || !(error instanceof LineageValidationError)) throw error;
|
|
146
|
+
return { lineage: {}, failure: error.reason };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assertCompletePropagationEnvelope(config: ObservMeConfig, env: NodeJS.ProcessEnv): void {
|
|
151
|
+
if (!hasAnyPropagationValue(config, env)) return;
|
|
152
|
+
|
|
153
|
+
if (env[config.agent.idEnv]) {
|
|
154
|
+
throwPropagationError("stale_envelope", `${config.agent.idEnv} must not be inherited by a child process.`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const requiredNames = [
|
|
158
|
+
config.workflow.idEnv,
|
|
159
|
+
config.agent.parentIdEnv,
|
|
160
|
+
config.agent.rootIdEnv,
|
|
161
|
+
config.agent.depthEnv,
|
|
162
|
+
config.agent.spawnIdEnv,
|
|
163
|
+
];
|
|
164
|
+
if (config.agent.propagateTraceContext) requiredNames.push("traceparent");
|
|
165
|
+
|
|
166
|
+
const missingNames = requiredNames.filter(name => !env[name]);
|
|
167
|
+
if (missingNames.length > 0) {
|
|
168
|
+
throwPropagationError("partial_envelope", `Propagated child context is missing ${missingNames.join(", ")}.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasAnyPropagationValue(config: ObservMeConfig, env: NodeJS.ProcessEnv): boolean {
|
|
173
|
+
return propagationEnvironmentKeys(config).some(name => Boolean(env[name]));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readValidatedParentTraceContext(env: NodeJS.ProcessEnv): ValidatedParentTraceContext | undefined {
|
|
177
|
+
if (env.TRACEPARENT || env.TRACESTATE) {
|
|
178
|
+
throwPropagationError("stale_envelope", "Uppercase W3C propagation variables are stale or ambiguous.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const traceparent = env.traceparent;
|
|
182
|
+
const tracestate = env.tracestate;
|
|
183
|
+
if (!traceparent) {
|
|
184
|
+
if (tracestate) throwPropagationError("partial_envelope", "tracestate requires traceparent.");
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const match = /^00-([a-f0-9]{32})-([a-f0-9]{16})-([a-f0-9]{2})$/iu.exec(traceparent);
|
|
189
|
+
if (!match || /^0{32}$/u.test(match[1]) || /^0{16}$/u.test(match[2])) {
|
|
190
|
+
throwPropagationError("malformed_envelope", "traceparent is malformed or contains an invalid zero identifier.");
|
|
191
|
+
}
|
|
192
|
+
if (tracestate && !isValidTracestate(tracestate)) {
|
|
193
|
+
throwPropagationError("malformed_envelope", "tracestate is malformed or oversized.");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
traceId: match[1].toLowerCase(),
|
|
198
|
+
spanId: match[2].toLowerCase(),
|
|
199
|
+
traceFlags: Number.parseInt(match[3], 16),
|
|
200
|
+
...(tracestate ? { tracestate } : {}),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isValidTracestate(value: string): boolean {
|
|
205
|
+
if (value.length > 512) return false;
|
|
206
|
+
const members = value.split(",");
|
|
207
|
+
if (members.length > 32) return false;
|
|
208
|
+
const keys = new Set<string>();
|
|
209
|
+
|
|
210
|
+
for (const rawMember of members) {
|
|
211
|
+
const member = rawMember.trim();
|
|
212
|
+
const separator = member.indexOf("=");
|
|
213
|
+
if (separator <= 0 || separator !== member.lastIndexOf("=")) return false;
|
|
214
|
+
const key = member.slice(0, separator);
|
|
215
|
+
const memberValue = member.slice(separator + 1);
|
|
216
|
+
if (!isValidTracestateKey(key) || !isValidTracestateValue(memberValue) || keys.has(key)) return false;
|
|
217
|
+
keys.add(key);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isValidTracestateKey(value: string): boolean {
|
|
224
|
+
return /^[a-z][a-z0-9_*/-]{0,255}$/u.test(value) || /^[a-z0-9][a-z0-9_*/-]{0,240}@[a-z][a-z0-9_*/-]{0,13}$/u.test(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isValidTracestateValue(value: string): boolean {
|
|
228
|
+
if (value.length === 0 || value.length > 256 || value.endsWith(" ")) return false;
|
|
229
|
+
|
|
230
|
+
for (const character of value) {
|
|
231
|
+
const code = character.codePointAt(0) ?? 0;
|
|
232
|
+
if (code < 0x20 || code > 0x7e || character === "," || character === "=") return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function assertConsistentParentTraceMetadata(
|
|
239
|
+
config: ObservMeConfig,
|
|
240
|
+
env: NodeJS.ProcessEnv,
|
|
241
|
+
traceContext: ValidatedParentTraceContext | undefined,
|
|
242
|
+
): void {
|
|
243
|
+
const parentTraceId = env[config.agent.parentTraceIdEnv];
|
|
244
|
+
const parentSpanId = env[config.agent.parentSpanIdEnv];
|
|
245
|
+
if (Boolean(parentTraceId) !== Boolean(parentSpanId)) {
|
|
246
|
+
throwPropagationError("partial_envelope", "Parent trace and span metadata must be supplied together.");
|
|
247
|
+
}
|
|
248
|
+
if (!traceContext || !parentTraceId || !parentSpanId) return;
|
|
249
|
+
if (parentTraceId.toLowerCase() !== traceContext.traceId || parentSpanId.toLowerCase() !== traceContext.spanId) {
|
|
250
|
+
throwPropagationError("stale_envelope", "Parent trace metadata does not match traceparent.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function throwPropagationError(reason: ParentPropagationFailureReason, message: string): never {
|
|
255
|
+
throw new LineageValidationError([{ code: "malformed_lineage_value", message }], reason);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function buildLineageAttributes(lineage: AgentLineageContext): LineageAttributes {
|
|
259
|
+
const attributes: LineageAttributes = {
|
|
260
|
+
[COMMON_SPAN_ATTRIBUTES.PI_WORKFLOW_ID]: lineage.workflowId,
|
|
261
|
+
[COMMON_SPAN_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: lineage.workflowRootAgentId,
|
|
262
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_ID]: lineage.agentId,
|
|
263
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_ROOT_ID]: lineage.rootAgentId,
|
|
264
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_ROLE]: lineage.role,
|
|
265
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_DEPTH]: lineage.depth,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
if (lineage.parentAgentId) attributes[COMMON_SPAN_ATTRIBUTES.PI_AGENT_PARENT_ID] = lineage.parentAgentId;
|
|
269
|
+
if (lineage.capability) attributes[AGENT_LINEAGE_ATTRIBUTES.PI_AGENT_CAPABILITY] = lineage.capability;
|
|
270
|
+
if (lineage.orphaned) attributes[AGENT_LINEAGE_ATTRIBUTES.PI_AGENT_ORPHANED] = true;
|
|
271
|
+
|
|
272
|
+
return attributes;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function buildResourceLineageAttributes(lineage: AgentLineageContext): LineageAttributes {
|
|
276
|
+
const attributes: LineageAttributes = {
|
|
277
|
+
[RESOURCE_ATTRIBUTES.PI_WORKFLOW_ID]: lineage.workflowId,
|
|
278
|
+
[RESOURCE_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: lineage.workflowRootAgentId,
|
|
279
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_ID]: lineage.agentId,
|
|
280
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_ROOT_ID]: lineage.rootAgentId,
|
|
281
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_ROLE]: lineage.role,
|
|
282
|
+
[RESOURCE_ATTRIBUTES.PI_AGENT_DEPTH]: lineage.depth,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (lineage.parentAgentId) attributes[RESOURCE_ATTRIBUTES.PI_AGENT_PARENT_ID] = lineage.parentAgentId;
|
|
286
|
+
if (lineage.capability) attributes[AGENT_LINEAGE_ATTRIBUTES.PI_AGENT_CAPABILITY] = lineage.capability;
|
|
287
|
+
|
|
288
|
+
return attributes;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function createPropagationEnvironment(
|
|
292
|
+
lineage: AgentLineageContext,
|
|
293
|
+
config: ObservMeConfig,
|
|
294
|
+
extra: NodeJS.ProcessEnv = {},
|
|
295
|
+
): NodeJS.ProcessEnv {
|
|
296
|
+
const sanitizedExtra = sanitizePropagationEnvironment(config, extra);
|
|
297
|
+
|
|
298
|
+
if (!config.workflow.enabled || !config.agent.propagateToSubagents) return sanitizedExtra;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
...sanitizedExtra,
|
|
302
|
+
[config.workflow.idEnv]: lineage.workflowId,
|
|
303
|
+
[config.agent.parentIdEnv]: lineage.agentId,
|
|
304
|
+
[config.agent.rootIdEnv]: lineage.rootAgentId,
|
|
305
|
+
[config.agent.depthEnv]: String(lineage.depth),
|
|
306
|
+
...(lineage.capability ? { [config.agent.capabilityEnv]: lineage.capability } : {}),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function sanitizePropagationEnvironment(config: ObservMeConfig, env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
311
|
+
const sanitized = { ...env };
|
|
312
|
+
|
|
313
|
+
for (const key of propagationEnvironmentKeys(config)) delete sanitized[key];
|
|
314
|
+
|
|
315
|
+
return sanitized;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const highCardinalityLineageKeyPatterns = [
|
|
319
|
+
/(?:workflow|session|trace|span|entry|spawn|tool_call)[._-]id/iu,
|
|
320
|
+
/agent[._-](?:id|parent[._-]id|root[._-]id|child[._-]id)/iu,
|
|
321
|
+
/(?:parent|child|root)[._-]agent[._-]id/iu,
|
|
322
|
+
/(?:^|[._-])id$/iu,
|
|
323
|
+
] as const;
|
|
324
|
+
|
|
325
|
+
export function isHighCardinalityLineageKey(key: string): boolean {
|
|
326
|
+
return highCardinalityLineageKeyPatterns.some(pattern => pattern.test(key));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function propagationEnvironmentKeys(config: ObservMeConfig): string[] {
|
|
330
|
+
return [
|
|
331
|
+
...new Set([
|
|
332
|
+
config.workflow.idEnv,
|
|
333
|
+
config.agent.idEnv,
|
|
334
|
+
config.agent.parentIdEnv,
|
|
335
|
+
config.agent.rootIdEnv,
|
|
336
|
+
config.agent.parentSessionIdEnv,
|
|
337
|
+
config.agent.parentTraceIdEnv,
|
|
338
|
+
config.agent.parentSpanIdEnv,
|
|
339
|
+
config.agent.depthEnv,
|
|
340
|
+
config.agent.spawnIdEnv,
|
|
341
|
+
config.agent.capabilityEnv,
|
|
342
|
+
"traceparent",
|
|
343
|
+
"tracestate",
|
|
344
|
+
"TRACEPARENT",
|
|
345
|
+
"TRACESTATE",
|
|
346
|
+
]),
|
|
347
|
+
];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function assertValidPropagatedLineage(config: ObservMeConfig, env: NodeJS.ProcessEnv): void {
|
|
351
|
+
const result = validateObservMeConfig(config, { env });
|
|
352
|
+
const lineageIssues = result.issues.filter(issue => issue.code === "malformed_lineage_value");
|
|
353
|
+
if (lineageIssues.length > 0) throw new LineageValidationError(lineageIssues);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function parsePropagatedDepth(value: string | undefined): number | undefined {
|
|
357
|
+
if (!value) return undefined;
|
|
358
|
+
return Number(value);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function resolveRootAgentId(agentId: string, propagatedRootAgentId: string | undefined, parentAgentId: string | undefined): string {
|
|
362
|
+
if (propagatedRootAgentId) return propagatedRootAgentId;
|
|
363
|
+
if (parentAgentId) return agentId;
|
|
364
|
+
return agentId;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function resolveDepth(propagatedDepth: number | undefined, parentAgentId: string | undefined): number {
|
|
368
|
+
if (!parentAgentId) return 0;
|
|
369
|
+
return (propagatedDepth ?? 0) + 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function resolveDefaultRole(parentAgentId: string | undefined): AgentRole {
|
|
373
|
+
return parentAgentId ? "subagent" : "root";
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function generateSafeLineageId(): string {
|
|
377
|
+
return randomUUID();
|
|
378
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { BoundedMap } from "../util/bounded-map.ts";
|
|
2
|
+
import type { AgentLineageContext, AgentRole } from "./agent-lineage.ts";
|
|
3
|
+
import { isHighCardinalityLineageKey } from "./agent-lineage.ts";
|
|
4
|
+
|
|
5
|
+
export type AgentChildStatus = "starting" | "active" | "completed" | "failed" | "cancelled" | "orphaned";
|
|
6
|
+
|
|
7
|
+
export interface AgentTreeNode {
|
|
8
|
+
readonly agentId: string;
|
|
9
|
+
readonly workflowId: string;
|
|
10
|
+
readonly rootAgentId: string;
|
|
11
|
+
readonly parentAgentId?: string;
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
readonly role: AgentRole;
|
|
14
|
+
readonly capability?: string;
|
|
15
|
+
readonly orphaned: boolean;
|
|
16
|
+
readonly childIds: readonly string[];
|
|
17
|
+
readonly activeChildren: number;
|
|
18
|
+
readonly fanoutCount: number;
|
|
19
|
+
readonly status: AgentChildStatus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AgentTreeSummary {
|
|
23
|
+
readonly activeChildren: number;
|
|
24
|
+
readonly fanoutCount: number;
|
|
25
|
+
readonly treeDepth: number;
|
|
26
|
+
readonly treeWidth: number;
|
|
27
|
+
readonly orphanCount: number;
|
|
28
|
+
readonly childStatuses: Readonly<Record<AgentChildStatus, number>>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface AgentTreeTrackerOptions {
|
|
32
|
+
readonly maxAgents: number;
|
|
33
|
+
readonly onEvict?: (node: AgentTreeNode) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface MutableAgentTreeNode {
|
|
37
|
+
agentId: string;
|
|
38
|
+
workflowId: string;
|
|
39
|
+
rootAgentId: string;
|
|
40
|
+
parentAgentId?: string;
|
|
41
|
+
depth: number;
|
|
42
|
+
role: AgentRole;
|
|
43
|
+
capability?: string;
|
|
44
|
+
orphaned: boolean;
|
|
45
|
+
childIds: Set<string>;
|
|
46
|
+
fanoutCount: number;
|
|
47
|
+
status: AgentChildStatus;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const activeChildStatuses = new Set<AgentChildStatus>(["starting", "active"]);
|
|
51
|
+
const agentStatusOrder: AgentChildStatus[] = ["starting", "active", "completed", "failed", "cancelled", "orphaned"];
|
|
52
|
+
|
|
53
|
+
export class AgentTreeTracker {
|
|
54
|
+
readonly #nodes: BoundedMap<string, MutableAgentTreeNode>;
|
|
55
|
+
|
|
56
|
+
constructor(options: AgentTreeTrackerOptions) {
|
|
57
|
+
this.#nodes = new BoundedMap({
|
|
58
|
+
maxSize: options.maxAgents,
|
|
59
|
+
onEvict: eviction => {
|
|
60
|
+
this.detachChildReference(eviction.key);
|
|
61
|
+
options.onEvict?.(snapshotNode(eviction.value, this.#nodes));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get size(): number {
|
|
67
|
+
return this.#nodes.size;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registerAgent(lineage: AgentLineageContext, status: AgentChildStatus = "active"): AgentTreeNode {
|
|
71
|
+
const node = createMutableNode(lineage, status, this.isOrphan(lineage));
|
|
72
|
+
this.retainParentForInsertion(node.parentAgentId);
|
|
73
|
+
this.#nodes.set(lineage.agentId, node);
|
|
74
|
+
this.linkParentToChild(node);
|
|
75
|
+
return snapshotNode(node, this.#nodes);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
updateStatus(agentId: string, status: AgentChildStatus): AgentTreeNode | undefined {
|
|
79
|
+
const node = this.#nodes.get(agentId);
|
|
80
|
+
if (!node) return undefined;
|
|
81
|
+
|
|
82
|
+
node.status = status;
|
|
83
|
+
if (status === "orphaned") node.orphaned = true;
|
|
84
|
+
return snapshotNode(node, this.#nodes);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getAgent(agentId: string): AgentTreeNode | undefined {
|
|
88
|
+
const node = this.#nodes.get(agentId);
|
|
89
|
+
return node ? snapshotNode(node, this.#nodes) : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getChildren(parentAgentId: string): AgentTreeNode[] {
|
|
93
|
+
const parent = this.#nodes.get(parentAgentId);
|
|
94
|
+
if (!parent) return [];
|
|
95
|
+
|
|
96
|
+
return [...parent.childIds].flatMap(childId => this.getExistingChildSnapshot(childId));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
summarize(rootAgentId?: string): AgentTreeSummary {
|
|
100
|
+
const nodes = this.nodesForSummary(rootAgentId);
|
|
101
|
+
const childStatuses = createEmptyStatusCounts();
|
|
102
|
+
const widthByDepth = new Map<number, number>();
|
|
103
|
+
let activeChildren = 0;
|
|
104
|
+
let fanoutCount = 0;
|
|
105
|
+
let treeDepth = 0;
|
|
106
|
+
let orphanCount = 0;
|
|
107
|
+
|
|
108
|
+
for (const node of nodes) {
|
|
109
|
+
childStatuses[node.status] += 1;
|
|
110
|
+
activeChildren += countActiveChildren(retainedChildIds(node, this.#nodes), this.#nodes);
|
|
111
|
+
fanoutCount += node.fanoutCount;
|
|
112
|
+
treeDepth = Math.max(treeDepth, node.depth);
|
|
113
|
+
orphanCount += node.orphaned ? 1 : 0;
|
|
114
|
+
widthByDepth.set(node.depth, (widthByDepth.get(node.depth) ?? 0) + 1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
activeChildren,
|
|
119
|
+
fanoutCount,
|
|
120
|
+
treeDepth,
|
|
121
|
+
treeWidth: maxMapValue(widthByDepth),
|
|
122
|
+
orphanCount,
|
|
123
|
+
childStatuses,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
metricLabels(status: AgentChildStatus, orphaned: boolean): Record<string, string> {
|
|
128
|
+
const labels = {
|
|
129
|
+
status,
|
|
130
|
+
reason: orphaned ? "orphaned" : "attached",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
assertNoHighCardinalityMetricLabels(labels);
|
|
134
|
+
return labels;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private isOrphan(lineage: AgentLineageContext): boolean {
|
|
138
|
+
if (lineage.orphaned) return true;
|
|
139
|
+
if (!lineage.parentAgentId) return false;
|
|
140
|
+
return !this.#nodes.has(lineage.parentAgentId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private retainParentForInsertion(parentAgentId: string | undefined): void {
|
|
144
|
+
if (!parentAgentId) return;
|
|
145
|
+
|
|
146
|
+
const parent = this.#nodes.get(parentAgentId);
|
|
147
|
+
if (!parent) return;
|
|
148
|
+
|
|
149
|
+
this.#nodes.delete(parentAgentId);
|
|
150
|
+
this.#nodes.set(parentAgentId, parent);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private linkParentToChild(node: MutableAgentTreeNode): void {
|
|
154
|
+
if (!node.parentAgentId) return;
|
|
155
|
+
|
|
156
|
+
const parent = this.#nodes.get(node.parentAgentId);
|
|
157
|
+
if (!parent) return;
|
|
158
|
+
|
|
159
|
+
const knownChild = parent.childIds.has(node.agentId);
|
|
160
|
+
parent.childIds.add(node.agentId);
|
|
161
|
+
if (!knownChild) parent.fanoutCount += 1;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private detachChildReference(agentId: string): void {
|
|
165
|
+
for (const node of this.#nodes.values()) node.childIds.delete(agentId);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private getExistingChildSnapshot(childId: string): AgentTreeNode[] {
|
|
169
|
+
const child = this.#nodes.get(childId);
|
|
170
|
+
return child ? [snapshotNode(child, this.#nodes)] : [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private nodesForSummary(rootAgentId?: string): MutableAgentTreeNode[] {
|
|
174
|
+
const nodes = [...this.#nodes.values()];
|
|
175
|
+
if (!rootAgentId) return nodes;
|
|
176
|
+
return nodes.filter(node => node.rootAgentId === rootAgentId || node.agentId === rootAgentId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function assertNoHighCardinalityMetricLabels(labels: Record<string, string>): void {
|
|
181
|
+
const forbiddenKey = Object.keys(labels).find(isHighCardinalityLineageKey);
|
|
182
|
+
if (forbiddenKey) throw new Error(`High-cardinality lineage value must not be used as a metric label: ${forbiddenKey}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function createMutableNode(
|
|
186
|
+
lineage: AgentLineageContext,
|
|
187
|
+
status: AgentChildStatus,
|
|
188
|
+
orphaned: boolean,
|
|
189
|
+
): MutableAgentTreeNode {
|
|
190
|
+
return {
|
|
191
|
+
agentId: lineage.agentId,
|
|
192
|
+
workflowId: lineage.workflowId,
|
|
193
|
+
rootAgentId: lineage.rootAgentId,
|
|
194
|
+
parentAgentId: lineage.parentAgentId,
|
|
195
|
+
depth: lineage.depth,
|
|
196
|
+
role: lineage.role,
|
|
197
|
+
capability: lineage.capability,
|
|
198
|
+
orphaned,
|
|
199
|
+
childIds: new Set(),
|
|
200
|
+
fanoutCount: 0,
|
|
201
|
+
status: orphaned ? "orphaned" : status,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function snapshotNode(node: MutableAgentTreeNode, nodes?: BoundedMap<string, MutableAgentTreeNode>): AgentTreeNode {
|
|
206
|
+
const childIds = retainedChildIds(node, nodes);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
agentId: node.agentId,
|
|
210
|
+
workflowId: node.workflowId,
|
|
211
|
+
rootAgentId: node.rootAgentId,
|
|
212
|
+
parentAgentId: node.parentAgentId,
|
|
213
|
+
depth: node.depth,
|
|
214
|
+
role: node.role,
|
|
215
|
+
capability: node.capability,
|
|
216
|
+
orphaned: node.orphaned,
|
|
217
|
+
childIds,
|
|
218
|
+
activeChildren: countActiveChildren(childIds, nodes),
|
|
219
|
+
fanoutCount: node.fanoutCount,
|
|
220
|
+
status: node.status,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function retainedChildIds(
|
|
225
|
+
node: MutableAgentTreeNode,
|
|
226
|
+
nodes?: BoundedMap<string, MutableAgentTreeNode>,
|
|
227
|
+
): string[] {
|
|
228
|
+
if (!nodes) return [...node.childIds];
|
|
229
|
+
return [...node.childIds].filter(childId => nodes.has(childId));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function countActiveChildren(childIds: readonly string[], nodes?: BoundedMap<string, MutableAgentTreeNode>): number {
|
|
233
|
+
if (!nodes) return childIds.length;
|
|
234
|
+
|
|
235
|
+
return childIds.filter(childId => isActiveChild(childId, nodes)).length;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function isActiveChild(childId: string, nodes: BoundedMap<string, MutableAgentTreeNode>): boolean {
|
|
239
|
+
const child = nodes.get(childId);
|
|
240
|
+
return Boolean(child && activeChildStatuses.has(child.status));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function createEmptyStatusCounts(): Record<AgentChildStatus, number> {
|
|
244
|
+
return {
|
|
245
|
+
starting: 0,
|
|
246
|
+
active: 0,
|
|
247
|
+
completed: 0,
|
|
248
|
+
failed: 0,
|
|
249
|
+
cancelled: 0,
|
|
250
|
+
orphaned: 0,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function maxMapValue(values: Map<number, number>): number {
|
|
255
|
+
return Math.max(0, ...values.values());
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const AGENT_TREE_STATUSES = agentStatusOrder;
|