@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,2154 @@
|
|
|
1
|
+
import type { Context, Counter, Histogram, Link, Span, SpanContext } from "@opentelemetry/api";
|
|
2
|
+
import {
|
|
3
|
+
context as otelContext,
|
|
4
|
+
createTraceState,
|
|
5
|
+
isSpanContextValid,
|
|
6
|
+
ROOT_CONTEXT,
|
|
7
|
+
SpanStatusCode,
|
|
8
|
+
TraceFlags,
|
|
9
|
+
trace,
|
|
10
|
+
} from "@opentelemetry/api";
|
|
11
|
+
import { recordObsSessionCost, recordObsSessionToolCall } from "../commands/obs-session.ts";
|
|
12
|
+
import { recordObsStatusQueueDrop } from "../commands/obs-status.ts";
|
|
13
|
+
import type { ObservMeConfig } from "../config/schema.ts";
|
|
14
|
+
import { trySha256 } from "../privacy/hash.ts";
|
|
15
|
+
import { applyContentCapturePolicy, type ContentCapturePolicyResult } from "../privacy/content-capture.ts";
|
|
16
|
+
import {
|
|
17
|
+
AGENT_RUN_ATTRIBUTES,
|
|
18
|
+
BASH_ATTRIBUTES,
|
|
19
|
+
BRANCH_ATTRIBUTES,
|
|
20
|
+
COMMON_SPAN_ATTRIBUTES,
|
|
21
|
+
COMPACTION_ATTRIBUTES,
|
|
22
|
+
LLM_ATTRIBUTES,
|
|
23
|
+
LOG_ATTRIBUTES,
|
|
24
|
+
SESSION_ATTRIBUTES,
|
|
25
|
+
TOOL_ATTRIBUTES,
|
|
26
|
+
TURN_ATTRIBUTES,
|
|
27
|
+
} from "../semconv/attributes.ts";
|
|
28
|
+
import { LOG_EVENT_NAMES } from "../semconv/metrics.ts";
|
|
29
|
+
import { SPAN_NAMES } from "../semconv/spans.ts";
|
|
30
|
+
import type { AgentLineageContext, ParentPropagationFailureReason } from "./agent-lineage.ts";
|
|
31
|
+
import type {
|
|
32
|
+
AttributeMap,
|
|
33
|
+
AttributePrimitive,
|
|
34
|
+
BranchPreparationState,
|
|
35
|
+
CapturedContent,
|
|
36
|
+
ObservMeHandlerContext,
|
|
37
|
+
ObservMeMetrics,
|
|
38
|
+
ObservMeTelemetrySession,
|
|
39
|
+
TelemetryLogger,
|
|
40
|
+
TelemetryTracer,
|
|
41
|
+
ToolCallState,
|
|
42
|
+
} from "./handler-types.ts";
|
|
43
|
+
import type { AgentWaitJoinState, SubagentSpawnState } from "./subagent-types.ts";
|
|
44
|
+
|
|
45
|
+
const OBSERVME_SEMCONV_VERSION = "0.1.0";
|
|
46
|
+
const REDACTION_FAILURE_REASON_MAX_CHARS = 240;
|
|
47
|
+
|
|
48
|
+
type LlmContentKind = "prompt" | "response" | "thinking";
|
|
49
|
+
|
|
50
|
+
export type SelfObservabilitySession = Pick<
|
|
51
|
+
ObservMeTelemetrySession,
|
|
52
|
+
"lineage" | "logger" | "metrics" | "sessionAttributes" | "currentAgentRunId" | "currentTurnId"
|
|
53
|
+
>;
|
|
54
|
+
export type TelemetryDropTarget = ObservMeMetrics | SelfObservabilitySession;
|
|
55
|
+
|
|
56
|
+
export function emitLifecycleLog(
|
|
57
|
+
logger: TelemetryLogger,
|
|
58
|
+
eventName: string,
|
|
59
|
+
attributes: AttributeMap,
|
|
60
|
+
severityText: "ERROR" | "INFO" = "INFO",
|
|
61
|
+
): void {
|
|
62
|
+
emitStructuredLog(logger, eventName, "lifecycle", attributes, severityText);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function emitStructuredLog(
|
|
66
|
+
logger: TelemetryLogger,
|
|
67
|
+
eventName: string,
|
|
68
|
+
category: string,
|
|
69
|
+
attributes: AttributeMap,
|
|
70
|
+
severityText: "ERROR" | "INFO" = "INFO",
|
|
71
|
+
): void {
|
|
72
|
+
logger.emit({
|
|
73
|
+
severityText,
|
|
74
|
+
body: eventName,
|
|
75
|
+
attributes: {
|
|
76
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: eventName,
|
|
77
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: category,
|
|
78
|
+
...attributes,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function recordTelemetryDrop(target: TelemetryDropTarget, reason: string, attributes: AttributeMap = {}): void {
|
|
84
|
+
const normalizedReason = normalizeMetricValue(reason, "telemetry_drop");
|
|
85
|
+
const session = resolveSelfObservabilitySession(target);
|
|
86
|
+
|
|
87
|
+
resolveSelfObservabilityMetrics(target).telemetryDropped.add(1, { reason: normalizedReason });
|
|
88
|
+
if (session) emitSelfObservabilityLog(session, LOG_EVENT_NAMES.TELEMETRY_DROPPED, "telemetry", buildTelemetryDropLogAttributes(normalizedReason, attributes));
|
|
89
|
+
recordObsStatusQueueDrop();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function recordRedactionFailure(
|
|
93
|
+
session: SelfObservabilitySession,
|
|
94
|
+
operation: string,
|
|
95
|
+
count = 1,
|
|
96
|
+
attributes: AttributeMap = {},
|
|
97
|
+
): void {
|
|
98
|
+
const normalizedOperation = normalizeMetricValue(operation, "redaction");
|
|
99
|
+
const errorClass = normalizeMetricValue(readString(attributes, LOG_ATTRIBUTES.ERROR_TYPE) ?? readString(attributes, "error_class") ?? "redaction_error", "redaction_error");
|
|
100
|
+
const logAttributes = { ...attributes, operation: normalizedOperation, [LOG_ATTRIBUTES.ERROR_TYPE]: errorClass };
|
|
101
|
+
|
|
102
|
+
session.metrics.redactionFailures.add(normalizeFailureCount(count), { operation: normalizedOperation, error_class: errorClass });
|
|
103
|
+
emitSelfObservabilityLog(session, LOG_EVENT_NAMES.REDACTION_FAILED, "redaction", logAttributes);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildTelemetryDropLogAttributes(reason: string, attributes: AttributeMap): AttributeMap {
|
|
107
|
+
return withoutUndefinedAttributes({
|
|
108
|
+
operation: normalizeMetricValue(readString(attributes, "operation") ?? reason, "telemetry"),
|
|
109
|
+
reason,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function emitSelfObservabilityLog(
|
|
114
|
+
session: SelfObservabilitySession,
|
|
115
|
+
eventName: string,
|
|
116
|
+
category: string,
|
|
117
|
+
attributes: AttributeMap,
|
|
118
|
+
): void {
|
|
119
|
+
emitStructuredLog(session.logger, eventName, category, buildSelfObservabilityLogAttributes(session, attributes), "ERROR");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function buildSelfObservabilityLogAttributes(session: SelfObservabilitySession, attributes: AttributeMap): AttributeMap {
|
|
123
|
+
return withoutUndefinedAttributes({
|
|
124
|
+
...buildLineageMetricSafeLogAttributes(session),
|
|
125
|
+
[LOG_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
126
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
127
|
+
operation: readString(attributes, "operation"),
|
|
128
|
+
reason: readString(attributes, "reason"),
|
|
129
|
+
[LOG_ATTRIBUTES.ERROR_TYPE]: readString(attributes, LOG_ATTRIBUTES.ERROR_TYPE),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSelfObservabilityMetrics(target: TelemetryDropTarget): ObservMeMetrics {
|
|
134
|
+
if (isSelfObservabilitySession(target)) return target.metrics;
|
|
135
|
+
return target;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolveSelfObservabilitySession(target: TelemetryDropTarget): SelfObservabilitySession | undefined {
|
|
139
|
+
if (isSelfObservabilitySession(target)) return target;
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isSelfObservabilitySession(target: TelemetryDropTarget): target is SelfObservabilitySession {
|
|
144
|
+
return "logger" in target && "metrics" in target;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeFailureCount(count: number): number {
|
|
148
|
+
if (!Number.isFinite(count) || count <= 0) return 1;
|
|
149
|
+
return Math.max(1, Math.floor(count));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function stringifyAttributes(attributes: AttributeMap): Record<string, string> {
|
|
153
|
+
return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [key, String(value)]));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function nextAgentRunId(session: ObservMeTelemetrySession, event: unknown): string {
|
|
157
|
+
const explicitRunId = readString(event, "agentRunId") ?? readString(event, "runId");
|
|
158
|
+
if (explicitRunId) return explicitRunId;
|
|
159
|
+
|
|
160
|
+
session.agentRunSequence += 1;
|
|
161
|
+
return formatAgentRunId(session.agentRunSequence);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function nextTurnIndex(session: ObservMeTelemetrySession, runId: string, event: unknown): number {
|
|
165
|
+
const explicitTurnIndex = readInteger(event, "turnIndex") ?? readInteger(event, "turn_index");
|
|
166
|
+
if (explicitTurnIndex !== undefined) {
|
|
167
|
+
session.turnSequences.set(runId, Math.max(session.turnSequences.get(runId) ?? 0, explicitTurnIndex));
|
|
168
|
+
return explicitTurnIndex;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const nextIndex = (session.turnSequences.get(runId) ?? 0) + 1;
|
|
172
|
+
session.turnSequences.set(runId, nextIndex);
|
|
173
|
+
return nextIndex;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function nextLlmRequestId(session: ObservMeTelemetrySession, event: unknown): string {
|
|
177
|
+
const explicitRequestId = readString(event, "llmRequestId") ?? readString(event, "requestId");
|
|
178
|
+
if (explicitRequestId) return explicitRequestId;
|
|
179
|
+
|
|
180
|
+
session.llmRequestSequence += 1;
|
|
181
|
+
return `llm-request-${formatSequence(session.llmRequestSequence)}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function resolveLlmParentSpan(session: ObservMeTelemetrySession): Span | undefined {
|
|
185
|
+
return resolveOperationParentSpan(session);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function resolveCurrentLlmSpan(session: ObservMeTelemetrySession, event: unknown): Span | undefined {
|
|
189
|
+
const requestId = readString(event, "llmRequestId") ?? readString(event, "requestId") ?? session.currentLlmRequestId;
|
|
190
|
+
return requestId ? session.spans.activeLlmRequests.get(requestId) : undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function deleteCurrentLlmRequest(session: ObservMeTelemetrySession, event: unknown): void {
|
|
194
|
+
const requestId = readString(event, "llmRequestId") ?? readString(event, "requestId") ?? session.currentLlmRequestId;
|
|
195
|
+
if (!requestId) return;
|
|
196
|
+
|
|
197
|
+
session.spans.activeLlmRequests.delete(requestId);
|
|
198
|
+
if (session.currentLlmRequestId === requestId) session.currentLlmRequestId = undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function nextToolCallId(session: ObservMeTelemetrySession, event: unknown): string {
|
|
202
|
+
const explicitToolCallId = readToolCallId(event);
|
|
203
|
+
if (explicitToolCallId) return explicitToolCallId;
|
|
204
|
+
|
|
205
|
+
session.toolCallSequence += 1;
|
|
206
|
+
return `tool-call-${formatSequence(session.toolCallSequence)}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function resolveCurrentToolCallId(session: ObservMeTelemetrySession, event: unknown): string | undefined {
|
|
210
|
+
const explicitToolCallId = readToolCallId(event);
|
|
211
|
+
if (explicitToolCallId) return explicitToolCallId;
|
|
212
|
+
|
|
213
|
+
// Legacy Pi tool events without ids can only fall back while one tool is active.
|
|
214
|
+
// Parallel tool events must carry explicit ids so telemetry cannot attach to a sibling span.
|
|
215
|
+
if (session.spans.activeToolCalls.size > 1) return undefined;
|
|
216
|
+
return session.currentToolCallId;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function toolEventHasAmbiguousMissingToolCallId(session: ObservMeTelemetrySession, event: unknown): boolean {
|
|
220
|
+
return readToolCallId(event) === undefined && session.spans.activeToolCalls.size > 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function recordMissingToolCallIdDrop(session: ObservMeTelemetrySession, operation: string): void {
|
|
224
|
+
recordTelemetryDrop(session, "tool_call_id_missing_ambiguous", { operation });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function resolveToolCallState(session: ObservMeTelemetrySession, event: unknown): ToolCallState | undefined {
|
|
228
|
+
const toolCallId = resolveCurrentToolCallId(session, event);
|
|
229
|
+
return toolCallId ? session.spans.activeToolCalls.get(toolCallId) : undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function deleteCurrentToolCall(session: ObservMeTelemetrySession, toolCallId: string): void {
|
|
233
|
+
session.spans.activeToolCalls.delete(toolCallId);
|
|
234
|
+
if (session.currentToolCallId === toolCallId) session.currentToolCallId = undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function startToolCallState(session: ObservMeTelemetrySession, event: unknown, toolCallId: string): ToolCallState {
|
|
238
|
+
const existingState = session.spans.activeToolCalls.get(toolCallId);
|
|
239
|
+
const attributes = buildToolStartAttributes(event, session, toolCallId);
|
|
240
|
+
|
|
241
|
+
if (existingState) {
|
|
242
|
+
existingState.span.setAttributes(attributes);
|
|
243
|
+
mergeToolStateLabels(existingState, attributes);
|
|
244
|
+
session.currentToolCallId = toolCallId;
|
|
245
|
+
return existingState;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const span = startActiveChildSpan(session, SPAN_NAMES.PI_TOOL_CALL, resolveToolParentSpan(session), attributes, "tool_call");
|
|
249
|
+
const state = {
|
|
250
|
+
span,
|
|
251
|
+
labels: toolMetricLabels(attributes),
|
|
252
|
+
completionLogAttributes: buildToolLogCorrelationAttributes(session, span, attributes),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
session.currentToolCallId = toolCallId;
|
|
256
|
+
session.spans.activeToolCalls.set(toolCallId, state);
|
|
257
|
+
session.metrics.toolCalls.add(1, state.labels);
|
|
258
|
+
recordObsSessionToolCall();
|
|
259
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.TOOL_CALL_STARTED, attributes);
|
|
260
|
+
|
|
261
|
+
return state;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function resolveToolParentSpan(session: ObservMeTelemetrySession): Span | undefined {
|
|
265
|
+
return resolveOperationParentSpan(session);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function resolveOperationParentSpan(session: ObservMeTelemetrySession): Span | undefined {
|
|
269
|
+
if (session.currentTurnId) return session.spans.activeTurns.get(session.currentTurnId) ?? session.sessionSpan;
|
|
270
|
+
if (session.currentAgentRunId) return session.spans.activeAgentRuns.get(session.currentAgentRunId) ?? session.sessionSpan;
|
|
271
|
+
return session.sessionSpan;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function deriveTurnId(agentRunId: string, turnIndex: number): string {
|
|
275
|
+
return `${agentRunId}-turn-${formatSequence(turnIndex)}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function formatAgentRunId(index: number): string {
|
|
279
|
+
return `agent-run-${formatSequence(index)}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function formatSequence(index: number): string {
|
|
283
|
+
return String(index).padStart(6, "0");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function buildAgentRunAttributes(event: unknown, session: ObservMeTelemetrySession, runId: string): AttributeMap {
|
|
287
|
+
const prompt = readString(event, "prompt") ?? readString(event, "userPrompt") ?? readString(event, "message");
|
|
288
|
+
|
|
289
|
+
return withoutUndefinedAttributes({
|
|
290
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
291
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
|
|
292
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_PARENT_ID]: session.lineage.parentAgentId,
|
|
293
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
|
|
294
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_ROLE]: session.lineage.role,
|
|
295
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_DEPTH]: session.lineage.depth,
|
|
296
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_RUN_ID]: runId,
|
|
297
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_RUN_INDEX]: session.agentRunSequence,
|
|
298
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_RUN_SOURCE]: readString(event, "source") ?? "unknown",
|
|
299
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_RUN_PROMPT_HASH]: prompt ? hashValue(prompt, session.config) : undefined,
|
|
300
|
+
[AGENT_RUN_ATTRIBUTES.PI_AGENT_RUN_PROMPT_LENGTH]: prompt?.length,
|
|
301
|
+
[AGENT_RUN_ATTRIBUTES.GEN_AI_AGENT_ID]: session.lineage.agentId,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function buildTurnAttributes(
|
|
306
|
+
event: unknown,
|
|
307
|
+
session: ObservMeTelemetrySession,
|
|
308
|
+
runId: string,
|
|
309
|
+
turnId: string,
|
|
310
|
+
turnIndex: number,
|
|
311
|
+
): AttributeMap {
|
|
312
|
+
const userMessage = readString(event, "userMessage") ?? readString(event, "prompt") ?? readString(event, "message");
|
|
313
|
+
|
|
314
|
+
return withoutUndefinedAttributes({
|
|
315
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
316
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: runId,
|
|
317
|
+
[TURN_ATTRIBUTES.PI_TURN_ID]: turnId,
|
|
318
|
+
[TURN_ATTRIBUTES.PI_TURN_INDEX]: turnIndex,
|
|
319
|
+
[TURN_ATTRIBUTES.PI_TURN_BRANCH_PATH_HASH]: readString(event, "branchPath") ? hashValue(readString(event, "branchPath")!, session.config) : undefined,
|
|
320
|
+
[TURN_ATTRIBUTES.PI_TURN_USER_MESSAGE_HASH]: userMessage ? hashValue(userMessage, session.config) : undefined,
|
|
321
|
+
[TURN_ATTRIBUTES.PI_TURN_USER_MESSAGE_LENGTH]: userMessage?.length,
|
|
322
|
+
[TURN_ATTRIBUTES.PI_TURN_USER_MESSAGE_IMAGE_COUNT]: readInteger(event, "imageCount") ?? 0,
|
|
323
|
+
[TURN_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveSessionModelProvider(event, {}, session),
|
|
324
|
+
[TURN_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveSessionModelId(event, {}, session),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function buildLlmRequestAttributes(
|
|
329
|
+
event: unknown,
|
|
330
|
+
ctx: ObservMeHandlerContext,
|
|
331
|
+
session: ObservMeTelemetrySession,
|
|
332
|
+
requestId: string,
|
|
333
|
+
): AttributeMap {
|
|
334
|
+
const payload = readUnknown(event, "payload");
|
|
335
|
+
const provider = resolveSessionModelProvider(event, ctx, session);
|
|
336
|
+
const model = resolveSessionModelId(event, ctx, session);
|
|
337
|
+
|
|
338
|
+
return withoutUndefinedAttributes({
|
|
339
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
340
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
341
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
342
|
+
[LLM_ATTRIBUTES.PI_LLM_REQUEST_ID]: requestId,
|
|
343
|
+
[LLM_ATTRIBUTES.GEN_AI_OPERATION_NAME]: readString(payload, "operation") ?? readString(payload, "operationName") ?? "chat",
|
|
344
|
+
[LLM_ATTRIBUTES.GEN_AI_PROVIDER_NAME]: provider,
|
|
345
|
+
[LLM_ATTRIBUTES.GEN_AI_REQUEST_MODEL]: model,
|
|
346
|
+
[LLM_ATTRIBUTES.GEN_AI_CONVERSATION_ID]: resolveCurrentSessionId(session),
|
|
347
|
+
[LLM_ATTRIBUTES.PI_LLM_API]: readString(payload, "api") ?? readString(ctx.model, "api") ?? provider,
|
|
348
|
+
[LLM_ATTRIBUTES.PI_LLM_REQUEST_THINKING_LEVEL]: resolveSessionThinkingLevel(event, ctx, session),
|
|
349
|
+
[LLM_ATTRIBUTES.PI_LLM_REQUEST_MESSAGE_COUNT]: countPayloadItems(payload, ["messages", "contents", "input", "prompt"]),
|
|
350
|
+
[LLM_ATTRIBUTES.PI_LLM_REQUEST_TOOL_SCHEMA_COUNT]: countPayloadItems(payload, ["tools", "toolSchemas"]),
|
|
351
|
+
[LLM_ATTRIBUTES.PI_LLM_REQUEST_INPUT_CHARS]: safeJsonLength(payload),
|
|
352
|
+
[LLM_ATTRIBUTES.GEN_AI_REQUEST_TEMPERATURE]: readNumber(payload, "temperature"),
|
|
353
|
+
[LLM_ATTRIBUTES.GEN_AI_REQUEST_MAX_TOKENS]: readNumber(payload, "max_tokens") ?? readNumber(payload, "maxTokens"),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function buildLlmResponseAttributes(event: unknown): AttributeMap {
|
|
358
|
+
return withoutUndefinedAttributes({
|
|
359
|
+
"http.response.status_code": readInteger(event, "status"),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function buildLlmFinalAttributes(message: Record<string, unknown>, session: ObservMeTelemetrySession): AttributeMap {
|
|
364
|
+
const usage = readUsage(message);
|
|
365
|
+
const cost = readCost(usage);
|
|
366
|
+
const stopReason = readString(message, "stopReason") ?? "unknown";
|
|
367
|
+
const errorMessage = readString(message, "errorMessage");
|
|
368
|
+
|
|
369
|
+
return withoutUndefinedAttributes({
|
|
370
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
371
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
372
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
373
|
+
[LLM_ATTRIBUTES.GEN_AI_OPERATION_NAME]: "chat",
|
|
374
|
+
[LLM_ATTRIBUTES.GEN_AI_PROVIDER_NAME]: readString(message, "provider") ?? "unknown",
|
|
375
|
+
[LLM_ATTRIBUTES.GEN_AI_REQUEST_MODEL]: readString(message, "model") ?? "unknown",
|
|
376
|
+
[LLM_ATTRIBUTES.GEN_AI_RESPONSE_MODEL]: readString(message, "responseModel"),
|
|
377
|
+
[LLM_ATTRIBUTES.GEN_AI_RESPONSE_ID]: readString(message, "responseId"),
|
|
378
|
+
[LLM_ATTRIBUTES.GEN_AI_RESPONSE_FINISH_REASONS]: [mapStopReason(stopReason)],
|
|
379
|
+
[LLM_ATTRIBUTES.GEN_AI_USAGE_INPUT_TOKENS]: readNumber(usage, "input"),
|
|
380
|
+
[LLM_ATTRIBUTES.GEN_AI_USAGE_OUTPUT_TOKENS]: readNumber(usage, "output"),
|
|
381
|
+
[LLM_ATTRIBUTES.GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS]: readNumber(usage, "cacheRead"),
|
|
382
|
+
[LLM_ATTRIBUTES.GEN_AI_USAGE_CACHE_CREATION_INPUT_TOKENS]: readNumber(usage, "cacheWrite"),
|
|
383
|
+
[LLM_ATTRIBUTES.GEN_AI_USAGE_REASONING_OUTPUT_TOKENS]: readNumber(usage, "reasoning"),
|
|
384
|
+
[LLM_ATTRIBUTES.GEN_AI_CONVERSATION_ID]: resolveCurrentSessionId(session),
|
|
385
|
+
[LLM_ATTRIBUTES.ERROR_TYPE]: isLlmError(message) ? "provider_error" : undefined,
|
|
386
|
+
[LLM_ATTRIBUTES.PI_LLM_API]: readString(message, "api"),
|
|
387
|
+
[LLM_ATTRIBUTES.PI_LLM_STOP_REASON]: stopReason,
|
|
388
|
+
[LLM_ATTRIBUTES.PI_LLM_ERROR_MESSAGE_HASH]: errorMessage ? hashValue(errorMessage, session.config) : undefined,
|
|
389
|
+
[LLM_ATTRIBUTES.PI_LLM_USAGE_TOTAL_TOKENS]: readNumber(usage, "totalTokens"),
|
|
390
|
+
[LLM_ATTRIBUTES.PI_LLM_USAGE_CACHE_WRITE_1H_TOKENS]: readNumber(usage, "cacheWrite1h"),
|
|
391
|
+
[LLM_ATTRIBUTES.PI_LLM_COST_INPUT_USD]: readNumber(cost, "input"),
|
|
392
|
+
[LLM_ATTRIBUTES.PI_LLM_COST_OUTPUT_USD]: readNumber(cost, "output"),
|
|
393
|
+
[LLM_ATTRIBUTES.PI_LLM_COST_CACHE_READ_USD]: readNumber(cost, "cacheRead"),
|
|
394
|
+
[LLM_ATTRIBUTES.PI_LLM_COST_CACHE_WRITE_USD]: readNumber(cost, "cacheWrite"),
|
|
395
|
+
[LLM_ATTRIBUTES.PI_LLM_COST_TOTAL_USD]: readNumber(cost, "total"),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function buildToolStartAttributes(event: unknown, session: ObservMeTelemetrySession, toolCallId: string): AttributeMap {
|
|
400
|
+
return withoutUndefinedAttributes({
|
|
401
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
402
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
403
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
404
|
+
...buildRequiredToolIdentityAttributes(event, toolCallId),
|
|
405
|
+
...buildToolArgumentsAttributes(event, session.config),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function buildToolCallInputAttributes(event: unknown, config: ObservMeConfig): AttributeMap {
|
|
410
|
+
return withoutUndefinedAttributes({
|
|
411
|
+
...buildOptionalToolIdentityAttributes(event),
|
|
412
|
+
...buildToolArgumentsAttributes(event, config),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function buildToolResultAttributes(event: unknown, config: ObservMeConfig): AttributeMap {
|
|
417
|
+
return withoutUndefinedAttributes({
|
|
418
|
+
...buildOptionalToolIdentityAttributes(event),
|
|
419
|
+
...buildToolResultPayloadAttributes(event, config),
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function buildToolFinalAttributes(event: unknown): AttributeMap {
|
|
424
|
+
const failed = toolExecutionFailed(event);
|
|
425
|
+
|
|
426
|
+
return withoutUndefinedAttributes({
|
|
427
|
+
[TOOL_ATTRIBUTES.PI_TOOL_SUCCESS]: !failed,
|
|
428
|
+
[TOOL_ATTRIBUTES.PI_TOOL_ERROR]: failed,
|
|
429
|
+
[TOOL_ATTRIBUTES.PI_TOOL_ERROR_CLASS]: failed ? toolErrorClass(event) : undefined,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function buildRequiredToolIdentityAttributes(event: unknown, toolCallId: string): AttributeMap {
|
|
434
|
+
const rawToolName = readToolName(event);
|
|
435
|
+
const toolName = safeToolName(rawToolName);
|
|
436
|
+
const category = resolveToolCategory(event, toolName);
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
[TOOL_ATTRIBUTES.PI_TOOL_CALL_ID]: toolCallId,
|
|
440
|
+
[TOOL_ATTRIBUTES.PI_TOOL_NAME]: toolName,
|
|
441
|
+
[TOOL_ATTRIBUTES.PI_TOOL_CATEGORY]: category,
|
|
442
|
+
[TOOL_ATTRIBUTES.GEN_AI_TOOL_CALL_ID]: toolCallId,
|
|
443
|
+
[TOOL_ATTRIBUTES.GEN_AI_TOOL_NAME]: toolName,
|
|
444
|
+
[TOOL_ATTRIBUTES.GEN_AI_TOOL_TYPE]: mapToolType(category),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function buildOptionalToolIdentityAttributes(event: unknown): AttributeMap {
|
|
449
|
+
const rawToolName = readToolName(event);
|
|
450
|
+
const explicitCategory = readToolCategory(event);
|
|
451
|
+
if (!rawToolName && !explicitCategory) return {};
|
|
452
|
+
|
|
453
|
+
const toolName = rawToolName ? safeToolName(rawToolName) : undefined;
|
|
454
|
+
const category = resolveToolCategory(event, toolName ?? "unknown");
|
|
455
|
+
|
|
456
|
+
return withoutUndefinedAttributes({
|
|
457
|
+
[TOOL_ATTRIBUTES.PI_TOOL_NAME]: toolName,
|
|
458
|
+
[TOOL_ATTRIBUTES.PI_TOOL_CATEGORY]: category,
|
|
459
|
+
[TOOL_ATTRIBUTES.GEN_AI_TOOL_NAME]: toolName,
|
|
460
|
+
[TOOL_ATTRIBUTES.GEN_AI_TOOL_TYPE]: mapToolType(category),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function buildToolLogCorrelationAttributes(
|
|
465
|
+
session: ObservMeTelemetrySession,
|
|
466
|
+
span: Span,
|
|
467
|
+
attributes: AttributeMap,
|
|
468
|
+
): AttributeMap {
|
|
469
|
+
return withoutUndefinedAttributes({
|
|
470
|
+
...buildLineageMetricSafeLogAttributes(session),
|
|
471
|
+
[LOG_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
472
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
473
|
+
...buildToolLogIdentityAttributes(attributes),
|
|
474
|
+
[LOG_ATTRIBUTES.TRACE_ID]: readSpanTraceId(span),
|
|
475
|
+
[LOG_ATTRIBUTES.SPAN_ID]: readSpanId(span),
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function buildToolCompletionLogAttributes(
|
|
480
|
+
state: ToolCallState,
|
|
481
|
+
finalAttributes: AttributeMap,
|
|
482
|
+
): AttributeMap {
|
|
483
|
+
const errorClass = readString(finalAttributes, TOOL_ATTRIBUTES.PI_TOOL_ERROR_CLASS);
|
|
484
|
+
const normalizedErrorClass = errorClass ? normalizeErrorClass(errorClass) : undefined;
|
|
485
|
+
|
|
486
|
+
return withoutUndefinedAttributes({
|
|
487
|
+
...state.completionLogAttributes,
|
|
488
|
+
[TOOL_ATTRIBUTES.PI_TOOL_SUCCESS]: readBoolean(finalAttributes, TOOL_ATTRIBUTES.PI_TOOL_SUCCESS),
|
|
489
|
+
[TOOL_ATTRIBUTES.PI_TOOL_ERROR]: readBoolean(finalAttributes, TOOL_ATTRIBUTES.PI_TOOL_ERROR),
|
|
490
|
+
[TOOL_ATTRIBUTES.PI_TOOL_ERROR_CLASS]: normalizedErrorClass,
|
|
491
|
+
[LOG_ATTRIBUTES.ERROR_TYPE]: normalizedErrorClass,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function buildToolLogIdentityAttributes(attributes: AttributeMap): AttributeMap {
|
|
496
|
+
return withoutUndefinedAttributes({
|
|
497
|
+
[TOOL_ATTRIBUTES.PI_TOOL_CALL_ID]: readString(attributes, TOOL_ATTRIBUTES.PI_TOOL_CALL_ID),
|
|
498
|
+
[TOOL_ATTRIBUTES.PI_TOOL_NAME]: readString(attributes, TOOL_ATTRIBUTES.PI_TOOL_NAME),
|
|
499
|
+
[TOOL_ATTRIBUTES.PI_TOOL_CATEGORY]: readString(attributes, TOOL_ATTRIBUTES.PI_TOOL_CATEGORY),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function buildToolArgumentsAttributes(event: unknown, config: ObservMeConfig): AttributeMap {
|
|
504
|
+
const value = readToolArgumentsText(event);
|
|
505
|
+
|
|
506
|
+
return buildToolPayloadAttributes(value, TOOL_ATTRIBUTES.PI_TOOL_ARGUMENTS_HASH, TOOL_ATTRIBUTES.PI_TOOL_ARGUMENTS_SIZE, config);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function buildToolResultPayloadAttributes(event: unknown, config: ObservMeConfig): AttributeMap {
|
|
510
|
+
const value = readToolResultText(event);
|
|
511
|
+
|
|
512
|
+
return buildToolPayloadAttributes(value, TOOL_ATTRIBUTES.PI_TOOL_RESULT_HASH, TOOL_ATTRIBUTES.PI_TOOL_RESULT_SIZE, config);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function buildToolPayloadAttributes(value: string | undefined, hashKey: string, sizeKey: string, config: ObservMeConfig): AttributeMap {
|
|
516
|
+
return withoutUndefinedAttributes({
|
|
517
|
+
[hashKey]: value === undefined ? undefined : hashValue(value, config),
|
|
518
|
+
[sizeKey]: value?.length,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export function recordLlmUsageMetrics(
|
|
523
|
+
session: ObservMeTelemetrySession,
|
|
524
|
+
message: Record<string, unknown>,
|
|
525
|
+
labels: Record<string, string>,
|
|
526
|
+
): void {
|
|
527
|
+
const usage = readUsage(message);
|
|
528
|
+
const cost = readCost(usage);
|
|
529
|
+
|
|
530
|
+
addCounterIfPresent(session.metrics.llmInputTokens, readNumber(usage, "input"), labels);
|
|
531
|
+
addCounterIfPresent(session.metrics.llmOutputTokens, readNumber(usage, "output"), labels);
|
|
532
|
+
addCounterIfPresent(session.metrics.llmCacheReadTokens, readNumber(usage, "cacheRead"), labels);
|
|
533
|
+
addCounterIfPresent(session.metrics.llmCacheWriteTokens, readNumber(usage, "cacheWrite"), labels);
|
|
534
|
+
addCounterIfPresent(session.metrics.llmCacheWrite1hTokens, readNumber(usage, "cacheWrite1h"), labels);
|
|
535
|
+
const totalCostUsd = readNumber(cost, "total");
|
|
536
|
+
|
|
537
|
+
addCounterIfPresent(session.metrics.llmReasoningTokens, readNumber(usage, "reasoning"), labels);
|
|
538
|
+
addCounterIfPresent(session.metrics.llmTotalTokens, readNumber(usage, "totalTokens"), labels);
|
|
539
|
+
addCounterIfPresent(session.metrics.llmCostUsd, totalCostUsd, labels);
|
|
540
|
+
recordObsSessionCost(totalCostUsd);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function recordLlmSizeMetrics(
|
|
544
|
+
session: ObservMeTelemetrySession,
|
|
545
|
+
message: Record<string, unknown>,
|
|
546
|
+
labels: Record<string, string>,
|
|
547
|
+
): void {
|
|
548
|
+
const responseText = extractAssistantText(message);
|
|
549
|
+
if (responseText) session.metrics.responseSizeChars.record(responseText.length, labels);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function recordPromptSizeMetric(session: ObservMeTelemetrySession, event: unknown, labels: Record<string, string>): void {
|
|
553
|
+
const payload = readUnknown(event, "payload");
|
|
554
|
+
const promptText = extractPayloadPromptText(payload);
|
|
555
|
+
if (promptText) session.metrics.promptSizeChars.record(promptText.length, labels);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function recordOptionalPromptContent(session: ObservMeTelemetrySession, span: Span, event: unknown): void {
|
|
559
|
+
if (!session.config.capture.prompts) return;
|
|
560
|
+
|
|
561
|
+
const payload = readUnknown(event, "payload");
|
|
562
|
+
const promptText = extractPayloadPromptText(payload);
|
|
563
|
+
if (!promptText) return;
|
|
564
|
+
|
|
565
|
+
const content = recordRedactedSpanContent(session, span, LLM_ATTRIBUTES.PI_LLM_PROMPT_REDACTED, promptText, "prompt");
|
|
566
|
+
if (content) emitCapturedContentLog(session, span, LOG_EVENT_NAMES.LLM_PROMPT_CAPTURED, "prompt", content);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
export function recordOptionalLlmContent(session: ObservMeTelemetrySession, span: Span | undefined, message: Record<string, unknown>): void {
|
|
570
|
+
if (!span) return;
|
|
571
|
+
|
|
572
|
+
if (session.config.capture.responses) {
|
|
573
|
+
const responseText = extractAssistantText(message);
|
|
574
|
+
if (responseText) {
|
|
575
|
+
const content = recordRedactedSpanContent(session, span, LLM_ATTRIBUTES.PI_LLM_RESPONSE_REDACTED, responseText, "response");
|
|
576
|
+
if (content) emitCapturedContentLog(session, span, LOG_EVENT_NAMES.LLM_RESPONSE_CAPTURED, "response", content);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (session.config.capture.thinking) {
|
|
581
|
+
const thinkingText = extractAssistantThinking(message);
|
|
582
|
+
if (thinkingText) {
|
|
583
|
+
const content = recordRedactedSpanContent(session, span, LLM_ATTRIBUTES.PI_LLM_THINKING_REDACTED, thinkingText, "thinking");
|
|
584
|
+
if (content) emitCapturedContentLog(session, span, LOG_EVENT_NAMES.LLM_THINKING_CAPTURED, "thinking", content);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function recordOptionalToolArguments(session: ObservMeTelemetrySession, span: Span, event: unknown): void {
|
|
590
|
+
if (!session.config.capture.toolArguments) return;
|
|
591
|
+
|
|
592
|
+
const value = readToolArgumentsText(event);
|
|
593
|
+
if (value === undefined) return;
|
|
594
|
+
|
|
595
|
+
recordRedactedToolContent(
|
|
596
|
+
session,
|
|
597
|
+
span,
|
|
598
|
+
TOOL_ATTRIBUTES.PI_TOOL_ARGUMENTS_REDACTED,
|
|
599
|
+
TOOL_ATTRIBUTES.GEN_AI_TOOL_CALL_ARGUMENTS,
|
|
600
|
+
value,
|
|
601
|
+
"toolArgument",
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function recordOptionalToolResult(
|
|
606
|
+
session: ObservMeTelemetrySession,
|
|
607
|
+
span: Span,
|
|
608
|
+
event: unknown,
|
|
609
|
+
): CapturedContent | undefined {
|
|
610
|
+
if (!session.config.capture.toolResults) return undefined;
|
|
611
|
+
|
|
612
|
+
const value = readToolResultText(event);
|
|
613
|
+
if (value === undefined) return undefined;
|
|
614
|
+
|
|
615
|
+
return recordRedactedToolContent(
|
|
616
|
+
session,
|
|
617
|
+
span,
|
|
618
|
+
TOOL_ATTRIBUTES.PI_TOOL_RESULT_REDACTED,
|
|
619
|
+
TOOL_ATTRIBUTES.GEN_AI_TOOL_CALL_RESULT,
|
|
620
|
+
value,
|
|
621
|
+
"toolResult",
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function recordOptionalBashContent(session: ObservMeTelemetrySession, span: Span, event: unknown): void {
|
|
626
|
+
if (session.config.capture.bashCommands) {
|
|
627
|
+
const command = readBashCommand(event);
|
|
628
|
+
if (command !== undefined) recordRedactedBashContent(session, span, BASH_ATTRIBUTES.PI_BASH_COMMAND_REDACTED, command, "command");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (session.config.capture.bashOutput) {
|
|
632
|
+
const output = readBashOutput(event);
|
|
633
|
+
if (output !== undefined) recordRedactedBashContent(session, span, BASH_ATTRIBUTES.PI_BASH_OUTPUT_REDACTED, output, "output");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
export function recordRedactedBashContent(
|
|
638
|
+
session: ObservMeTelemetrySession,
|
|
639
|
+
span: Span,
|
|
640
|
+
attributeKey: string,
|
|
641
|
+
value: string,
|
|
642
|
+
kind: "command" | "output",
|
|
643
|
+
): void {
|
|
644
|
+
const result = applyContentCapturePolicy({
|
|
645
|
+
captureEnabled: true,
|
|
646
|
+
value,
|
|
647
|
+
kind: kind === "output" ? "bashOutput" : "logBody",
|
|
648
|
+
config: session.config,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!result.captured || result.value === undefined) {
|
|
652
|
+
recordRedactionFailure(session, "bash_content_capture", result.redactionFailures, buildContentCaptureFailureAttributes(result));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
span.setAttribute(attributeKey, result.value);
|
|
657
|
+
if (result.truncated) span.setAttributes(buildBashCaptureTruncationAttributes(kind, result.originalLength ?? value.length));
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function buildBashCaptureTruncationAttributes(kind: "command" | "output", originalLength: number): AttributeMap {
|
|
661
|
+
return withoutUndefinedAttributes({
|
|
662
|
+
[BASH_ATTRIBUTES.PI_BASH_TRUNCATED]: kind === "output" ? true : undefined,
|
|
663
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_TRUNCATED]: true,
|
|
664
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_ORIGINAL_LENGTH]: originalLength,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export function recordRedactedToolContent(
|
|
669
|
+
session: ObservMeTelemetrySession,
|
|
670
|
+
span: Span,
|
|
671
|
+
attributeKey: string,
|
|
672
|
+
aliasAttributeKey: string,
|
|
673
|
+
value: string,
|
|
674
|
+
kind: "toolArgument" | "toolResult",
|
|
675
|
+
): CapturedContent | undefined {
|
|
676
|
+
const result = applyContentCapturePolicy({
|
|
677
|
+
captureEnabled: true,
|
|
678
|
+
value,
|
|
679
|
+
kind,
|
|
680
|
+
config: session.config,
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
if (!result.captured || result.value === undefined) {
|
|
684
|
+
recordRedactionFailure(session, "tool_content_capture", result.redactionFailures, buildContentCaptureFailureAttributes(result));
|
|
685
|
+
return undefined;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
span.setAttribute(attributeKey, result.value);
|
|
689
|
+
span.setAttribute(aliasAttributeKey, result.value);
|
|
690
|
+
if (result.truncated) span.setAttributes(result.attributes);
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
value: result.value,
|
|
694
|
+
truncated: result.truncated,
|
|
695
|
+
originalLength: result.originalLength,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export function recordRedactedSpanContent(
|
|
700
|
+
session: ObservMeTelemetrySession,
|
|
701
|
+
span: Span,
|
|
702
|
+
attributeKey: string,
|
|
703
|
+
value: string,
|
|
704
|
+
kind: LlmContentKind,
|
|
705
|
+
): CapturedContent | undefined {
|
|
706
|
+
const result = applyContentCapturePolicy({
|
|
707
|
+
captureEnabled: true,
|
|
708
|
+
value,
|
|
709
|
+
kind: kind === "prompt" ? "prompt" : "response",
|
|
710
|
+
config: session.config,
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
if (!result.captured || result.value === undefined) {
|
|
714
|
+
recordRedactionFailure(session, "llm_content_capture", result.redactionFailures, buildContentCaptureFailureAttributes(result));
|
|
715
|
+
return undefined;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
span.setAttribute(attributeKey, result.value);
|
|
719
|
+
if (result.truncated) span.setAttributes(result.attributes);
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
value: result.value,
|
|
723
|
+
truncated: result.truncated,
|
|
724
|
+
originalLength: result.originalLength,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function buildContentCaptureFailureAttributes(result: ContentCapturePolicyResult): AttributeMap {
|
|
729
|
+
return withoutUndefinedAttributes({ reason: summarizeContentCaptureErrors(result.errors) });
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function summarizeContentCaptureErrors(errors: readonly string[]): string | undefined {
|
|
733
|
+
const firstError = errors.find(error => error.trim().length > 0);
|
|
734
|
+
if (!firstError) return undefined;
|
|
735
|
+
if (firstError.length <= REDACTION_FAILURE_REASON_MAX_CHARS) return firstError;
|
|
736
|
+
return `${firstError.slice(0, REDACTION_FAILURE_REASON_MAX_CHARS - 1)}…`;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function emitCapturedContentLog(
|
|
740
|
+
session: ObservMeTelemetrySession,
|
|
741
|
+
span: Span,
|
|
742
|
+
eventName: string,
|
|
743
|
+
kind: LlmContentKind,
|
|
744
|
+
content: CapturedContent,
|
|
745
|
+
): void {
|
|
746
|
+
session.logger.emit({
|
|
747
|
+
severityText: "INFO",
|
|
748
|
+
body: content.value,
|
|
749
|
+
attributes: buildCapturedContentLogAttributes(session, span, eventName, kind, content),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export function buildCapturedContentLogAttributes(
|
|
754
|
+
session: ObservMeTelemetrySession,
|
|
755
|
+
span: Span,
|
|
756
|
+
eventName: string,
|
|
757
|
+
kind: LlmContentKind,
|
|
758
|
+
content: CapturedContent,
|
|
759
|
+
): AttributeMap {
|
|
760
|
+
return withoutUndefinedAttributes({
|
|
761
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: eventName,
|
|
762
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "llm_content",
|
|
763
|
+
...buildLineageMetricSafeLogAttributes(session),
|
|
764
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
765
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
766
|
+
[LLM_ATTRIBUTES.GEN_AI_PROVIDER_NAME]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT),
|
|
767
|
+
[LLM_ATTRIBUTES.GEN_AI_REQUEST_MODEL]: readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT),
|
|
768
|
+
[LLM_ATTRIBUTES.PI_LLM_CONTENT_KIND]: kind,
|
|
769
|
+
[LOG_ATTRIBUTES.TRACE_ID]: readSpanTraceId(span),
|
|
770
|
+
[LOG_ATTRIBUTES.SPAN_ID]: readSpanId(span),
|
|
771
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_TRUNCATED]: content.truncated ? true : undefined,
|
|
772
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_ORIGINAL_LENGTH]: content.truncated ? content.originalLength : undefined,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
export function emitCapturedToolErrorLog(
|
|
777
|
+
session: ObservMeTelemetrySession,
|
|
778
|
+
completionLogAttributes: AttributeMap,
|
|
779
|
+
content: CapturedContent,
|
|
780
|
+
): void {
|
|
781
|
+
session.logger.emit({
|
|
782
|
+
severityText: "ERROR",
|
|
783
|
+
body: content.value,
|
|
784
|
+
attributes: withoutUndefinedAttributes({
|
|
785
|
+
...completionLogAttributes,
|
|
786
|
+
[LOG_ATTRIBUTES.EVENT_NAME]: LOG_EVENT_NAMES.TOOL_ERROR_CAPTURED,
|
|
787
|
+
[LOG_ATTRIBUTES.EVENT_CATEGORY]: "tool_content",
|
|
788
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_TRUNCATED]: content.truncated ? true : undefined,
|
|
789
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_ORIGINAL_LENGTH]: content.truncated ? content.originalLength : undefined,
|
|
790
|
+
}),
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
export function addCounterIfPresent(counter: Counter, value: number | undefined, labels: Record<string, string>): void {
|
|
795
|
+
if (value === undefined || value <= 0) return;
|
|
796
|
+
counter.add(value, labels);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function readSpanTraceId(span: Span | undefined): string | undefined {
|
|
800
|
+
const traceId = span?.spanContext?.().traceId;
|
|
801
|
+
return typeof traceId === "string" ? traceId : undefined;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function readSpanId(span: Span | undefined): string | undefined {
|
|
805
|
+
const spanId = span?.spanContext?.().spanId;
|
|
806
|
+
return typeof spanId === "string" ? spanId : undefined;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const spanStartTimesMs = new WeakMap<Span, number>();
|
|
810
|
+
const activeSpanOperations = new WeakMap<Span, string>();
|
|
811
|
+
|
|
812
|
+
export type SessionTraceContextFailureReason = ParentPropagationFailureReason | "trace_context_unavailable";
|
|
813
|
+
|
|
814
|
+
export interface SessionTraceParentResolution {
|
|
815
|
+
readonly parentContext?: Context;
|
|
816
|
+
readonly links?: Link[];
|
|
817
|
+
readonly continued: boolean;
|
|
818
|
+
readonly failureReason?: SessionTraceContextFailureReason;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export interface ActiveRootSpanOptions {
|
|
822
|
+
readonly parentContext?: Context;
|
|
823
|
+
readonly links?: Link[];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
export function resolveSessionTraceParent(lineage: AgentLineageContext): SessionTraceParentResolution {
|
|
827
|
+
const propagated = lineage.propagatedTraceContext;
|
|
828
|
+
if (propagated) {
|
|
829
|
+
const spanContext = createRemoteSpanContext(
|
|
830
|
+
propagated.traceId,
|
|
831
|
+
propagated.spanId,
|
|
832
|
+
propagated.traceFlags,
|
|
833
|
+
propagated.tracestate,
|
|
834
|
+
);
|
|
835
|
+
if (spanContext) {
|
|
836
|
+
return {
|
|
837
|
+
parentContext: trace.setSpanContext(ROOT_CONTEXT, spanContext),
|
|
838
|
+
continued: true,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const fallbackSpanContext = createRemoteSpanContext(
|
|
844
|
+
lineage.parentTraceId,
|
|
845
|
+
lineage.parentSpanId,
|
|
846
|
+
TraceFlags.NONE,
|
|
847
|
+
);
|
|
848
|
+
if (fallbackSpanContext) {
|
|
849
|
+
return {
|
|
850
|
+
links: [{ context: fallbackSpanContext }],
|
|
851
|
+
continued: false,
|
|
852
|
+
failureReason: lineage.propagationFailure ?? "trace_context_unavailable",
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (lineage.propagationFailure || lineage.parentAgentId) {
|
|
857
|
+
return {
|
|
858
|
+
continued: false,
|
|
859
|
+
failureReason: lineage.propagationFailure ?? "trace_context_unavailable",
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return { continued: false };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function createRemoteSpanContext(
|
|
867
|
+
traceId: string | undefined,
|
|
868
|
+
spanId: string | undefined,
|
|
869
|
+
traceFlags: number,
|
|
870
|
+
tracestate?: string,
|
|
871
|
+
): SpanContext | undefined {
|
|
872
|
+
if (!traceId || !spanId) return undefined;
|
|
873
|
+
|
|
874
|
+
const spanContext: SpanContext = {
|
|
875
|
+
traceId,
|
|
876
|
+
spanId,
|
|
877
|
+
traceFlags: traceFlags & TraceFlags.SAMPLED,
|
|
878
|
+
isRemote: true,
|
|
879
|
+
...(tracestate ? { traceState: createTraceState(tracestate) } : {}),
|
|
880
|
+
};
|
|
881
|
+
return isSpanContextValid(spanContext) ? spanContext : undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
export function startActiveRootSpan(
|
|
885
|
+
session: ObservMeTelemetrySession,
|
|
886
|
+
name: string,
|
|
887
|
+
attributes: AttributeMap,
|
|
888
|
+
operation: string,
|
|
889
|
+
options: ActiveRootSpanOptions = {},
|
|
890
|
+
): Span {
|
|
891
|
+
const span = session.tracer.startSpan(
|
|
892
|
+
name,
|
|
893
|
+
{ attributes, ...(options.links && options.links.length > 0 ? { links: options.links } : {}) },
|
|
894
|
+
options.parentContext ?? ROOT_CONTEXT,
|
|
895
|
+
);
|
|
896
|
+
spanStartTimesMs.set(span, Date.now());
|
|
897
|
+
recordActiveSpanStart(session.metrics, span, operation);
|
|
898
|
+
return span;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function startActiveChildSpan(
|
|
902
|
+
session: ObservMeTelemetrySession,
|
|
903
|
+
name: string,
|
|
904
|
+
parent: Span | undefined,
|
|
905
|
+
attributes: AttributeMap,
|
|
906
|
+
operation: string,
|
|
907
|
+
): Span {
|
|
908
|
+
const span = startChildSpan(session.tracer, name, parent, attributes);
|
|
909
|
+
recordActiveSpanStart(session.metrics, span, operation);
|
|
910
|
+
return span;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
export function recordActiveSpanStart(metrics: Pick<ObservMeMetrics, "activeSpans">, span: Span, operation: string): void {
|
|
914
|
+
if (activeSpanOperations.has(span)) return;
|
|
915
|
+
|
|
916
|
+
const normalizedOperation = normalizeMetricValue(operation, "span");
|
|
917
|
+
activeSpanOperations.set(span, normalizedOperation);
|
|
918
|
+
metrics.activeSpans.add(1, { operation: normalizedOperation });
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
export function endActiveSpan(session: ObservMeTelemetrySession, span: Span | undefined): void {
|
|
922
|
+
if (!span) return;
|
|
923
|
+
|
|
924
|
+
recordActiveSpanEnd(session.metrics, span);
|
|
925
|
+
span.end();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
export function recordActiveSpanEnd(metrics: Pick<ObservMeMetrics, "activeSpans">, span: Span | undefined): void {
|
|
929
|
+
if (!span) return;
|
|
930
|
+
|
|
931
|
+
const operation = activeSpanOperations.get(span);
|
|
932
|
+
if (!operation) return;
|
|
933
|
+
|
|
934
|
+
metrics.activeSpans.add(-1, { operation });
|
|
935
|
+
activeSpanOperations.delete(span);
|
|
936
|
+
spanStartTimesMs.delete(span);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function startChildSpan(tracer: TelemetryTracer, name: string, parent: Span | undefined, attributes: AttributeMap): Span {
|
|
940
|
+
const parentContext = parent ? trace.setSpan(otelContext.active(), parent) : otelContext.active();
|
|
941
|
+
const span = tracer.startSpan(name, { attributes }, parentContext);
|
|
942
|
+
spanStartTimesMs.set(span, Date.now());
|
|
943
|
+
return span;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
export function recordSpanDurationMs(span: Span | undefined, histogram: Histogram, labels: Record<string, string>): void {
|
|
947
|
+
if (!span) return;
|
|
948
|
+
|
|
949
|
+
const startTimeMs = spanStartTimesMs.get(span);
|
|
950
|
+
if (startTimeMs === undefined) return;
|
|
951
|
+
|
|
952
|
+
histogram.record(Math.max(0, Date.now() - startTimeMs), labels);
|
|
953
|
+
spanStartTimesMs.delete(span);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export function evictSpan(span: Span, target: TelemetryDropTarget): void {
|
|
957
|
+
const operation = activeSpanOperations.get(span) ?? "span_registry";
|
|
958
|
+
const metrics = resolveSelfObservabilityMetrics(target);
|
|
959
|
+
|
|
960
|
+
span.setAttribute(COMMON_SPAN_ATTRIBUTES.OBSERVME_EVICTED, true);
|
|
961
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: "span_registry_full" });
|
|
962
|
+
recordActiveSpanEnd(metrics, span);
|
|
963
|
+
span.end();
|
|
964
|
+
recordTelemetryDrop(target, "span_registry_full", { operation });
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export function evictToolCallState(state: ToolCallState, target: TelemetryDropTarget): void {
|
|
968
|
+
evictSpan(state.span, target);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function evictSubagentSpawnState(state: SubagentSpawnState, target: TelemetryDropTarget): void {
|
|
972
|
+
evictSpan(state.span, target);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
export function evictWaitJoinState(state: AgentWaitJoinState, target: TelemetryDropTarget): void {
|
|
976
|
+
evictSpan(state.span, target);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export function closePendingUserBashOperation(
|
|
980
|
+
session: ObservMeTelemetrySession,
|
|
981
|
+
reason: "bash_overlap_ambiguous" | "bash_session_shutdown",
|
|
982
|
+
evicted: boolean,
|
|
983
|
+
): void {
|
|
984
|
+
const pending = session.pendingUserBash;
|
|
985
|
+
if (!pending) return;
|
|
986
|
+
|
|
987
|
+
if (evicted) pending.span.setAttribute(COMMON_SPAN_ATTRIBUTES.OBSERVME_EVICTED, true);
|
|
988
|
+
pending.span.setStatus({ code: SpanStatusCode.ERROR, message: "bash_execution_incomplete" });
|
|
989
|
+
endActiveSpan(session, pending.span);
|
|
990
|
+
session.pendingUserBash = undefined;
|
|
991
|
+
recordTelemetryDrop(session, reason, { operation: "bash_execution" });
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
export function endAllActiveSpans(session: ObservMeTelemetrySession): void {
|
|
995
|
+
closePendingUserBashOperation(session, "bash_session_shutdown", false);
|
|
996
|
+
for (const state of session.spans.activeAgentJoins.values()) endActiveSpan(session, state.span);
|
|
997
|
+
for (const state of session.spans.activeAgentWaits.values()) endActiveSpan(session, state.span);
|
|
998
|
+
for (const state of session.spans.activeSubagentSpawns.values()) endActiveSpan(session, state.span);
|
|
999
|
+
for (const span of session.spans.activeLlmRequests.values()) endActiveSpan(session, span);
|
|
1000
|
+
for (const state of session.spans.activeToolCalls.values()) endActiveSpan(session, state.span);
|
|
1001
|
+
for (const span of session.spans.activeTurns.values()) endActiveSpan(session, span);
|
|
1002
|
+
for (const span of session.spans.activeAgentRuns.values()) endActiveSpan(session, span);
|
|
1003
|
+
session.spans.activeAgentJoins.clear();
|
|
1004
|
+
session.spans.activeAgentWaits.clear();
|
|
1005
|
+
session.spans.activeSubagentSpawns.clear();
|
|
1006
|
+
session.spans.activeLlmRequests.clear();
|
|
1007
|
+
session.spans.activeToolCalls.clear();
|
|
1008
|
+
session.spans.activeTurns.clear();
|
|
1009
|
+
session.spans.activeAgentRuns.clear();
|
|
1010
|
+
session.turnSequences.clear();
|
|
1011
|
+
session.childFailureAccounting?.clear();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
export function resolveCurrentSessionId(session: SelfObservabilitySession): string {
|
|
1015
|
+
return readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_SESSION_ID) ?? `session-${session.lineage.workflowId}`;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function buildLineageMetricSafeLogAttributes(session: SelfObservabilitySession): AttributeMap {
|
|
1019
|
+
return withoutUndefinedAttributes({
|
|
1020
|
+
[LOG_ATTRIBUTES.PI_SESSION_ID]: resolveCurrentSessionId(session),
|
|
1021
|
+
[LOG_ATTRIBUTES.PI_WORKFLOW_ID]: session.lineage.workflowId,
|
|
1022
|
+
[LOG_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: session.lineage.workflowRootAgentId,
|
|
1023
|
+
[LOG_ATTRIBUTES.PI_AGENT_ID]: session.lineage.agentId,
|
|
1024
|
+
[LOG_ATTRIBUTES.PI_AGENT_PARENT_ID]: session.lineage.parentAgentId,
|
|
1025
|
+
[LOG_ATTRIBUTES.PI_AGENT_ROOT_ID]: session.lineage.rootAgentId,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
export function buildModelChangeAttributes(event: unknown, ctx: ObservMeHandlerContext, session: ObservMeTelemetrySession): AttributeMap {
|
|
1030
|
+
return withoutUndefinedAttributes({
|
|
1031
|
+
...buildLineageMetricSafeLogAttributes(session),
|
|
1032
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
1033
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
1034
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_ID]: readChangeEntryId(event, "model_change"),
|
|
1035
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_PARENT_ID]: readChangeEntryParentId(event, "model_change"),
|
|
1036
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_TYPE]: readChangeEntryType(event, "model_change"),
|
|
1037
|
+
[SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT]: resolveSessionModelProvider(event, ctx, session),
|
|
1038
|
+
[SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT]: resolveSessionModelId(event, ctx, session),
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
export function buildThinkingLevelChangeAttributes(event: unknown, ctx: ObservMeHandlerContext, session: ObservMeTelemetrySession): AttributeMap {
|
|
1043
|
+
return withoutUndefinedAttributes({
|
|
1044
|
+
...buildLineageMetricSafeLogAttributes(session),
|
|
1045
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
1046
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
1047
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_ID]: readChangeEntryId(event, "thinking_level_change"),
|
|
1048
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_PARENT_ID]: readChangeEntryParentId(event, "thinking_level_change"),
|
|
1049
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_TYPE]: readChangeEntryType(event, "thinking_level_change"),
|
|
1050
|
+
[SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]: resolveSessionThinkingLevel(event, ctx, session),
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
export function buildBranchPreparationState(event: unknown, config: ObservMeConfig): BranchPreparationState {
|
|
1055
|
+
const preparation = readBranchPreparation(event);
|
|
1056
|
+
|
|
1057
|
+
return {
|
|
1058
|
+
targetId: readBranchTargetId(preparation),
|
|
1059
|
+
oldLeafId: readBranchOldLeafId(preparation),
|
|
1060
|
+
commonAncestorId: readBranchCommonAncestorIdFromValue(preparation),
|
|
1061
|
+
pathHash: readBranchPathHashFromValue(preparation, config),
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
export function buildBranchAttributes(event: unknown, session: ObservMeTelemetrySession): AttributeMap {
|
|
1066
|
+
const summaryEntry = readBranchSummaryEntry(event);
|
|
1067
|
+
const summary = readBranchSummary(summaryEntry, event);
|
|
1068
|
+
const fromId = readBranchFromId(event, summaryEntry, session.currentBranchPreparation);
|
|
1069
|
+
const toId = readBranchToId(event, summaryEntry, session.currentBranchPreparation);
|
|
1070
|
+
const leafId = readBranchLeafId(event, summaryEntry, toId);
|
|
1071
|
+
const commonAncestorId = readBranchCommonAncestorId(event, session.currentBranchPreparation);
|
|
1072
|
+
const pathHash = readBranchPathHash(event, session.currentBranchPreparation, fromId, toId, leafId, commonAncestorId, session.config);
|
|
1073
|
+
const entry = summaryEntry ?? readUnknown(event, "entry");
|
|
1074
|
+
|
|
1075
|
+
return withoutUndefinedAttributes({
|
|
1076
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
1077
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
1078
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
1079
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_ID]: readString(entry, "id") ?? leafId,
|
|
1080
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_PARENT_ID]: readBranchEntryParentId(entry, event),
|
|
1081
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_TYPE]: readString(entry, "type") ?? "session_tree",
|
|
1082
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_FROM_ID]: fromId,
|
|
1083
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_TO_ID]: toId,
|
|
1084
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_COMMON_ANCESTOR_ID]: commonAncestorId,
|
|
1085
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_PATH_HASH]: pathHash,
|
|
1086
|
+
[BRANCH_ATTRIBUTES.PI_LEAF_ID]: leafId,
|
|
1087
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_SUMMARY_HASH]: summary ? hashValue(summary, session.config) : undefined,
|
|
1088
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_SUMMARY_LENGTH]: summary?.length,
|
|
1089
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_FROM_HOOK]: readBranchFromHook(summaryEntry, event),
|
|
1090
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_READ_FILES_COUNT]: readBranchFileCount(summaryEntry ?? event, "read"),
|
|
1091
|
+
[BRANCH_ATTRIBUTES.PI_BRANCH_MODIFIED_FILES_COUNT]: readBranchFileCount(summaryEntry ?? event, "modified"),
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
export function readBranchPreparation(event: unknown): unknown {
|
|
1096
|
+
return readUnknown(event, "preparation") ?? event;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export function readBranchSummaryEntry(event: unknown): unknown {
|
|
1100
|
+
const explicitEntry = readUnknown(event, "summaryEntry") ?? readUnknown(event, "summary_entry");
|
|
1101
|
+
if (explicitEntry !== undefined) return explicitEntry;
|
|
1102
|
+
|
|
1103
|
+
const entry = readUnknown(event, "entry");
|
|
1104
|
+
if (readString(entry, "type") === "branch_summary") return entry;
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export function readBranchSummary(summaryEntry: unknown, event: unknown): string | undefined {
|
|
1109
|
+
return readString(summaryEntry, "summary") ?? readString(event, "summary");
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export function readBranchFromId(event: unknown, summaryEntry: unknown, preparation: BranchPreparationState | undefined): string | undefined {
|
|
1113
|
+
return (
|
|
1114
|
+
readTreeId(event, "fromId") ??
|
|
1115
|
+
readTreeId(event, "from_id") ??
|
|
1116
|
+
readBranchOldLeafId(event) ??
|
|
1117
|
+
preparation?.oldLeafId ??
|
|
1118
|
+
readTreeId(summaryEntry, "fromId") ??
|
|
1119
|
+
readTreeId(summaryEntry, "from_id")
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export function readBranchToId(event: unknown, summaryEntry: unknown, preparation: BranchPreparationState | undefined): string | undefined {
|
|
1124
|
+
return (
|
|
1125
|
+
readTreeId(event, "toId") ??
|
|
1126
|
+
readTreeId(event, "to_id") ??
|
|
1127
|
+
readBranchNewLeafId(event) ??
|
|
1128
|
+
readTreeId(summaryEntry, "id") ??
|
|
1129
|
+
preparation?.targetId ??
|
|
1130
|
+
readTreeId(summaryEntry, "parentId") ??
|
|
1131
|
+
readTreeId(summaryEntry, "parent_id")
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export function readBranchLeafId(event: unknown, summaryEntry: unknown, fallbackToId: string | undefined): string | undefined {
|
|
1136
|
+
return readTreeId(event, "leafId") ?? readTreeId(event, "leaf_id") ?? readBranchNewLeafId(event) ?? readTreeId(summaryEntry, "id") ?? fallbackToId;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function readBranchTargetId(value: unknown): string | undefined {
|
|
1140
|
+
return readTreeId(value, "targetId") ?? readTreeId(value, "target_id");
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
export function readBranchOldLeafId(value: unknown): string | undefined {
|
|
1144
|
+
return readTreeId(value, "oldLeafId") ?? readTreeId(value, "old_leaf_id");
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
export function readBranchNewLeafId(value: unknown): string | undefined {
|
|
1148
|
+
return readTreeId(value, "newLeafId") ?? readTreeId(value, "new_leaf_id");
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export function readBranchCommonAncestorId(event: unknown, preparation: BranchPreparationState | undefined): string | undefined {
|
|
1152
|
+
return readBranchCommonAncestorIdFromValue(event) ?? readBranchCommonAncestorIdFromValue(readBranchPreparation(event)) ?? preparation?.commonAncestorId;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export function readBranchCommonAncestorIdFromValue(value: unknown): string | undefined {
|
|
1156
|
+
return readString(value, "commonAncestorId") ?? readString(value, "common_ancestor_id");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export function readBranchEntryParentId(entry: unknown, event: unknown): string | undefined {
|
|
1160
|
+
return readTreeId(entry, "parentId") ?? readTreeId(entry, "parent_id") ?? readTreeId(event, "parentId") ?? readTreeId(event, "parent_id");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export function readBranchPathHash(
|
|
1164
|
+
event: unknown,
|
|
1165
|
+
preparation: BranchPreparationState | undefined,
|
|
1166
|
+
fromId: string | undefined,
|
|
1167
|
+
toId: string | undefined,
|
|
1168
|
+
leafId: string | undefined,
|
|
1169
|
+
commonAncestorId: string | undefined,
|
|
1170
|
+
config: ObservMeConfig,
|
|
1171
|
+
): string | undefined {
|
|
1172
|
+
const pathHash = readBranchPathHashFromValue(event, config) ?? readBranchPathHashFromValue(readBranchPreparation(event), config) ?? preparation?.pathHash;
|
|
1173
|
+
if (pathHash !== undefined) return pathHash;
|
|
1174
|
+
|
|
1175
|
+
return hashBranchIds([fromId, toId, leafId, commonAncestorId], config);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function readBranchPathHashFromValue(value: unknown, config: ObservMeConfig): string | undefined {
|
|
1179
|
+
const explicitHash = readString(value, "pathHash") ?? readString(value, "path_hash") ?? readString(value, "branchPathHash") ?? readString(value, "branch_path_hash");
|
|
1180
|
+
if (explicitHash !== undefined) return normalizeBranchPathHash(explicitHash, config);
|
|
1181
|
+
|
|
1182
|
+
const pathText = readBranchPathText(value);
|
|
1183
|
+
return pathText === undefined ? undefined : hashValue(pathText, config);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export function normalizeBranchPathHash(value: string, config: ObservMeConfig): string | undefined {
|
|
1187
|
+
return hashValue(value, config);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export function readBranchPathText(value: unknown): string | undefined {
|
|
1191
|
+
return (
|
|
1192
|
+
serializeBranchPath(readUnknown(value, "branchPathIds")) ??
|
|
1193
|
+
serializeBranchPath(readUnknown(value, "branch_path_ids")) ??
|
|
1194
|
+
serializeBranchPath(readUnknown(value, "pathIds")) ??
|
|
1195
|
+
serializeBranchPath(readUnknown(value, "path_ids")) ??
|
|
1196
|
+
serializeBranchPath(readUnknown(value, "branchPath")) ??
|
|
1197
|
+
serializeBranchPath(readUnknown(value, "branch_path")) ??
|
|
1198
|
+
serializeBranchPath(readUnknown(value, "path")) ??
|
|
1199
|
+
serializeBranchPath(readArray(value, "entriesToSummarize")) ??
|
|
1200
|
+
serializeBranchPath(readArray(value, "entries_to_summarize"))
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
export function serializeBranchPath(value: unknown): string | undefined {
|
|
1205
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
1206
|
+
if (!Array.isArray(value)) return undefined;
|
|
1207
|
+
|
|
1208
|
+
const ids = value.map(branchPathItemId).filter((item): item is string => item !== undefined);
|
|
1209
|
+
return ids.length > 0 ? ids.join("->") : undefined;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
export function branchPathItemId(item: unknown): string | undefined {
|
|
1213
|
+
if (typeof item === "string" && item.length > 0) return item;
|
|
1214
|
+
return readTreeId(item, "id");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
export function hashBranchIds(ids: Array<string | undefined>, config: ObservMeConfig): string | undefined {
|
|
1218
|
+
const presentIds = ids.filter((id): id is string => id !== undefined);
|
|
1219
|
+
return presentIds.length === 0 ? undefined : hashValue(presentIds.join("->"), config);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
export function readBranchFromHook(summaryEntry: unknown, event: unknown): boolean | undefined {
|
|
1223
|
+
return (
|
|
1224
|
+
readBoolean(summaryEntry, "fromHook") ??
|
|
1225
|
+
readBoolean(summaryEntry, "from_hook") ??
|
|
1226
|
+
readBoolean(event, "fromExtension") ??
|
|
1227
|
+
readBoolean(event, "from_extension") ??
|
|
1228
|
+
readBoolean(event, "fromHook") ??
|
|
1229
|
+
readBoolean(event, "from_hook")
|
|
1230
|
+
);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export function readBranchFileCount(source: unknown, kind: "modified" | "read"): number | undefined {
|
|
1234
|
+
const directCount = readInteger(source, `${kind}FilesCount`) ?? readInteger(source, `${kind}_files_count`);
|
|
1235
|
+
if (directCount !== undefined) return directCount;
|
|
1236
|
+
|
|
1237
|
+
const details = readUnknown(source, "details");
|
|
1238
|
+
const camelCaseKey = kind === "read" ? "readFiles" : "modifiedFiles";
|
|
1239
|
+
const snakeCaseKey = kind === "read" ? "read_files" : "modified_files";
|
|
1240
|
+
return readArray(details, camelCaseKey)?.length ?? readArray(details, snakeCaseKey)?.length;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
export function buildCompactionAttributes(event: unknown, session: ObservMeTelemetrySession): AttributeMap {
|
|
1244
|
+
const entry = readCompactionEntry(event);
|
|
1245
|
+
const summary = readCompactionSummary(entry, event);
|
|
1246
|
+
|
|
1247
|
+
return withoutUndefinedAttributes({
|
|
1248
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
1249
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
1250
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
1251
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_ID]: readString(entry, "id"),
|
|
1252
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_PARENT_ID]: readString(entry, "parentId") ?? readString(entry, "parent_id"),
|
|
1253
|
+
[COMMON_SPAN_ATTRIBUTES.PI_ENTRY_TYPE]: readString(entry, "type") ?? "compaction",
|
|
1254
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_FIRST_KEPT_ENTRY_ID]: readCompactionFirstKeptEntryId(entry, event),
|
|
1255
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_TOKENS_BEFORE]: readCompactionTokensBefore(entry, event),
|
|
1256
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_SUMMARY_HASH]: summary ? hashValue(summary, session.config) : undefined,
|
|
1257
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_SUMMARY_LENGTH]: summary?.length,
|
|
1258
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_FROM_HOOK]: readCompactionFromHook(entry, event),
|
|
1259
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_REASON]: readCompactionReason(entry, event),
|
|
1260
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_WILL_RETRY]: readCompactionWillRetry(entry, event),
|
|
1261
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_READ_FILES_COUNT]: readCompactionFileCount(entry, "read"),
|
|
1262
|
+
[COMPACTION_ATTRIBUTES.PI_COMPACTION_MODIFIED_FILES_COUNT]: readCompactionFileCount(entry, "modified"),
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
export function readCompactionEntry(event: unknown): unknown {
|
|
1267
|
+
const explicitEntry = readUnknown(event, "compactionEntry") ?? readUnknown(event, "compaction_entry");
|
|
1268
|
+
if (explicitEntry !== undefined) return explicitEntry;
|
|
1269
|
+
|
|
1270
|
+
const entry = readUnknown(event, "entry");
|
|
1271
|
+
if (readString(entry, "type") === "compaction") return entry;
|
|
1272
|
+
return event;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export function readCompactionSummary(entry: unknown, event: unknown): string | undefined {
|
|
1276
|
+
return readString(entry, "summary") ?? readString(event, "summary");
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
export function readCompactionFirstKeptEntryId(entry: unknown, event: unknown): string | undefined {
|
|
1280
|
+
return (
|
|
1281
|
+
readString(entry, "firstKeptEntryId") ??
|
|
1282
|
+
readString(entry, "first_kept_entry_id") ??
|
|
1283
|
+
readString(entry, "firstKeptId") ??
|
|
1284
|
+
readString(entry, "first_kept_id") ??
|
|
1285
|
+
readString(event, "firstKeptEntryId") ??
|
|
1286
|
+
readString(event, "first_kept_entry_id")
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
export function readCompactionTokensBefore(entry: unknown, event: unknown): number | undefined {
|
|
1291
|
+
return readInteger(entry, "tokensBefore") ?? readInteger(entry, "tokens_before") ?? readInteger(event, "tokensBefore") ?? readInteger(event, "tokens_before");
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
export function readCompactionFromHook(entry: unknown, event: unknown): boolean | undefined {
|
|
1295
|
+
return (
|
|
1296
|
+
readBoolean(entry, "fromHook") ??
|
|
1297
|
+
readBoolean(entry, "from_hook") ??
|
|
1298
|
+
readBoolean(event, "fromHook") ??
|
|
1299
|
+
readBoolean(event, "from_hook") ??
|
|
1300
|
+
readBoolean(event, "fromExtension") ??
|
|
1301
|
+
readBoolean(event, "summaryFromExtension")
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
export function readCompactionReason(entry: unknown, event: unknown): string | undefined {
|
|
1306
|
+
return readString(event, "reason") ?? readString(entry, "reason");
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
export function readCompactionWillRetry(entry: unknown, event: unknown): boolean | undefined {
|
|
1310
|
+
return readBoolean(event, "willRetry") ?? readBoolean(event, "will_retry") ?? readBoolean(entry, "willRetry") ?? readBoolean(entry, "will_retry");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
export function readCompactionFileCount(entry: unknown, kind: "modified" | "read"): number | undefined {
|
|
1314
|
+
const directCount = readInteger(entry, `${kind}FilesCount`) ?? readInteger(entry, `${kind}_files_count`);
|
|
1315
|
+
if (directCount !== undefined) return directCount;
|
|
1316
|
+
|
|
1317
|
+
const details = readUnknown(entry, "details");
|
|
1318
|
+
const camelCaseKey = kind === "read" ? "readFiles" : "modifiedFiles";
|
|
1319
|
+
const snakeCaseKey = kind === "read" ? "read_files" : "modified_files";
|
|
1320
|
+
return readArray(details, camelCaseKey)?.length ?? readArray(details, snakeCaseKey)?.length;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function updateCurrentSessionAttributes(
|
|
1324
|
+
session: ObservMeTelemetrySession,
|
|
1325
|
+
attributes: AttributeMap,
|
|
1326
|
+
keys: readonly string[],
|
|
1327
|
+
): void {
|
|
1328
|
+
const currentAttributes = session.sessionAttributes ?? {};
|
|
1329
|
+
const updates = Object.fromEntries(keys.map(key => [key, attributes[key]]).filter((entry): entry is [string, AttributePrimitive] => entry[1] !== undefined));
|
|
1330
|
+
|
|
1331
|
+
session.sessionAttributes = { ...currentAttributes, ...updates };
|
|
1332
|
+
session.sessionSpan?.setAttributes(updates);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export function modelChangeMetricLabels(session: ObservMeTelemetrySession, attributes: AttributeMap): Record<string, string> {
|
|
1336
|
+
return {
|
|
1337
|
+
...metricLabels(session.config, session.lineage),
|
|
1338
|
+
provider: String(attributes[SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT] ?? "unknown"),
|
|
1339
|
+
model: String(attributes[SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT] ?? "unknown"),
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
export function thinkingLevelChangeMetricLabels(session: ObservMeTelemetrySession): Record<string, string> {
|
|
1344
|
+
return metricLabels(session.config, session.lineage);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
export function buildCommonSessionSpanAttributes(
|
|
1348
|
+
sessionId: string,
|
|
1349
|
+
config: ObservMeConfig,
|
|
1350
|
+
lineage: AgentLineageContext,
|
|
1351
|
+
): AttributeMap {
|
|
1352
|
+
return withoutUndefinedAttributes({
|
|
1353
|
+
[COMMON_SPAN_ATTRIBUTES.PI_SESSION_ID]: sessionId,
|
|
1354
|
+
[COMMON_SPAN_ATTRIBUTES.PI_WORKFLOW_ID]: lineage.workflowId,
|
|
1355
|
+
[COMMON_SPAN_ATTRIBUTES.PI_WORKFLOW_ROOT_AGENT_ID]: lineage.workflowRootAgentId,
|
|
1356
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_ID]: lineage.agentId,
|
|
1357
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_PARENT_ID]: lineage.parentAgentId,
|
|
1358
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_ROOT_ID]: lineage.rootAgentId,
|
|
1359
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_CAPTURE_PROMPTS]: config.capture.prompts,
|
|
1360
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_CAPTURE_RESPONSES]: config.capture.responses,
|
|
1361
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_CAPTURE_TOOL_ARGUMENTS]: config.capture.toolArguments,
|
|
1362
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_REDACTION_ENABLED]: config.privacy.redactionEnabled,
|
|
1363
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_SEMCONV_VERSION]: OBSERVME_SEMCONV_VERSION,
|
|
1364
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_REPLAYED]: false,
|
|
1365
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_EVICTED]: false,
|
|
1366
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_TRUNCATED]: false,
|
|
1367
|
+
[COMMON_SPAN_ATTRIBUTES.OBSERVME_ORIGINAL_LENGTH]: 0,
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
export function metricLabels(config: ObservMeConfig, lineage: AgentLineageContext): Record<string, string> {
|
|
1372
|
+
return {
|
|
1373
|
+
environment: config.environment,
|
|
1374
|
+
agent_role: lineage.role,
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
export function llmMetricLabels(session: ObservMeTelemetrySession, attributes: AttributeMap): Record<string, string> {
|
|
1379
|
+
return {
|
|
1380
|
+
...metricLabels(session.config, session.lineage),
|
|
1381
|
+
provider: String(attributes[LLM_ATTRIBUTES.GEN_AI_PROVIDER_NAME] ?? "unknown"),
|
|
1382
|
+
model: String(attributes[LLM_ATTRIBUTES.GEN_AI_REQUEST_MODEL] ?? attributes[LLM_ATTRIBUTES.GEN_AI_RESPONSE_MODEL] ?? "unknown"),
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
export function toolMetricLabels(attributes: AttributeMap): Record<string, string> {
|
|
1387
|
+
return {
|
|
1388
|
+
tool_name: String(attributes[TOOL_ATTRIBUTES.PI_TOOL_NAME] ?? "unknown"),
|
|
1389
|
+
tool_category: String(attributes[TOOL_ATTRIBUTES.PI_TOOL_CATEGORY] ?? "unknown"),
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
export function mergeToolStateLabels(state: ToolCallState, attributes: AttributeMap): void {
|
|
1394
|
+
state.labels = {
|
|
1395
|
+
...state.labels,
|
|
1396
|
+
...toolMetricLabelUpdates(attributes),
|
|
1397
|
+
};
|
|
1398
|
+
state.completionLogAttributes = {
|
|
1399
|
+
...state.completionLogAttributes,
|
|
1400
|
+
...buildToolLogIdentityAttributes(attributes),
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
export function toolMetricLabelUpdates(attributes: AttributeMap): Record<string, string> {
|
|
1405
|
+
const updates: Record<string, string> = {};
|
|
1406
|
+
const toolName = readString(attributes, TOOL_ATTRIBUTES.PI_TOOL_NAME);
|
|
1407
|
+
const toolCategory = readString(attributes, TOOL_ATTRIBUTES.PI_TOOL_CATEGORY);
|
|
1408
|
+
|
|
1409
|
+
if (toolName) updates.tool_name = toolName;
|
|
1410
|
+
if (toolCategory) updates.tool_category = toolCategory;
|
|
1411
|
+
|
|
1412
|
+
return updates;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
export function buildBashPreExecutionAttributes(event: unknown, session: ObservMeTelemetrySession): AttributeMap {
|
|
1416
|
+
const command = readBashCommand(event);
|
|
1417
|
+
|
|
1418
|
+
return withoutUndefinedAttributes({
|
|
1419
|
+
...buildCommonSessionSpanAttributes(resolveCurrentSessionId(session), session.config, session.lineage),
|
|
1420
|
+
[COMMON_SPAN_ATTRIBUTES.PI_AGENT_RUN_ID]: session.currentAgentRunId,
|
|
1421
|
+
[LOG_ATTRIBUTES.PI_TURN_ID]: session.currentTurnId,
|
|
1422
|
+
[BASH_ATTRIBUTES.PI_BASH_COMMAND_HASH]: command === undefined ? undefined : hashValue(command, session.config),
|
|
1423
|
+
[BASH_ATTRIBUTES.PI_BASH_EXCLUDE_FROM_CONTEXT]: readBashExcludeFromContext(event),
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
export function buildBashExecutionAttributes(event: unknown, session: ObservMeTelemetrySession): AttributeMap {
|
|
1428
|
+
const output = readBashOutput(event);
|
|
1429
|
+
|
|
1430
|
+
return withoutUndefinedAttributes({
|
|
1431
|
+
...buildBashPreExecutionAttributes(event, session),
|
|
1432
|
+
[BASH_ATTRIBUTES.PI_BASH_EXIT_CODE]: readBashExitCode(event),
|
|
1433
|
+
[BASH_ATTRIBUTES.PI_BASH_CANCELLED]: readBashCancelled(event) ?? false,
|
|
1434
|
+
[BASH_ATTRIBUTES.PI_BASH_TRUNCATED]: readBashTruncated(event) ?? false,
|
|
1435
|
+
[BASH_ATTRIBUTES.PI_BASH_OUTPUT_SIZE]: output === undefined ? undefined : output.length,
|
|
1436
|
+
[BASH_ATTRIBUTES.PI_BASH_OUTPUT_HASH]: output === undefined ? undefined : hashValue(output, session.config),
|
|
1437
|
+
[BASH_ATTRIBUTES.PI_BASH_FULL_OUTPUT_PATH_PRESENT]: readBashFullOutputPathPresent(event),
|
|
1438
|
+
[BASH_ATTRIBUTES.PI_BASH_EXCLUDE_FROM_CONTEXT]: readBashExcludeFromContext(event) ?? false,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export function bashExecutionMetricLabels(
|
|
1443
|
+
session: ObservMeTelemetrySession,
|
|
1444
|
+
event: unknown,
|
|
1445
|
+
failed: boolean,
|
|
1446
|
+
): Record<string, string> {
|
|
1447
|
+
return {
|
|
1448
|
+
...metricLabels(session.config, session.lineage),
|
|
1449
|
+
status: bashStatusLabel(event, failed),
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export function bashFailureMetricLabels(session: ObservMeTelemetrySession, event: unknown): Record<string, string> {
|
|
1454
|
+
return {
|
|
1455
|
+
...bashExecutionMetricLabels(session, event, true),
|
|
1456
|
+
error_class: bashErrorClass(event),
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
export function bashStatusLabel(event: unknown, failed: boolean): string {
|
|
1461
|
+
if (readBashCancelled(event)) return "cancelled";
|
|
1462
|
+
if (normalizedStatus(readString(event, "status")) === "timeout") return "timeout";
|
|
1463
|
+
return failed ? "error" : "ok";
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export function readBashPayload(event: unknown): unknown {
|
|
1467
|
+
const message = readMessage(event);
|
|
1468
|
+
if (isBashExecutionMessage(message)) return message;
|
|
1469
|
+
|
|
1470
|
+
const bashExecution = readUnknown(event, "bashExecution") ?? readUnknown(event, "bash_execution");
|
|
1471
|
+
if (isRecord(bashExecution)) return bashExecution;
|
|
1472
|
+
|
|
1473
|
+
const entryMessage = readUnknown(readUnknown(event, "entry"), "message");
|
|
1474
|
+
if (isBashExecutionMessage(entryMessage)) return entryMessage;
|
|
1475
|
+
|
|
1476
|
+
return event;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
export function isBashExecutionMessage(value: unknown): value is Record<string, unknown> {
|
|
1480
|
+
return isRecord(value) && readString(value, "role") === "bashExecution";
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export function readBashCommand(event: unknown): string | undefined {
|
|
1484
|
+
const payload = readBashPayload(event);
|
|
1485
|
+
return readOptionalString(payload, "command") ?? readOptionalString(payload, "cmd") ?? readOptionalString(payload, "input");
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
export function readBashOutput(event: unknown): string | undefined {
|
|
1489
|
+
const payload = readBashPayload(event);
|
|
1490
|
+
const directOutput = readOptionalString(payload, "output") ?? readOptionalString(payload, "content");
|
|
1491
|
+
if (directOutput !== undefined) return directOutput;
|
|
1492
|
+
|
|
1493
|
+
return combineBashStreams(readOptionalString(payload, "stdout"), readOptionalString(payload, "stderr"));
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
export function combineBashStreams(stdout: string | undefined, stderr: string | undefined): string | undefined {
|
|
1497
|
+
if (stdout === undefined) return stderr;
|
|
1498
|
+
if (stderr === undefined) return stdout;
|
|
1499
|
+
if (stdout.length === 0) return stderr;
|
|
1500
|
+
if (stderr.length === 0) return stdout;
|
|
1501
|
+
return `${stdout}\n${stderr}`;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
export function readBashExitCode(event: unknown): number | undefined {
|
|
1505
|
+
const payload = readBashPayload(event);
|
|
1506
|
+
const result = readUnknown(payload, "result");
|
|
1507
|
+
return readInteger(payload, "exitCode") ?? readInteger(payload, "exit_code") ?? readInteger(result, "exitCode") ?? readInteger(result, "exit_code");
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
export function readBashCancelled(event: unknown): boolean | undefined {
|
|
1511
|
+
const payload = readBashPayload(event);
|
|
1512
|
+
const explicit = readBoolean(payload, "cancelled") ?? readBoolean(payload, "canceled") ?? readBoolean(payload, "isCancelled");
|
|
1513
|
+
if (explicit !== undefined) return explicit;
|
|
1514
|
+
|
|
1515
|
+
const status = normalizedStatus(readString(payload, "status"));
|
|
1516
|
+
return status === "cancelled" || status === "canceled" ? true : undefined;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
export function readBashTruncated(event: unknown): boolean | undefined {
|
|
1520
|
+
const payload = readBashPayload(event);
|
|
1521
|
+
return readBoolean(payload, "truncated") ?? readBoolean(payload, "outputTruncated") ?? readBoolean(payload, "output_truncated");
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
export function readBashFullOutputPathPresent(event: unknown): boolean {
|
|
1525
|
+
const payload = readBashPayload(event);
|
|
1526
|
+
const explicit = readBoolean(payload, "fullOutputPathPresent") ?? readBoolean(payload, "full_output_path_present");
|
|
1527
|
+
if (explicit !== undefined) return explicit;
|
|
1528
|
+
|
|
1529
|
+
return readOptionalString(payload, "fullOutputPath") !== undefined || readOptionalString(payload, "full_output_path") !== undefined;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
export function readBashExcludeFromContext(event: unknown): boolean | undefined {
|
|
1533
|
+
const payload = readBashPayload(event);
|
|
1534
|
+
return readBoolean(payload, "excludeFromContext") ?? readBoolean(payload, "exclude_from_context");
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
const bashStartTimestampKeys = [
|
|
1538
|
+
"startedAtMs",
|
|
1539
|
+
"started_at_ms",
|
|
1540
|
+
"startTimeMs",
|
|
1541
|
+
"start_time_ms",
|
|
1542
|
+
"startedAt",
|
|
1543
|
+
"started_at",
|
|
1544
|
+
"startTimestamp",
|
|
1545
|
+
"start_timestamp",
|
|
1546
|
+
] as const;
|
|
1547
|
+
const bashCompletionTimestampKeys = [
|
|
1548
|
+
"completedAtMs",
|
|
1549
|
+
"completed_at_ms",
|
|
1550
|
+
"endedAtMs",
|
|
1551
|
+
"ended_at_ms",
|
|
1552
|
+
"endTimeMs",
|
|
1553
|
+
"end_time_ms",
|
|
1554
|
+
"completedAt",
|
|
1555
|
+
"completed_at",
|
|
1556
|
+
"endedAt",
|
|
1557
|
+
"ended_at",
|
|
1558
|
+
"endTimestamp",
|
|
1559
|
+
"end_timestamp",
|
|
1560
|
+
"timestamp",
|
|
1561
|
+
] as const;
|
|
1562
|
+
|
|
1563
|
+
export function readBashPreExecutionTimestampMs(event: unknown): number | undefined {
|
|
1564
|
+
return readBashTimestampMs(event, bashStartTimestampKeys) ?? readBashTimestampMs(event, ["timestamp"]);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
export function readBashCompletionTimestampMs(event: unknown): number | undefined {
|
|
1568
|
+
return readBashTimestampMs(event, bashCompletionTimestampKeys);
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export function resolveBashEventDurationMs(startedAtMs: number | undefined, completionEvent: unknown): number | undefined {
|
|
1572
|
+
return safeElapsedDurationMs(startedAtMs, readBashCompletionTimestampMs(completionEvent));
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
export function resolveStandaloneBashEventDurationMs(event: unknown): number | undefined {
|
|
1576
|
+
return safeElapsedDurationMs(readBashTimestampMs(event, bashStartTimestampKeys), readBashCompletionTimestampMs(event));
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
export function safeElapsedDurationMs(startedAtMs: number | undefined, completedAtMs: number | undefined): number | undefined {
|
|
1580
|
+
if (!isSafeNonNegativeTimeMs(startedAtMs) || !isSafeNonNegativeTimeMs(completedAtMs) || completedAtMs < startedAtMs) return undefined;
|
|
1581
|
+
|
|
1582
|
+
const durationMs = completedAtMs - startedAtMs;
|
|
1583
|
+
return Number.isFinite(durationMs) && durationMs <= Number.MAX_SAFE_INTEGER ? durationMs : undefined;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
function readBashTimestampMs(event: unknown, keys: readonly string[]): number | undefined {
|
|
1587
|
+
const payload = readBashPayload(event);
|
|
1588
|
+
const payloadTimestamp = readTimestampMsForKeys(payload, keys);
|
|
1589
|
+
if (payloadTimestamp !== undefined) return payloadTimestamp;
|
|
1590
|
+
|
|
1591
|
+
const eventTimestamp = readTimestampMsForKeys(event, keys);
|
|
1592
|
+
if (eventTimestamp !== undefined) return eventTimestamp;
|
|
1593
|
+
|
|
1594
|
+
return readTimestampMsForKeys(readUnknown(event, "entry"), keys);
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
function readTimestampMsForKeys(value: unknown, keys: readonly string[]): number | undefined {
|
|
1598
|
+
for (const key of keys) {
|
|
1599
|
+
const timestamp = parseTimestampMs(readUnknown(value, key));
|
|
1600
|
+
if (timestamp !== undefined) return timestamp;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return undefined;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function parseTimestampMs(value: unknown): number | undefined {
|
|
1607
|
+
if (typeof value === "number") return isSafeNonNegativeTimeMs(value) ? value : undefined;
|
|
1608
|
+
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
1609
|
+
|
|
1610
|
+
const numericTimestamp = Number(value);
|
|
1611
|
+
if (isSafeNonNegativeTimeMs(numericTimestamp)) return numericTimestamp;
|
|
1612
|
+
|
|
1613
|
+
const parsedTimestamp = Date.parse(value);
|
|
1614
|
+
return isSafeNonNegativeTimeMs(parsedTimestamp) ? parsedTimestamp : undefined;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
function isSafeNonNegativeTimeMs(value: number | undefined): value is number {
|
|
1618
|
+
return value !== undefined && Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
export function hasBashCompletionResult(event: unknown): boolean {
|
|
1622
|
+
const payload = readBashPayload(event);
|
|
1623
|
+
const status = normalizedStatus(readString(payload, "status"));
|
|
1624
|
+
|
|
1625
|
+
return (
|
|
1626
|
+
readBashExitCode(payload) !== undefined ||
|
|
1627
|
+
readBashCancelled(payload) !== undefined ||
|
|
1628
|
+
readBashTruncated(payload) !== undefined ||
|
|
1629
|
+
readBashOutput(payload) !== undefined ||
|
|
1630
|
+
readBoolean(payload, "failed") !== undefined ||
|
|
1631
|
+
status === "ok" ||
|
|
1632
|
+
status === "success" ||
|
|
1633
|
+
status === "completed" ||
|
|
1634
|
+
statusIndicatesToolFailure(status)
|
|
1635
|
+
);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
export function bashExecutionFailed(event: unknown): boolean {
|
|
1639
|
+
if (readBashCancelled(event)) return true;
|
|
1640
|
+
if (bashExitCodeIndicatesFailure(event)) return true;
|
|
1641
|
+
|
|
1642
|
+
const payload = readBashPayload(event);
|
|
1643
|
+
return readBoolean(payload, "failed") === true || statusIndicatesToolFailure(readString(payload, "status"));
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
export function bashErrorClass(event: unknown): string {
|
|
1647
|
+
const payload = readBashPayload(event);
|
|
1648
|
+
const explicit = readString(payload, "errorClass") ?? readString(payload, "error_class");
|
|
1649
|
+
if (explicit) return normalizeErrorClass(explicit);
|
|
1650
|
+
if (readBashCancelled(payload)) return "cancelled";
|
|
1651
|
+
if (bashExitCodeIndicatesFailure(payload)) return "non_zero_exit";
|
|
1652
|
+
if (normalizedStatus(readString(payload, "status")) === "timeout") return "timeout";
|
|
1653
|
+
return "bash_error";
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
export function bashExitCodeIndicatesFailure(event: unknown): boolean {
|
|
1657
|
+
const exitCode = readBashExitCode(event);
|
|
1658
|
+
return exitCode !== undefined && exitCode !== 0;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
export function normalizedStatus(status: string | undefined): string | undefined {
|
|
1662
|
+
return status?.trim().toLowerCase();
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
export function readToolCallId(event: unknown): string | undefined {
|
|
1666
|
+
const toolCall = readUnknown(event, "toolCall");
|
|
1667
|
+
|
|
1668
|
+
return (
|
|
1669
|
+
readString(event, "toolCallId") ??
|
|
1670
|
+
readString(event, "tool_call_id") ??
|
|
1671
|
+
readString(event, "callId") ??
|
|
1672
|
+
readString(event, "id") ??
|
|
1673
|
+
readString(toolCall, "id") ??
|
|
1674
|
+
readString(toolCall, "toolCallId")
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
export function readToolName(event: unknown): string | undefined {
|
|
1679
|
+
const tool = readUnknown(event, "tool");
|
|
1680
|
+
const toolCall = readUnknown(event, "toolCall");
|
|
1681
|
+
|
|
1682
|
+
return (
|
|
1683
|
+
readString(event, "toolName") ??
|
|
1684
|
+
readString(event, "tool_name") ??
|
|
1685
|
+
readString(event, "name") ??
|
|
1686
|
+
readString(tool, "name") ??
|
|
1687
|
+
readString(toolCall, "name") ??
|
|
1688
|
+
readString(toolCall, "toolName")
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
export function readToolCategory(event: unknown): string | undefined {
|
|
1693
|
+
return normalizeToolCategory(readString(event, "toolCategory") ?? readString(event, "tool_category") ?? readString(event, "category"));
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
export function safeToolName(rawName: string | undefined): string {
|
|
1697
|
+
if (!rawName) return "unknown";
|
|
1698
|
+
|
|
1699
|
+
const normalizedName = rawName.trim().toLowerCase();
|
|
1700
|
+
if (/^[a-z][a-z0-9_.:-]{0,63}$/u.test(normalizedName)) return normalizedName;
|
|
1701
|
+
return "custom";
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
export function resolveToolCategory(event: unknown, toolName: string): string {
|
|
1705
|
+
const explicitCategory = readToolCategory(event);
|
|
1706
|
+
if (explicitCategory) return explicitCategory;
|
|
1707
|
+
if (isShellToolName(toolName)) return "shell";
|
|
1708
|
+
if (isFilesystemToolName(toolName)) return "filesystem";
|
|
1709
|
+
if (isNetworkToolName(toolName)) return "network";
|
|
1710
|
+
if (toolName === "unknown") return "unknown";
|
|
1711
|
+
return "custom";
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
export function normalizeToolCategory(value: string | undefined): string | undefined {
|
|
1715
|
+
const normalizedValue = value?.trim().toLowerCase();
|
|
1716
|
+
if (normalizedValue === "shell" || normalizedValue === "filesystem" || normalizedValue === "network" || normalizedValue === "custom" || normalizedValue === "unknown") return normalizedValue;
|
|
1717
|
+
return undefined;
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
export function isShellToolName(toolName: string): boolean {
|
|
1721
|
+
return toolName === "bash" || toolName === "shell" || toolName === "user_bash" || toolName.includes("bash");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
export function isFilesystemToolName(toolName: string): boolean {
|
|
1725
|
+
return /^(read|write|edit|ls|grep|rg|find|glob|file|filesystem|path)([_.:-]|$)/u.test(toolName);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
export function isNetworkToolName(toolName: string): boolean {
|
|
1729
|
+
return /^(aws|http|fetch|curl|web|network)([_.:-]|$)/u.test(toolName);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
export function mapToolType(category: string): string {
|
|
1733
|
+
if (category === "filesystem" || category === "network") return "extension";
|
|
1734
|
+
return "function";
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
export function readToolArgumentsText(event: unknown): string | undefined {
|
|
1738
|
+
const toolCall = readUnknown(event, "toolCall");
|
|
1739
|
+
const value =
|
|
1740
|
+
readUnknown(event, "arguments") ??
|
|
1741
|
+
readUnknown(event, "args") ??
|
|
1742
|
+
readUnknown(event, "input") ??
|
|
1743
|
+
readUnknown(event, "parameters") ??
|
|
1744
|
+
readUnknown(event, "params") ??
|
|
1745
|
+
readUnknown(toolCall, "arguments") ??
|
|
1746
|
+
readUnknown(toolCall, "input");
|
|
1747
|
+
|
|
1748
|
+
return serializeToolPayload(value);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
export function readToolResultText(event: unknown): string | undefined {
|
|
1752
|
+
const error = readUnknown(event, "error");
|
|
1753
|
+
const value =
|
|
1754
|
+
readUnknown(event, "result") ??
|
|
1755
|
+
readUnknown(event, "output") ??
|
|
1756
|
+
readUnknown(event, "response") ??
|
|
1757
|
+
readUnknown(event, "content") ??
|
|
1758
|
+
readUnknown(event, "errorMessage") ??
|
|
1759
|
+
readUnknown(event, "error_message") ??
|
|
1760
|
+
readString(error, "message") ??
|
|
1761
|
+
error;
|
|
1762
|
+
|
|
1763
|
+
return serializeToolPayload(value);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
export function serializeToolPayload(value: unknown): string | undefined {
|
|
1767
|
+
if (value === undefined) return undefined;
|
|
1768
|
+
if (typeof value === "string") return value;
|
|
1769
|
+
if (value instanceof Error) return value.name;
|
|
1770
|
+
|
|
1771
|
+
try {
|
|
1772
|
+
return JSON.stringify(value) ?? formatNonJsonToolPayload(value);
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
return formatUnserializableToolPayload(value, error);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function formatNonJsonToolPayload(value: unknown): string {
|
|
1779
|
+
if (typeof value === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
1780
|
+
if (typeof value === "symbol") return value.description ? `Symbol(${value.description})` : "Symbol()";
|
|
1781
|
+
if (typeof value === "bigint") return value.toString();
|
|
1782
|
+
if (isRecord(value)) return `[Unserializable ${readObjectTypeName(value)}]`;
|
|
1783
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1784
|
+
if (value === null) return "null";
|
|
1785
|
+
return "unserializable value";
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
function formatUnserializableToolPayload(value: unknown, error: unknown): string {
|
|
1789
|
+
if (typeof value === "bigint") return value.toString();
|
|
1790
|
+
if (!isRecord(value)) return formatNonJsonToolPayload(value);
|
|
1791
|
+
|
|
1792
|
+
const failureKind = error instanceof Error ? error.name : "unknown serialization failure";
|
|
1793
|
+
return `[Unserializable ${readObjectTypeName(value)}: ${failureKind}]`;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
function readObjectTypeName(value: object): string {
|
|
1797
|
+
return value.constructor?.name || "Object";
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
export function toolExecutionFailed(event: unknown): boolean {
|
|
1801
|
+
const success = readBoolean(event, "success") ?? readBoolean(readUnknown(event, "result"), "success");
|
|
1802
|
+
if (success !== undefined) return !success;
|
|
1803
|
+
|
|
1804
|
+
return (
|
|
1805
|
+
readBoolean(event, "isError") === true ||
|
|
1806
|
+
readBoolean(event, "failed") === true ||
|
|
1807
|
+
readBoolean(readUnknown(event, "result"), "isError") === true ||
|
|
1808
|
+
readUnknown(event, "error") !== undefined ||
|
|
1809
|
+
statusIndicatesToolFailure(readString(event, "status")) ||
|
|
1810
|
+
exitCodeIndicatesFailure(event)
|
|
1811
|
+
);
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
export function statusIndicatesToolFailure(status: string | undefined): boolean {
|
|
1815
|
+
const normalizedValue = normalizedStatus(status);
|
|
1816
|
+
return normalizedValue === "error" || normalizedValue === "failed" || normalizedValue === "failure" || normalizedValue === "timeout" || normalizedValue === "cancelled" || normalizedValue === "canceled";
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
export function exitCodeIndicatesFailure(event: unknown): boolean {
|
|
1820
|
+
const exitCode = readInteger(event, "exitCode") ?? readInteger(event, "exit_code");
|
|
1821
|
+
return exitCode !== undefined && exitCode !== 0;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
export function toolErrorClass(event: unknown): string {
|
|
1825
|
+
const result = readUnknown(event, "result");
|
|
1826
|
+
const explicit = readString(event, "errorClass") ?? readString(event, "error_class") ?? readString(result, "errorClass") ?? readString(result, "error_class");
|
|
1827
|
+
if (explicit) return normalizeErrorClass(explicit);
|
|
1828
|
+
|
|
1829
|
+
const error = readUnknown(event, "error") ?? readUnknown(result, "error");
|
|
1830
|
+
if (error instanceof Error) return normalizeErrorClass(error.name);
|
|
1831
|
+
if (isRecord(error)) return normalizeErrorClass(readString(error, "name") ?? readString(error, "code") ?? readString(error, "type") ?? "tool_error");
|
|
1832
|
+
|
|
1833
|
+
const status = readString(event, "status");
|
|
1834
|
+
if (statusIndicatesToolFailure(status)) return normalizeErrorClass(status ?? "tool_error");
|
|
1835
|
+
return "tool_error";
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
export function normalizeErrorClass(value: string): string {
|
|
1839
|
+
const trimmedValue = value.trim();
|
|
1840
|
+
if (/^[A-Za-z][A-Za-z0-9_.:-]{0,63}$/u.test(trimmedValue)) return trimmedValue;
|
|
1841
|
+
return "tool_error";
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
export function readMessage(event: unknown): unknown {
|
|
1845
|
+
return readUnknown(event, "message") ?? event;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
export function isAssistantMessage(message: unknown): message is Record<string, unknown> {
|
|
1849
|
+
return isRecord(message) && readString(message, "role") === "assistant";
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
export function isLlmError(message: Record<string, unknown>): boolean {
|
|
1853
|
+
return readString(message, "stopReason") === "error" || Boolean(readString(message, "errorMessage"));
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
export function readUsage(message: Record<string, unknown>): Record<string, unknown> {
|
|
1857
|
+
const usage = readUnknown(message, "usage");
|
|
1858
|
+
return isRecord(usage) ? usage : {};
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
export function readCost(usage: Record<string, unknown>): Record<string, unknown> {
|
|
1862
|
+
const cost = readUnknown(usage, "cost");
|
|
1863
|
+
return isRecord(cost) ? cost : {};
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
export function mapStopReason(stopReason: string): string {
|
|
1867
|
+
if (stopReason === "toolUse") return "tool_calls";
|
|
1868
|
+
if (stopReason === "length") return "length";
|
|
1869
|
+
if (stopReason === "error") return "error";
|
|
1870
|
+
if (stopReason === "aborted") return "cancelled";
|
|
1871
|
+
return "stop";
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
export function countPayloadItems(payload: unknown, keys: readonly string[]): number | undefined {
|
|
1875
|
+
const counts = keys.map(key => countPayloadItemSource(readUnknown(payload, key))).filter((value): value is number => value !== undefined);
|
|
1876
|
+
if (counts.length === 0) return undefined;
|
|
1877
|
+
return counts.reduce((total, value) => total + value, 0);
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
export function countPayloadItemSource(value: unknown): number | undefined {
|
|
1881
|
+
if (Array.isArray(value)) return value.length;
|
|
1882
|
+
if (typeof value === "string" && value.trim() !== "") return 1;
|
|
1883
|
+
if (isRecord(value)) return 1;
|
|
1884
|
+
return undefined;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
interface SafeJsonLengthResult {
|
|
1888
|
+
readonly length?: number;
|
|
1889
|
+
readonly failureKind?: string;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
export function safeJsonLength(value: unknown): number | undefined {
|
|
1893
|
+
if (value === undefined) return undefined;
|
|
1894
|
+
|
|
1895
|
+
return readSafeJsonLength(value).length;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function readSafeJsonLength(value: unknown): SafeJsonLengthResult {
|
|
1899
|
+
try {
|
|
1900
|
+
return { length: JSON.stringify(value)?.length };
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
return { failureKind: readJsonStringifyFailureKind(error) };
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function readJsonStringifyFailureKind(error: unknown): string {
|
|
1907
|
+
if (error instanceof Error) return error.name || "Error";
|
|
1908
|
+
return typeof error;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
const promptPayloadTextKeys = ["messages", "contents", "input", "prompt"] as const;
|
|
1912
|
+
|
|
1913
|
+
export function extractPayloadPromptText(payload: unknown): string | undefined {
|
|
1914
|
+
for (const key of promptPayloadTextKeys) {
|
|
1915
|
+
const text = extractContentText(readUnknown(payload, key)).join("\n").trim();
|
|
1916
|
+
if (text.length > 0) return text;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
return undefined;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
export function extractAssistantText(message: Record<string, unknown>): string | undefined {
|
|
1923
|
+
const content = readUnknown(message, "content");
|
|
1924
|
+
const text = extractContentText(content).join("\n").trim();
|
|
1925
|
+
return text.length === 0 ? undefined : text;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
export function extractAssistantThinking(message: Record<string, unknown>): string | undefined {
|
|
1929
|
+
const content = readArray(message, "content") ?? [];
|
|
1930
|
+
const thinking = content.map(extractThinkingText).filter((value): value is string => value !== undefined).join("\n").trim();
|
|
1931
|
+
return thinking.length === 0 ? undefined : thinking;
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
export function extractContentText(value: unknown): string[] {
|
|
1935
|
+
if (typeof value === "string") return [value];
|
|
1936
|
+
if (Array.isArray(value)) return value.flatMap(extractContentText);
|
|
1937
|
+
if (!isRecord(value)) return [];
|
|
1938
|
+
|
|
1939
|
+
const directText = readString(value, "text");
|
|
1940
|
+
if (directText) return [directText];
|
|
1941
|
+
|
|
1942
|
+
return extractContentText(readUnknown(value, "content"));
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
export function extractThinkingText(value: unknown): string | undefined {
|
|
1946
|
+
if (!isRecord(value) || readString(value, "type") !== "thinking") return undefined;
|
|
1947
|
+
return readString(value, "thinking");
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
export function resolveSessionFilePath(event: unknown, ctx: ObservMeHandlerContext): string | undefined {
|
|
1951
|
+
return readString(event, "sessionFile") ?? readString(event, "session_file") ?? readString(ctx, "sessionFile") ?? readString(ctx, "session_file");
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
export function resolveSessionId(event: unknown, ctx: ObservMeHandlerContext, lineage: AgentLineageContext): string {
|
|
1955
|
+
return (
|
|
1956
|
+
readString(event, "sessionId") ??
|
|
1957
|
+
readString(event, "session_id") ??
|
|
1958
|
+
readString(event, "id") ??
|
|
1959
|
+
readString(ctx, "sessionId") ??
|
|
1960
|
+
readString(ctx, "session_id") ??
|
|
1961
|
+
`session-${lineage.workflowId}`
|
|
1962
|
+
);
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
export function resolveModelProvider(event: unknown, ctx: ObservMeHandlerContext): string {
|
|
1966
|
+
return readModelProvider(event, ctx) ?? "unknown";
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
export function resolveModelId(event: unknown, ctx: ObservMeHandlerContext): string {
|
|
1970
|
+
return readModelId(event, ctx) ?? "unknown";
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
export function resolveThinkingLevel(event: unknown, ctx: ObservMeHandlerContext): string {
|
|
1974
|
+
return readThinkingLevel(event, ctx) ?? "unknown";
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
export function resolveSessionModelProvider(
|
|
1978
|
+
event: unknown,
|
|
1979
|
+
ctx: ObservMeHandlerContext,
|
|
1980
|
+
session: ObservMeTelemetrySession,
|
|
1981
|
+
): string {
|
|
1982
|
+
return readModelProvider(event, ctx) ?? readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT) ?? "unknown";
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
export function resolveSessionModelId(event: unknown, ctx: ObservMeHandlerContext, session: ObservMeTelemetrySession): string {
|
|
1986
|
+
return readModelId(event, ctx) ?? readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT) ?? "unknown";
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
export function resolveSessionThinkingLevel(
|
|
1990
|
+
event: unknown,
|
|
1991
|
+
ctx: ObservMeHandlerContext,
|
|
1992
|
+
session: ObservMeTelemetrySession,
|
|
1993
|
+
): string {
|
|
1994
|
+
return readThinkingLevel(event, ctx) ?? readString(session.sessionAttributes, SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT) ?? "unknown";
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
export function readModelProvider(event: unknown, ctx: ObservMeHandlerContext): string | undefined {
|
|
1998
|
+
const payload = readChangePayload(event, "model_change");
|
|
1999
|
+
const selectedModel = readModelObject(payload);
|
|
2000
|
+
|
|
2001
|
+
return (
|
|
2002
|
+
readString(payload, "provider") ??
|
|
2003
|
+
readString(payload, "modelProvider") ??
|
|
2004
|
+
readString(payload, "model_provider") ??
|
|
2005
|
+
readString(selectedModel, "provider") ??
|
|
2006
|
+
readString(selectedModel, "modelProvider") ??
|
|
2007
|
+
readString(ctx.model, "provider") ??
|
|
2008
|
+
readString(ctx, "modelProvider") ??
|
|
2009
|
+
readString(ctx, "model_provider")
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
export function readModelId(event: unknown, ctx: ObservMeHandlerContext): string | undefined {
|
|
2014
|
+
const payload = readChangePayload(event, "model_change");
|
|
2015
|
+
const selectedModel = readModelObject(payload);
|
|
2016
|
+
|
|
2017
|
+
return (
|
|
2018
|
+
readString(payload, "modelId") ??
|
|
2019
|
+
readString(payload, "model_id") ??
|
|
2020
|
+
readString(payload, "modelName") ??
|
|
2021
|
+
readString(payload, "selectedModel") ??
|
|
2022
|
+
readString(payload, "selection") ??
|
|
2023
|
+
readString(payload, "model") ??
|
|
2024
|
+
readModelIdFromObject(selectedModel) ??
|
|
2025
|
+
readString(ctx.model, "id") ??
|
|
2026
|
+
readString(ctx.model, "model") ??
|
|
2027
|
+
readString(ctx, "modelId") ??
|
|
2028
|
+
readString(ctx, "model_id")
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
export function readThinkingLevel(event: unknown, ctx: ObservMeHandlerContext): string | undefined {
|
|
2033
|
+
const payload = readChangePayload(event, "thinking_level_change");
|
|
2034
|
+
const selectedThinking = readUnknown(payload, "thinking") ?? readUnknown(payload, "selection");
|
|
2035
|
+
|
|
2036
|
+
return (
|
|
2037
|
+
readString(payload, "thinkingLevel") ??
|
|
2038
|
+
readString(payload, "thinking_level") ??
|
|
2039
|
+
readString(payload, "level") ??
|
|
2040
|
+
readString(payload, "selection") ??
|
|
2041
|
+
readString(selectedThinking, "level") ??
|
|
2042
|
+
readString(ctx.thinking, "level") ??
|
|
2043
|
+
readString(ctx, "thinkingLevel") ??
|
|
2044
|
+
readString(ctx, "thinking_level")
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
export function readModelObject(value: unknown): unknown {
|
|
2049
|
+
const selectedModel = readUnknown(value, "selectedModel") ?? readUnknown(value, "selection") ?? readUnknown(value, "model");
|
|
2050
|
+
return typeof selectedModel === "string" ? undefined : selectedModel;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
export function readModelIdFromObject(value: unknown): string | undefined {
|
|
2054
|
+
return readString(value, "id") ?? readString(value, "model") ?? readString(value, "modelId") ?? readString(value, "name");
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
export function readChangePayload(event: unknown, entryType: string): unknown {
|
|
2058
|
+
const entry = readUnknown(event, "entry");
|
|
2059
|
+
if (readString(entry, "type") === entryType) return entry;
|
|
2060
|
+
return event;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
export function readChangeEntryId(event: unknown, entryType: string): string | undefined {
|
|
2064
|
+
return readString(readChangePayload(event, entryType), "id");
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
export function readChangeEntryParentId(event: unknown, entryType: string): string | undefined {
|
|
2068
|
+
const payload = readChangePayload(event, entryType);
|
|
2069
|
+
return readString(payload, "parentId") ?? readString(payload, "parent_id");
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
export function readChangeEntryType(event: unknown, entryType: string): string {
|
|
2073
|
+
return readString(readChangePayload(event, entryType), "type") ?? entryType;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
export function withoutUndefinedAttributes(attributes: Record<string, AttributePrimitive | undefined>): AttributeMap {
|
|
2077
|
+
return Object.fromEntries(Object.entries(attributes).filter((entry): entry is [string, AttributePrimitive] => entry[1] !== undefined));
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
export function hashValue(value: string, config: ObservMeConfig): string | undefined {
|
|
2081
|
+
return trySha256(value, config);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
export function readString(value: unknown, key: string): string | undefined {
|
|
2085
|
+
const child = readUnknown(value, key);
|
|
2086
|
+
if (typeof child === "string" && child.length > 0) return child;
|
|
2087
|
+
if (typeof child === "number" || typeof child === "boolean") return String(child);
|
|
2088
|
+
return undefined;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
export function readTreeId(value: unknown, key: string): string | undefined {
|
|
2092
|
+
const child = readUnknown(value, key);
|
|
2093
|
+
if (child === null) return "root";
|
|
2094
|
+
if (typeof child === "string" && child.length > 0) return child;
|
|
2095
|
+
if (typeof child === "number" || typeof child === "boolean") return String(child);
|
|
2096
|
+
return undefined;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
export function readOptionalString(value: unknown, key: string): string | undefined {
|
|
2100
|
+
const child = readUnknown(value, key);
|
|
2101
|
+
if (typeof child === "string") return child;
|
|
2102
|
+
if (typeof child === "number" || typeof child === "boolean") return String(child);
|
|
2103
|
+
return undefined;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
export function readBoolean(value: unknown, key: string): boolean | undefined {
|
|
2107
|
+
const child = readUnknown(value, key);
|
|
2108
|
+
return typeof child === "boolean" ? child : undefined;
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
export function readInteger(value: unknown, key: string): number | undefined {
|
|
2112
|
+
const child = readUnknown(value, key);
|
|
2113
|
+
if (typeof child === "number" && Number.isInteger(child)) return child;
|
|
2114
|
+
if (typeof child === "string" && /^\d+$/u.test(child)) return Number(child);
|
|
2115
|
+
return undefined;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
export function readNumber(value: unknown, key: string): number | undefined {
|
|
2119
|
+
const child = readUnknown(value, key);
|
|
2120
|
+
if (typeof child === "number" && Number.isFinite(child)) return child;
|
|
2121
|
+
if (typeof child === "string" && child.trim() !== "") {
|
|
2122
|
+
const parsed = Number(child);
|
|
2123
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
2124
|
+
}
|
|
2125
|
+
return undefined;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
export function readArray(value: unknown, key: string): unknown[] | undefined {
|
|
2129
|
+
const child = readUnknown(value, key);
|
|
2130
|
+
return Array.isArray(child) ? child : undefined;
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
export function readUnknown(value: unknown, key: string): unknown {
|
|
2134
|
+
if (!isRecord(value)) return undefined;
|
|
2135
|
+
return value[key];
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
2139
|
+
return typeof value === "object" && value !== null;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
export function isMissingFileError(error: unknown): boolean {
|
|
2143
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
export function errorClass(error: unknown): string {
|
|
2147
|
+
return error instanceof Error ? error.name : typeof error;
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
export function normalizeMetricValue(value: string, fallback: string): string {
|
|
2151
|
+
const normalizedValue = value.trim().toLowerCase().replaceAll(/[^a-z0-9_.:-]/gu, "_");
|
|
2152
|
+
if (/^[a-z][a-z0-9_.:-]{0,63}$/u.test(normalizedValue)) return normalizedValue;
|
|
2153
|
+
return fallback;
|
|
2154
|
+
}
|