@senad-d/observme 0.1.4 → 0.1.6

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 (71) 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 +14 -3
  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/docs/review-validation.md +10 -0
  14. package/examples/integrations/subagent-runner.ts +8 -5
  15. package/examples/observme.yaml +2 -2
  16. package/package.json +12 -4
  17. package/src/commands/obs-agents.ts +17 -12
  18. package/src/commands/obs-backfill.ts +2 -2
  19. package/src/commands/obs-command-support.ts +2 -1
  20. package/src/commands/obs-cost.ts +15 -7
  21. package/src/commands/obs-health.ts +22 -2
  22. package/src/commands/obs-loki-summary.ts +4 -8
  23. package/src/commands/obs-session.ts +35 -28
  24. package/src/commands/obs-status.ts +56 -13
  25. package/src/commands/obs-tools.ts +19 -8
  26. package/src/commands/obs-trace.ts +9 -0
  27. package/src/commands/obs.ts +6 -1
  28. package/src/config/bootstrap-project-config.ts +49 -32
  29. package/src/config/defaults.ts +0 -2
  30. package/src/config/load-config.ts +270 -92
  31. package/src/config/project-paths.ts +318 -6
  32. package/src/config/query-limits.ts +10 -0
  33. package/src/config/schema.ts +9 -8
  34. package/src/config/transport-security.ts +107 -0
  35. package/src/config/validate.ts +68 -11
  36. package/src/extension.ts +2 -12
  37. package/src/integration.ts +6 -2
  38. package/src/otel/logs.ts +24 -13
  39. package/src/otel/metrics.ts +24 -13
  40. package/src/otel/otlp-endpoint.ts +41 -2
  41. package/src/otel/otlp-http-options.ts +11 -0
  42. package/src/otel/sdk.ts +155 -9
  43. package/src/otel/shutdown.ts +39 -11
  44. package/src/otel/traces.ts +34 -16
  45. package/src/pi/agent-tree-tracker.ts +14 -1
  46. package/src/pi/compatibility.ts +30 -0
  47. package/src/pi/event-handlers/agent-turn.ts +16 -9
  48. package/src/pi/event-handlers/lifecycle.ts +207 -75
  49. package/src/pi/event-handlers/llm.ts +17 -9
  50. package/src/pi/event-handlers/session-events.ts +53 -19
  51. package/src/pi/event-handlers/tool-bash.ts +21 -27
  52. package/src/pi/handler-internals.ts +16 -26
  53. package/src/pi/handler-runtime.ts +142 -55
  54. package/src/pi/handler-types.ts +30 -15
  55. package/src/pi/handlers.ts +12 -1
  56. package/src/pi/integration-api.ts +27 -15
  57. package/src/pi/session-correlation.ts +167 -0
  58. package/src/pi/subagent-spawn.ts +164 -31
  59. package/src/privacy/redact.ts +574 -68
  60. package/src/privacy/secret-patterns.ts +6 -1
  61. package/src/query/grafana-readiness.ts +18 -2
  62. package/src/query/grafana-transport.ts +150 -27
  63. package/src/query/grafana-url.ts +36 -0
  64. package/src/query/grafana.ts +4 -136
  65. package/src/query/loki.ts +2 -4
  66. package/src/query/prometheus.ts +8 -10
  67. package/src/query/tempo.ts +2 -4
  68. package/src/query/trace-link.ts +186 -0
  69. package/src/safety/display-bounds.ts +84 -0
  70. package/src/semconv/metrics.ts +1 -0
  71. package/src/semconv/values.ts +2 -0
@@ -3,15 +3,24 @@ import type { Resource } from "@opentelemetry/resources";
3
3
  import { resourceFromAttributes } from "@opentelemetry/resources";
4
4
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
5
5
  import type { Sampler, SpanExporter, SpanProcessor } from "@opentelemetry/sdk-trace-base";
6
- import { BatchSpanProcessor, ParentBasedSampler, TraceIdRatioBasedSampler } from "@opentelemetry/sdk-trace-base";
6
+ import {
7
+ AlwaysOffSampler,
8
+ BasicTracerProvider,
9
+ BatchSpanProcessor,
10
+ ParentBasedSampler,
11
+ TraceIdRatioBasedSampler,
12
+ } from "@opentelemetry/sdk-trace-base";
7
13
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
8
14
  import type { ObservMeConfig, TraceBatchConfig } from "../config/schema.ts";
9
15
  import { appendOtlpSignalPath } from "./otlp-endpoint.ts";
16
+ import { buildOtlpHttpAgentOptions, type OtlpHttpAgentOptions } from "./otlp-http-options.ts";
10
17
  import type { StartOtelSdkFactoryOptions } from "./sdk.ts";
11
18
 
12
19
  export const OBSERVME_TRACER_NAME = "@senad-d/observme";
13
20
  export const OTLP_TRACE_SIGNAL_PATH = "/v1/traces";
14
21
 
22
+ const noopTracer = new BasicTracerProvider({ sampler: new AlwaysOffSampler() }).getTracer(OBSERVME_TRACER_NAME);
23
+
15
24
  export const DOCUMENTED_TRACE_BATCH_DEFAULTS = {
16
25
  maxQueueSize: 2048,
17
26
  maxExportBatchSize: 512,
@@ -25,6 +34,7 @@ export interface OtlpTraceExporterOptions {
25
34
  readonly url: string;
26
35
  readonly headers: Record<string, string>;
27
36
  readonly timeoutMillis: number;
37
+ readonly httpAgentOptions: OtlpHttpAgentOptions;
28
38
  }
29
39
 
30
40
  export interface TraceProviderOptions {
@@ -35,7 +45,6 @@ export interface TraceProviderOptions {
35
45
  }
36
46
 
37
47
  export interface TraceProviderLike {
38
- register?: () => void;
39
48
  getTracer: (name: string) => Tracer;
40
49
  forceFlush?: () => Promise<void> | void;
41
50
  shutdown?: () => Promise<void> | void;
@@ -48,7 +57,6 @@ export type TraceProviderFactory = (options: TraceProviderOptions) => TraceProvi
48
57
  export interface ObservMeTraceSdkOptions {
49
58
  readonly config: ObservMeConfig;
50
59
  readonly tracerName?: string;
51
- readonly registerGlobal?: boolean;
52
60
  readonly exporterFactory?: TraceExporterFactory;
53
61
  readonly spanProcessorFactory?: TraceSpanProcessorFactory;
54
62
  readonly tracerProviderFactory?: TraceProviderFactory;
@@ -63,18 +71,18 @@ export interface TraceExporterWiring {
63
71
  export class ObservMeTraceSdk {
64
72
  readonly #config: ObservMeConfig;
65
73
  readonly #tracerName: string;
66
- readonly #registerGlobal: boolean;
67
74
  readonly #exporterFactory: TraceExporterFactory;
68
75
  readonly #spanProcessorFactory: TraceSpanProcessorFactory;
69
76
  readonly #tracerProviderFactory: TraceProviderFactory;
77
+ #exporter?: SpanExporter;
78
+ #processor?: SpanProcessor;
70
79
  #provider?: TraceProviderLike;
71
- #tracer?: Tracer;
80
+ #tracer: Tracer = noopTracer;
72
81
  #state: TracePipelineState = "idle";
73
82
 
74
83
  constructor(options: ObservMeTraceSdkOptions) {
75
84
  this.#config = options.config;
76
85
  this.#tracerName = options.tracerName ?? OBSERVME_TRACER_NAME;
77
- this.#registerGlobal = options.registerGlobal ?? true;
78
86
  this.#exporterFactory = options.exporterFactory ?? createOtlpTraceExporter;
79
87
  this.#spanProcessorFactory = options.spanProcessorFactory ?? createBatchSpanProcessor;
80
88
  this.#tracerProviderFactory = options.tracerProviderFactory ?? createNodeTraceProvider;
@@ -84,28 +92,27 @@ export class ObservMeTraceSdk {
84
92
  return this.#state;
85
93
  }
86
94
 
87
- get tracer(): Tracer | undefined {
95
+ get tracer(): Tracer {
88
96
  return this.#tracer;
89
97
  }
90
98
 
91
99
  start(): void {
92
100
  if (this.#state === "started" || this.#state === "disabled") return;
93
- if (!this.#config.traces.enabled) {
101
+ if (!this.#config.enabled || !this.#config.traces.enabled) {
94
102
  this.#state = "disabled";
95
103
  return;
96
104
  }
97
105
 
98
106
  const wiring = buildTraceExporterWiring(this.#config);
99
- const exporter = this.#exporterFactory(wiring.exporter);
100
- const processor = this.#spanProcessorFactory(exporter, wiring.batch);
107
+ this.#exporter = this.#exporterFactory(wiring.exporter);
108
+ this.#processor = this.#spanProcessorFactory(this.#exporter, wiring.batch);
101
109
  this.#provider = this.#tracerProviderFactory({
102
110
  resource: createTraceResource(this.#config),
103
111
  sampler: createTraceSampler(this.#config),
104
- spanProcessors: [processor],
112
+ spanProcessors: [this.#processor],
105
113
  forceFlushTimeoutMillis: this.#config.shutdown.flushTimeoutMs,
106
114
  });
107
115
 
108
- if (this.#registerGlobal) this.#provider.register?.();
109
116
  this.#tracer = this.#provider.getTracer(this.#tracerName);
110
117
  this.#state = "started";
111
118
  }
@@ -115,9 +122,19 @@ export class ObservMeTraceSdk {
115
122
  }
116
123
 
117
124
  async shutdown(): Promise<void> {
118
- await this.#provider?.shutdown?.();
119
- this.#state = "shutdown";
120
- this.#tracer = undefined;
125
+ if (this.#state === "shutdown") return;
126
+
127
+ try {
128
+ if (this.#provider?.shutdown) await this.#provider.shutdown();
129
+ else if (this.#processor) await this.#processor.shutdown();
130
+ else await this.#exporter?.shutdown();
131
+ } finally {
132
+ this.#provider = undefined;
133
+ this.#processor = undefined;
134
+ this.#exporter = undefined;
135
+ this.#state = "shutdown";
136
+ this.#tracer = noopTracer;
137
+ }
121
138
  }
122
139
  }
123
140
 
@@ -127,7 +144,7 @@ export function createTraceSessionScopedOtelSdk(options: StartOtelSdkFactoryOpti
127
144
 
128
145
  export function buildTraceExporterWiring(config: ObservMeConfig): TraceExporterWiring {
129
146
  return {
130
- enabled: config.traces.enabled,
147
+ enabled: config.enabled && config.traces.enabled,
131
148
  exporter: buildOtlpTraceExporterOptions(config),
132
149
  batch: { ...config.traces.batch },
133
150
  };
@@ -138,6 +155,7 @@ export function buildOtlpTraceExporterOptions(config: ObservMeConfig): OtlpTrace
138
155
  url: resolveTraceEndpoint(config),
139
156
  headers: { ...config.otlp.headers },
140
157
  timeoutMillis: config.otlp.timeoutMs,
158
+ httpAgentOptions: buildOtlpHttpAgentOptions(config),
141
159
  };
142
160
  }
143
161
 
@@ -48,6 +48,7 @@ interface MutableAgentTreeNode {
48
48
  }
49
49
 
50
50
  const activeChildStatuses = new Set<AgentChildStatus>(["starting", "active"]);
51
+ const terminalChildStatuses = new Set<AgentChildStatus>(["completed", "failed", "cancelled", "orphaned"]);
51
52
  const agentStatusOrder: AgentChildStatus[] = ["starting", "active", "completed", "failed", "cancelled", "orphaned"];
52
53
 
53
54
  export class AgentTreeTracker {
@@ -68,6 +69,11 @@ export class AgentTreeTracker {
68
69
  }
69
70
 
70
71
  registerAgent(lineage: AgentLineageContext, status: AgentChildStatus = "active"): AgentTreeNode {
72
+ const existingNode = this.#nodes.get(lineage.agentId);
73
+ if (existingNode && !isAgentStatusTransitionAllowed(existingNode.status, status)) {
74
+ return snapshotNode(existingNode, this.#nodes);
75
+ }
76
+
71
77
  const node = createMutableNode(lineage, status, this.isOrphan(lineage));
72
78
  this.retainParentForInsertion(node.parentAgentId);
73
79
  this.#nodes.set(lineage.agentId, node);
@@ -77,7 +83,7 @@ export class AgentTreeTracker {
77
83
 
78
84
  updateStatus(agentId: string, status: AgentChildStatus): AgentTreeNode | undefined {
79
85
  const node = this.#nodes.get(agentId);
80
- if (!node) return undefined;
86
+ if (!node || !isAgentStatusTransitionAllowed(node.status, status)) return undefined;
81
87
 
82
88
  node.status = status;
83
89
  if (status === "orphaned") node.orphaned = true;
@@ -177,6 +183,13 @@ export class AgentTreeTracker {
177
183
  }
178
184
  }
179
185
 
186
+ export function isAgentStatusTransitionAllowed(current: AgentChildStatus, next: AgentChildStatus): boolean {
187
+ if (current === next) return true;
188
+ if (terminalChildStatuses.has(current)) return false;
189
+ if (current === "starting") return true;
190
+ return next !== "starting";
191
+ }
192
+
180
193
  export function assertNoHighCardinalityMetricLabels(labels: Record<string, string>): void {
181
194
  const forbiddenKey = Object.keys(labels).find(isHighCardinalityLineageKey);
182
195
  if (forbiddenKey) throw new Error(`High-cardinality lineage value must not be used as a metric label: ${forbiddenKey}`);
@@ -0,0 +1,30 @@
1
+ export const EARLIEST_TESTED_PI_VERSION = "0.80.5";
2
+ export const RELEASE_TESTED_PI_VERSION = "0.81.1";
3
+ export const PI_RUNTIME_COMPATIBILITY_POLICY = "required-api-capabilities" as const;
4
+
5
+ const capabilityErrorPrefix = "ObservMe/Pi API capability error";
6
+ const requiredPiMethods = ["on", "registerCommand"] as const;
7
+
8
+ /** Pi's reported version is intentionally not part of this startup decision. */
9
+ export function assertObservMePiCapabilities(pi: unknown): void {
10
+ const missingMethods = findMissingPiMethods(pi);
11
+ if (missingMethods.length === 0) return;
12
+
13
+ throw new TypeError(
14
+ `${capabilityErrorPrefix}: ObservMe requires ExtensionAPI method(s): ${missingMethods.join(", ")}. Pi version is not used as a startup gate. No ObservMe event handlers or commands were registered.`,
15
+ );
16
+ }
17
+
18
+ function findMissingPiMethods(pi: unknown): string[] {
19
+ if (!isRecord(pi)) return [...requiredPiMethods];
20
+
21
+ const missing: string[] = [];
22
+ for (const method of requiredPiMethods) {
23
+ if (typeof pi[method] !== "function") missing.push(method);
24
+ }
25
+ return missing;
26
+ }
27
+
28
+ function isRecord(value: unknown): value is Record<string, unknown> {
29
+ return typeof value === "object" && value !== null;
30
+ }
@@ -1,4 +1,11 @@
1
1
  import { SpanStatusCode } from "@opentelemetry/api";
2
+ import type {
3
+ AgentEndEvent,
4
+ AgentStartEvent,
5
+ ExtensionContext,
6
+ TurnEndEvent,
7
+ TurnStartEvent,
8
+ } from "@earendil-works/pi-coding-agent";
2
9
  import { recordObsSessionTurn } from "../../commands/obs-session.ts";
3
10
  import { AGENT_RUN_ATTRIBUTES, TURN_ATTRIBUTES } from "../../semconv/attributes.ts";
4
11
  import { LOG_EVENT_NAMES } from "../../semconv/metrics.ts";
@@ -21,7 +28,7 @@ import {
21
28
  } from "../handler-internals.ts";
22
29
  import { workflowFailed } from "../handler-runtime.ts";
23
30
  import type { HandlerRegistrar } from "../handler-runtime.ts";
24
- import type { Handler, HandlerSessionState, ObservMeHandlerContext } from "../handler-types.ts";
31
+ import type { HandlerSessionState, PiHandler } from "../handler-types.ts";
25
32
 
26
33
  export function registerAgentTurnHandlers(registrar: HandlerRegistrar, state: HandlerSessionState): void {
27
34
  registrar.add("agent_start", createAgentStartHandler(state));
@@ -30,11 +37,11 @@ export function registerAgentTurnHandlers(registrar: HandlerRegistrar, state: Ha
30
37
  registrar.add("agent_end", createAgentEndHandler(state));
31
38
  }
32
39
 
33
- function createAgentStartHandler(state: HandlerSessionState): Handler {
40
+ function createAgentStartHandler(state: HandlerSessionState): PiHandler<"agent_start"> {
34
41
  return handleAgentStart.bind(undefined, state);
35
42
  }
36
43
 
37
- function handleAgentStart(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
44
+ function handleAgentStart(state: HandlerSessionState, event: AgentStartEvent, _ctx: ExtensionContext): void {
38
45
  const session = state.session;
39
46
  if (!session) return;
40
47
 
@@ -48,11 +55,11 @@ function handleAgentStart(state: HandlerSessionState, event: unknown, _ctx: Obse
48
55
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.AGENT_RUN_STARTED, attributes);
49
56
  }
50
57
 
51
- function createAgentEndHandler(state: HandlerSessionState): Handler {
58
+ function createAgentEndHandler(state: HandlerSessionState): PiHandler<"agent_end"> {
52
59
  return handleAgentEnd.bind(undefined, state);
53
60
  }
54
61
 
55
- function handleAgentEnd(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
62
+ function handleAgentEnd(state: HandlerSessionState, event: AgentEndEvent, _ctx: ExtensionContext): void {
56
63
  const session = state.session;
57
64
  if (!session) return;
58
65
 
@@ -75,11 +82,11 @@ function handleAgentEnd(state: HandlerSessionState, event: unknown, _ctx: Observ
75
82
  });
76
83
  }
77
84
 
78
- function createTurnStartHandler(state: HandlerSessionState): Handler {
85
+ function createTurnStartHandler(state: HandlerSessionState): PiHandler<"turn_start"> {
79
86
  return handleTurnStart.bind(undefined, state);
80
87
  }
81
88
 
82
- function handleTurnStart(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
89
+ function handleTurnStart(state: HandlerSessionState, event: TurnStartEvent, _ctx: ExtensionContext): void {
83
90
  const session = state.session;
84
91
  if (!session) return;
85
92
 
@@ -99,11 +106,11 @@ function handleTurnStart(state: HandlerSessionState, event: unknown, _ctx: Obser
99
106
  emitLifecycleLog(session.logger, LOG_EVENT_NAMES.TURN_STARTED, attributes);
100
107
  }
101
108
 
102
- function createTurnEndHandler(state: HandlerSessionState): Handler {
109
+ function createTurnEndHandler(state: HandlerSessionState): PiHandler<"turn_end"> {
103
110
  return handleTurnEnd.bind(undefined, state);
104
111
  }
105
112
 
106
- function handleTurnEnd(state: HandlerSessionState, event: unknown, _ctx: ObservMeHandlerContext): void {
113
+ function handleTurnEnd(state: HandlerSessionState, event: TurnEndEvent, _ctx: ExtensionContext): void {
107
114
  const session = state.session;
108
115
  if (!session) return;
109
116