@senad-d/observme 0.1.3 → 0.1.5
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 +4 -0
- package/CHANGELOG.md +48 -0
- package/README.md +36 -11
- package/dashboards/observme-agent-node-graphs.json +5 -5
- package/dashboards/observme-agents.json +169 -4
- package/dashboards/observme-alerts.yaml +16 -3
- package/dashboards/observme-overview.json +6 -6
- package/dashboards/observme-trace-journey.json +4 -4
- package/docs/agent-subagent-observability-requirements.md +19 -10
- package/docs/compatibility-matrix.md +21 -4
- package/docs/configuration.md +7 -2
- package/docs/extension-integration.md +20 -13
- package/docs/reference/03-pi-event-and-session-model.md +6 -6
- package/docs/reference/04-telemetry-semantic-conventions.md +39 -1
- package/docs/reference/05-otel-pipeline-and-collector.md +23 -10
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/09-dashboards-alerts-slos.md +132 -3
- package/docs/reference/10-testing-release-operations.md +44 -14
- package/docs/reference/11-deployment-runbooks.md +82 -5
- package/docs/reference/12-configuration-reference.md +48 -5
- package/docs/review-validation.md +17 -0
- package/examples/README.md +7 -2
- package/examples/collector.yaml +8 -8
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +3 -2
- package/package.json +14 -4
- package/src/commands/obs-agents.ts +17 -12
- package/src/commands/obs-backfill.ts +2 -2
- package/src/commands/obs-command-support.ts +2 -1
- package/src/commands/obs-cost.ts +15 -7
- package/src/commands/obs-health.ts +22 -2
- package/src/commands/obs-loki-summary.ts +4 -8
- package/src/commands/obs-session.ts +35 -28
- package/src/commands/obs-status.ts +56 -13
- package/src/commands/obs-tools.ts +19 -8
- package/src/commands/obs-trace.ts +9 -0
- package/src/commands/obs.ts +6 -1
- package/src/config/bootstrap-project-config.ts +50 -32
- package/src/config/defaults.ts +1 -2
- package/src/config/load-config.ts +270 -81
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +18 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +88 -12
- package/src/extension.ts +2 -12
- package/src/integration.ts +6 -2
- package/src/otel/logs.ts +24 -13
- package/src/otel/metrics.ts +24 -13
- package/src/otel/otlp-endpoint.ts +41 -2
- package/src/otel/otlp-http-options.ts +11 -0
- package/src/otel/sdk.ts +155 -9
- package/src/otel/shutdown.ts +39 -11
- package/src/otel/traces.ts +34 -16
- package/src/pi/active-agent-lease.ts +101 -0
- package/src/pi/agent-tree-tracker.ts +14 -1
- package/src/pi/compatibility.ts +121 -0
- package/src/pi/event-handlers/agent-turn.ts +16 -9
- package/src/pi/event-handlers/lifecycle.ts +312 -106
- package/src/pi/event-handlers/llm.ts +17 -9
- package/src/pi/event-handlers/session-events.ts +53 -19
- package/src/pi/event-handlers/tool-bash.ts +21 -27
- package/src/pi/handler-internals.ts +16 -26
- package/src/pi/handler-runtime.ts +159 -54
- package/src/pi/handler-types.ts +40 -17
- package/src/pi/handlers.ts +12 -1
- package/src/pi/integration-api.ts +27 -15
- package/src/pi/session-correlation.ts +167 -0
- package/src/pi/subagent-spawn.ts +164 -31
- package/src/privacy/redact.ts +574 -68
- package/src/privacy/secret-patterns.ts +6 -1
- package/src/query/grafana-readiness.ts +18 -2
- package/src/query/grafana-transport.ts +150 -27
- package/src/query/grafana-url.ts +36 -0
- package/src/query/grafana.ts +4 -136
- package/src/query/loki.ts +2 -4
- package/src/query/prometheus.ts +8 -10
- package/src/query/tempo.ts +2 -4
- package/src/query/trace-link.ts +186 -0
- package/src/safety/display-bounds.ts +84 -0
- package/src/semconv/metrics.ts +7 -0
- package/src/semconv/values.ts +2 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
3
|
import type { Histogram, Span } from "@opentelemetry/api";
|
|
4
|
-
import { trace } from "@opentelemetry/api";
|
|
5
4
|
import type { ObservMeConfig } from "../config/schema.ts";
|
|
6
|
-
import { ObservMeLogSdk } from "../otel/logs.ts";
|
|
7
|
-
import { ObservMeMetricSdk } from "../otel/metrics.ts";
|
|
8
|
-
import { startOtelSdk } from "../otel/sdk.ts";
|
|
9
|
-
import {
|
|
5
|
+
import { createLogSessionScopedOtelSdk, type ObservMeLogSdk } from "../otel/logs.ts";
|
|
6
|
+
import { createMetricSessionScopedOtelSdk, type ObservMeMetricSdk } from "../otel/metrics.ts";
|
|
7
|
+
import { startOtelSdk, toOtelStartupError, type SessionScopedOtelSdk } from "../otel/sdk.ts";
|
|
8
|
+
import { runBoundedOtelOperation } from "../otel/shutdown.ts";
|
|
9
|
+
import { createTraceSessionScopedOtelSdk, type ObservMeTraceSdk } from "../otel/traces.ts";
|
|
10
10
|
import { inheritTenantSaltEnvironment } from "../privacy/hash.ts";
|
|
11
11
|
import { LOG_ATTRIBUTES, RESOURCE_ATTRIBUTES } from "../semconv/attributes.ts";
|
|
12
12
|
import {
|
|
13
13
|
LOG_EVENT_NAMES,
|
|
14
|
+
OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
|
|
14
15
|
OBSERVME_COUNTER_METRIC_NAMES,
|
|
15
16
|
OBSERVME_GAUGE_METRIC_NAMES,
|
|
16
17
|
OBSERVME_HISTOGRAM_METRIC_NAMES,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
OFFICIAL_GENAI_METRIC_NAMES,
|
|
19
20
|
} from "../semconv/metrics.ts";
|
|
20
21
|
import { BoundedMap, type BoundedMapEviction } from "../util/bounded-map.ts";
|
|
22
|
+
import { createActiveAgentLease } from "./active-agent-lease.ts";
|
|
21
23
|
import type { AgentLineageContext } from "./agent-lineage.ts";
|
|
22
24
|
import { buildResourceLineageAttributes } from "./agent-lineage.ts";
|
|
23
25
|
import type { AgentTreeNode } from "./agent-tree-tracker.ts";
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
evictToolCallState,
|
|
31
33
|
evictWaitJoinState,
|
|
32
34
|
isRecord,
|
|
35
|
+
metricLabels,
|
|
33
36
|
normalizeMetricValue,
|
|
34
37
|
readBoolean,
|
|
35
38
|
readString,
|
|
@@ -46,9 +49,12 @@ import type {
|
|
|
46
49
|
HandlerRegistration,
|
|
47
50
|
HandlerSessionState,
|
|
48
51
|
ObservMeHandlerContext,
|
|
52
|
+
PiEventName,
|
|
53
|
+
PiHandler,
|
|
49
54
|
ObservMeMetrics,
|
|
50
55
|
ObservMePiApi,
|
|
51
56
|
ObservMeTelemetrySession,
|
|
57
|
+
RuntimeHandler,
|
|
52
58
|
SpanRegistry,
|
|
53
59
|
StartSessionTelemetryOptions,
|
|
54
60
|
TelemetryMeter,
|
|
@@ -63,6 +69,7 @@ const piApiCompatibilityErrorMessage =
|
|
|
63
69
|
"ObservMe/Pi API compatibility error: expected Pi ExtensionAPI with on(eventName, handler) before registering ObservMe handlers.";
|
|
64
70
|
const eventRegistrationOrder = [
|
|
65
71
|
"session_start",
|
|
72
|
+
"session_info_changed",
|
|
66
73
|
"agent_start",
|
|
67
74
|
"turn_start",
|
|
68
75
|
"before_provider_request",
|
|
@@ -73,18 +80,15 @@ const eventRegistrationOrder = [
|
|
|
73
80
|
"tool_result",
|
|
74
81
|
"tool_execution_end",
|
|
75
82
|
"user_bash",
|
|
76
|
-
"bashExecution",
|
|
77
83
|
"model_select",
|
|
78
|
-
"model_change",
|
|
79
84
|
"thinking_level_select",
|
|
80
|
-
"thinking_level_change",
|
|
81
85
|
"session_before_tree",
|
|
82
86
|
"session_tree",
|
|
83
87
|
"session_compact",
|
|
84
88
|
"turn_end",
|
|
85
89
|
"agent_end",
|
|
86
90
|
"session_shutdown",
|
|
87
|
-
] as const;
|
|
91
|
+
] as const satisfies readonly PiEventName[];
|
|
88
92
|
const eventRegistrationIndexes = createEventRegistrationIndexes();
|
|
89
93
|
|
|
90
94
|
let defaultHandlerErrorRecorder: HandlerErrorRecorder = recordInternalErrorFallback;
|
|
@@ -101,8 +105,8 @@ export class HandlerRegistrar {
|
|
|
101
105
|
this.#errorRecorder = errorRecorder;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
add(eventName:
|
|
105
|
-
this.#registrations.push({ eventName, handler });
|
|
108
|
+
add<Name extends PiEventName>(eventName: Name, handler: PiHandler<Name>): void {
|
|
109
|
+
this.#registrations.push({ eventName, handler: handler as unknown as RuntimeHandler });
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
commit(): void {
|
|
@@ -122,11 +126,12 @@ export class HandlerRegistrar {
|
|
|
122
126
|
export class SerializedLifecycleQueue {
|
|
123
127
|
#previous = Promise.resolve();
|
|
124
128
|
|
|
125
|
-
wrap(fn:
|
|
126
|
-
|
|
129
|
+
wrap<Name extends PiEventName>(fn: PiHandler<Name>): PiHandler<Name> {
|
|
130
|
+
const runtimeHandler = fn as unknown as RuntimeHandler;
|
|
131
|
+
return runSerializedLifecycleHandler.bind(undefined, this, runtimeHandler) as PiHandler<Name>;
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
run(fn:
|
|
134
|
+
run(fn: RuntimeHandler, event: unknown, ctx: Parameters<RuntimeHandler>[1]): Promise<void> {
|
|
130
135
|
// Pi docs guarantee /new, /resume, and /fork emit session_shutdown before session_start;
|
|
131
136
|
// ctx.reload() emits session_shutdown then session_start while the old command call frame can
|
|
132
137
|
// continue. ObservMe queues both lifecycle handlers together so async startup, shutdown,
|
|
@@ -143,19 +148,30 @@ export function resolveObservMePiApi(pi: unknown): ObservMePiApi {
|
|
|
143
148
|
const on = pi.on;
|
|
144
149
|
if (typeof on !== "function") throw new TypeError(piApiCompatibilityErrorMessage);
|
|
145
150
|
|
|
146
|
-
|
|
151
|
+
const appendEntry = typeof pi.appendEntry === "function"
|
|
152
|
+
? (pi.appendEntry.bind(pi) as NonNullable<ObservMePiApi["appendEntry"]>)
|
|
153
|
+
: undefined;
|
|
154
|
+
const getThinkingLevel = typeof pi.getThinkingLevel === "function"
|
|
155
|
+
? (pi.getThinkingLevel.bind(pi) as NonNullable<ObservMePiApi["getThinkingLevel"]>)
|
|
156
|
+
: undefined;
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
on: on.bind(pi) as ObservMePiApi["on"],
|
|
160
|
+
appendEntry,
|
|
161
|
+
getThinkingLevel,
|
|
162
|
+
};
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
export function setDefaultHandlerErrorRecorder(recorder: HandlerErrorRecorder): void {
|
|
150
166
|
defaultHandlerErrorRecorder = recorder;
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
export function safeHandler(
|
|
169
|
+
export function safeHandler<Event = unknown, Context = ObservMeHandlerContext>(
|
|
154
170
|
name: string,
|
|
155
|
-
fn: Handler,
|
|
171
|
+
fn: Handler<Event, Context>,
|
|
156
172
|
recorder: HandlerErrorRecorder = defaultHandlerErrorRecorder,
|
|
157
|
-
): Handler {
|
|
158
|
-
return runSafeHandler.bind(undefined, name, fn, recorder);
|
|
173
|
+
): Handler<Event, Context> {
|
|
174
|
+
return (runSafeHandler<Event, Context>).bind(undefined, name, fn, recorder);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
export function createStatefulHandlerErrorRecorder(
|
|
@@ -166,20 +182,37 @@ export function createStatefulHandlerErrorRecorder(
|
|
|
166
182
|
}
|
|
167
183
|
|
|
168
184
|
export async function startSessionTelemetry(options: StartSessionTelemetryOptions): Promise<ObservMeTelemetrySession> {
|
|
185
|
+
if (!options.config.enabled) throw new Error("ObservMe telemetry cannot start while ObservMe is disabled.");
|
|
186
|
+
|
|
169
187
|
const config = withTelemetrySessionResourceAttributes(options.config, options.lineage);
|
|
170
|
-
const traceSdk =
|
|
171
|
-
const metricSdk =
|
|
172
|
-
const logSdk =
|
|
173
|
-
const signalSdk = createCompositeOtelSignalSdk(
|
|
188
|
+
const traceSdk = createTraceSessionScopedOtelSdk({ config, agent: options.lineage });
|
|
189
|
+
const metricSdk = createMetricSessionScopedOtelSdk({ config, agent: options.lineage });
|
|
190
|
+
const logSdk = createLogSessionScopedOtelSdk({ config, agent: options.lineage });
|
|
191
|
+
const signalSdk = createCompositeOtelSignalSdk(
|
|
192
|
+
traceSdk,
|
|
193
|
+
metricSdk,
|
|
194
|
+
logSdk,
|
|
195
|
+
config.shutdown.flushTimeoutMs,
|
|
196
|
+
);
|
|
174
197
|
const controller = await startOtelSdk({
|
|
175
198
|
config,
|
|
176
199
|
agent: options.lineage,
|
|
177
200
|
sdkFactory: returnCompositeSignalSdk.bind(undefined, signalSdk),
|
|
178
201
|
});
|
|
179
|
-
const tracer = traceSdk.tracer
|
|
202
|
+
const tracer = traceSdk.tracer;
|
|
180
203
|
const metrics = createObservMeMetrics(metricSdk.meter);
|
|
181
204
|
const sessionReference: HandlerSessionState = {};
|
|
182
205
|
const getTelemetryDropTarget = resolveSessionTelemetryDropTarget.bind(undefined, sessionReference, metrics);
|
|
206
|
+
const spans = createSpanRegistry(config, metrics, getTelemetryDropTarget);
|
|
207
|
+
const agentTree = createAgentTreeTracker(config, options.lineage, metrics, getTelemetryDropTarget);
|
|
208
|
+
const turnSequences = createTurnSequenceRegistry(config, metrics, getTelemetryDropTarget);
|
|
209
|
+
const activeAgentLease = createActiveAgentLease({
|
|
210
|
+
instrument: metrics.agentLeaseExpiresUnixTimeSeconds,
|
|
211
|
+
leaseDurationMillis: config.metrics.activeAgentLeaseDurationMillis,
|
|
212
|
+
attributes: metricLabels(config, options.lineage),
|
|
213
|
+
wallClockNow: options.wallClockNow,
|
|
214
|
+
enabled: config.metrics.enabled,
|
|
215
|
+
});
|
|
183
216
|
const session: ObservMeTelemetrySession = {
|
|
184
217
|
config,
|
|
185
218
|
lineage: options.lineage,
|
|
@@ -188,14 +221,15 @@ export async function startSessionTelemetry(options: StartSessionTelemetryOption
|
|
|
188
221
|
meter: metricSdk.meter,
|
|
189
222
|
logger: logSdk.logger,
|
|
190
223
|
metrics,
|
|
191
|
-
spans
|
|
192
|
-
|
|
224
|
+
spans,
|
|
225
|
+
activeAgentLease,
|
|
226
|
+
agentTree,
|
|
193
227
|
now: options.now ?? monotonicNowMs,
|
|
194
228
|
activeAgentRecorded: false,
|
|
195
229
|
agentRunSequence: 0,
|
|
196
230
|
llmRequestSequence: 0,
|
|
197
231
|
toolCallSequence: 0,
|
|
198
|
-
turnSequences
|
|
232
|
+
turnSequences,
|
|
199
233
|
};
|
|
200
234
|
|
|
201
235
|
sessionReference.session = session;
|
|
@@ -206,8 +240,9 @@ export function createCompositeOtelSignalSdk(
|
|
|
206
240
|
traceSdk: ObservMeTraceSdk,
|
|
207
241
|
metricSdk: ObservMeMetricSdk,
|
|
208
242
|
logSdk: ObservMeLogSdk,
|
|
243
|
+
cleanupTimeoutMs = 3_000,
|
|
209
244
|
): CompositeOtelSignalSdk {
|
|
210
|
-
return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk);
|
|
245
|
+
return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk, cleanupTimeoutMs);
|
|
211
246
|
}
|
|
212
247
|
|
|
213
248
|
export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
|
|
@@ -252,6 +287,10 @@ export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
|
|
|
252
287
|
eventsObserved: meter.createCounter(OBSERVME_COUNTER_METRIC_NAMES.EVENTS_OBSERVED_TOTAL),
|
|
253
288
|
activeSpans: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_SPANS),
|
|
254
289
|
activeAgents: meter.createUpDownCounter(OBSERVME_GAUGE_METRIC_NAMES.ACTIVE_AGENTS),
|
|
290
|
+
agentLeaseExpiresUnixTimeSeconds: meter.createObservableGauge(
|
|
291
|
+
OBSERVME_GAUGE_METRIC_NAMES.AGENT_LEASE_EXPIRES_UNIXTIME_SECONDS,
|
|
292
|
+
OBSERVME_AGENT_LEASE_METRIC_OPTIONS,
|
|
293
|
+
),
|
|
255
294
|
workflowDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.WORKFLOW_DURATION_MS),
|
|
256
295
|
agentRunDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_RUN_DURATION_MS),
|
|
257
296
|
agentLifetimeDurationMs: createHistogram(meter, OBSERVME_HISTOGRAM_METRIC_NAMES.AGENT_LIFETIME_DURATION_MS),
|
|
@@ -378,8 +417,8 @@ export function buildTelemetryInstanceResourceAttributes(instanceId: string): Re
|
|
|
378
417
|
};
|
|
379
418
|
}
|
|
380
419
|
|
|
381
|
-
function createEventRegistrationIndexes(): Map<
|
|
382
|
-
const indexes = new Map<
|
|
420
|
+
function createEventRegistrationIndexes(): Map<PiEventName, number> {
|
|
421
|
+
const indexes = new Map<PiEventName, number>();
|
|
383
422
|
for (const [index, name] of eventRegistrationOrder.entries()) indexes.set(name, index);
|
|
384
423
|
return indexes;
|
|
385
424
|
}
|
|
@@ -392,30 +431,30 @@ function compareHandlerRegistrations(left: HandlerRegistration, right: HandlerRe
|
|
|
392
431
|
return registrationIndex(left.eventName) - registrationIndex(right.eventName);
|
|
393
432
|
}
|
|
394
433
|
|
|
395
|
-
function registrationIndex(eventName:
|
|
434
|
+
function registrationIndex(eventName: PiEventName): number {
|
|
396
435
|
return eventRegistrationIndexes.get(eventName) ?? eventRegistrationOrder.length;
|
|
397
436
|
}
|
|
398
437
|
|
|
399
438
|
function registerObservedHandler(
|
|
400
439
|
api: ObservMePiApi,
|
|
401
|
-
name:
|
|
402
|
-
handler:
|
|
440
|
+
name: PiEventName,
|
|
441
|
+
handler: RuntimeHandler,
|
|
403
442
|
state: HandlerSessionState,
|
|
404
443
|
errorRecorder: HandlerErrorRecorder,
|
|
405
444
|
): void {
|
|
406
445
|
api.on(name, safeHandler(name, observeHandler(name, handler, state), errorRecorder));
|
|
407
446
|
}
|
|
408
447
|
|
|
409
|
-
function observeHandler(name: string, fn:
|
|
448
|
+
function observeHandler(name: string, fn: RuntimeHandler, state: HandlerSessionState): RuntimeHandler {
|
|
410
449
|
return runObservedHandler.bind(undefined, name, fn, state);
|
|
411
450
|
}
|
|
412
451
|
|
|
413
452
|
async function runObservedHandler(
|
|
414
453
|
name: string,
|
|
415
|
-
fn:
|
|
454
|
+
fn: RuntimeHandler,
|
|
416
455
|
state: HandlerSessionState,
|
|
417
456
|
event: unknown,
|
|
418
|
-
ctx:
|
|
457
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
419
458
|
): Promise<void> {
|
|
420
459
|
const startedAtMs = Date.now();
|
|
421
460
|
const sessionBefore = state.session;
|
|
@@ -453,12 +492,12 @@ function recordHandlerObservation(
|
|
|
453
492
|
session.metrics.handlerDurationMs.record(Math.max(0, Date.now() - startedAtMs), { operation, status });
|
|
454
493
|
}
|
|
455
494
|
|
|
456
|
-
async function runSafeHandler(
|
|
495
|
+
async function runSafeHandler<Event, Context>(
|
|
457
496
|
name: string,
|
|
458
|
-
fn: Handler,
|
|
497
|
+
fn: Handler<Event, Context>,
|
|
459
498
|
recorder: HandlerErrorRecorder,
|
|
460
|
-
event:
|
|
461
|
-
ctx:
|
|
499
|
+
event: Event,
|
|
500
|
+
ctx: Context,
|
|
462
501
|
): Promise<void> {
|
|
463
502
|
try {
|
|
464
503
|
await fn(event, ctx);
|
|
@@ -469,14 +508,18 @@ async function runSafeHandler(
|
|
|
469
508
|
|
|
470
509
|
async function runSerializedLifecycleHandler(
|
|
471
510
|
queue: SerializedLifecycleQueue,
|
|
472
|
-
fn:
|
|
511
|
+
fn: RuntimeHandler,
|
|
473
512
|
event: unknown,
|
|
474
|
-
ctx:
|
|
513
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
475
514
|
): Promise<void> {
|
|
476
515
|
await queue.run(fn, event, ctx);
|
|
477
516
|
}
|
|
478
517
|
|
|
479
|
-
async function invokeHandler(
|
|
518
|
+
async function invokeHandler(
|
|
519
|
+
fn: RuntimeHandler,
|
|
520
|
+
event: unknown,
|
|
521
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
522
|
+
): Promise<void> {
|
|
480
523
|
await fn(event, ctx);
|
|
481
524
|
}
|
|
482
525
|
|
|
@@ -484,10 +527,6 @@ function ignoreLifecycleQueueError(): undefined {
|
|
|
484
527
|
return undefined;
|
|
485
528
|
}
|
|
486
529
|
|
|
487
|
-
function callPiOn(pi: Record<string, unknown>, on: ObservMePiApi["on"], eventName: string, handler: Handler): void {
|
|
488
|
-
on.call(pi, eventName, handler);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
530
|
function returnCompositeSignalSdk(signalSdk: CompositeOtelSignalSdk): CompositeOtelSignalSdk {
|
|
492
531
|
return signalSdk;
|
|
493
532
|
}
|
|
@@ -610,17 +649,46 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
|
|
|
610
649
|
readonly traceSdk: ObservMeTraceSdk;
|
|
611
650
|
readonly metricSdk: ObservMeMetricSdk;
|
|
612
651
|
readonly logSdk: ObservMeLogSdk;
|
|
613
|
-
|
|
614
|
-
|
|
652
|
+
readonly #cleanupTimeoutMs: number;
|
|
653
|
+
readonly #startedSignalSdks: SessionScopedOtelSdk[] = [];
|
|
654
|
+
#startingSignalSdk?: SessionScopedOtelSdk;
|
|
655
|
+
#state: CompositeOtelSignalSdk["state"] = "idle";
|
|
656
|
+
|
|
657
|
+
constructor(
|
|
658
|
+
traceSdk: ObservMeTraceSdk,
|
|
659
|
+
metricSdk: ObservMeMetricSdk,
|
|
660
|
+
logSdk: ObservMeLogSdk,
|
|
661
|
+
cleanupTimeoutMs: number,
|
|
662
|
+
) {
|
|
615
663
|
this.traceSdk = traceSdk;
|
|
616
664
|
this.metricSdk = metricSdk;
|
|
617
665
|
this.logSdk = logSdk;
|
|
666
|
+
this.#cleanupTimeoutMs = cleanupTimeoutMs;
|
|
618
667
|
}
|
|
619
668
|
|
|
620
|
-
|
|
621
|
-
this
|
|
622
|
-
|
|
623
|
-
|
|
669
|
+
get state(): CompositeOtelSignalSdk["state"] {
|
|
670
|
+
return this.#state;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async start(): Promise<void> {
|
|
674
|
+
if (this.#state === "started") return;
|
|
675
|
+
if (this.#state !== "idle") throw new Error(`ObservMe OTEL signal SDK cannot start from ${this.#state}.`);
|
|
676
|
+
|
|
677
|
+
this.#state = "starting";
|
|
678
|
+
try {
|
|
679
|
+
await this.startSignalSdk(this.traceSdk);
|
|
680
|
+
await this.startSignalSdk(this.metricSdk);
|
|
681
|
+
await this.startSignalSdk(this.logSdk);
|
|
682
|
+
this.#state = "started";
|
|
683
|
+
} catch (error) {
|
|
684
|
+
const cleanup = await runBoundedOtelOperation(
|
|
685
|
+
"shutdown",
|
|
686
|
+
this.shutdownStartedSignalSdks.bind(this),
|
|
687
|
+
this.#cleanupTimeoutMs,
|
|
688
|
+
);
|
|
689
|
+
this.#state = "failed";
|
|
690
|
+
throw toOtelStartupError(error, cleanup);
|
|
691
|
+
}
|
|
624
692
|
}
|
|
625
693
|
|
|
626
694
|
async forceFlush(): Promise<void> {
|
|
@@ -628,6 +696,43 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
|
|
|
628
696
|
}
|
|
629
697
|
|
|
630
698
|
async shutdown(): Promise<void> {
|
|
631
|
-
|
|
699
|
+
if (this.#state === "shutdown") return;
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
await this.shutdownStartedSignalSdks();
|
|
703
|
+
} finally {
|
|
704
|
+
this.#state = "shutdown";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private async startSignalSdk(sdk: SessionScopedOtelSdk): Promise<void> {
|
|
709
|
+
this.#startingSignalSdk = sdk;
|
|
710
|
+
await sdk.start?.();
|
|
711
|
+
this.#startedSignalSdks.push(sdk);
|
|
712
|
+
this.#startingSignalSdk = undefined;
|
|
632
713
|
}
|
|
714
|
+
|
|
715
|
+
private async shutdownStartedSignalSdks(): Promise<void> {
|
|
716
|
+
const signalSdks = this.takeStartedSignalSdks();
|
|
717
|
+
const results = await Promise.allSettled(signalSdks.map(shutdownCompositeSignalSdk));
|
|
718
|
+
|
|
719
|
+
for (const result of results) {
|
|
720
|
+
if (result.status === "rejected") throw result.reason;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private takeStartedSignalSdks(): SessionScopedOtelSdk[] {
|
|
725
|
+
const signalSdks = [...this.#startedSignalSdks];
|
|
726
|
+
if (this.#startingSignalSdk && !signalSdks.includes(this.#startingSignalSdk)) {
|
|
727
|
+
signalSdks.push(this.#startingSignalSdk);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
this.#startedSignalSdks.length = 0;
|
|
731
|
+
this.#startingSignalSdk = undefined;
|
|
732
|
+
return signalSdks.reverse();
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function shutdownCompositeSignalSdk(sdk: SessionScopedOtelSdk): Promise<void> {
|
|
737
|
+
await sdk.shutdown?.();
|
|
633
738
|
}
|
package/src/pi/handler-types.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import type { Counter, Histogram, Meter, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
|
|
1
|
+
import type { Counter, Histogram, Meter, ObservableGauge, Span, Tracer, UpDownCounter } from "@opentelemetry/api";
|
|
2
2
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionContext,
|
|
6
|
+
ExtensionEvent,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
3
8
|
import type { EnsureProjectConfig as BootstrapEnsureProjectConfig } from "../config/bootstrap-project-config.ts";
|
|
4
9
|
import type {
|
|
5
10
|
LoadSessionConfigOptions,
|
|
@@ -13,6 +18,7 @@ import type { ObservMeOtelSdkController } from "../otel/sdk.ts";
|
|
|
13
18
|
import type { ObservMeTraceSdk } from "../otel/traces.ts";
|
|
14
19
|
import type { BoundedMap } from "../util/bounded-map.ts";
|
|
15
20
|
import type { AgentLineageContext } from "./agent-lineage.ts";
|
|
21
|
+
import type { ActiveAgentLeaseController } from "./active-agent-lease.ts";
|
|
16
22
|
import type { AgentTreeTracker } from "./agent-tree-tracker.ts";
|
|
17
23
|
import type {
|
|
18
24
|
AgentWaitJoinState,
|
|
@@ -22,11 +28,24 @@ import type {
|
|
|
22
28
|
|
|
23
29
|
export type AttributePrimitive = boolean | number | string | string[];
|
|
24
30
|
export type AttributeMap = Record<string, AttributePrimitive>;
|
|
25
|
-
export type TelemetryMeter = Pick<
|
|
31
|
+
export type TelemetryMeter = Pick<
|
|
32
|
+
Meter,
|
|
33
|
+
"createCounter" | "createHistogram" | "createObservableGauge" | "createUpDownCounter"
|
|
34
|
+
>;
|
|
26
35
|
export type TelemetryTracer = Pick<Tracer, "startSpan">;
|
|
27
36
|
export type TelemetryLogger = Pick<Logger, "emit">;
|
|
28
|
-
export type
|
|
37
|
+
export type PiEventName = ExtensionEvent["type"];
|
|
38
|
+
export type PiEvent<Name extends PiEventName> = Extract<ExtensionEvent, { type: Name }>;
|
|
39
|
+
export type Handler<Event = unknown, Context = ObservMeHandlerContext> = (
|
|
40
|
+
event: Event,
|
|
41
|
+
ctx: Context,
|
|
42
|
+
) => Promise<void> | void;
|
|
43
|
+
export type PiHandler<Name extends PiEventName> = Handler<PiEvent<Name>, ExtensionContext>;
|
|
44
|
+
export type RuntimeHandler = Handler<unknown, ExtensionContext>;
|
|
29
45
|
export type HandlerErrorRecorder = (name: string, error: unknown) => void;
|
|
46
|
+
export type AppendEntry = ExtensionAPI["appendEntry"];
|
|
47
|
+
export type GetThinkingLevel = ExtensionAPI["getThinkingLevel"];
|
|
48
|
+
export type ObservMeSessionManager = ExtensionContext["sessionManager"];
|
|
30
49
|
export type LoadSessionConfig = (
|
|
31
50
|
options: LoadSessionConfigOptions,
|
|
32
51
|
) => Promise<ObservMeConfig | LoadSessionConfigResult>;
|
|
@@ -52,7 +71,6 @@ export interface SessionRecoveryHeader {
|
|
|
52
71
|
readonly timestamp?: string;
|
|
53
72
|
readonly cwd?: string;
|
|
54
73
|
readonly parentSession?: string;
|
|
55
|
-
readonly correlation?: MinimalSessionCorrelation;
|
|
56
74
|
}
|
|
57
75
|
|
|
58
76
|
export interface StartupRecoveryState {
|
|
@@ -63,24 +81,21 @@ export interface StartupRecoveryState {
|
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
export interface ObservMeHandlerContext {
|
|
66
|
-
readonly cwd?:
|
|
67
|
-
readonly hasUI?:
|
|
68
|
-
readonly
|
|
69
|
-
readonly
|
|
70
|
-
readonly sessionId?: string;
|
|
71
|
-
readonly session_id?: string;
|
|
72
|
-
readonly model?: unknown;
|
|
73
|
-
readonly thinking?: unknown;
|
|
84
|
+
readonly cwd?: ExtensionContext["cwd"];
|
|
85
|
+
readonly hasUI?: ExtensionContext["hasUI"];
|
|
86
|
+
readonly sessionManager?: ObservMeSessionManager;
|
|
87
|
+
readonly model?: Exclude<ExtensionContext["model"], undefined>;
|
|
74
88
|
readonly ui?: {
|
|
75
89
|
notify?: (message: string, level?: "warning" | "info" | "error") => Promise<void> | void;
|
|
76
90
|
setStatus?: (key: string, value: string | undefined) => Promise<void> | void;
|
|
77
91
|
};
|
|
78
92
|
readonly isProjectTrusted?: () => boolean | Promise<boolean>;
|
|
79
|
-
readonly [key: string]: unknown;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
export interface ObservMePiApi {
|
|
83
|
-
on: (eventName:
|
|
96
|
+
on: <Name extends PiEventName>(eventName: Name, handler: PiHandler<Name>) => void;
|
|
97
|
+
appendEntry?: AppendEntry;
|
|
98
|
+
getThinkingLevel?: GetThinkingLevel;
|
|
84
99
|
}
|
|
85
100
|
|
|
86
101
|
export interface RegisterHandlersOptions {
|
|
@@ -88,18 +103,22 @@ export interface RegisterHandlersOptions {
|
|
|
88
103
|
readonly startTelemetry?: StartSessionTelemetry;
|
|
89
104
|
readonly env?: NodeJS.ProcessEnv;
|
|
90
105
|
readonly now?: () => number;
|
|
106
|
+
readonly wallClockNow?: () => number;
|
|
91
107
|
readonly configDirName?: string;
|
|
92
108
|
readonly trustedParentContext?: boolean;
|
|
93
109
|
readonly requireCompleteParentEnvelope?: boolean;
|
|
94
110
|
readonly readSessionHeader?: ReadSessionHeader;
|
|
95
111
|
readonly ensureProjectConfig?: EnsureProjectConfig;
|
|
96
112
|
readonly onHandlerError?: HandlerErrorRecorder;
|
|
113
|
+
readonly appendEntry?: AppendEntry;
|
|
114
|
+
readonly getThinkingLevel?: GetThinkingLevel;
|
|
97
115
|
}
|
|
98
116
|
|
|
99
117
|
export interface StartSessionTelemetryOptions {
|
|
100
118
|
readonly config: ObservMeConfig;
|
|
101
119
|
readonly lineage: AgentLineageContext;
|
|
102
120
|
readonly now?: () => number;
|
|
121
|
+
readonly wallClockNow?: () => number;
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
export interface TurnSequenceRegistry {
|
|
@@ -119,6 +138,7 @@ export interface ObservMeTelemetrySession {
|
|
|
119
138
|
readonly logger: TelemetryLogger;
|
|
120
139
|
readonly metrics: ObservMeMetrics;
|
|
121
140
|
readonly spans: SpanRegistry;
|
|
141
|
+
readonly activeAgentLease?: ActiveAgentLeaseController;
|
|
122
142
|
agentTree: AgentTreeTracker;
|
|
123
143
|
sessionSpan?: Span;
|
|
124
144
|
sessionAttributes?: AttributeMap;
|
|
@@ -179,6 +199,7 @@ export interface ObservMeMetrics {
|
|
|
179
199
|
readonly eventsObserved: Counter;
|
|
180
200
|
readonly activeSpans: UpDownCounter;
|
|
181
201
|
readonly activeAgents: UpDownCounter;
|
|
202
|
+
readonly agentLeaseExpiresUnixTimeSeconds: ObservableGauge;
|
|
182
203
|
readonly workflowDurationMs: Histogram;
|
|
183
204
|
readonly agentRunDurationMs: Histogram;
|
|
184
205
|
readonly agentLifetimeDurationMs: Histogram;
|
|
@@ -241,18 +262,20 @@ export interface CompositeOtelSignalSdk {
|
|
|
241
262
|
readonly traceSdk: ObservMeTraceSdk;
|
|
242
263
|
readonly metricSdk: ObservMeMetricSdk;
|
|
243
264
|
readonly logSdk: ObservMeLogSdk;
|
|
244
|
-
|
|
265
|
+
readonly state: "idle" | "starting" | "started" | "failed" | "shutdown";
|
|
266
|
+
start: () => Promise<void>;
|
|
245
267
|
forceFlush: () => Promise<void>;
|
|
246
268
|
shutdown: () => Promise<void>;
|
|
247
269
|
}
|
|
248
270
|
|
|
249
271
|
export interface HandlerSessionState {
|
|
250
272
|
session?: ObservMeTelemetrySession;
|
|
273
|
+
pendingCleanup?: ObservMeTelemetrySession;
|
|
251
274
|
}
|
|
252
275
|
|
|
253
276
|
export interface HandlerRegistration {
|
|
254
|
-
readonly eventName:
|
|
255
|
-
readonly handler:
|
|
277
|
+
readonly eventName: PiEventName;
|
|
278
|
+
readonly handler: RuntimeHandler;
|
|
256
279
|
}
|
|
257
280
|
|
|
258
281
|
export interface SessionConfigLoadResult {
|
package/src/pi/handlers.ts
CHANGED
|
@@ -31,11 +31,13 @@ export {
|
|
|
31
31
|
workflowFailed,
|
|
32
32
|
} from "./handler-runtime.ts";
|
|
33
33
|
export type {
|
|
34
|
+
AppendEntry,
|
|
34
35
|
AttributeMap,
|
|
35
36
|
AttributePrimitive,
|
|
36
37
|
BranchPreparationState,
|
|
37
38
|
CompositeOtelSignalSdk,
|
|
38
39
|
EnsureProjectConfig,
|
|
40
|
+
GetThinkingLevel,
|
|
39
41
|
Handler,
|
|
40
42
|
HandlerErrorRecorder,
|
|
41
43
|
LoadSessionConfig,
|
|
@@ -43,8 +45,12 @@ export type {
|
|
|
43
45
|
ObservMeHandlerContext,
|
|
44
46
|
ObservMeMetrics,
|
|
45
47
|
ObservMePiApi,
|
|
48
|
+
ObservMeSessionManager,
|
|
46
49
|
ObservMeTelemetrySession,
|
|
47
50
|
PendingBashOperationState,
|
|
51
|
+
PiEvent,
|
|
52
|
+
PiEventName,
|
|
53
|
+
PiHandler,
|
|
48
54
|
ReadSessionHeader,
|
|
49
55
|
RegisterHandlersOptions,
|
|
50
56
|
SessionRecoveryHeader,
|
|
@@ -65,9 +71,14 @@ export function registerHandlers(pi: unknown, options: RegisterHandlersOptions =
|
|
|
65
71
|
const errorRecorder = createStatefulHandlerErrorRecorder(state, options.onHandlerError);
|
|
66
72
|
const registrar = new HandlerRegistrar(api, state, errorRecorder);
|
|
67
73
|
const lifecycleQueue = new SerializedLifecycleQueue();
|
|
74
|
+
const runtimeOptions = {
|
|
75
|
+
...options,
|
|
76
|
+
appendEntry: options.appendEntry ?? api.appendEntry,
|
|
77
|
+
getThinkingLevel: options.getThinkingLevel ?? api.getThinkingLevel,
|
|
78
|
+
};
|
|
68
79
|
|
|
69
80
|
setDefaultHandlerErrorRecorder(errorRecorder);
|
|
70
|
-
registerLifecycleHandlers(registrar, state,
|
|
81
|
+
registerLifecycleHandlers(registrar, state, runtimeOptions, lifecycleQueue);
|
|
71
82
|
registerAgentTurnHandlers(registrar, state);
|
|
72
83
|
registerLlmHandlers(registrar, state);
|
|
73
84
|
registerToolBashHandlers(registrar, state);
|