@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,975 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { Counter, Histogram, Span, SpanContext, UpDownCounter } from "@opentelemetry/api";
|
|
3
|
+
import { context as otelContext, isSpanContextValid, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
4
|
+
import {
|
|
5
|
+
recordObsAgentWaitJoinHint,
|
|
6
|
+
updateObsAgentsRuntimeStateFromTree,
|
|
7
|
+
} from "../commands/obs-agents-runtime.ts";
|
|
8
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
9
|
+
import { trySha256 } from "../privacy/hash.ts";
|
|
10
|
+
import {
|
|
11
|
+
AGENT_SPAWN_ATTRIBUTES,
|
|
12
|
+
AGENT_WAIT_JOIN_ATTRIBUTES,
|
|
13
|
+
COMMON_SPAN_ATTRIBUTES,
|
|
14
|
+
LOG_ATTRIBUTES,
|
|
15
|
+
SESSION_ATTRIBUTES,
|
|
16
|
+
} from "../semconv/attributes.ts";
|
|
17
|
+
import { LOG_EVENT_NAMES } from "../semconv/metrics.ts";
|
|
18
|
+
import { SPAN_NAMES } from "../semconv/spans.ts";
|
|
19
|
+
import {
|
|
20
|
+
AGENT_WAIT_REASON_VALUES,
|
|
21
|
+
SUBAGENT_SPAWN_REASON_VALUES,
|
|
22
|
+
type AgentWaitReason,
|
|
23
|
+
type SubagentSpawnReason,
|
|
24
|
+
} from "../semconv/values.ts";
|
|
25
|
+
import { BoundedMap } from "../util/bounded-map.ts";
|
|
26
|
+
import type { AgentChildStatus, AgentTreeNode, AgentTreeSummary } from "./agent-tree-tracker.ts";
|
|
27
|
+
import { AgentTreeTracker } from "./agent-tree-tracker.ts";
|
|
28
|
+
import type { AgentLineageContext, AgentRole } from "./agent-lineage.ts";
|
|
29
|
+
import { createAgentLineageContext, createPropagationEnvironment, sanitizePropagationEnvironment } from "./agent-lineage.ts";
|
|
30
|
+
import { recordActiveSpanEnd, recordActiveSpanStart } from "./handler-internals.ts";
|
|
31
|
+
import type { TelemetryLogger, TelemetryTracer } from "./handler-types.ts";
|
|
32
|
+
import type {
|
|
33
|
+
AgentWaitJoinState,
|
|
34
|
+
ChildFailureAccountingState,
|
|
35
|
+
SubagentSpawnState,
|
|
36
|
+
TestableSpan,
|
|
37
|
+
} from "./subagent-types.ts";
|
|
38
|
+
export type {
|
|
39
|
+
AgentWaitJoinState,
|
|
40
|
+
ChildFailureAccountingState,
|
|
41
|
+
SubagentSpawnState,
|
|
42
|
+
TestableSpan,
|
|
43
|
+
} from "./subagent-types.ts";
|
|
44
|
+
|
|
45
|
+
export type SubagentSpawnType = "command" | "tool" | "extension" | "unknown";
|
|
46
|
+
export type AgentJoinStatus = "completed" | "failed" | "cancelled" | "timeout" | "unknown" | "waiting";
|
|
47
|
+
export type AttributePrimitive = boolean | number | string;
|
|
48
|
+
export type AttributeMap = Record<string, AttributePrimitive>;
|
|
49
|
+
|
|
50
|
+
export interface SubagentSpanRegistry {
|
|
51
|
+
readonly activeAgentRuns: Pick<BoundedMap<string, Span>, "get">;
|
|
52
|
+
readonly activeTurns: Pick<BoundedMap<string, Span>, "get">;
|
|
53
|
+
readonly activeSubagentSpawns: BoundedMap<string, SubagentSpawnState>;
|
|
54
|
+
readonly activeAgentWaits: BoundedMap<string, AgentWaitJoinState>;
|
|
55
|
+
readonly activeAgentJoins: BoundedMap<string, AgentWaitJoinState>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SubagentMetrics {
|
|
59
|
+
readonly subagentsSpawned: Counter;
|
|
60
|
+
readonly subagentSpawnFailures: Counter;
|
|
61
|
+
readonly orphanAgents: Counter;
|
|
62
|
+
readonly traceContextPropagationFailures: Counter;
|
|
63
|
+
readonly activeSpans: UpDownCounter;
|
|
64
|
+
readonly agentFanoutCount: Histogram;
|
|
65
|
+
readonly agentTreeDepth: Histogram;
|
|
66
|
+
readonly agentTreeWidth: Histogram;
|
|
67
|
+
readonly agentWaitDurationMs: Histogram;
|
|
68
|
+
readonly agentJoinDurationMs: Histogram;
|
|
69
|
+
readonly subagentSpawnDurationMs: Histogram;
|
|
70
|
+
readonly childAgentFailures: Counter;
|
|
71
|
+
readonly parentRecoveredFromChildFailure: Counter;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SubagentTelemetrySession {
|
|
75
|
+
readonly config: ObservMeConfig;
|
|
76
|
+
readonly lineage: AgentLineageContext;
|
|
77
|
+
readonly tracer: TelemetryTracer;
|
|
78
|
+
readonly logger: TelemetryLogger;
|
|
79
|
+
readonly metrics: SubagentMetrics;
|
|
80
|
+
readonly spans: SubagentSpanRegistry;
|
|
81
|
+
sessionSpan?: Span;
|
|
82
|
+
sessionAttributes?: AttributeMap;
|
|
83
|
+
currentAgentRunId?: string;
|
|
84
|
+
currentTurnId?: string;
|
|
85
|
+
agentTree?: AgentTreeTracker;
|
|
86
|
+
childFailureAccounting?: BoundedMap<string, ChildFailureAccountingState>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface BuildSubagentPropagationEnvironmentOptions {
|
|
90
|
+
readonly config: ObservMeConfig;
|
|
91
|
+
readonly lineage: AgentLineageContext;
|
|
92
|
+
readonly spawnId: string;
|
|
93
|
+
readonly parentSessionId?: string;
|
|
94
|
+
readonly spanContext?: SpanContext;
|
|
95
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SubagentPropagationEnvironment {
|
|
99
|
+
readonly env: NodeJS.ProcessEnv;
|
|
100
|
+
readonly traceContextPropagated: boolean;
|
|
101
|
+
readonly traceparent?: string;
|
|
102
|
+
readonly tracestate?: string;
|
|
103
|
+
readonly parentTraceId?: string;
|
|
104
|
+
readonly parentSpanId?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface StartSubagentSpawnOptions {
|
|
108
|
+
readonly spawnId?: string;
|
|
109
|
+
readonly command?: string;
|
|
110
|
+
readonly args?: readonly string[];
|
|
111
|
+
readonly childAgentId?: string;
|
|
112
|
+
readonly spawnType?: string;
|
|
113
|
+
readonly spawnReason?: SubagentSpawnReason;
|
|
114
|
+
readonly toolCallId?: string;
|
|
115
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
116
|
+
readonly now?: () => number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface StartedSubagentSpawn {
|
|
120
|
+
readonly spawnId: string;
|
|
121
|
+
readonly childAgentId: string;
|
|
122
|
+
readonly env: NodeJS.ProcessEnv;
|
|
123
|
+
readonly span: TestableSpan;
|
|
124
|
+
readonly traceContextPropagated: boolean;
|
|
125
|
+
readonly attributes: AttributeMap;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface CompleteSubagentSpawnOptions {
|
|
129
|
+
readonly childAgentId?: string;
|
|
130
|
+
readonly childStatus?: AgentChildStatus;
|
|
131
|
+
readonly outcome?: "completed" | "failed" | "cancelled";
|
|
132
|
+
readonly now?: () => number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface FailSubagentSpawnOptions {
|
|
136
|
+
readonly childAgentId?: string;
|
|
137
|
+
readonly errorClass?: string;
|
|
138
|
+
readonly now?: () => number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SubagentRunnerOptions {
|
|
142
|
+
readonly env: NodeJS.ProcessEnv;
|
|
143
|
+
readonly signal?: AbortSignal;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export type SubagentRunner<Result> = (
|
|
147
|
+
command: string,
|
|
148
|
+
args: readonly string[],
|
|
149
|
+
options: SubagentRunnerOptions,
|
|
150
|
+
) => Promise<Result>;
|
|
151
|
+
|
|
152
|
+
export interface RunSubagentOptions extends StartSubagentSpawnOptions {
|
|
153
|
+
readonly signal?: AbortSignal;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface AgentWaitJoinOptions {
|
|
157
|
+
readonly id?: string;
|
|
158
|
+
readonly spawnId?: string;
|
|
159
|
+
readonly childAgentId?: string;
|
|
160
|
+
readonly childStatus?: AgentChildStatus;
|
|
161
|
+
readonly joinStatus?: AgentJoinStatus;
|
|
162
|
+
readonly reason?: AgentWaitReason;
|
|
163
|
+
readonly failurePropagated?: boolean;
|
|
164
|
+
readonly durationMs?: number;
|
|
165
|
+
readonly now?: () => number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface StartedAgentWaitJoin {
|
|
169
|
+
readonly id: string;
|
|
170
|
+
readonly span: TestableSpan;
|
|
171
|
+
readonly attributes: AttributeMap;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface ObserveTrustedSubagentLineageOptions {
|
|
175
|
+
readonly role?: AgentRole;
|
|
176
|
+
readonly capability?: string;
|
|
177
|
+
readonly status?: AgentChildStatus;
|
|
178
|
+
readonly generateId?: () => string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const waitJoinMetricStatusValues = [
|
|
182
|
+
"starting",
|
|
183
|
+
"active",
|
|
184
|
+
"completed",
|
|
185
|
+
"failed",
|
|
186
|
+
"cancelled",
|
|
187
|
+
"orphaned",
|
|
188
|
+
"timeout",
|
|
189
|
+
"unknown",
|
|
190
|
+
"waiting",
|
|
191
|
+
] as const satisfies readonly (AgentChildStatus | AgentJoinStatus)[];
|
|
192
|
+
|
|
193
|
+
type WaitJoinMetricStatus = (typeof waitJoinMetricStatusValues)[number];
|
|
194
|
+
|
|
195
|
+
export async function runSubagentWithObservability<Result>(
|
|
196
|
+
session: SubagentTelemetrySession,
|
|
197
|
+
command: string,
|
|
198
|
+
args: readonly string[],
|
|
199
|
+
runner: SubagentRunner<Result>,
|
|
200
|
+
options: RunSubagentOptions = {},
|
|
201
|
+
): Promise<Result> {
|
|
202
|
+
const started = startSubagentSpawn(session, { ...options, command, args });
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const result = await runner(command, args, { env: started.env, signal: options.signal });
|
|
206
|
+
completeSubagentSpawn(session, started.spawnId, {
|
|
207
|
+
childAgentId: started.childAgentId,
|
|
208
|
+
childStatus: "completed",
|
|
209
|
+
now: options.now,
|
|
210
|
+
});
|
|
211
|
+
return result;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
failSubagentSpawn(session, started.spawnId, {
|
|
214
|
+
childAgentId: started.childAgentId,
|
|
215
|
+
errorClass: errorClass(error),
|
|
216
|
+
now: options.now,
|
|
217
|
+
});
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function startSubagentSpawn(
|
|
223
|
+
session: SubagentTelemetrySession,
|
|
224
|
+
options: StartSubagentSpawnOptions = {},
|
|
225
|
+
): StartedSubagentSpawn {
|
|
226
|
+
const spawnId = options.spawnId ?? `spawn-${randomUUID()}`;
|
|
227
|
+
const childAgentId = options.childAgentId ?? `child-${spawnId}`;
|
|
228
|
+
const spawnReason = normalizeSpawnReason(options.spawnReason);
|
|
229
|
+
const labels = subagentSpawnMetricLabels(session, options, spawnReason);
|
|
230
|
+
const parentSpan = resolveSubagentParentSpan(session);
|
|
231
|
+
const initialAttributes = buildSubagentSpawnAttributes(session, spawnId, childAgentId, options, spawnReason);
|
|
232
|
+
const span = startActiveSubagentSpan(session, SPAN_NAMES.PI_AGENT_SPAWN, parentSpan, initialAttributes, "subagent_spawn");
|
|
233
|
+
const propagation = buildSubagentPropagationEnvironment({
|
|
234
|
+
config: session.config,
|
|
235
|
+
lineage: session.lineage,
|
|
236
|
+
spawnId,
|
|
237
|
+
parentSessionId: resolveCurrentSessionId(session),
|
|
238
|
+
spanContext: readSpanContext(span),
|
|
239
|
+
env: options.env,
|
|
240
|
+
});
|
|
241
|
+
const treeSummary = recordAgentTreeSpawn(session, childAgentId);
|
|
242
|
+
const attributes = {
|
|
243
|
+
...initialAttributes,
|
|
244
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_TRACE_CONTEXT_PROPAGATED]: propagation.traceContextPropagated,
|
|
245
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILDREN_ACTIVE]: treeSummary.activeChildren,
|
|
246
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILD_COUNT]: treeSummary.fanoutCount,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
span.setAttributes(attributes);
|
|
250
|
+
session.spans.activeSubagentSpawns.set(spawnId, {
|
|
251
|
+
span,
|
|
252
|
+
childAgentId,
|
|
253
|
+
startedAtMs: now(options),
|
|
254
|
+
labels,
|
|
255
|
+
spawnReason,
|
|
256
|
+
traceContextPropagated: propagation.traceContextPropagated,
|
|
257
|
+
});
|
|
258
|
+
session.metrics.subagentsSpawned.add(1, labels);
|
|
259
|
+
recordAgentTreeMetrics(session, treeSummary, labels);
|
|
260
|
+
recordObsAgentsTreeState(session);
|
|
261
|
+
span.addEvent(LOG_EVENT_NAMES.AGENT_SPAWN_STARTED, attributes);
|
|
262
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.AGENT_SPAWN_STARTED, attributes);
|
|
263
|
+
recordTraceContextFallbackWhenMissing(session, span, spawnId, propagation);
|
|
264
|
+
|
|
265
|
+
return { spawnId, childAgentId, env: propagation.env, span, traceContextPropagated: propagation.traceContextPropagated, attributes };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function completeSubagentSpawn(
|
|
269
|
+
session: SubagentTelemetrySession,
|
|
270
|
+
spawnId: string,
|
|
271
|
+
options: CompleteSubagentSpawnOptions = {},
|
|
272
|
+
): void {
|
|
273
|
+
const state = session.spans.activeSubagentSpawns.get(spawnId);
|
|
274
|
+
if (!state) return;
|
|
275
|
+
|
|
276
|
+
const childAgentId = options.childAgentId ?? state.childAgentId;
|
|
277
|
+
const childStatus = options.childStatus ?? "completed";
|
|
278
|
+
const attributes = buildSubagentCompletionAttributes(
|
|
279
|
+
session,
|
|
280
|
+
spawnId,
|
|
281
|
+
childAgentId,
|
|
282
|
+
options.outcome ?? "completed",
|
|
283
|
+
state.spawnReason,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
updateChildStatus(session, childAgentId, childStatus);
|
|
287
|
+
recordChildFailureCompletion(session, childAgentId, childStatus);
|
|
288
|
+
recordObsAgentsTreeState(session);
|
|
289
|
+
recordSubagentSpawnDuration(session, state, options);
|
|
290
|
+
state.span.setAttributes(attributes);
|
|
291
|
+
state.span.setStatus({ code: SpanStatusCode.OK });
|
|
292
|
+
state.span.addEvent(LOG_EVENT_NAMES.AGENT_SPAWN_COMPLETED, attributes);
|
|
293
|
+
endSubagentSpan(session, state.span);
|
|
294
|
+
session.spans.activeSubagentSpawns.delete(spawnId);
|
|
295
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.AGENT_SPAWN_COMPLETED, attributes);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function failSubagentSpawn(
|
|
299
|
+
session: SubagentTelemetrySession,
|
|
300
|
+
spawnId: string,
|
|
301
|
+
options: FailSubagentSpawnOptions = {},
|
|
302
|
+
): void {
|
|
303
|
+
const state = session.spans.activeSubagentSpawns.get(spawnId);
|
|
304
|
+
if (!state) return;
|
|
305
|
+
|
|
306
|
+
const childAgentId = options.childAgentId ?? state.childAgentId;
|
|
307
|
+
const attributes = {
|
|
308
|
+
...buildSubagentCompletionAttributes(session, spawnId, childAgentId, "failed", state.spawnReason),
|
|
309
|
+
[LOG_ATTRIBUTES.ERROR_TYPE]: normalizeMetricLabel(options.errorClass ?? "subagent_spawn_error", "subagent_spawn_error"),
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
updateChildStatus(session, childAgentId, "failed");
|
|
313
|
+
recordObsAgentsTreeState(session);
|
|
314
|
+
recordSubagentSpawnDuration(session, state, options);
|
|
315
|
+
state.span.setAttributes(attributes);
|
|
316
|
+
state.span.setStatus({ code: SpanStatusCode.ERROR, message: String(attributes[LOG_ATTRIBUTES.ERROR_TYPE]) });
|
|
317
|
+
state.span.addEvent(LOG_EVENT_NAMES.AGENT_SPAWN_FAILED, attributes);
|
|
318
|
+
endSubagentSpan(session, state.span);
|
|
319
|
+
session.spans.activeSubagentSpawns.delete(spawnId);
|
|
320
|
+
session.metrics.subagentSpawnFailures.add(1, subagentFailureMetricLabels(state.labels, attributes));
|
|
321
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.AGENT_SPAWN_FAILED, attributes, "ERROR");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function buildSubagentPropagationEnvironment(
|
|
325
|
+
options: BuildSubagentPropagationEnvironmentOptions,
|
|
326
|
+
): SubagentPropagationEnvironment {
|
|
327
|
+
const baseEnv = options.env ?? process.env;
|
|
328
|
+
const sanitizedBaseEnv = sanitizePropagationEnvironment(options.config, baseEnv);
|
|
329
|
+
|
|
330
|
+
if (!options.config.workflow.enabled || !options.config.agent.propagateToSubagents) {
|
|
331
|
+
return { env: sanitizedBaseEnv, traceContextPropagated: false };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const parentTraceId = options.spanContext?.traceId;
|
|
335
|
+
const parentSpanId = options.spanContext?.spanId;
|
|
336
|
+
const traceparent = buildTraceparent(options.config, options.spanContext);
|
|
337
|
+
const tracestate = traceparent ? serializeTraceState(options.spanContext) : undefined;
|
|
338
|
+
const propagatedParentTraceId = traceparent ? parentTraceId : undefined;
|
|
339
|
+
const propagatedParentSpanId = traceparent ? parentSpanId : undefined;
|
|
340
|
+
const env = sanitizeTraceContextEnvironment(
|
|
341
|
+
{
|
|
342
|
+
...createPropagationEnvironment(options.lineage, options.config, sanitizedBaseEnv),
|
|
343
|
+
...definedEnvValue(options.config.agent.parentSessionIdEnv, options.parentSessionId),
|
|
344
|
+
...definedEnvValue(options.config.agent.parentTraceIdEnv, propagatedParentTraceId),
|
|
345
|
+
...definedEnvValue(options.config.agent.parentSpanIdEnv, propagatedParentSpanId),
|
|
346
|
+
...definedEnvValue(options.config.agent.spawnIdEnv, options.spawnId),
|
|
347
|
+
...definedEnvValue("traceparent", traceparent),
|
|
348
|
+
...definedEnvValue("tracestate", tracestate),
|
|
349
|
+
},
|
|
350
|
+
traceparent,
|
|
351
|
+
tracestate,
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
env,
|
|
356
|
+
traceContextPropagated: Boolean(traceparent),
|
|
357
|
+
traceparent,
|
|
358
|
+
tracestate,
|
|
359
|
+
parentTraceId: propagatedParentTraceId,
|
|
360
|
+
parentSpanId: propagatedParentSpanId,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function startAgentWait(
|
|
365
|
+
session: SubagentTelemetrySession,
|
|
366
|
+
options: AgentWaitJoinOptions = {},
|
|
367
|
+
): StartedAgentWaitJoin {
|
|
368
|
+
return startWaitJoinSpan(session, options, "wait");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function endAgentWait(
|
|
372
|
+
session: SubagentTelemetrySession,
|
|
373
|
+
waitId: string,
|
|
374
|
+
options: AgentWaitJoinOptions = {},
|
|
375
|
+
): void {
|
|
376
|
+
endWaitJoinSpan(session, waitId, options, "wait");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function recordAgentWait(session: SubagentTelemetrySession, options: AgentWaitJoinOptions = {}): StartedAgentWaitJoin {
|
|
380
|
+
const started = startAgentWait(session, options);
|
|
381
|
+
endAgentWait(session, started.id, options);
|
|
382
|
+
return started;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function startAgentJoin(
|
|
386
|
+
session: SubagentTelemetrySession,
|
|
387
|
+
options: AgentWaitJoinOptions = {},
|
|
388
|
+
): StartedAgentWaitJoin {
|
|
389
|
+
return startWaitJoinSpan(session, options, "join");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function endAgentJoin(
|
|
393
|
+
session: SubagentTelemetrySession,
|
|
394
|
+
joinId: string,
|
|
395
|
+
options: AgentWaitJoinOptions = {},
|
|
396
|
+
): void {
|
|
397
|
+
endWaitJoinSpan(session, joinId, options, "join");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function recordAgentJoin(session: SubagentTelemetrySession, options: AgentWaitJoinOptions = {}): StartedAgentWaitJoin {
|
|
401
|
+
const started = startAgentJoin(session, options);
|
|
402
|
+
endAgentJoin(session, started.id, options);
|
|
403
|
+
return started;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function observeTrustedSubagentLineage(
|
|
407
|
+
session: SubagentTelemetrySession,
|
|
408
|
+
env: NodeJS.ProcessEnv,
|
|
409
|
+
options: ObserveTrustedSubagentLineageOptions = {},
|
|
410
|
+
): AgentTreeNode | undefined {
|
|
411
|
+
try {
|
|
412
|
+
const lineage = createAgentLineageContext({
|
|
413
|
+
config: session.config,
|
|
414
|
+
env,
|
|
415
|
+
trustedParentContext: true,
|
|
416
|
+
role: options.role ?? "subagent",
|
|
417
|
+
capability: options.capability,
|
|
418
|
+
generateId: options.generateId,
|
|
419
|
+
});
|
|
420
|
+
return recordSubagentLineageObservation(session, lineage, options.status ?? "active");
|
|
421
|
+
} catch (error) {
|
|
422
|
+
recordMalformedLineage(session, error);
|
|
423
|
+
return undefined;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function recordSubagentLineageObservation(
|
|
428
|
+
session: SubagentTelemetrySession,
|
|
429
|
+
lineage: AgentLineageContext,
|
|
430
|
+
status: AgentChildStatus = "active",
|
|
431
|
+
): AgentTreeNode {
|
|
432
|
+
const tree = ensureAgentTree(session);
|
|
433
|
+
const node = tree.registerAgent(lineage, status);
|
|
434
|
+
const summary = tree.summarize(session.lineage.rootAgentId);
|
|
435
|
+
const labels = tree.metricLabels(node.status, node.orphaned);
|
|
436
|
+
|
|
437
|
+
recordAgentTreeMetrics(session, summary, labels);
|
|
438
|
+
if (node.orphaned) recordOrphanAgent(session, node);
|
|
439
|
+
recordObsAgentsTreeState(session);
|
|
440
|
+
return node;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function startWaitJoinSpan(
|
|
444
|
+
session: SubagentTelemetrySession,
|
|
445
|
+
options: AgentWaitJoinOptions,
|
|
446
|
+
kind: "wait" | "join",
|
|
447
|
+
): StartedAgentWaitJoin {
|
|
448
|
+
const id = options.id ?? `${kind}-${options.spawnId ?? randomUUID()}`;
|
|
449
|
+
const reason = normalizeWaitReason(options.reason, options, kind);
|
|
450
|
+
const attributes = buildWaitJoinAttributes(session, options, reason);
|
|
451
|
+
const spanName = kind === "wait" ? SPAN_NAMES.PI_AGENT_WAIT : SPAN_NAMES.PI_AGENT_JOIN;
|
|
452
|
+
const eventName = kind === "wait" ? LOG_EVENT_NAMES.AGENT_WAIT_STARTED : LOG_EVENT_NAMES.AGENT_JOIN_STARTED;
|
|
453
|
+
const operation = kind === "wait" ? "agent_wait" : "agent_join";
|
|
454
|
+
const span = startActiveSubagentSpan(session, spanName, resolveSubagentParentSpan(session), attributes, operation);
|
|
455
|
+
const labels = waitJoinMetricLabels(session, options, reason);
|
|
456
|
+
const state = { span, startedAtMs: now(options), labels, reason };
|
|
457
|
+
|
|
458
|
+
waitJoinRegistry(session, kind).set(id, state);
|
|
459
|
+
recordObsAgentWaitJoinHint(createWaitJoinRuntimeHint(id, options, kind, reason, true));
|
|
460
|
+
span.addEvent(eventName, attributes);
|
|
461
|
+
emitSubagentLog(session, eventName, attributes);
|
|
462
|
+
|
|
463
|
+
return { id, span, attributes };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function endWaitJoinSpan(
|
|
467
|
+
session: SubagentTelemetrySession,
|
|
468
|
+
id: string,
|
|
469
|
+
options: AgentWaitJoinOptions,
|
|
470
|
+
kind: "wait" | "join",
|
|
471
|
+
): void {
|
|
472
|
+
const registry = waitJoinRegistry(session, kind);
|
|
473
|
+
const state = registry.get(id);
|
|
474
|
+
if (!state) return;
|
|
475
|
+
|
|
476
|
+
if (kind === "join" && options.childAgentId && options.childStatus) updateChildStatus(session, options.childAgentId, options.childStatus);
|
|
477
|
+
|
|
478
|
+
const attributes = buildWaitJoinAttributes(session, options, state.reason);
|
|
479
|
+
const eventName = kind === "wait" ? LOG_EVENT_NAMES.AGENT_WAIT_COMPLETED : LOG_EVENT_NAMES.AGENT_JOIN_COMPLETED;
|
|
480
|
+
const durationMs = resolveDurationMs(state, options);
|
|
481
|
+
|
|
482
|
+
state.span.setAttributes(attributes);
|
|
483
|
+
if (waitJoinFailed(options)) state.span.setStatus({ code: SpanStatusCode.ERROR, message: options.joinStatus ?? options.childStatus });
|
|
484
|
+
state.span.addEvent(eventName, attributes);
|
|
485
|
+
endSubagentSpan(session, state.span);
|
|
486
|
+
registry.delete(id);
|
|
487
|
+
recordObsAgentWaitJoinHint(createWaitJoinRuntimeHint(id, options, kind, state.reason, false, durationMs));
|
|
488
|
+
recordObsAgentsTreeState(session);
|
|
489
|
+
recordWaitJoinDuration(session, durationMs, state.labels, kind);
|
|
490
|
+
if (kind === "join") recordChildJoinAccounting(session, options);
|
|
491
|
+
emitSubagentLog(session, eventName, attributes, waitJoinFailed(options) ? "ERROR" : "INFO");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function waitJoinRegistry(
|
|
495
|
+
session: SubagentTelemetrySession,
|
|
496
|
+
kind: "wait" | "join",
|
|
497
|
+
): BoundedMap<string, AgentWaitJoinState> {
|
|
498
|
+
return kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function recordWaitJoinDuration(
|
|
502
|
+
session: SubagentTelemetrySession,
|
|
503
|
+
durationMs: number,
|
|
504
|
+
labels: Record<string, string>,
|
|
505
|
+
kind: "wait" | "join",
|
|
506
|
+
): void {
|
|
507
|
+
if (kind === "wait") {
|
|
508
|
+
session.metrics.agentWaitDurationMs.record(durationMs, labels);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
session.metrics.agentJoinDurationMs.record(durationMs, labels);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function recordSubagentSpawnDuration(
|
|
516
|
+
session: SubagentTelemetrySession,
|
|
517
|
+
state: SubagentSpawnState,
|
|
518
|
+
options: { readonly now?: () => number },
|
|
519
|
+
): void {
|
|
520
|
+
session.metrics.subagentSpawnDurationMs.record(Math.max(0, now(options) - state.startedAtMs), state.labels);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function recordChildFailureCompletion(
|
|
524
|
+
session: SubagentTelemetrySession,
|
|
525
|
+
childAgentId: string,
|
|
526
|
+
childStatus: AgentChildStatus,
|
|
527
|
+
): void {
|
|
528
|
+
if (childStatus !== "failed") return;
|
|
529
|
+
recordChildFailureAccounting(session, childAgentId, false);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function recordChildJoinAccounting(
|
|
533
|
+
session: SubagentTelemetrySession,
|
|
534
|
+
options: AgentWaitJoinOptions,
|
|
535
|
+
): void {
|
|
536
|
+
if (!options.childAgentId || options.childStatus !== "failed") return;
|
|
537
|
+
|
|
538
|
+
const recoveryConfirmed = options.failurePropagated === false && options.joinStatus === "completed";
|
|
539
|
+
recordChildFailureAccounting(session, options.childAgentId, recoveryConfirmed);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function recordChildFailureAccounting(
|
|
543
|
+
session: SubagentTelemetrySession,
|
|
544
|
+
childAgentId: string,
|
|
545
|
+
recoveryConfirmed: boolean,
|
|
546
|
+
): void {
|
|
547
|
+
const registry = ensureChildFailureAccounting(session);
|
|
548
|
+
const current = registry.get(childAgentId);
|
|
549
|
+
const labels = childFailureMetricLabels(session);
|
|
550
|
+
const failureRecorded = current?.failureRecorded === true;
|
|
551
|
+
const recoveryRecorded = current?.recoveryRecorded === true;
|
|
552
|
+
|
|
553
|
+
if (!failureRecorded) session.metrics.childAgentFailures.add(1, labels);
|
|
554
|
+
if (recoveryConfirmed && !recoveryRecorded) session.metrics.parentRecoveredFromChildFailure.add(1, labels);
|
|
555
|
+
|
|
556
|
+
registry.set(childAgentId, {
|
|
557
|
+
failureRecorded: true,
|
|
558
|
+
recoveryRecorded: recoveryRecorded || recoveryConfirmed,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function ensureChildFailureAccounting(
|
|
563
|
+
session: SubagentTelemetrySession,
|
|
564
|
+
): BoundedMap<string, ChildFailureAccountingState> {
|
|
565
|
+
if (session.childFailureAccounting) return session.childFailureAccounting;
|
|
566
|
+
|
|
567
|
+
session.childFailureAccounting = new BoundedMap({
|
|
568
|
+
maxSize: Math.max(1, session.config.limits.maxActiveSubagentSpawns),
|
|
569
|
+
});
|
|
570
|
+
return session.childFailureAccounting;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function childFailureMetricLabels(session: SubagentTelemetrySession): Record<string, string> {
|
|
574
|
+
return {
|
|
575
|
+
agent_role: session.lineage.role,
|
|
576
|
+
subagent_depth: subagentDepthLabel(session),
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function buildWaitJoinAttributes(
|
|
581
|
+
session: SubagentTelemetrySession,
|
|
582
|
+
options: AgentWaitJoinOptions,
|
|
583
|
+
reason: AgentWaitReason,
|
|
584
|
+
): AttributeMap {
|
|
585
|
+
const summary = ensureAgentTree(session).summarize(session.lineage.rootAgentId);
|
|
586
|
+
|
|
587
|
+
return withoutUndefinedAttributes({
|
|
588
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
589
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
|
|
590
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_CHILD_ID]: options.childAgentId,
|
|
591
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_SPAWN_ID]: options.spawnId,
|
|
592
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_WAIT_REASON]: reason,
|
|
593
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_JOIN_STATUS]: options.joinStatus ?? "waiting",
|
|
594
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_CHILD_STATUS]: options.childStatus ?? "active",
|
|
595
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_FAILURE_PROPAGATED]: options.failurePropagated ?? false,
|
|
596
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_CHILDREN_ACTIVE]: summary.activeChildren,
|
|
597
|
+
[AGENT_WAIT_JOIN_ATTRIBUTES.PI_AGENT_CHILD_COUNT]: summary.fanoutCount,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function waitJoinMetricLabels(
|
|
602
|
+
session: SubagentTelemetrySession,
|
|
603
|
+
options: AgentWaitJoinOptions,
|
|
604
|
+
reason: AgentWaitReason,
|
|
605
|
+
): Record<string, string> {
|
|
606
|
+
return {
|
|
607
|
+
agent_role: session.lineage.role,
|
|
608
|
+
subagent_depth: subagentDepthLabel(session),
|
|
609
|
+
status: normalizeWaitJoinStatus(options.joinStatus ?? options.childStatus ?? "waiting"),
|
|
610
|
+
reason,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function waitJoinFailed(options: AgentWaitJoinOptions): boolean {
|
|
615
|
+
return options.failurePropagated === true || options.childStatus === "failed" || options.joinStatus === "failed";
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function resolveDurationMs(state: AgentWaitJoinState, options: AgentWaitJoinOptions): number {
|
|
619
|
+
if (options.durationMs !== undefined) return Math.max(0, options.durationMs);
|
|
620
|
+
return Math.max(0, now(options) - state.startedAtMs);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function buildSubagentSpawnAttributes(
|
|
624
|
+
session: SubagentTelemetrySession,
|
|
625
|
+
spawnId: string,
|
|
626
|
+
childAgentId: string,
|
|
627
|
+
options: StartSubagentSpawnOptions,
|
|
628
|
+
spawnReason: SubagentSpawnReason,
|
|
629
|
+
): AttributeMap {
|
|
630
|
+
return withoutUndefinedAttributes({
|
|
631
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_ID]: spawnId,
|
|
632
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_TYPE]: normalizeSpawnType(options.spawnType),
|
|
633
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_REASON]: spawnReason,
|
|
634
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_TOOL_CALL_ID]: options.toolCallId,
|
|
635
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_COMMAND_HASH]: hashCommand(options.command, options.args, session.config),
|
|
636
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILD_ID]: childAgentId,
|
|
637
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_PARENT_ID]: session.lineage.agentId,
|
|
638
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
|
|
639
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
640
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: session.lineage.workflowRootAgentId,
|
|
641
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_DEPTH]: session.lineage.depth + 1,
|
|
642
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_SESSION_ID]: resolveCurrentSessionId(session),
|
|
643
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function buildSubagentCompletionAttributes(
|
|
648
|
+
session: SubagentTelemetrySession,
|
|
649
|
+
spawnId: string,
|
|
650
|
+
childAgentId: string,
|
|
651
|
+
outcome: "completed" | "failed" | "cancelled",
|
|
652
|
+
spawnReason: SubagentSpawnReason,
|
|
653
|
+
): AttributeMap {
|
|
654
|
+
const summary = ensureAgentTree(session).summarize(session.lineage.rootAgentId);
|
|
655
|
+
|
|
656
|
+
return withoutUndefinedAttributes({
|
|
657
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_ID]: spawnId,
|
|
658
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILD_ID]: childAgentId,
|
|
659
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_REASON]: spawnReason,
|
|
660
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_OUTCOME]: outcome,
|
|
661
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILDREN_ACTIVE]: summary.activeChildren,
|
|
662
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_CHILD_COUNT]: summary.fanoutCount,
|
|
663
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
664
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_PARENT_ID]: session.lineage.agentId,
|
|
665
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function recordAgentTreeSpawn(session: SubagentTelemetrySession, childAgentId: string): AgentTreeSummary {
|
|
670
|
+
const tree = ensureAgentTree(session);
|
|
671
|
+
tree.registerAgent(createSyntheticChildLineage(session, childAgentId), "starting");
|
|
672
|
+
return tree.summarize(session.lineage.rootAgentId);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function recordAgentTreeMetrics(
|
|
676
|
+
session: SubagentTelemetrySession,
|
|
677
|
+
summary: AgentTreeSummary,
|
|
678
|
+
labels: Record<string, string>,
|
|
679
|
+
): void {
|
|
680
|
+
session.metrics.agentFanoutCount.record(summary.fanoutCount, labels);
|
|
681
|
+
session.metrics.agentTreeDepth.record(summary.treeDepth, labels);
|
|
682
|
+
session.metrics.agentTreeWidth.record(summary.treeWidth, labels);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function recordObsAgentsTreeState(session: SubagentTelemetrySession): void {
|
|
686
|
+
updateObsAgentsRuntimeStateFromTree(session.lineage, ensureAgentTree(session), { sessionId: resolveCurrentSessionId(session) });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function createWaitJoinRuntimeHint(
|
|
690
|
+
id: string,
|
|
691
|
+
options: AgentWaitJoinOptions,
|
|
692
|
+
kind: "wait" | "join",
|
|
693
|
+
reason: AgentWaitReason,
|
|
694
|
+
active: boolean,
|
|
695
|
+
durationMs?: number,
|
|
696
|
+
) {
|
|
697
|
+
return {
|
|
698
|
+
kind,
|
|
699
|
+
id,
|
|
700
|
+
active,
|
|
701
|
+
spawnId: options.spawnId,
|
|
702
|
+
childAgentId: options.childAgentId,
|
|
703
|
+
childStatus: options.childStatus,
|
|
704
|
+
joinStatus: options.joinStatus,
|
|
705
|
+
reason,
|
|
706
|
+
durationMs,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function ensureAgentTree(session: SubagentTelemetrySession): AgentTreeTracker {
|
|
711
|
+
if (session.agentTree) return session.agentTree;
|
|
712
|
+
|
|
713
|
+
session.agentTree = new AgentTreeTracker({ maxAgents: Math.max(2, session.config.limits.maxActiveSubagentSpawns + 1) });
|
|
714
|
+
session.agentTree.registerAgent(session.lineage);
|
|
715
|
+
return session.agentTree;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function createSyntheticChildLineage(session: SubagentTelemetrySession, childAgentId: string): AgentLineageContext {
|
|
719
|
+
return {
|
|
720
|
+
workflowId: session.lineage.workflowId,
|
|
721
|
+
workflowRootAgentId: session.lineage.workflowRootAgentId,
|
|
722
|
+
agentId: childAgentId,
|
|
723
|
+
parentAgentId: session.lineage.agentId,
|
|
724
|
+
rootAgentId: session.lineage.rootAgentId,
|
|
725
|
+
depth: session.lineage.depth + 1,
|
|
726
|
+
role: "subagent",
|
|
727
|
+
capability: session.lineage.capability,
|
|
728
|
+
parentSessionId: resolveCurrentSessionId(session),
|
|
729
|
+
parentTraceId: session.lineage.parentTraceId,
|
|
730
|
+
parentSpanId: session.lineage.parentSpanId,
|
|
731
|
+
orphaned: false,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function updateChildStatus(
|
|
736
|
+
session: SubagentTelemetrySession,
|
|
737
|
+
childAgentId: string,
|
|
738
|
+
status: AgentChildStatus,
|
|
739
|
+
): void {
|
|
740
|
+
ensureAgentTree(session).updateStatus(childAgentId, status);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function recordOrphanAgent(session: SubagentTelemetrySession, node: AgentTreeNode): void {
|
|
744
|
+
const labels = ensureAgentTree(session).metricLabels(node.status, node.orphaned);
|
|
745
|
+
const attributes = {
|
|
746
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: LOG_EVENT_NAMES.AGENT_ORPHANED,
|
|
747
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "agent-tree",
|
|
748
|
+
[LOG_ATTRIBUTES.PI_WORKFLOW_ID]: node.workflowId,
|
|
749
|
+
[LOG_ATTRIBUTES.PI_AGENT_ID]: node.agentId,
|
|
750
|
+
[LOG_ATTRIBUTES.PI_AGENT_PARENT_ID]: node.parentAgentId ?? "unknown",
|
|
751
|
+
[LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: node.rootAgentId,
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
session.metrics.orphanAgents.add(1, labels);
|
|
755
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.AGENT_ORPHANED, attributes, "ERROR");
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function recordMalformedLineage(session: SubagentTelemetrySession, error: unknown): void {
|
|
759
|
+
const attributes = {
|
|
760
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED,
|
|
761
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "agent-tree",
|
|
762
|
+
[LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
763
|
+
[LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
|
|
764
|
+
[LOG_ATTRIBUTES.ERROR_TYPE]: errorClass(error),
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
session.metrics.traceContextPropagationFailures.add(1, traceContextFailureMetricLabels(session));
|
|
768
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED, attributes, "ERROR");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function recordTraceContextFallbackWhenMissing(
|
|
772
|
+
session: SubagentTelemetrySession,
|
|
773
|
+
span: Span,
|
|
774
|
+
spawnId: string,
|
|
775
|
+
propagation: SubagentPropagationEnvironment,
|
|
776
|
+
): void {
|
|
777
|
+
if (propagation.traceContextPropagated) return;
|
|
778
|
+
|
|
779
|
+
const attributes = withoutUndefinedAttributes({
|
|
780
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED,
|
|
781
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "agent-tree",
|
|
782
|
+
[LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
783
|
+
[LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
|
|
784
|
+
[LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
|
|
785
|
+
[AGENT_SPAWN_ATTRIBUTES.PI_AGENT_SPAWN_ID]: spawnId,
|
|
786
|
+
[LOG_ATTRIBUTES.TRACE_ID]: propagation.parentTraceId,
|
|
787
|
+
[LOG_ATTRIBUTES.SPAN_ID]: propagation.parentSpanId,
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
session.metrics.traceContextPropagationFailures.add(1, traceContextFailureMetricLabels(session));
|
|
791
|
+
span.addEvent(LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED, attributes);
|
|
792
|
+
emitSubagentLog(session, LOG_EVENT_NAMES.TRACE_CONTEXT_PROPAGATION_FAILED, attributes, "ERROR");
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function traceContextFailureMetricLabels(session: SubagentTelemetrySession): Record<string, string> {
|
|
796
|
+
return {
|
|
797
|
+
agent_role: session.lineage.role,
|
|
798
|
+
subagent_depth: subagentDepthLabel(session),
|
|
799
|
+
reason: "trace_context_fallback",
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function subagentSpawnMetricLabels(
|
|
804
|
+
session: SubagentTelemetrySession,
|
|
805
|
+
options: StartSubagentSpawnOptions,
|
|
806
|
+
spawnReason: SubagentSpawnReason,
|
|
807
|
+
): Record<string, string> {
|
|
808
|
+
return {
|
|
809
|
+
agent_role: session.lineage.role,
|
|
810
|
+
subagent_depth: subagentDepthLabel(session),
|
|
811
|
+
spawn_type: normalizeSpawnType(options.spawnType),
|
|
812
|
+
spawn_reason: spawnReason,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function subagentFailureMetricLabels(labels: Record<string, string>, attributes: AttributeMap): Record<string, string> {
|
|
817
|
+
return {
|
|
818
|
+
spawn_type: labels.spawn_type ?? "unknown",
|
|
819
|
+
error_class: String(attributes[LOG_ATTRIBUTES.ERROR_TYPE] ?? "subagent_spawn_error"),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function subagentDepthLabel(session: SubagentTelemetrySession): string {
|
|
824
|
+
return String(Math.max(0, Math.min(session.lineage.depth + 1, session.config.workflow.maxDepthWarning)));
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function normalizeSpawnType(value: string | undefined): SubagentSpawnType {
|
|
828
|
+
if (value === "command" || value === "tool" || value === "extension") return value;
|
|
829
|
+
return "unknown";
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function normalizeSpawnReason(value: unknown): SubagentSpawnReason {
|
|
833
|
+
if (SUBAGENT_SPAWN_REASON_VALUES.includes(value as SubagentSpawnReason)) return value as SubagentSpawnReason;
|
|
834
|
+
return "unknown";
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeWaitReason(
|
|
838
|
+
value: unknown,
|
|
839
|
+
options: AgentWaitJoinOptions,
|
|
840
|
+
kind: "wait" | "join",
|
|
841
|
+
): AgentWaitReason {
|
|
842
|
+
if (AGENT_WAIT_REASON_VALUES.includes(value as AgentWaitReason)) return value as AgentWaitReason;
|
|
843
|
+
if (value === undefined && isWaitingForActiveChild(options, kind)) return "child_running";
|
|
844
|
+
return "unknown";
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function isWaitingForActiveChild(options: AgentWaitJoinOptions, kind: "wait" | "join"): boolean {
|
|
848
|
+
if (options.childStatus !== "active") return false;
|
|
849
|
+
return kind === "wait" || options.joinStatus === "waiting";
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function normalizeWaitJoinStatus(value: unknown): WaitJoinMetricStatus {
|
|
853
|
+
if (waitJoinMetricStatusValues.includes(value as WaitJoinMetricStatus)) return value as WaitJoinMetricStatus;
|
|
854
|
+
return "unknown";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function normalizeMetricLabel(value: string, fallback: string): string {
|
|
858
|
+
const normalizedValue = value.trim().toLowerCase().replaceAll(/[^a-z0-9_.:-]/gu, "_");
|
|
859
|
+
if (/^[a-z][a-z0-9_.:-]{0,63}$/u.test(normalizedValue)) return normalizedValue;
|
|
860
|
+
return fallback;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function buildTraceparent(config: ObservMeConfig, spanContext: SpanContext | undefined): string | undefined {
|
|
864
|
+
if (!config.agent.propagateTraceContext || !spanContext || !isSpanContextValid(spanContext)) return undefined;
|
|
865
|
+
const flags = (spanContext.traceFlags & 0xff).toString(16).padStart(2, "0");
|
|
866
|
+
return `00-${spanContext.traceId}-${spanContext.spanId}-${flags}`;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function serializeTraceState(spanContext: SpanContext | undefined): string | undefined {
|
|
870
|
+
const serialized = spanContext?.traceState?.serialize();
|
|
871
|
+
return serialized && serialized.length > 0 ? serialized : undefined;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function readSpanContext(span: Span): SpanContext | undefined {
|
|
875
|
+
try {
|
|
876
|
+
const spanContext = span.spanContext();
|
|
877
|
+
return isSpanContextValid(spanContext) ? spanContext : undefined;
|
|
878
|
+
} catch {
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function startActiveSubagentSpan(
|
|
884
|
+
session: SubagentTelemetrySession,
|
|
885
|
+
name: string,
|
|
886
|
+
parent: Span | undefined,
|
|
887
|
+
attributes: AttributeMap,
|
|
888
|
+
operation: string,
|
|
889
|
+
): TestableSpan {
|
|
890
|
+
const span = startChildSpan(session.tracer, name, parent, attributes);
|
|
891
|
+
recordActiveSpanStart(session.metrics, span, operation);
|
|
892
|
+
return span;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function startChildSpan(tracer: TelemetryTracer, name: string, parent: Span | undefined, attributes: AttributeMap): TestableSpan {
|
|
896
|
+
const parentContext = parent ? trace.setSpan(otelContext.active(), parent) : otelContext.active();
|
|
897
|
+
return tracer.startSpan(name, { attributes }, parentContext) as TestableSpan;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function endSubagentSpan(session: SubagentTelemetrySession, span: Span): void {
|
|
901
|
+
recordActiveSpanEnd(session.metrics, span);
|
|
902
|
+
span.end();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function resolveSubagentParentSpan(session: SubagentTelemetrySession): Span | undefined {
|
|
906
|
+
if (session.currentTurnId) return session.spans.activeTurns.get(session.currentTurnId) ?? session.sessionSpan;
|
|
907
|
+
if (session.currentAgentRunId) return session.spans.activeAgentRuns.get(session.currentAgentRunId) ?? session.sessionSpan;
|
|
908
|
+
return session.sessionSpan;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function resolveCurrentSessionId(session: SubagentTelemetrySession): string {
|
|
912
|
+
return session.sessionAttributes?.[SESSION_ATTRIBUTES.PI_SESSION_ID]?.toString() ?? `session-${session.lineage.workflowId}`;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function hashCommand(command: string | undefined, args: readonly string[] | undefined, config: ObservMeConfig): string | undefined {
|
|
916
|
+
if (command === undefined) return undefined;
|
|
917
|
+
return trySha256(`${command}\0${(args ?? []).join("\0")}`, config);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function definedEnvValue(name: string, value: string | undefined): NodeJS.ProcessEnv {
|
|
921
|
+
return value === undefined || value === "" ? {} : { [name]: value };
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function sanitizeTraceContextEnvironment(
|
|
925
|
+
env: NodeJS.ProcessEnv,
|
|
926
|
+
traceparent: string | undefined,
|
|
927
|
+
tracestate: string | undefined,
|
|
928
|
+
): NodeJS.ProcessEnv {
|
|
929
|
+
const sanitized = { ...env };
|
|
930
|
+
|
|
931
|
+
delete sanitized.traceparent;
|
|
932
|
+
delete sanitized.tracestate;
|
|
933
|
+
delete sanitized.TRACEPARENT;
|
|
934
|
+
delete sanitized.TRACESTATE;
|
|
935
|
+
|
|
936
|
+
if (!traceparent) return sanitized;
|
|
937
|
+
|
|
938
|
+
sanitized.traceparent = traceparent;
|
|
939
|
+
if (tracestate) sanitized.tracestate = tracestate;
|
|
940
|
+
return sanitized;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function withoutUndefinedAttributes(attributes: Record<string, AttributePrimitive | undefined>): AttributeMap {
|
|
944
|
+
return Object.fromEntries(Object.entries(attributes).filter(isDefinedAttributeEntry));
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function isDefinedAttributeEntry(entry: [string, AttributePrimitive | undefined]): entry is [string, AttributePrimitive] {
|
|
948
|
+
return entry[1] !== undefined;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function now(options: { readonly now?: () => number }): number {
|
|
952
|
+
return options.now?.() ?? Date.now();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function emitSubagentLog(
|
|
956
|
+
session: SubagentTelemetrySession,
|
|
957
|
+
eventName: string,
|
|
958
|
+
attributes: AttributeMap,
|
|
959
|
+
severityText: "ERROR" | "INFO" = "INFO",
|
|
960
|
+
): void {
|
|
961
|
+
session.logger.emit({
|
|
962
|
+
severityText,
|
|
963
|
+
body: eventName,
|
|
964
|
+
attributes: {
|
|
965
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: eventName,
|
|
966
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "agent-tree",
|
|
967
|
+
...attributes,
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function errorClass(error: unknown): string {
|
|
973
|
+
if (error instanceof Error) return normalizeMetricLabel(error.name, "error");
|
|
974
|
+
return normalizeMetricLabel(typeof error, "error");
|
|
975
|
+
}
|