@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -8
  3. package/docs/agent-subagent-observability-requirements.md +4 -3
  4. package/docs/compatibility-matrix.md +11 -2
  5. package/docs/configuration.md +2 -2
  6. package/docs/extension-integration.md +20 -13
  7. package/docs/reference/03-pi-event-and-session-model.md +6 -6
  8. package/docs/reference/04-telemetry-semantic-conventions.md +1 -0
  9. package/docs/reference/06-security-privacy-redaction.md +13 -6
  10. package/docs/reference/08-query-grafana-integration.md +30 -7
  11. package/docs/reference/11-deployment-runbooks.md +21 -0
  12. package/docs/reference/12-configuration-reference.md +30 -4
  13. package/examples/integrations/subagent-runner.ts +8 -5
  14. package/examples/observme.yaml +2 -2
  15. package/package.json +12 -4
  16. package/src/commands/obs-agents.ts +17 -12
  17. package/src/commands/obs-backfill.ts +2 -2
  18. package/src/commands/obs-command-support.ts +2 -1
  19. package/src/commands/obs-cost.ts +15 -7
  20. package/src/commands/obs-health.ts +22 -2
  21. package/src/commands/obs-loki-summary.ts +4 -8
  22. package/src/commands/obs-session.ts +35 -28
  23. package/src/commands/obs-status.ts +56 -13
  24. package/src/commands/obs-tools.ts +19 -8
  25. package/src/commands/obs-trace.ts +9 -0
  26. package/src/commands/obs.ts +6 -1
  27. package/src/config/bootstrap-project-config.ts +49 -32
  28. package/src/config/defaults.ts +0 -2
  29. package/src/config/load-config.ts +270 -92
  30. package/src/config/project-paths.ts +318 -6
  31. package/src/config/query-limits.ts +10 -0
  32. package/src/config/schema.ts +9 -8
  33. package/src/config/transport-security.ts +107 -0
  34. package/src/config/validate.ts +68 -11
  35. package/src/extension.ts +2 -12
  36. package/src/integration.ts +6 -2
  37. package/src/otel/logs.ts +24 -13
  38. package/src/otel/metrics.ts +24 -13
  39. package/src/otel/otlp-endpoint.ts +41 -2
  40. package/src/otel/otlp-http-options.ts +11 -0
  41. package/src/otel/sdk.ts +155 -9
  42. package/src/otel/shutdown.ts +39 -11
  43. package/src/otel/traces.ts +34 -16
  44. package/src/pi/agent-tree-tracker.ts +14 -1
  45. package/src/pi/compatibility.ts +121 -0
  46. package/src/pi/event-handlers/agent-turn.ts +16 -9
  47. package/src/pi/event-handlers/lifecycle.ts +207 -75
  48. package/src/pi/event-handlers/llm.ts +17 -9
  49. package/src/pi/event-handlers/session-events.ts +53 -19
  50. package/src/pi/event-handlers/tool-bash.ts +21 -27
  51. package/src/pi/handler-internals.ts +16 -26
  52. package/src/pi/handler-runtime.ts +138 -51
  53. package/src/pi/handler-types.ts +30 -15
  54. package/src/pi/handlers.ts +12 -1
  55. package/src/pi/integration-api.ts +27 -15
  56. package/src/pi/session-correlation.ts +167 -0
  57. package/src/pi/subagent-spawn.ts +164 -31
  58. package/src/privacy/redact.ts +574 -68
  59. package/src/privacy/secret-patterns.ts +6 -1
  60. package/src/query/grafana-readiness.ts +18 -2
  61. package/src/query/grafana-transport.ts +150 -27
  62. package/src/query/grafana-url.ts +36 -0
  63. package/src/query/grafana.ts +4 -136
  64. package/src/query/loki.ts +2 -4
  65. package/src/query/prometheus.ts +8 -10
  66. package/src/query/tempo.ts +2 -4
  67. package/src/query/trace-link.ts +186 -0
  68. package/src/safety/display-bounds.ts +84 -0
  69. package/src/semconv/metrics.ts +1 -0
  70. 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 { ObservMeTraceSdk } from "../otel/traces.ts";
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: string, handler: Handler): void {
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: Handler): Handler {
129
- return runSerializedLifecycleHandler.bind(undefined, this, fn);
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: Handler, event: unknown, ctx: ObservMeHandlerContext): Promise<void> {
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
- return { on: callPiOn.bind(undefined, pi, on as ObservMePiApi["on"]) };
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 = new ObservMeTraceSdk({ config });
174
- const metricSdk = new ObservMeMetricSdk({ config });
175
- const logSdk = new ObservMeLogSdk({ config });
176
- const signalSdk = createCompositeOtelSignalSdk(traceSdk, metricSdk, logSdk);
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 ?? trace.getTracer("@senad-d/observme");
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<string, number> {
400
- const indexes = new Map<string, number>();
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: string): number {
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: string,
420
- handler: 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: Handler, state: HandlerSessionState): Handler {
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: Handler,
454
+ fn: RuntimeHandler,
434
455
  state: HandlerSessionState,
435
456
  event: unknown,
436
- ctx: ObservMeHandlerContext,
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: unknown,
479
- ctx: ObservMeHandlerContext,
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: Handler,
511
+ fn: RuntimeHandler,
491
512
  event: unknown,
492
- ctx: ObservMeHandlerContext,
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(fn: Handler, event: unknown, ctx: ObservMeHandlerContext): Promise<void> {
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
- constructor(traceSdk: ObservMeTraceSdk, metricSdk: ObservMeMetricSdk, logSdk: ObservMeLogSdk) {
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
- start(): void {
639
- this.traceSdk.start();
640
- this.metricSdk.start();
641
- this.logSdk.start();
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
- await Promise.all([this.traceSdk.shutdown(), this.metricSdk.shutdown(), this.logSdk.shutdown()]);
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
  }
@@ -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 Handler = (event: unknown, ctx: ObservMeHandlerContext) => Promise<void> | void;
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?: string;
71
- readonly hasUI?: boolean;
72
- readonly sessionFile?: string;
73
- readonly session_file?: string;
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: string, handler: Handler) => void;
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
- start: () => void;
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: string;
263
- readonly handler: Handler;
277
+ readonly eventName: PiEventName;
278
+ readonly handler: RuntimeHandler;
264
279
  }
265
280
 
266
281
  export interface SessionConfigLoadResult {
@@ -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, options, lifecycleQueue);
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
- if (options.spawnId && session.spans.activeSubagentSpawns.has(options.spawnId)) {
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
- if (!session.spans.activeSubagentSpawns.has(spawnId)) return integrationFailure("spawn_not_found");
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
- if (!session.spans.activeSubagentSpawns.has(spawnId)) return integrationFailure("spawn_not_found");
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
- if (kind === "wait") endAgentWait(session, id, options);
211
- else endAgentJoin(session, id, options);
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
- isOptionalChildStatus(options.childStatus) &&
260
- (options.outcome === undefined || options.outcome === "completed" || options.outcome === "failed" || options.outcome === "cancelled")
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 ||