@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,115 @@
|
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { recordObsSessionLlmCall } from "../../commands/obs-session.ts";
|
|
3
|
+
import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
|
|
4
|
+
import { SPAN_NAMES } from "../../semconv/spans.ts";
|
|
5
|
+
import {
|
|
6
|
+
buildLlmFinalAttributes,
|
|
7
|
+
buildLlmRequestAttributes,
|
|
8
|
+
buildLlmResponseAttributes,
|
|
9
|
+
deleteCurrentLlmRequest,
|
|
10
|
+
emitLifecycleLog,
|
|
11
|
+
endActiveSpan,
|
|
12
|
+
isAssistantMessage,
|
|
13
|
+
isBashExecutionMessage,
|
|
14
|
+
isLlmError,
|
|
15
|
+
llmMetricLabels,
|
|
16
|
+
nextLlmRequestId,
|
|
17
|
+
readMessage,
|
|
18
|
+
recordLlmSizeMetrics,
|
|
19
|
+
recordLlmUsageMetrics,
|
|
20
|
+
recordOptionalLlmContent,
|
|
21
|
+
recordOptionalPromptContent,
|
|
22
|
+
recordPromptSizeMetric,
|
|
23
|
+
recordSpanDurationMs,
|
|
24
|
+
resolveCurrentLlmSpan,
|
|
25
|
+
resolveLlmParentSpan,
|
|
26
|
+
startActiveChildSpan,
|
|
27
|
+
} from "../handler-internals.ts";
|
|
28
|
+
import type { HandlerRegistrar } from "../handler-runtime.ts";
|
|
29
|
+
import type { Handler, HandlerSessionState, ObservMeHandlerContext } from "../handler-types.ts";
|
|
30
|
+
import { recordBashExecution } from "./tool-bash.ts";
|
|
31
|
+
|
|
32
|
+
export function registerLlmHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
|
|
33
|
+
registrar.add("before_provider_request", createBeforeProviderRequestHandler(state));
|
|
34
|
+
registrar.add("after_provider_response", createAfterProviderResponseHandler(state));
|
|
35
|
+
registrar.add("message_end", createMessageEndHandler(state));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createBeforeProviderRequestHandler(state: HandlerSessionState): Handler {
|
|
39
|
+
return handleBeforeProviderRequest.bind(undefined, state);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleBeforeProviderRequest(
|
|
43
|
+
state: HandlerSessionState,
|
|
44
|
+
event: unknown,
|
|
45
|
+
ctx: ObservMeHandlerContext,
|
|
46
|
+
): void {
|
|
47
|
+
const session = state.session;
|
|
48
|
+
if (!session) return;
|
|
49
|
+
|
|
50
|
+
const requestId = nextLlmRequestId(session, event);
|
|
51
|
+
const parentSpan = resolveLlmParentSpan(session);
|
|
52
|
+
const attributes = buildLlmRequestAttributes(event, ctx, session, requestId);
|
|
53
|
+
const span = startActiveChildSpan(session, SPAN_NAMES.PI_LLM_REQUEST, parentSpan, attributes, "llm_request");
|
|
54
|
+
|
|
55
|
+
recordPromptSizeMetric(session, event, llmMetricLabels(session, attributes));
|
|
56
|
+
recordOptionalPromptContent(session, span, event);
|
|
57
|
+
session.currentLlmRequestId = requestId;
|
|
58
|
+
session.spans.activeLlmRequests.set(requestId, span);
|
|
59
|
+
session.metrics.llmRequests.add(1, llmMetricLabels(session, attributes));
|
|
60
|
+
recordObsSessionLlmCall();
|
|
61
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.LLM_REQUEST_STARTED, attributes);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createAfterProviderResponseHandler(state: HandlerSessionState): Handler {
|
|
65
|
+
return handleAfterProviderResponse.bind(undefined, state);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleAfterProviderResponse(
|
|
69
|
+
state: HandlerSessionState,
|
|
70
|
+
event: unknown,
|
|
71
|
+
_ctx: ObservMeHandlerContext,
|
|
72
|
+
): void {
|
|
73
|
+
const session = state.session;
|
|
74
|
+
if (!session) return;
|
|
75
|
+
|
|
76
|
+
const span = resolveCurrentLlmSpan(session, event);
|
|
77
|
+
span?.setAttributes(buildLlmResponseAttributes(event));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createMessageEndHandler(state: HandlerSessionState): Handler {
|
|
81
|
+
return handleMessageEnd.bind(undefined, state);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleMessageEnd(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
85
|
+
const session = state.session;
|
|
86
|
+
if (!session) return;
|
|
87
|
+
|
|
88
|
+
const message = readMessage(event);
|
|
89
|
+
if (isBashExecutionMessage(message)) {
|
|
90
|
+
recordBashExecution(session, event);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!isAssistantMessage(message)) return;
|
|
94
|
+
|
|
95
|
+
const span = resolveCurrentLlmSpan(session, event);
|
|
96
|
+
const attributes = buildLlmFinalAttributes(message, session);
|
|
97
|
+
const labels = llmMetricLabels(session, attributes);
|
|
98
|
+
|
|
99
|
+
span?.setAttributes(attributes);
|
|
100
|
+
recordLlmUsageMetrics(session, message, labels);
|
|
101
|
+
recordLlmSizeMetrics(session, message, labels);
|
|
102
|
+
recordOptionalLlmContent(session, span, message);
|
|
103
|
+
|
|
104
|
+
if (isLlmError(message)) {
|
|
105
|
+
span?.setStatus({ code: SpanStatusCode.ERROR, message: "provider_error" });
|
|
106
|
+
session.metrics.llmErrors.add(1, labels);
|
|
107
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.LLM_REQUEST_FAILED, attributes, "ERROR");
|
|
108
|
+
} else {
|
|
109
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.LLM_REQUEST_COMPLETED, attributes);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
recordSpanDurationMs(span, session.metrics.llmRequestDurationMs, labels);
|
|
113
|
+
endActiveSpan(session, span);
|
|
114
|
+
deleteCurrentLlmRequest(session, event);
|
|
115
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { COMPACTION_ATTRIBUTES, SESSION_ATTRIBUTES } from "../../semconv/attributes.ts";
|
|
3
|
+
import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
|
|
4
|
+
import { SPAN_NAMES } from "../../semconv/spans.ts";
|
|
5
|
+
import {
|
|
6
|
+
buildBranchAttributes,
|
|
7
|
+
buildBranchPreparationState,
|
|
8
|
+
buildCompactionAttributes,
|
|
9
|
+
buildModelChangeAttributes,
|
|
10
|
+
buildThinkingLevelChangeAttributes,
|
|
11
|
+
emitStructuredLog,
|
|
12
|
+
endActiveSpan,
|
|
13
|
+
metricLabels,
|
|
14
|
+
modelChangeMetricLabels,
|
|
15
|
+
resolveOperationParentSpan,
|
|
16
|
+
startActiveChildSpan,
|
|
17
|
+
thinkingLevelChangeMetricLabels,
|
|
18
|
+
updateCurrentSessionAttributes,
|
|
19
|
+
} from "../handler-internals.ts";
|
|
20
|
+
import type { HandlerRegistrar } from "../handler-runtime.ts";
|
|
21
|
+
import type {
|
|
22
|
+
AttributeMap,
|
|
23
|
+
Handler,
|
|
24
|
+
HandlerSessionState,
|
|
25
|
+
ObservMeHandlerContext,
|
|
26
|
+
ObservMeTelemetrySession,
|
|
27
|
+
} from "../handler-types.ts";
|
|
28
|
+
|
|
29
|
+
export function registerSessionEventHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
|
|
30
|
+
registrar.add("model_select", createModelChangeHandler(state));
|
|
31
|
+
registrar.add("model_change", createModelChangeHandler(state));
|
|
32
|
+
registrar.add("thinking_level_select", createThinkingLevelChangeHandler(state));
|
|
33
|
+
registrar.add("thinking_level_change", createThinkingLevelChangeHandler(state));
|
|
34
|
+
registrar.add("session_before_tree", createSessionBeforeTreeHandler(state));
|
|
35
|
+
registrar.add("session_tree", createBranchHandler(state));
|
|
36
|
+
registrar.add("session_compact", createCompactionHandler(state));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createModelChangeHandler(state: HandlerSessionState): Handler {
|
|
40
|
+
return handleModelChange.bind(undefined, state);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function handleModelChange(state: HandlerSessionState, event: unknown, ctx: ObservMeHandlerContext): void {
|
|
44
|
+
const session = state.session;
|
|
45
|
+
if (!session) return;
|
|
46
|
+
|
|
47
|
+
const attributes = buildModelChangeAttributes(event, ctx, session);
|
|
48
|
+
|
|
49
|
+
updateCurrentSessionAttributes(session, attributes, [
|
|
50
|
+
SESSION_ATTRIBUTES.PI_MODEL_PROVIDER_CURRENT,
|
|
51
|
+
SESSION_ATTRIBUTES.PI_MODEL_ID_CURRENT,
|
|
52
|
+
]);
|
|
53
|
+
session.metrics.modelChanges.add(1, modelChangeMetricLabels(session, attributes));
|
|
54
|
+
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.MODEL_CHANGED, attributes);
|
|
55
|
+
emitStructuredLog(session.logger, LOG_EVENT_NAMES.MODEL_CHANGED, "model", attributes);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createThinkingLevelChangeHandler(state: HandlerSessionState): Handler {
|
|
59
|
+
return handleThinkingLevelChange.bind(undefined, state);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleThinkingLevelChange(
|
|
63
|
+
state: HandlerSessionState,
|
|
64
|
+
event: unknown,
|
|
65
|
+
ctx: ObservMeHandlerContext,
|
|
66
|
+
): void {
|
|
67
|
+
const session = state.session;
|
|
68
|
+
if (!session) return;
|
|
69
|
+
|
|
70
|
+
const attributes = buildThinkingLevelChangeAttributes(event, ctx, session);
|
|
71
|
+
|
|
72
|
+
updateCurrentSessionAttributes(session, attributes, [SESSION_ATTRIBUTES.PI_THINKING_LEVEL_CURRENT]);
|
|
73
|
+
session.metrics.thinkingLevelChanges.add(1, thinkingLevelChangeMetricLabels(session));
|
|
74
|
+
session.sessionSpan?.addEvent(LOG_EVENT_NAMES.THINKING_CHANGED, attributes);
|
|
75
|
+
emitStructuredLog(session.logger, LOG_EVENT_NAMES.THINKING_CHANGED, "thinking", attributes);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function createSessionBeforeTreeHandler(state: HandlerSessionState): Handler {
|
|
79
|
+
return handleSessionBeforeTree.bind(undefined, state);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function handleSessionBeforeTree(
|
|
83
|
+
state: HandlerSessionState,
|
|
84
|
+
event: unknown,
|
|
85
|
+
_ctx: ObservMeHandlerContext,
|
|
86
|
+
): void {
|
|
87
|
+
const session = state.session;
|
|
88
|
+
if (!session) return;
|
|
89
|
+
|
|
90
|
+
session.currentBranchPreparation = buildBranchPreparationState(event, session.config);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createBranchHandler(state: HandlerSessionState): Handler {
|
|
94
|
+
return handleBranch.bind(undefined, state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleBranch(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
98
|
+
const session = state.session;
|
|
99
|
+
if (!session) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
recordBranch(session, event);
|
|
103
|
+
} finally {
|
|
104
|
+
session.currentBranchPreparation = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function recordBranch(session: ObservMeTelemetrySession, event: unknown): void {
|
|
109
|
+
const attributes = buildBranchAttributes(event, session);
|
|
110
|
+
const labels = metricLabels(session.config, session.lineage);
|
|
111
|
+
const span = startActiveChildSpan(session, SPAN_NAMES.PI_BRANCH, resolveOperationParentSpan(session), attributes, "branch");
|
|
112
|
+
|
|
113
|
+
span.addEvent(LOG_EVENT_NAMES.BRANCH_CREATED, attributes);
|
|
114
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
115
|
+
endActiveSpan(session, span);
|
|
116
|
+
session.metrics.branches.add(1, labels);
|
|
117
|
+
emitStructuredLog(session.logger, LOG_EVENT_NAMES.BRANCH_CREATED, "branch", attributes);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createCompactionHandler(state: HandlerSessionState): Handler {
|
|
121
|
+
return handleCompaction.bind(undefined, state);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function handleCompaction(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
125
|
+
const session = state.session;
|
|
126
|
+
if (!session) return;
|
|
127
|
+
|
|
128
|
+
recordCompaction(session, event);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function recordCompaction(session: ObservMeTelemetrySession, event: unknown): void {
|
|
132
|
+
const attributes = buildCompactionAttributes(event, session);
|
|
133
|
+
const labels = metricLabels(session.config, session.lineage);
|
|
134
|
+
const span = startActiveChildSpan(
|
|
135
|
+
session,
|
|
136
|
+
SPAN_NAMES.PI_COMPACTION,
|
|
137
|
+
resolveOperationParentSpan(session),
|
|
138
|
+
attributes,
|
|
139
|
+
"compaction",
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
span.addEvent(LOG_EVENT_NAMES.COMPACTION_CREATED, attributes);
|
|
143
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
144
|
+
endActiveSpan(session, span);
|
|
145
|
+
session.metrics.compactions.add(1, labels);
|
|
146
|
+
recordCompactionTokensBefore(session, attributes, labels);
|
|
147
|
+
emitStructuredLog(session.logger, LOG_EVENT_NAMES.COMPACTION_CREATED, "compaction", attributes);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function recordCompactionTokensBefore(
|
|
151
|
+
session: ObservMeTelemetrySession,
|
|
152
|
+
attributes: AttributeMap,
|
|
153
|
+
labels: Record<string, string>,
|
|
154
|
+
): void {
|
|
155
|
+
const tokensBefore = attributes[COMPACTION_ATTRIBUTES.PI_COMPACTION_TOKENS_BEFORE];
|
|
156
|
+
if (typeof tokensBefore !== "number") return;
|
|
157
|
+
|
|
158
|
+
session.metrics.compactionTokensBefore.record(tokensBefore, labels);
|
|
159
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
|
+
import { TOOL_ATTRIBUTES } from "../../semconv/attributes.ts";
|
|
3
|
+
import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
|
|
4
|
+
import { SPAN_NAMES } from "../../semconv/spans.ts";
|
|
5
|
+
import {
|
|
6
|
+
bashErrorClass,
|
|
7
|
+
bashExecutionFailed,
|
|
8
|
+
bashExecutionMetricLabels,
|
|
9
|
+
bashFailureMetricLabels,
|
|
10
|
+
buildBashExecutionAttributes,
|
|
11
|
+
buildBashPreExecutionAttributes,
|
|
12
|
+
buildToolCallInputAttributes,
|
|
13
|
+
buildToolCompletionLogAttributes,
|
|
14
|
+
buildToolFinalAttributes,
|
|
15
|
+
buildToolResultAttributes,
|
|
16
|
+
closePendingUserBashOperation,
|
|
17
|
+
deleteCurrentToolCall,
|
|
18
|
+
emitCapturedToolErrorLog,
|
|
19
|
+
emitLifecycleLog,
|
|
20
|
+
endActiveSpan,
|
|
21
|
+
hasBashCompletionResult,
|
|
22
|
+
mergeToolStateLabels,
|
|
23
|
+
nextToolCallId,
|
|
24
|
+
readBashPayload,
|
|
25
|
+
readBashPreExecutionTimestampMs,
|
|
26
|
+
recordMissingToolCallIdDrop,
|
|
27
|
+
recordOptionalBashContent,
|
|
28
|
+
recordOptionalToolArguments,
|
|
29
|
+
recordOptionalToolResult,
|
|
30
|
+
recordSpanDurationMs,
|
|
31
|
+
recordTelemetryDrop,
|
|
32
|
+
resolveBashEventDurationMs,
|
|
33
|
+
resolveCurrentToolCallId,
|
|
34
|
+
resolveStandaloneBashEventDurationMs,
|
|
35
|
+
resolveToolCallState,
|
|
36
|
+
resolveToolParentSpan,
|
|
37
|
+
safeElapsedDurationMs,
|
|
38
|
+
startActiveChildSpan,
|
|
39
|
+
startToolCallState,
|
|
40
|
+
toolEventHasAmbiguousMissingToolCallId,
|
|
41
|
+
} from "../handler-internals.ts";
|
|
42
|
+
import { monotonicNowMs } from "../handler-runtime.ts";
|
|
43
|
+
import type { HandlerRegistrar } from "../handler-runtime.ts";
|
|
44
|
+
import type {
|
|
45
|
+
Handler,
|
|
46
|
+
HandlerSessionState,
|
|
47
|
+
ObservMeHandlerContext,
|
|
48
|
+
ObservMeTelemetrySession,
|
|
49
|
+
PendingBashOperationState,
|
|
50
|
+
ToolCallState,
|
|
51
|
+
} from "../handler-types.ts";
|
|
52
|
+
|
|
53
|
+
export function registerToolBashHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
|
|
54
|
+
registrar.add("tool_execution_start", createToolExecutionStartHandler(state));
|
|
55
|
+
registrar.add("tool_call", createToolCallHandler(state));
|
|
56
|
+
registrar.add("tool_result", createToolResultHandler(state));
|
|
57
|
+
registrar.add("tool_execution_end", createToolExecutionEndHandler(state));
|
|
58
|
+
registrar.add("user_bash", createUserBashPreExecutionHandler(state));
|
|
59
|
+
registrar.add("bashExecution", createBashExecutionHandler(state));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function recordBashExecution(session: ObservMeTelemetrySession, event: unknown): void {
|
|
63
|
+
const payload = readBashPayload(event);
|
|
64
|
+
if (!hasBashCompletionResult(payload)) {
|
|
65
|
+
recordTelemetryDrop(session, "bash_completion_incomplete", { operation: "bash_execution" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pending = session.pendingUserBash;
|
|
70
|
+
const attributes = buildBashExecutionAttributes(payload, session);
|
|
71
|
+
const span = pending?.span ?? startActiveChildSpan(
|
|
72
|
+
session,
|
|
73
|
+
SPAN_NAMES.PI_BASH_EXECUTION,
|
|
74
|
+
resolveToolParentSpan(session),
|
|
75
|
+
attributes,
|
|
76
|
+
"bash_execution",
|
|
77
|
+
);
|
|
78
|
+
const failed = bashExecutionFailed(payload);
|
|
79
|
+
const labels = bashExecutionMetricLabels(session, payload, failed);
|
|
80
|
+
const durationMs = resolveBashDurationMs(session, pending, payload);
|
|
81
|
+
|
|
82
|
+
span.setAttributes(attributes);
|
|
83
|
+
recordOptionalBashContent(session, span, payload);
|
|
84
|
+
session.metrics.bashExecutions.add(1, labels);
|
|
85
|
+
|
|
86
|
+
if (failed) {
|
|
87
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: bashErrorClass(payload) });
|
|
88
|
+
session.metrics.bashFailures.add(1, bashFailureMetricLabels(session, payload));
|
|
89
|
+
} else {
|
|
90
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
span.addEvent(LOG_EVENT_NAMES.BASH_COMPLETED, attributes);
|
|
94
|
+
if (durationMs !== undefined) session.metrics.bashDurationMs.record(durationMs, labels);
|
|
95
|
+
endActiveSpan(session, span);
|
|
96
|
+
if (session.pendingUserBash === pending) session.pendingUserBash = undefined;
|
|
97
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.BASH_COMPLETED, attributes, failed ? "ERROR" : "INFO");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createToolExecutionStartHandler(state: HandlerSessionState): Handler {
|
|
101
|
+
return handleToolExecutionStart.bind(undefined, state);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleToolExecutionStart(
|
|
105
|
+
state: HandlerSessionState,
|
|
106
|
+
event: unknown,
|
|
107
|
+
_ctx: ObservMeHandlerContext,
|
|
108
|
+
): void {
|
|
109
|
+
const session = state.session;
|
|
110
|
+
if (!session) return;
|
|
111
|
+
|
|
112
|
+
const toolCallId = nextToolCallId(session, event);
|
|
113
|
+
const toolState = startToolCallState(session, event, toolCallId);
|
|
114
|
+
recordOptionalToolArguments(session, toolState.span, event);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createToolCallHandler(state: HandlerSessionState): Handler {
|
|
118
|
+
return handleToolCall.bind(undefined, state);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleToolCall(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
122
|
+
const session = state.session;
|
|
123
|
+
if (!session) return;
|
|
124
|
+
if (dropAmbiguousToolLifecycleEvent(session, event, "tool_call")) return;
|
|
125
|
+
|
|
126
|
+
const toolCallId = resolveCurrentToolCallId(session, event) ?? nextToolCallId(session, event);
|
|
127
|
+
const toolState = resolveToolCallState(session, event) ?? startToolCallState(session, event, toolCallId);
|
|
128
|
+
const attributes = buildToolCallInputAttributes(event, session.config);
|
|
129
|
+
|
|
130
|
+
toolState.span.setAttributes(attributes);
|
|
131
|
+
mergeToolStateLabels(toolState, attributes);
|
|
132
|
+
recordOptionalToolArguments(session, toolState.span, event);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createToolResultHandler(state: HandlerSessionState): Handler {
|
|
136
|
+
return handleToolResult.bind(undefined, state);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function handleToolResult(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
140
|
+
const session = state.session;
|
|
141
|
+
if (!session) return;
|
|
142
|
+
if (dropAmbiguousToolLifecycleEvent(session, event, "tool_result")) return;
|
|
143
|
+
|
|
144
|
+
const toolState = resolveToolCallState(session, event);
|
|
145
|
+
if (!toolState) return;
|
|
146
|
+
|
|
147
|
+
const attributes = buildToolResultAttributes(event, session.config);
|
|
148
|
+
toolState.span.setAttributes(attributes);
|
|
149
|
+
mergeToolStateLabels(toolState, attributes);
|
|
150
|
+
captureToolResult(session, toolState, event);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function captureToolResult(session: ObservMeTelemetrySession, toolState: ToolCallState, event: unknown): void {
|
|
154
|
+
const capturedResult = recordOptionalToolResult(session, toolState.span, event);
|
|
155
|
+
if (capturedResult) toolState.capturedResult = capturedResult;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createToolExecutionEndHandler(state: HandlerSessionState): Handler {
|
|
159
|
+
return handleToolExecutionEnd.bind(undefined, state);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function handleToolExecutionEnd(
|
|
163
|
+
state: HandlerSessionState,
|
|
164
|
+
event: unknown,
|
|
165
|
+
_ctx: ObservMeHandlerContext,
|
|
166
|
+
): void {
|
|
167
|
+
const session = state.session;
|
|
168
|
+
if (!session) return;
|
|
169
|
+
if (dropAmbiguousToolLifecycleEvent(session, event, "tool_execution_end")) return;
|
|
170
|
+
|
|
171
|
+
const toolCallId = resolveCurrentToolCallId(session, event);
|
|
172
|
+
const toolState = toolCallId ? resolveToolCallState(session, event) : undefined;
|
|
173
|
+
if (!toolCallId || !toolState) {
|
|
174
|
+
recordTelemetryDrop(session, "tool_call_missing_end", { operation: "tool_execution_end" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const resultAttributes = buildToolResultAttributes(event, session.config);
|
|
179
|
+
const finalAttributes = buildToolFinalAttributes(event);
|
|
180
|
+
const failed = finalAttributes[TOOL_ATTRIBUTES.PI_TOOL_SUCCESS] === false;
|
|
181
|
+
|
|
182
|
+
toolState.span.setAttributes({ ...resultAttributes, ...finalAttributes });
|
|
183
|
+
mergeToolStateLabels(toolState, resultAttributes);
|
|
184
|
+
captureToolResult(session, toolState, event);
|
|
185
|
+
const resultSizeChars = resultAttributes[TOOL_ATTRIBUTES.PI_TOOL_RESULT_SIZE];
|
|
186
|
+
if (typeof resultSizeChars === "number") session.metrics.toolResultSizeChars.record(resultSizeChars, toolState.labels);
|
|
187
|
+
const completionLogAttributes = buildToolCompletionLogAttributes(toolState, finalAttributes);
|
|
188
|
+
|
|
189
|
+
if (failed) {
|
|
190
|
+
const errorClass = String(finalAttributes[TOOL_ATTRIBUTES.PI_TOOL_ERROR_CLASS] ?? "tool_error");
|
|
191
|
+
toolState.span.setStatus({ code: SpanStatusCode.ERROR, message: errorClass });
|
|
192
|
+
session.metrics.toolFailures.add(1, toolState.labels);
|
|
193
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.TOOL_CALL_FAILED, completionLogAttributes, "ERROR");
|
|
194
|
+
if (toolState.capturedResult) emitCapturedToolErrorLog(session, completionLogAttributes, toolState.capturedResult);
|
|
195
|
+
} else {
|
|
196
|
+
toolState.span.setStatus({ code: SpanStatusCode.OK });
|
|
197
|
+
emitLifecycleLog(session.logger, LOG_EVENT_NAMES.TOOL_CALL_COMPLETED, completionLogAttributes);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
recordSpanDurationMs(toolState.span, session.metrics.toolDurationMs, toolState.labels);
|
|
201
|
+
endActiveSpan(session, toolState.span);
|
|
202
|
+
deleteCurrentToolCall(session, toolCallId);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function createUserBashPreExecutionHandler(state: HandlerSessionState): Handler {
|
|
206
|
+
return handleUserBashPreExecution.bind(undefined, state);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function handleUserBashPreExecution(
|
|
210
|
+
state: HandlerSessionState,
|
|
211
|
+
event: unknown,
|
|
212
|
+
_ctx: ObservMeHandlerContext,
|
|
213
|
+
): void {
|
|
214
|
+
const session = state.session;
|
|
215
|
+
if (!session) return;
|
|
216
|
+
|
|
217
|
+
startPendingUserBashOperation(session, event);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createBashExecutionHandler(state: HandlerSessionState): Handler {
|
|
221
|
+
return handleBashExecution.bind(undefined, state);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function handleBashExecution(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
|
|
225
|
+
const session = state.session;
|
|
226
|
+
if (!session) return;
|
|
227
|
+
|
|
228
|
+
recordBashExecution(session, event);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function dropAmbiguousToolLifecycleEvent(
|
|
232
|
+
session: ObservMeTelemetrySession,
|
|
233
|
+
event: unknown,
|
|
234
|
+
operation: string,
|
|
235
|
+
): boolean {
|
|
236
|
+
if (!toolEventHasAmbiguousMissingToolCallId(session, event)) return false;
|
|
237
|
+
|
|
238
|
+
recordMissingToolCallIdDrop(session, operation);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function startPendingUserBashOperation(session: ObservMeTelemetrySession, event: unknown): void {
|
|
243
|
+
if (session.pendingUserBash) {
|
|
244
|
+
closePendingUserBashOperation(session, "bash_overlap_ambiguous", true);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const attributes = buildBashPreExecutionAttributes(event, session);
|
|
249
|
+
const span = startActiveChildSpan(
|
|
250
|
+
session,
|
|
251
|
+
SPAN_NAMES.PI_BASH_EXECUTION,
|
|
252
|
+
resolveToolParentSpan(session),
|
|
253
|
+
attributes,
|
|
254
|
+
"bash_execution",
|
|
255
|
+
);
|
|
256
|
+
session.pendingUserBash = {
|
|
257
|
+
span,
|
|
258
|
+
startedAtMs: session.now?.() ?? monotonicNowMs(),
|
|
259
|
+
eventStartedAtMs: readBashPreExecutionTimestampMs(event),
|
|
260
|
+
};
|
|
261
|
+
recordOptionalBashContent(session, span, event);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resolveBashDurationMs(
|
|
265
|
+
session: ObservMeTelemetrySession,
|
|
266
|
+
pending: PendingBashOperationState | undefined,
|
|
267
|
+
completionEvent: unknown,
|
|
268
|
+
): number | undefined {
|
|
269
|
+
if (!pending) return resolveStandaloneBashEventDurationMs(completionEvent);
|
|
270
|
+
|
|
271
|
+
const eventDurationMs = resolveBashEventDurationMs(pending.eventStartedAtMs, completionEvent);
|
|
272
|
+
if (eventDurationMs !== undefined) return eventDurationMs;
|
|
273
|
+
|
|
274
|
+
return safeElapsedDurationMs(pending.startedAtMs, session.now?.() ?? monotonicNowMs());
|
|
275
|
+
}
|