@senad-d/observme 0.1.4 → 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/CHANGELOG.md +35 -0
- package/README.md +24 -8
- package/docs/agent-subagent-observability-requirements.md +4 -3
- package/docs/compatibility-matrix.md +11 -2
- package/docs/configuration.md +2 -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 +1 -0
- package/docs/reference/06-security-privacy-redaction.md +13 -6
- package/docs/reference/08-query-grafana-integration.md +30 -7
- package/docs/reference/11-deployment-runbooks.md +21 -0
- package/docs/reference/12-configuration-reference.md +30 -4
- package/examples/integrations/subagent-runner.ts +8 -5
- package/examples/observme.yaml +2 -2
- package/package.json +12 -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 +49 -32
- package/src/config/defaults.ts +0 -2
- package/src/config/load-config.ts +270 -92
- package/src/config/project-paths.ts +318 -6
- package/src/config/query-limits.ts +10 -0
- package/src/config/schema.ts +9 -8
- package/src/config/transport-security.ts +107 -0
- package/src/config/validate.ts +68 -11
- 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/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 +207 -75
- 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 +138 -51
- package/src/pi/handler-types.ts +30 -15
- 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 +1 -0
- package/src/semconv/values.ts +2 -0
|
@@ -1,12 +1,12 @@
|
|
|
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 {
|
|
@@ -49,9 +49,12 @@ import type {
|
|
|
49
49
|
HandlerRegistration,
|
|
50
50
|
HandlerSessionState,
|
|
51
51
|
ObservMeHandlerContext,
|
|
52
|
+
PiEventName,
|
|
53
|
+
PiHandler,
|
|
52
54
|
ObservMeMetrics,
|
|
53
55
|
ObservMePiApi,
|
|
54
56
|
ObservMeTelemetrySession,
|
|
57
|
+
RuntimeHandler,
|
|
55
58
|
SpanRegistry,
|
|
56
59
|
StartSessionTelemetryOptions,
|
|
57
60
|
TelemetryMeter,
|
|
@@ -66,6 +69,7 @@ const piApiCompatibilityErrorMessage =
|
|
|
66
69
|
"ObservMe/Pi API compatibility error: expected Pi ExtensionAPI with on(eventName, handler) before registering ObservMe handlers.";
|
|
67
70
|
const eventRegistrationOrder = [
|
|
68
71
|
"session_start",
|
|
72
|
+
"session_info_changed",
|
|
69
73
|
"agent_start",
|
|
70
74
|
"turn_start",
|
|
71
75
|
"before_provider_request",
|
|
@@ -76,18 +80,15 @@ const eventRegistrationOrder = [
|
|
|
76
80
|
"tool_result",
|
|
77
81
|
"tool_execution_end",
|
|
78
82
|
"user_bash",
|
|
79
|
-
"bashExecution",
|
|
80
83
|
"model_select",
|
|
81
|
-
"model_change",
|
|
82
84
|
"thinking_level_select",
|
|
83
|
-
"thinking_level_change",
|
|
84
85
|
"session_before_tree",
|
|
85
86
|
"session_tree",
|
|
86
87
|
"session_compact",
|
|
87
88
|
"turn_end",
|
|
88
89
|
"agent_end",
|
|
89
90
|
"session_shutdown",
|
|
90
|
-
] as const;
|
|
91
|
+
] as const satisfies readonly PiEventName[];
|
|
91
92
|
const eventRegistrationIndexes = createEventRegistrationIndexes();
|
|
92
93
|
|
|
93
94
|
let defaultHandlerErrorRecorder: HandlerErrorRecorder = recordInternalErrorFallback;
|
|
@@ -104,8 +105,8 @@ export class HandlerRegistrar {
|
|
|
104
105
|
this.#errorRecorder = errorRecorder;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
add(eventName:
|
|
108
|
-
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 });
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
commit(): void {
|
|
@@ -125,11 +126,12 @@ export class HandlerRegistrar {
|
|
|
125
126
|
export class SerializedLifecycleQueue {
|
|
126
127
|
#previous = Promise.resolve();
|
|
127
128
|
|
|
128
|
-
wrap(fn:
|
|
129
|
-
|
|
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>;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
run(fn:
|
|
134
|
+
run(fn: RuntimeHandler, event: unknown, ctx: Parameters<RuntimeHandler>[1]): Promise<void> {
|
|
133
135
|
// Pi docs guarantee /new, /resume, and /fork emit session_shutdown before session_start;
|
|
134
136
|
// ctx.reload() emits session_shutdown then session_start while the old command call frame can
|
|
135
137
|
// continue. ObservMe queues both lifecycle handlers together so async startup, shutdown,
|
|
@@ -146,19 +148,30 @@ export function resolveObservMePiApi(pi: unknown): ObservMePiApi {
|
|
|
146
148
|
const on = pi.on;
|
|
147
149
|
if (typeof on !== "function") throw new TypeError(piApiCompatibilityErrorMessage);
|
|
148
150
|
|
|
149
|
-
|
|
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
|
+
};
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
export function setDefaultHandlerErrorRecorder(recorder: HandlerErrorRecorder): void {
|
|
153
166
|
defaultHandlerErrorRecorder = recorder;
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
export function safeHandler(
|
|
169
|
+
export function safeHandler<Event = unknown, Context = ObservMeHandlerContext>(
|
|
157
170
|
name: string,
|
|
158
|
-
fn: Handler,
|
|
171
|
+
fn: Handler<Event, Context>,
|
|
159
172
|
recorder: HandlerErrorRecorder = defaultHandlerErrorRecorder,
|
|
160
|
-
): Handler {
|
|
161
|
-
return runSafeHandler.bind(undefined, name, fn, recorder);
|
|
173
|
+
): Handler<Event, Context> {
|
|
174
|
+
return (runSafeHandler<Event, Context>).bind(undefined, name, fn, recorder);
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
export function createStatefulHandlerErrorRecorder(
|
|
@@ -169,17 +182,24 @@ export function createStatefulHandlerErrorRecorder(
|
|
|
169
182
|
}
|
|
170
183
|
|
|
171
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
|
+
|
|
172
187
|
const config = withTelemetrySessionResourceAttributes(options.config, options.lineage);
|
|
173
|
-
const traceSdk =
|
|
174
|
-
const metricSdk =
|
|
175
|
-
const logSdk =
|
|
176
|
-
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
|
+
);
|
|
177
197
|
const controller = await startOtelSdk({
|
|
178
198
|
config,
|
|
179
199
|
agent: options.lineage,
|
|
180
200
|
sdkFactory: returnCompositeSignalSdk.bind(undefined, signalSdk),
|
|
181
201
|
});
|
|
182
|
-
const tracer = traceSdk.tracer
|
|
202
|
+
const tracer = traceSdk.tracer;
|
|
183
203
|
const metrics = createObservMeMetrics(metricSdk.meter);
|
|
184
204
|
const sessionReference: HandlerSessionState = {};
|
|
185
205
|
const getTelemetryDropTarget = resolveSessionTelemetryDropTarget.bind(undefined, sessionReference, metrics);
|
|
@@ -220,8 +240,9 @@ export function createCompositeOtelSignalSdk(
|
|
|
220
240
|
traceSdk: ObservMeTraceSdk,
|
|
221
241
|
metricSdk: ObservMeMetricSdk,
|
|
222
242
|
logSdk: ObservMeLogSdk,
|
|
243
|
+
cleanupTimeoutMs = 3_000,
|
|
223
244
|
): CompositeOtelSignalSdk {
|
|
224
|
-
return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk);
|
|
245
|
+
return new ObservMeCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk, cleanupTimeoutMs);
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
export function createObservMeMetrics(meter: TelemetryMeter): ObservMeMetrics {
|
|
@@ -396,8 +417,8 @@ export function buildTelemetryInstanceResourceAttributes(instanceId: string): Re
|
|
|
396
417
|
};
|
|
397
418
|
}
|
|
398
419
|
|
|
399
|
-
function createEventRegistrationIndexes(): Map<
|
|
400
|
-
const indexes = new Map<
|
|
420
|
+
function createEventRegistrationIndexes(): Map<PiEventName, number> {
|
|
421
|
+
const indexes = new Map<PiEventName, number>();
|
|
401
422
|
for (const [index, name] of eventRegistrationOrder.entries()) indexes.set(name, index);
|
|
402
423
|
return indexes;
|
|
403
424
|
}
|
|
@@ -410,30 +431,30 @@ function compareHandlerRegistrations(left: HandlerRegistration, right: HandlerRe
|
|
|
410
431
|
return registrationIndex(left.eventName) - registrationIndex(right.eventName);
|
|
411
432
|
}
|
|
412
433
|
|
|
413
|
-
function registrationIndex(eventName:
|
|
434
|
+
function registrationIndex(eventName: PiEventName): number {
|
|
414
435
|
return eventRegistrationIndexes.get(eventName) ?? eventRegistrationOrder.length;
|
|
415
436
|
}
|
|
416
437
|
|
|
417
438
|
function registerObservedHandler(
|
|
418
439
|
api: ObservMePiApi,
|
|
419
|
-
name:
|
|
420
|
-
handler:
|
|
440
|
+
name: PiEventName,
|
|
441
|
+
handler: RuntimeHandler,
|
|
421
442
|
state: HandlerSessionState,
|
|
422
443
|
errorRecorder: HandlerErrorRecorder,
|
|
423
444
|
): void {
|
|
424
445
|
api.on(name, safeHandler(name, observeHandler(name, handler, state), errorRecorder));
|
|
425
446
|
}
|
|
426
447
|
|
|
427
|
-
function observeHandler(name: string, fn:
|
|
448
|
+
function observeHandler(name: string, fn: RuntimeHandler, state: HandlerSessionState): RuntimeHandler {
|
|
428
449
|
return runObservedHandler.bind(undefined, name, fn, state);
|
|
429
450
|
}
|
|
430
451
|
|
|
431
452
|
async function runObservedHandler(
|
|
432
453
|
name: string,
|
|
433
|
-
fn:
|
|
454
|
+
fn: RuntimeHandler,
|
|
434
455
|
state: HandlerSessionState,
|
|
435
456
|
event: unknown,
|
|
436
|
-
ctx:
|
|
457
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
437
458
|
): Promise<void> {
|
|
438
459
|
const startedAtMs = Date.now();
|
|
439
460
|
const sessionBefore = state.session;
|
|
@@ -471,12 +492,12 @@ function recordHandlerObservation(
|
|
|
471
492
|
session.metrics.handlerDurationMs.record(Math.max(0, Date.now() - startedAtMs), { operation, status });
|
|
472
493
|
}
|
|
473
494
|
|
|
474
|
-
async function runSafeHandler(
|
|
495
|
+
async function runSafeHandler<Event, Context>(
|
|
475
496
|
name: string,
|
|
476
|
-
fn: Handler,
|
|
497
|
+
fn: Handler<Event, Context>,
|
|
477
498
|
recorder: HandlerErrorRecorder,
|
|
478
|
-
event:
|
|
479
|
-
ctx:
|
|
499
|
+
event: Event,
|
|
500
|
+
ctx: Context,
|
|
480
501
|
): Promise<void> {
|
|
481
502
|
try {
|
|
482
503
|
await fn(event, ctx);
|
|
@@ -487,14 +508,18 @@ async function runSafeHandler(
|
|
|
487
508
|
|
|
488
509
|
async function runSerializedLifecycleHandler(
|
|
489
510
|
queue: SerializedLifecycleQueue,
|
|
490
|
-
fn:
|
|
511
|
+
fn: RuntimeHandler,
|
|
491
512
|
event: unknown,
|
|
492
|
-
ctx:
|
|
513
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
493
514
|
): Promise<void> {
|
|
494
515
|
await queue.run(fn, event, ctx);
|
|
495
516
|
}
|
|
496
517
|
|
|
497
|
-
async function invokeHandler(
|
|
518
|
+
async function invokeHandler(
|
|
519
|
+
fn: RuntimeHandler,
|
|
520
|
+
event: unknown,
|
|
521
|
+
ctx: Parameters<RuntimeHandler>[1],
|
|
522
|
+
): Promise<void> {
|
|
498
523
|
await fn(event, ctx);
|
|
499
524
|
}
|
|
500
525
|
|
|
@@ -502,10 +527,6 @@ function ignoreLifecycleQueueError(): undefined {
|
|
|
502
527
|
return undefined;
|
|
503
528
|
}
|
|
504
529
|
|
|
505
|
-
function callPiOn(pi: Record<string, unknown>, on: ObservMePiApi["on"], eventName: string, handler: Handler): void {
|
|
506
|
-
on.call(pi, eventName, handler);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
530
|
function returnCompositeSignalSdk(signalSdk: CompositeOtelSignalSdk): CompositeOtelSignalSdk {
|
|
510
531
|
return signalSdk;
|
|
511
532
|
}
|
|
@@ -628,17 +649,46 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
|
|
|
628
649
|
readonly traceSdk: ObservMeTraceSdk;
|
|
629
650
|
readonly metricSdk: ObservMeMetricSdk;
|
|
630
651
|
readonly logSdk: ObservMeLogSdk;
|
|
631
|
-
|
|
632
|
-
|
|
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
|
+
) {
|
|
633
663
|
this.traceSdk = traceSdk;
|
|
634
664
|
this.metricSdk = metricSdk;
|
|
635
665
|
this.logSdk = logSdk;
|
|
666
|
+
this.#cleanupTimeoutMs = cleanupTimeoutMs;
|
|
636
667
|
}
|
|
637
668
|
|
|
638
|
-
|
|
639
|
-
this
|
|
640
|
-
|
|
641
|
-
|
|
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
|
+
}
|
|
642
692
|
}
|
|
643
693
|
|
|
644
694
|
async forceFlush(): Promise<void> {
|
|
@@ -646,6 +696,43 @@ class ObservMeCompositeOtelSignalSdk implements CompositeOtelSignalSdk {
|
|
|
646
696
|
}
|
|
647
697
|
|
|
648
698
|
async shutdown(): Promise<void> {
|
|
649
|
-
|
|
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;
|
|
650
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?.();
|
|
651
738
|
}
|
package/src/pi/handler-types.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
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,
|
|
@@ -29,8 +34,18 @@ export type TelemetryMeter = Pick<
|
|
|
29
34
|
>;
|
|
30
35
|
export type TelemetryTracer = Pick<Tracer, "startSpan">;
|
|
31
36
|
export type TelemetryLogger = Pick<Logger, "emit">;
|
|
32
|
-
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>;
|
|
33
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"];
|
|
34
49
|
export type LoadSessionConfig = (
|
|
35
50
|
options: LoadSessionConfigOptions,
|
|
36
51
|
) => Promise<ObservMeConfig | LoadSessionConfigResult>;
|
|
@@ -56,7 +71,6 @@ export interface SessionRecoveryHeader {
|
|
|
56
71
|
readonly timestamp?: string;
|
|
57
72
|
readonly cwd?: string;
|
|
58
73
|
readonly parentSession?: string;
|
|
59
|
-
readonly correlation?: MinimalSessionCorrelation;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
export interface StartupRecoveryState {
|
|
@@ -67,24 +81,21 @@ export interface StartupRecoveryState {
|
|
|
67
81
|
}
|
|
68
82
|
|
|
69
83
|
export interface ObservMeHandlerContext {
|
|
70
|
-
readonly cwd?:
|
|
71
|
-
readonly hasUI?:
|
|
72
|
-
readonly
|
|
73
|
-
readonly
|
|
74
|
-
readonly sessionId?: string;
|
|
75
|
-
readonly session_id?: string;
|
|
76
|
-
readonly model?: unknown;
|
|
77
|
-
readonly thinking?: unknown;
|
|
84
|
+
readonly cwd?: ExtensionContext["cwd"];
|
|
85
|
+
readonly hasUI?: ExtensionContext["hasUI"];
|
|
86
|
+
readonly sessionManager?: ObservMeSessionManager;
|
|
87
|
+
readonly model?: Exclude<ExtensionContext["model"], undefined>;
|
|
78
88
|
readonly ui?: {
|
|
79
89
|
notify?: (message: string, level?: "warning" | "info" | "error") => Promise<void> | void;
|
|
80
90
|
setStatus?: (key: string, value: string | undefined) => Promise<void> | void;
|
|
81
91
|
};
|
|
82
92
|
readonly isProjectTrusted?: () => boolean | Promise<boolean>;
|
|
83
|
-
readonly [key: string]: unknown;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
export interface ObservMePiApi {
|
|
87
|
-
on: (eventName:
|
|
96
|
+
on: <Name extends PiEventName>(eventName: Name, handler: PiHandler<Name>) => void;
|
|
97
|
+
appendEntry?: AppendEntry;
|
|
98
|
+
getThinkingLevel?: GetThinkingLevel;
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
export interface RegisterHandlersOptions {
|
|
@@ -99,6 +110,8 @@ export interface RegisterHandlersOptions {
|
|
|
99
110
|
readonly readSessionHeader?: ReadSessionHeader;
|
|
100
111
|
readonly ensureProjectConfig?: EnsureProjectConfig;
|
|
101
112
|
readonly onHandlerError?: HandlerErrorRecorder;
|
|
113
|
+
readonly appendEntry?: AppendEntry;
|
|
114
|
+
readonly getThinkingLevel?: GetThinkingLevel;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
export interface StartSessionTelemetryOptions {
|
|
@@ -249,18 +262,20 @@ export interface CompositeOtelSignalSdk {
|
|
|
249
262
|
readonly traceSdk: ObservMeTraceSdk;
|
|
250
263
|
readonly metricSdk: ObservMeMetricSdk;
|
|
251
264
|
readonly logSdk: ObservMeLogSdk;
|
|
252
|
-
|
|
265
|
+
readonly state: "idle" | "starting" | "started" | "failed" | "shutdown";
|
|
266
|
+
start: () => Promise<void>;
|
|
253
267
|
forceFlush: () => Promise<void>;
|
|
254
268
|
shutdown: () => Promise<void>;
|
|
255
269
|
}
|
|
256
270
|
|
|
257
271
|
export interface HandlerSessionState {
|
|
258
272
|
session?: ObservMeTelemetrySession;
|
|
273
|
+
pendingCleanup?: ObservMeTelemetrySession;
|
|
259
274
|
}
|
|
260
275
|
|
|
261
276
|
export interface HandlerRegistration {
|
|
262
|
-
readonly eventName:
|
|
263
|
-
readonly handler:
|
|
277
|
+
readonly eventName: PiEventName;
|
|
278
|
+
readonly handler: RuntimeHandler;
|
|
264
279
|
}
|
|
265
280
|
|
|
266
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);
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
endAgentJoin,
|
|
20
20
|
endAgentWait,
|
|
21
21
|
failSubagentSpawn,
|
|
22
|
+
resolveSubagentSpawnIdentity,
|
|
22
23
|
startAgentJoin,
|
|
23
24
|
startAgentWait,
|
|
24
25
|
startSubagentSpawn,
|
|
@@ -94,11 +95,15 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
94
95
|
if (!session) return integrationFailure("session_unavailable");
|
|
95
96
|
try {
|
|
96
97
|
if (!isValidStartSubagentOptions(options)) return integrationFailure("invalid_request");
|
|
97
|
-
|
|
98
|
+
const identity = resolveSubagentSpawnIdentity(options);
|
|
99
|
+
if (session.spans.activeSubagentSpawns.has(identity.spawnId)) {
|
|
98
100
|
return integrationFailure("spawn_already_exists");
|
|
99
101
|
}
|
|
102
|
+
if (isChildAgentIdentifierRetained(session, identity.childAgentId)) {
|
|
103
|
+
return integrationFailure("child_agent_already_exists");
|
|
104
|
+
}
|
|
100
105
|
|
|
101
|
-
const started = startSubagentSpawn(session, options);
|
|
106
|
+
const started = startSubagentSpawn(session, { ...options, ...identity });
|
|
102
107
|
return {
|
|
103
108
|
ok: true,
|
|
104
109
|
spawnId: started.spawnId,
|
|
@@ -121,10 +126,8 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
121
126
|
if (!isValidIntegrationIdentifier(spawnId) || !isValidCompleteSubagentOptions(options)) {
|
|
122
127
|
return integrationFailure("invalid_request");
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
completeSubagentSpawn(session, spawnId, options);
|
|
127
|
-
return integrationSuccess();
|
|
129
|
+
const result = completeSubagentSpawn(session, spawnId, options);
|
|
130
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
128
131
|
} catch {
|
|
129
132
|
return integrationFailure("operation_failed");
|
|
130
133
|
}
|
|
@@ -140,10 +143,8 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
140
143
|
if (!isValidIntegrationIdentifier(spawnId) || !isValidFailSubagentOptions(options)) {
|
|
141
144
|
return integrationFailure("invalid_request");
|
|
142
145
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
failSubagentSpawn(session, spawnId, options);
|
|
146
|
-
return integrationSuccess();
|
|
146
|
+
const result = failSubagentSpawn(session, spawnId, options);
|
|
147
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
147
148
|
} catch {
|
|
148
149
|
return integrationFailure("operation_failed");
|
|
149
150
|
}
|
|
@@ -207,15 +208,22 @@ export class SessionBackedObservMeIntegrationApi implements ObservMeIntegrationA
|
|
|
207
208
|
const registry = kind === "wait" ? session.spans.activeAgentWaits : session.spans.activeAgentJoins;
|
|
208
209
|
if (!registry.has(id)) return integrationFailure(kind === "wait" ? "wait_not_found" : "join_not_found");
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return integrationSuccess();
|
|
211
|
+
const result = kind === "wait" ? endAgentWait(session, id, options) : endAgentJoin(session, id, options);
|
|
212
|
+
return result.ok ? integrationSuccess() : integrationFailure(result.reason);
|
|
213
213
|
} catch {
|
|
214
214
|
return integrationFailure("operation_failed");
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
function isChildAgentIdentifierRetained(session: ObservMeTelemetrySession, childAgentId: string): boolean {
|
|
220
|
+
if (session.agentTree.getAgent(childAgentId)) return true;
|
|
221
|
+
for (const activeSpawn of session.spans.activeSubagentSpawns.values()) {
|
|
222
|
+
if (activeSpawn.childAgentId === childAgentId) return true;
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
219
227
|
function resolveIntegrationEventBus(pi: unknown): IntegrationEventBus | undefined {
|
|
220
228
|
if (!pi || typeof pi !== "object") return undefined;
|
|
221
229
|
try {
|
|
@@ -256,8 +264,8 @@ function isValidCompleteSubagentOptions(value: unknown): value is ObservMeComple
|
|
|
256
264
|
const options = value as Partial<ObservMeCompleteSubagentOptions>;
|
|
257
265
|
return (
|
|
258
266
|
isOptionalIntegrationIdentifier(options.childAgentId) &&
|
|
259
|
-
|
|
260
|
-
(options.outcome
|
|
267
|
+
isOptionalTerminalChildStatus(options.childStatus) &&
|
|
268
|
+
isOptionalTerminalChildStatus(options.outcome)
|
|
261
269
|
);
|
|
262
270
|
}
|
|
263
271
|
|
|
@@ -349,6 +357,10 @@ function isOptionalChildStatus(value: unknown): boolean {
|
|
|
349
357
|
);
|
|
350
358
|
}
|
|
351
359
|
|
|
360
|
+
function isOptionalTerminalChildStatus(value: unknown): boolean {
|
|
361
|
+
return value === undefined || value === "completed" || value === "failed" || value === "cancelled";
|
|
362
|
+
}
|
|
363
|
+
|
|
352
364
|
function isOptionalJoinStatus(value: unknown): boolean {
|
|
353
365
|
return (
|
|
354
366
|
value === undefined ||
|