@telemetry-dev/omp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 telemetry.dev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # @telemetry-dev/omp
2
+
3
+ Telemetry integration for the [Oh My Pi (omp)](https://www.npmjs.com/package/@oh-my-pi/pi-coding-agent)
4
+ coding agent and [telemetry.dev](https://telemetry.dev). It records omp agent loops, model calls,
5
+ tool executions, and lifecycle events without changing omp's behavior.
6
+
7
+ ## Install
8
+
9
+ Create `~/.omp/agent/extensions/telemetry-dev.ts` (global) or
10
+ `.omp/extensions/telemetry-dev.ts` (project):
11
+
12
+ ```ts
13
+ import { telemetryDevExtension } from "@telemetry-dev/omp";
14
+ export default telemetryDevExtension();
15
+ ```
16
+
17
+ Install `@telemetry-dev/omp` somewhere that file can resolve it. The package also ships an
18
+ `omp.extensions` manifest pointing at `@telemetry-dev/omp/register`, so installing it as an omp
19
+ package loads the extension automatically with environment-driven configuration.
20
+
21
+ The integration targets `@oh-my-pi/pi-coding-agent` 17.x. The omp package is an optional peer
22
+ dependency because omp bundles its extension API and provides it to loaded extensions.
23
+
24
+ ## Environment
25
+
26
+ | Variable | Required | Default | Notes |
27
+ | --------------------------- | -------- | ------------------------------ | ----------------------------------------------------------------------- |
28
+ | `TELEMETRY_DEV_API_KEY` | yes | — | Ingest key (`td_live_…`). No key ⇒ the integration is a complete no-op. |
29
+ | `TELEMETRY_DEV_BASE_URL` | no | `https://ingest.telemetry.dev` | Trailing slashes are stripped. |
30
+ | `TELEMETRY_DEV_ENVIRONMENT` | no | `production` | Environment label on every trace/log. |
31
+ | `OTEL_SERVICE_NAME` | no | `omp` | OpenTelemetry service name. |
32
+
33
+ All four are also settable through SDK options when loading the extension directly:
34
+
35
+ ```ts
36
+ import { telemetryDevExtension } from "@telemetry-dev/omp";
37
+
38
+ export default telemetryDevExtension({
39
+ agentName: "pair-programmer",
40
+ captureInput: false,
41
+ captureOutput: true,
42
+ });
43
+ ```
44
+
45
+ Initialization is one-shot; the first extension factory initialized in a process supplies the SDK
46
+ options.
47
+
48
+ ## Trace shape
49
+
50
+ A typical prompt produces this hierarchy:
51
+
52
+ ```text
53
+ invoke_agent
54
+ ├── chat {model} (assistant message)
55
+ │ ├── execute_tool {toolName} (tool calls issued by that message)
56
+ │ └── execute_tool {toolName}
57
+ └── chat {model} (final assistant message)
58
+ ```
59
+
60
+ - one `invoke_agent` span for the full agent loop, including the prompt and final assistant text;
61
+ - one nested `chat {model}` span per completed assistant message, with provider, model, response id,
62
+ finish reason, usage (including cache and reasoning tokens), TTFT, and provider-reported duration;
63
+ - one `execute_tool {toolName}` span per tool execution, nested under the `chat` span whose tool
64
+ call issued it (tool call ids are matched against the assistant message's tool-call blocks;
65
+ executions with no matching message fall back to the `invoke_agent` span), including the tool
66
+ call id, arguments, result, and error state;
67
+ - lifecycle logs for sessions, turns, compaction, and auto-retry.
68
+
69
+ Every span and log reads `ctx.sessionManager.getSessionId()` when its event fires and records it as
70
+ `gen_ai.conversation.id`, so session switches, branches, and tree navigation attach telemetry to the
71
+ correct session. Logs also carry `gen_ai.agent.name` and the raw omp event type as `eventName`.
72
+
73
+ `agent_end` closes any unfinished tool spans and starts an asynchronous flush. `session_shutdown`
74
+ closes an unfinished agent loop and awaits a final flush before the process exits.
75
+
76
+ Chat spans use omp's own message timestamps: start = `message.timestamp`, end = start +
77
+ `message.duration`, so span timing matches provider-reported request duration rather than local
78
+ event-dispatch time.
79
+
80
+ ## Content capture
81
+
82
+ Prompt input, assistant text output, tool arguments, and tool results follow the telemetry.dev SDK
83
+ `captureInput` and `captureOutput` settings. Both default to `true`. Use `mask` to redact values
84
+ before capture and `maxAttributeLength` to bound serialized attributes.
85
+
86
+ Thinking and tool-call content blocks are not copied into assistant output; only text blocks are
87
+ joined. Usage metadata still includes reasoning-token counts when omp reports them.
88
+
89
+ ## Limitations
90
+
91
+ - `tool_call` and `tool_result` are intentionally not intercepted. In omp, an uncaught `tool_call`
92
+ handler error blocks the tool (fail-closed), so this integration uses the fail-open
93
+ `tool_execution_start` / `tool_execution_end` events instead.
94
+ - An assistant message with `stopReason: "aborted"` is recorded with finish reason `aborted`, not as
95
+ an error. Only `stopReason: "error"` marks chat and agent spans as errors.
96
+ - omp does not report client-side cost; telemetry.dev computes cost server-side from usage and
97
+ pricing data.
98
+ - Subagents (task tool) run in separate processes with their own extension instances, so their
99
+ telemetry appears as separate sessions.
100
+ - Initialization is one-shot. Loading the extension more than once does not replace the first
101
+ configuration.
@@ -0,0 +1,316 @@
1
+ import { flush, init, log, startSpan } from "@telemetry-dev/sdk";
2
+ //#region src/config.ts
3
+ let initialized = false;
4
+ /** Initializes the telemetry.dev SDK exactly once per process for this integration. */
5
+ function ensureInit(options = {}, overrides) {
6
+ if (initialized) return;
7
+ initialized = true;
8
+ init({
9
+ ...options,
10
+ serviceName: options.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "omp",
11
+ registerGlobal: false
12
+ }, overrides);
13
+ }
14
+ //#endregion
15
+ //#region src/extension.ts
16
+ function asRecord(value) {
17
+ if (value === null || Array.isArray(value) || !(value instanceof Object)) return void 0;
18
+ return value;
19
+ }
20
+ function stringField(record, key) {
21
+ const value = record?.[key];
22
+ if (value === void 0 || value === null || value.constructor !== String || value === "") return void 0;
23
+ return value;
24
+ }
25
+ function numberField(record, key) {
26
+ const value = record?.[key];
27
+ if (value === void 0 || value === null || value.constructor !== Number) return void 0;
28
+ const number = value;
29
+ return Number.isFinite(number) ? number : void 0;
30
+ }
31
+ function booleanField(record, key) {
32
+ const value = record?.[key];
33
+ if (value === void 0 || value === null || value.constructor !== Boolean) return void 0;
34
+ return value;
35
+ }
36
+ function reportError(onError, cause) {
37
+ try {
38
+ onError?.(cause instanceof Error ? cause : new Error(String(cause)));
39
+ } catch {}
40
+ }
41
+ function sessionId(ctx) {
42
+ const id = ctx.sessionManager.getSessionId();
43
+ return id && id.length > 0 ? id : void 0;
44
+ }
45
+ /** Joins the text blocks of an omp message content array. */
46
+ function textContent(content) {
47
+ if (!Array.isArray(content)) return void 0;
48
+ const parts = [];
49
+ for (const block of content) {
50
+ const record = asRecord(block);
51
+ const text = record?.text;
52
+ if (record?.type === "text" && text !== void 0 && text !== null && text.constructor === String) parts.push(text);
53
+ }
54
+ return parts.length > 0 ? parts.join("\n") : void 0;
55
+ }
56
+ function usageFields(message) {
57
+ const usage = asRecord(message.usage);
58
+ if (!usage) return void 0;
59
+ return {
60
+ inputTokens: numberField(usage, "input"),
61
+ outputTokens: numberField(usage, "output"),
62
+ totalTokens: numberField(usage, "totalTokens"),
63
+ cacheReadInputTokens: numberField(usage, "cacheRead"),
64
+ cacheCreationInputTokens: numberField(usage, "cacheWrite"),
65
+ reasoningOutputTokens: numberField(usage, "reasoningTokens")
66
+ };
67
+ }
68
+ function assistantMessageKey(message) {
69
+ const timestamp = numberField(message, "timestamp");
70
+ if (timestamp === void 0) return void 0;
71
+ return JSON.stringify([
72
+ timestamp,
73
+ stringField(message, "provider") ?? "",
74
+ stringField(message, "model") ?? "",
75
+ stringField(message, "responseId") ?? "",
76
+ stringField(message, "stopReason") ?? ""
77
+ ]);
78
+ }
79
+ /** Named error so `error.type` reflects the failure class instead of "Error". */
80
+ function failureError(name, message) {
81
+ const error = new Error(message ?? name);
82
+ error.name = name;
83
+ return error;
84
+ }
85
+ /**
86
+ * Creates an omp extension factory that exports agent loops, model calls, and
87
+ * tool executions to telemetry.dev.
88
+ *
89
+ * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
90
+ * child span per assistant message and one `execute_tool {tool}` child span per
91
+ * tool execution. Session lifecycle, compaction, and retry events are emitted
92
+ * as logs joined via `gen_ai.conversation.id`.
93
+ */
94
+ function telemetryDevExtension(options = {}, overrides) {
95
+ const { agentName = "omp", ...sdkOptions } = options;
96
+ const onError = sdkOptions.onError;
97
+ return (pi) => {
98
+ try {
99
+ ensureInit(sdkOptions, overrides);
100
+ } catch (error) {
101
+ reportError(onError, error);
102
+ }
103
+ /** Calls through `pi` so the host's `on` keeps its `this` binding. */
104
+ function register(event, handler) {
105
+ pi.on.bind(pi)(event, handler);
106
+ }
107
+ let agentSpan;
108
+ /** Stable key of the latest assistant message emitted for the active agent span. */
109
+ let agentMessageKey;
110
+ let pendingPrompt;
111
+ /** Chat span that issued each pending tool call, so tool spans nest under it. */
112
+ const chatSpanByToolCall = /* @__PURE__ */ new Map();
113
+ const toolSpans = /* @__PURE__ */ new Map();
114
+ function baseAttributes(ctx) {
115
+ return {
116
+ "gen_ai.conversation.id": sessionId(ctx),
117
+ "gen_ai.agent.name": agentName
118
+ };
119
+ }
120
+ function emit(eventName, ctx, level, message, attributes = {}) {
121
+ log(message, {
122
+ level,
123
+ eventName,
124
+ attributes: {
125
+ ...baseAttributes(ctx),
126
+ ...attributes
127
+ }
128
+ });
129
+ }
130
+ function spanAttributes(ctx) {
131
+ const id = sessionId(ctx);
132
+ return id ? { "gen_ai.conversation.id": id } : {};
133
+ }
134
+ function endDanglingToolSpans() {
135
+ for (const span of toolSpans.values()) span.end({ error: failureError("incomplete", "tool execution did not complete") });
136
+ toolSpans.clear();
137
+ chatSpanByToolCall.clear();
138
+ }
139
+ function endAgentSpan(fields) {
140
+ endDanglingToolSpans();
141
+ agentSpan?.end(fields);
142
+ agentSpan = void 0;
143
+ agentMessageKey = void 0;
144
+ }
145
+ function on(event, handler) {
146
+ register(event, (payload, ctx) => {
147
+ try {
148
+ return handler(payload, ctx);
149
+ } catch (error) {
150
+ reportError(onError, error);
151
+ return;
152
+ }
153
+ });
154
+ }
155
+ register("before_agent_start", (event) => {
156
+ try {
157
+ pendingPrompt = stringField(event, "prompt");
158
+ } catch (error) {
159
+ reportError(onError, error);
160
+ }
161
+ });
162
+ on("agent_start", (_event, ctx) => {
163
+ if (agentSpan) {
164
+ if (pendingPrompt === void 0) return;
165
+ endAgentSpan({ finishReason: "incomplete" });
166
+ }
167
+ agentSpan = startSpan("invoke_agent", {
168
+ type: "agent",
169
+ agentName,
170
+ input: pendingPrompt,
171
+ attributes: spanAttributes(ctx)
172
+ });
173
+ pendingPrompt = void 0;
174
+ });
175
+ on("agent_end", (event, _ctx) => {
176
+ if (!agentSpan) return;
177
+ if (event.willContinue === true) {
178
+ flush();
179
+ return;
180
+ }
181
+ const messages = Array.isArray(event.messages) ? event.messages : [];
182
+ let lastAssistant;
183
+ for (const message of messages) {
184
+ const record = asRecord(message);
185
+ if (record?.role === "assistant") lastAssistant = record;
186
+ }
187
+ if (lastAssistant) {
188
+ const eventMessageKey = assistantMessageKey(lastAssistant);
189
+ if (eventMessageKey === void 0 || eventMessageKey !== agentMessageKey) {
190
+ flush();
191
+ return;
192
+ }
193
+ }
194
+ const stopReason = stringField(lastAssistant, "stopReason");
195
+ const errorMessage = stringField(lastAssistant, "errorMessage");
196
+ endAgentSpan({
197
+ output: textContent(lastAssistant?.content),
198
+ finishReason: stopReason,
199
+ error: stopReason === "error" ? failureError(stopReason, errorMessage) : void 0
200
+ });
201
+ flush();
202
+ });
203
+ on("message_end", (event, ctx) => {
204
+ const message = asRecord(event.message);
205
+ if (message?.role !== "assistant") return;
206
+ if (agentSpan) agentMessageKey = assistantMessageKey(message);
207
+ const model = stringField(message, "model");
208
+ const startTime = numberField(message, "timestamp");
209
+ const duration = numberField(message, "duration");
210
+ const errorMessage = stringField(message, "errorMessage");
211
+ const stopReason = stringField(message, "stopReason");
212
+ const span = startSpan(model ? `chat ${model}` : "chat", {
213
+ type: "generation",
214
+ parent: agentSpan,
215
+ startTime,
216
+ model,
217
+ provider: stringField(message, "provider"),
218
+ responseId: stringField(message, "responseId"),
219
+ usage: usageFields(message),
220
+ finishReason: stopReason,
221
+ timeToFirstChunkMs: numberField(message, "ttft"),
222
+ output: textContent(message.content),
223
+ error: stopReason === "error" ? failureError(stopReason, errorMessage) : void 0,
224
+ attributes: spanAttributes(ctx)
225
+ });
226
+ for (const block of Array.isArray(message.content) ? message.content : []) {
227
+ const record = asRecord(block);
228
+ const id = stringField(record, "id");
229
+ if (record?.type === "toolCall" && id !== void 0) chatSpanByToolCall.set(id, span);
230
+ }
231
+ span.end({ endTime: startTime !== void 0 && duration !== void 0 ? startTime + duration : void 0 });
232
+ });
233
+ on("tool_execution_start", (event, ctx) => {
234
+ const span = startSpan(`execute_tool ${event.toolName}`, {
235
+ type: "tool",
236
+ parent: chatSpanByToolCall.get(event.toolCallId) ?? agentSpan,
237
+ toolName: event.toolName,
238
+ toolCallId: event.toolCallId,
239
+ input: event.args,
240
+ attributes: spanAttributes(ctx)
241
+ });
242
+ chatSpanByToolCall.delete(event.toolCallId);
243
+ toolSpans.set(event.toolCallId, span);
244
+ });
245
+ on("tool_execution_end", (event, _ctx) => {
246
+ const span = toolSpans.get(event.toolCallId);
247
+ if (!span) return;
248
+ toolSpans.delete(event.toolCallId);
249
+ const resultText = textContent(asRecord(event.result)?.content);
250
+ span.end({
251
+ output: event.result,
252
+ error: event.isError ? failureError("ToolExecutionError", resultText) : void 0
253
+ });
254
+ });
255
+ on("turn_start", (event, ctx) => {
256
+ emit("turn_start", ctx, "debug", "Turn started", { "omp.turn.index": event.turnIndex });
257
+ });
258
+ on("turn_end", (event, ctx) => {
259
+ emit("turn_end", ctx, "debug", "Turn completed", { "omp.turn.index": event.turnIndex });
260
+ });
261
+ on("session_start", (_event, ctx) => {
262
+ const model = asRecord(ctx.model);
263
+ emit("session_start", ctx, "info", "Session started", {
264
+ "gen_ai.request.model": stringField(model, "id"),
265
+ "gen_ai.provider.name": stringField(model, "provider"),
266
+ "omp.cwd": ctx.cwd
267
+ });
268
+ });
269
+ on("session_switch", (_event, ctx) => {
270
+ emit("session_switch", ctx, "info", "Session switched");
271
+ });
272
+ on("session_branch", (_event, ctx) => {
273
+ emit("session_branch", ctx, "info", "Session branched");
274
+ });
275
+ on("session_compact", (_event, ctx) => {
276
+ emit("session_compact", ctx, "info", "Session compacted");
277
+ });
278
+ on("auto_compaction_start", (event, ctx) => {
279
+ emit("auto_compaction_start", ctx, "info", `Auto-compaction started (${event.reason})`, {
280
+ "omp.compaction.reason": event.reason,
281
+ "omp.compaction.action": event.action
282
+ });
283
+ });
284
+ on("auto_compaction_end", (event, ctx) => {
285
+ const record = asRecord(event);
286
+ const errorMessage = stringField(record, "errorMessage");
287
+ emit("auto_compaction_end", ctx, errorMessage ? "error" : "info", errorMessage ? `Auto-compaction failed: ${errorMessage}` : "Auto-compaction completed", {
288
+ "omp.compaction.action": event.action,
289
+ "omp.compaction.aborted": booleanField(record, "aborted"),
290
+ "omp.compaction.skipped": booleanField(record, "skipped")
291
+ });
292
+ });
293
+ on("auto_retry_start", (event, ctx) => {
294
+ emit("auto_retry_start", ctx, "warn", `Auto-retry attempt ${event.attempt}/${event.maxAttempts}: ${event.errorMessage}`, {
295
+ "omp.retry.attempt": event.attempt,
296
+ "omp.retry.max_attempts": event.maxAttempts,
297
+ "omp.retry.delay_ms": event.delayMs
298
+ });
299
+ });
300
+ on("auto_retry_end", (event, ctx) => {
301
+ const finalError = stringField(asRecord(event), "finalError");
302
+ emit("auto_retry_end", ctx, event.success ? "info" : "error", event.success ? `Auto-retry succeeded after ${event.attempt} attempt(s)` : `Auto-retry failed: ${finalError ?? "unknown error"}`, { "omp.retry.attempt": event.attempt });
303
+ });
304
+ register("session_shutdown", async (_event, ctx) => {
305
+ try {
306
+ if (agentSpan) endAgentSpan({ finishReason: "incomplete" });
307
+ emit("session_shutdown", ctx, "info", "Session shutdown");
308
+ await flush();
309
+ } catch (error) {
310
+ reportError(onError, error);
311
+ }
312
+ });
313
+ };
314
+ }
315
+ //#endregion
316
+ export { telemetryDevExtension as t };
@@ -0,0 +1,59 @@
1
+ import { ClientOverrides, TelemetryOptions } from "@telemetry-dev/sdk";
2
+
3
+ //#region src/config.d.ts
4
+ /**
5
+ * SDK options accepted by @telemetry-dev/omp. `registerGlobal` is excluded on
6
+ * purpose: omp runs its own OpenTelemetry pipeline in-process, so this
7
+ * integration never touches the global provider — every span is created
8
+ * through the SDK's own tracer with explicit parenting.
9
+ */
10
+ type TelemetryDevOmpOptions = Omit<TelemetryOptions, "registerGlobal">;
11
+ //#endregion
12
+ //#region src/extension.d.ts
13
+ /** Options for {@link telemetryDevExtension}. */
14
+ interface TelemetryDevExtensionOptions extends TelemetryDevOmpOptions {
15
+ /** Value for `gen_ai.agent.name` on spans and logs. Defaults to `"omp"`. */
16
+ agentName?: string;
17
+ }
18
+ /**
19
+ * Structural slice of omp's `ExtensionContext` read by this integration.
20
+ *
21
+ * The host package (`@oh-my-pi/pi-coding-agent`) is an optional peer, so its
22
+ * types must not appear in this package's emitted declarations; assignability
23
+ * to the real host types is asserted in this package's tests.
24
+ */
25
+ interface TelemetryDevExtensionContext {
26
+ /** Current working directory. */
27
+ cwd: string;
28
+ /** Current model descriptor, when one is selected. */
29
+ model: {
30
+ id?: string;
31
+ provider?: string;
32
+ } | undefined;
33
+ /** Read-only session manager exposing the session id. */
34
+ sessionManager: {
35
+ getSessionId(): string | undefined;
36
+ };
37
+ }
38
+ /**
39
+ * Structural stand-in for the host `ExtensionAPI` passed to extension
40
+ * factories. The `never` parameters make any host event-subscription surface
41
+ * assignable; this integration is not meant to be called through this type.
42
+ */
43
+ interface TelemetryDevExtensionHost {
44
+ on(event: never, handler: never): void;
45
+ }
46
+ /** Structural stand-in for the host `ExtensionFactory` type. */
47
+ type TelemetryDevExtension = (pi: TelemetryDevExtensionHost) => void;
48
+ /**
49
+ * Creates an omp extension factory that exports agent loops, model calls, and
50
+ * tool executions to telemetry.dev.
51
+ *
52
+ * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
53
+ * child span per assistant message and one `execute_tool {tool}` child span per
54
+ * tool execution. Session lifecycle, compaction, and retry events are emitted
55
+ * as logs joined via `gen_ai.conversation.id`.
56
+ */
57
+ declare function telemetryDevExtension(options?: TelemetryDevExtensionOptions, overrides?: ClientOverrides): TelemetryDevExtension;
58
+ //#endregion
59
+ export { telemetryDevExtension as a, TelemetryDevExtensionOptions as i, TelemetryDevExtensionContext as n, TelemetryDevOmpOptions as o, TelemetryDevExtensionHost as r, TelemetryDevExtension as t };
@@ -0,0 +1,2 @@
1
+ import { a as telemetryDevExtension, i as TelemetryDevExtensionOptions, n as TelemetryDevExtensionContext, o as TelemetryDevOmpOptions, r as TelemetryDevExtensionHost, t as TelemetryDevExtension } from "./extension-o2etehO0.mjs";
2
+ export { type TelemetryDevExtension, type TelemetryDevExtensionContext, type TelemetryDevExtensionHost, type TelemetryDevExtensionOptions, type TelemetryDevOmpOptions, telemetryDevExtension };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as telemetryDevExtension } from "./extension-CItLaHEs.mjs";
2
+ export { telemetryDevExtension };
@@ -0,0 +1,11 @@
1
+ import { t as TelemetryDevExtension } from "./extension-o2etehO0.mjs";
2
+
3
+ //#region src/register.d.ts
4
+ /**
5
+ * Ready-to-load omp extension entry configured from `TELEMETRY_DEV_*`
6
+ * environment variables. Referenced by this package's `omp.extensions`
7
+ * manifest so `@telemetry-dev/omp` works as an installed omp package.
8
+ */
9
+ declare const _default: TelemetryDevExtension;
10
+ //#endregion
11
+ export { _default as default };
@@ -0,0 +1,10 @@
1
+ import { t as telemetryDevExtension } from "./extension-CItLaHEs.mjs";
2
+ //#region src/register.ts
3
+ /**
4
+ * Ready-to-load omp extension entry configured from `TELEMETRY_DEV_*`
5
+ * environment variables. Referenced by this package's `omp.extensions`
6
+ * manifest so `@telemetry-dev/omp` works as an installed omp package.
7
+ */
8
+ var register_default = telemetryDevExtension();
9
+ //#endregion
10
+ export { register_default as default };
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@telemetry-dev/omp",
3
+ "version": "0.1.0",
4
+ "description": "Oh My Pi (omp) coding-agent telemetry integration for telemetry.dev.",
5
+ "keywords": [
6
+ "agents",
7
+ "coding-agent",
8
+ "genai",
9
+ "llm",
10
+ "observability",
11
+ "oh-my-pi",
12
+ "omp",
13
+ "opentelemetry",
14
+ "telemetry",
15
+ "tracing"
16
+ ],
17
+ "homepage": "https://telemetry.dev",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/telemetry-dev/telemetry.dev.git",
22
+ "directory": "packages/omp"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.mts",
32
+ "import": "./dist/index.mjs",
33
+ "default": "./dist/index.mjs"
34
+ },
35
+ "./register": {
36
+ "types": "./dist/register.d.mts",
37
+ "import": "./dist/register.mjs",
38
+ "default": "./dist/register.mjs"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@opentelemetry/api": "^1.9.1",
47
+ "@telemetry-dev/sdk": "^0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "@oh-my-pi/pi-coding-agent": "17.1.8",
51
+ "@opentelemetry/sdk-logs": "^0.218.0",
52
+ "@opentelemetry/sdk-trace-base": "^2.7.1",
53
+ "@types/node": "^25.5.0",
54
+ "@typescript/native-preview": "7.0.0-dev.20260328.1",
55
+ "typescript": "^6.0.2",
56
+ "vite-plus": "0.1.20",
57
+ "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.20"
58
+ },
59
+ "peerDependencies": {
60
+ "@oh-my-pi/pi-coding-agent": ">=17"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@oh-my-pi/pi-coding-agent": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "omp": {
68
+ "extensions": [
69
+ "./dist/register.mjs"
70
+ ]
71
+ },
72
+ "scripts": {
73
+ "build": "pnpm exec vp pack",
74
+ "dev": "pnpm exec vp pack --watch",
75
+ "test": "vp test",
76
+ "check": "vp check"
77
+ }
78
+ }
package/src/config.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { init, shutdown, type ClientOverrides, type TelemetryOptions } from "@telemetry-dev/sdk";
2
+
3
+ /**
4
+ * SDK options accepted by @telemetry-dev/omp. `registerGlobal` is excluded on
5
+ * purpose: omp runs its own OpenTelemetry pipeline in-process, so this
6
+ * integration never touches the global provider — every span is created
7
+ * through the SDK's own tracer with explicit parenting.
8
+ */
9
+ export type TelemetryDevOmpOptions = Omit<TelemetryOptions, "registerGlobal">;
10
+
11
+ export type { ClientOverrides };
12
+
13
+ let initialized = false;
14
+
15
+ /** Initializes the telemetry.dev SDK exactly once per process for this integration. */
16
+ export function ensureInit(
17
+ options: TelemetryDevOmpOptions = {},
18
+ overrides?: ClientOverrides,
19
+ ): void {
20
+ if (initialized) return;
21
+ initialized = true;
22
+ init(
23
+ {
24
+ ...options,
25
+ serviceName: options.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "omp",
26
+ // Enforce the guarantee above at runtime for untyped (plain JS) callers.
27
+ registerGlobal: false,
28
+ },
29
+ overrides,
30
+ );
31
+ }
32
+
33
+ /** Test seam for unit tests that need a fresh SDK singleton. */
34
+ export async function resetForTesting(): Promise<void> {
35
+ initialized = false;
36
+ await shutdown();
37
+ }
@@ -0,0 +1,482 @@
1
+ import {
2
+ flush,
3
+ log,
4
+ startSpan,
5
+ type LogLevel,
6
+ type SpanHandle,
7
+ type TokenUsage,
8
+ } from "@telemetry-dev/sdk";
9
+
10
+ import { ensureInit, type ClientOverrides, type TelemetryDevOmpOptions } from "./config.ts";
11
+
12
+ /** Options for {@link telemetryDevExtension}. */
13
+ export interface TelemetryDevExtensionOptions extends TelemetryDevOmpOptions {
14
+ /** Value for `gen_ai.agent.name` on spans and logs. Defaults to `"omp"`. */
15
+ agentName?: string;
16
+ }
17
+
18
+ /**
19
+ * Structural slice of omp's `ExtensionContext` read by this integration.
20
+ *
21
+ * The host package (`@oh-my-pi/pi-coding-agent`) is an optional peer, so its
22
+ * types must not appear in this package's emitted declarations; assignability
23
+ * to the real host types is asserted in this package's tests.
24
+ */
25
+ export interface TelemetryDevExtensionContext {
26
+ /** Current working directory. */
27
+ cwd: string;
28
+ /** Current model descriptor, when one is selected. */
29
+ model: { id?: string; provider?: string } | undefined;
30
+ /** Read-only session manager exposing the session id. */
31
+ sessionManager: { getSessionId(): string | undefined };
32
+ }
33
+
34
+ /**
35
+ * Structural stand-in for the host `ExtensionAPI` passed to extension
36
+ * factories. The `never` parameters make any host event-subscription surface
37
+ * assignable; this integration is not meant to be called through this type.
38
+ */
39
+ export interface TelemetryDevExtensionHost {
40
+ on(event: never, handler: never): void;
41
+ }
42
+
43
+ /** Structural stand-in for the host `ExtensionFactory` type. */
44
+ export type TelemetryDevExtension = (pi: TelemetryDevExtensionHost) => void;
45
+
46
+ type JsonValue =
47
+ | string
48
+ | number
49
+ | boolean
50
+ | null
51
+ | undefined
52
+ | JsonValue[]
53
+ | { [key: string]: JsonValue };
54
+ type Attrs = { [key: string]: JsonValue };
55
+ type JsonRecord = { [key: string]: JsonValue };
56
+
57
+ function asRecord(value: JsonValue): JsonRecord | undefined {
58
+ if (value === null || Array.isArray(value) || !(value instanceof Object)) return undefined;
59
+ return value as JsonRecord;
60
+ }
61
+
62
+ function stringField(record: JsonRecord | undefined, key: string): string | undefined {
63
+ const value = record?.[key];
64
+ if (value === undefined || value === null || value.constructor !== String || value === "")
65
+ return undefined;
66
+ return value as string;
67
+ }
68
+
69
+ function numberField(record: JsonRecord | undefined, key: string): number | undefined {
70
+ const value = record?.[key];
71
+ if (value === undefined || value === null || value.constructor !== Number) return undefined;
72
+ const number = value as number;
73
+ return Number.isFinite(number) ? number : undefined;
74
+ }
75
+
76
+ function booleanField(record: JsonRecord | undefined, key: string): boolean | undefined {
77
+ const value = record?.[key];
78
+ if (value === undefined || value === null || value.constructor !== Boolean) return undefined;
79
+ return value as boolean;
80
+ }
81
+
82
+ function reportError(onError: ((error: Error) => void) | undefined, cause: unknown): void {
83
+ try {
84
+ onError?.(cause instanceof Error ? cause : new Error(String(cause)));
85
+ } catch {
86
+ // telemetry must never fail an omp session.
87
+ }
88
+ }
89
+
90
+ function sessionId(ctx: TelemetryDevExtensionContext): string | undefined {
91
+ const id = ctx.sessionManager.getSessionId();
92
+ return id && id.length > 0 ? id : undefined;
93
+ }
94
+
95
+ /** Joins the text blocks of an omp message content array. */
96
+ function textContent(content: JsonValue): string | undefined {
97
+ if (!Array.isArray(content)) return undefined;
98
+ const parts: string[] = [];
99
+ for (const block of content) {
100
+ const record = asRecord(block);
101
+ const text = record?.text;
102
+ if (
103
+ record?.type === "text" &&
104
+ text !== undefined &&
105
+ text !== null &&
106
+ text.constructor === String
107
+ ) {
108
+ parts.push(text as string);
109
+ }
110
+ }
111
+ return parts.length > 0 ? parts.join("\n") : undefined;
112
+ }
113
+
114
+ function usageFields(message: JsonRecord): TokenUsage | undefined {
115
+ const usage = asRecord(message.usage);
116
+ if (!usage) return undefined;
117
+ return {
118
+ inputTokens: numberField(usage, "input"),
119
+ outputTokens: numberField(usage, "output"),
120
+ totalTokens: numberField(usage, "totalTokens"),
121
+ cacheReadInputTokens: numberField(usage, "cacheRead"),
122
+ cacheCreationInputTokens: numberField(usage, "cacheWrite"),
123
+ reasoningOutputTokens: numberField(usage, "reasoningTokens"),
124
+ };
125
+ }
126
+
127
+ function assistantMessageKey(message: JsonRecord): string | undefined {
128
+ const timestamp = numberField(message, "timestamp");
129
+ if (timestamp === undefined) return undefined;
130
+ return JSON.stringify([
131
+ timestamp,
132
+ stringField(message, "provider") ?? "",
133
+ stringField(message, "model") ?? "",
134
+ stringField(message, "responseId") ?? "",
135
+ stringField(message, "stopReason") ?? "",
136
+ ]);
137
+ }
138
+
139
+ /** Named error so `error.type` reflects the failure class instead of "Error". */
140
+ function failureError(name: string, message: string | undefined): Error {
141
+ const error = new Error(message ?? name);
142
+ error.name = name;
143
+ return error;
144
+ }
145
+
146
+ /**
147
+ * Creates an omp extension factory that exports agent loops, model calls, and
148
+ * tool executions to telemetry.dev.
149
+ *
150
+ * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
151
+ * child span per assistant message and one `execute_tool {tool}` child span per
152
+ * tool execution. Session lifecycle, compaction, and retry events are emitted
153
+ * as logs joined via `gen_ai.conversation.id`.
154
+ */
155
+ export function telemetryDevExtension(
156
+ options: TelemetryDevExtensionOptions = {},
157
+ overrides?: ClientOverrides,
158
+ ): TelemetryDevExtension {
159
+ const { agentName = "omp", ...sdkOptions } = options;
160
+ const onError = sdkOptions.onError;
161
+
162
+ return (pi) => {
163
+ try {
164
+ ensureInit(sdkOptions, overrides);
165
+ } catch (error) {
166
+ reportError(onError, error);
167
+ }
168
+
169
+ /** Calls through `pi` so the host's `on` keeps its `this` binding. */
170
+ function register<E extends JsonValue>(
171
+ event: string,
172
+ handler: (event: E, ctx: TelemetryDevExtensionContext) => void | Promise<void>,
173
+ ): void {
174
+ const registerEvent = pi.on.bind(pi) as (
175
+ event: string,
176
+ handler: (event: E, ctx: TelemetryDevExtensionContext) => void | Promise<void>,
177
+ ) => void;
178
+ registerEvent(event, handler);
179
+ }
180
+
181
+ let agentSpan: SpanHandle | undefined;
182
+ /** Stable key of the latest assistant message emitted for the active agent span. */
183
+ let agentMessageKey: string | undefined;
184
+ let pendingPrompt: string | undefined;
185
+ /** Chat span that issued each pending tool call, so tool spans nest under it. */
186
+ const chatSpanByToolCall = new Map<string, SpanHandle>();
187
+ const toolSpans = new Map<string, SpanHandle>();
188
+
189
+ function baseAttributes(ctx: TelemetryDevExtensionContext) {
190
+ return {
191
+ "gen_ai.conversation.id": sessionId(ctx),
192
+ "gen_ai.agent.name": agentName,
193
+ } satisfies Attrs;
194
+ }
195
+
196
+ function emit(
197
+ eventName: string,
198
+ ctx: TelemetryDevExtensionContext,
199
+ level: LogLevel,
200
+ message: string,
201
+ attributes: Attrs = {},
202
+ ): void {
203
+ log(message, { level, eventName, attributes: { ...baseAttributes(ctx), ...attributes } });
204
+ }
205
+
206
+ function spanAttributes(ctx: TelemetryDevExtensionContext): Record<string, string> {
207
+ const id = sessionId(ctx);
208
+ return id ? { "gen_ai.conversation.id": id } : {};
209
+ }
210
+
211
+ function endDanglingToolSpans(): void {
212
+ for (const span of toolSpans.values()) {
213
+ span.end({ error: failureError("incomplete", "tool execution did not complete") });
214
+ }
215
+ toolSpans.clear();
216
+ chatSpanByToolCall.clear();
217
+ }
218
+
219
+ function endAgentSpan(fields: Parameters<SpanHandle["end"]>[0]): void {
220
+ endDanglingToolSpans();
221
+ agentSpan?.end(fields);
222
+ agentSpan = undefined;
223
+ agentMessageKey = undefined;
224
+ }
225
+
226
+ function on<E extends JsonValue>(
227
+ event: string,
228
+ handler: (event: E, ctx: TelemetryDevExtensionContext) => void | Promise<void>,
229
+ ): void {
230
+ register<E>(event, (payload, ctx) => {
231
+ try {
232
+ return handler(payload, ctx);
233
+ } catch (error) {
234
+ reportError(onError, error);
235
+ return undefined;
236
+ }
237
+ });
238
+ }
239
+
240
+ register("before_agent_start", (event: { prompt?: JsonValue }) => {
241
+ try {
242
+ pendingPrompt = stringField(event, "prompt");
243
+ } catch (error) {
244
+ reportError(onError, error);
245
+ }
246
+ });
247
+
248
+ on("agent_start", (_event, ctx) => {
249
+ if (agentSpan) {
250
+ if (pendingPrompt === undefined) {
251
+ // Continuations (auto-retry, compaction, queued continuations)
252
+ // re-enter the loop without a fresh before_agent_start prompt, and
253
+ // the host launches the willContinue agent_end notification without
254
+ // awaiting it, so this agent_start can arrive before that event.
255
+ // Keep the original prompt span open until the terminal agent_end.
256
+ return;
257
+ }
258
+ // A fresh prompt while a previous loop never emitted agent_end: close
259
+ // the dangling span instead of silently merging two prompts into it.
260
+ endAgentSpan({ finishReason: "incomplete" });
261
+ }
262
+ agentSpan = startSpan("invoke_agent", {
263
+ type: "agent",
264
+ agentName,
265
+ input: pendingPrompt,
266
+ attributes: spanAttributes(ctx),
267
+ });
268
+ pendingPrompt = undefined;
269
+ });
270
+
271
+ on("agent_end", (event: { messages: JsonValue[]; willContinue?: boolean }, _ctx) => {
272
+ if (!agentSpan) return;
273
+ if (event.willContinue === true) {
274
+ // The host scheduled another loop for this prompt (auto-retry,
275
+ // compaction, or a queued continuation); this agent_end is not
276
+ // terminal, so keep the prompt span open.
277
+ void flush();
278
+ return;
279
+ }
280
+ const messages = Array.isArray(event.messages) ? event.messages : [];
281
+ let lastAssistant: JsonRecord | undefined;
282
+ for (const message of messages) {
283
+ const record = asRecord(message);
284
+ if (record?.role === "assistant") lastAssistant = record;
285
+ }
286
+ if (lastAssistant) {
287
+ const eventMessageKey = assistantMessageKey(lastAssistant);
288
+ if (eventMessageKey === undefined || eventMessageKey !== agentMessageKey) {
289
+ void flush();
290
+ return;
291
+ }
292
+ }
293
+ const stopReason = stringField(lastAssistant, "stopReason");
294
+ const errorMessage = stringField(lastAssistant, "errorMessage");
295
+ endAgentSpan({
296
+ output: textContent(lastAssistant?.content),
297
+ finishReason: stopReason,
298
+ error: stopReason === "error" ? failureError(stopReason, errorMessage) : undefined,
299
+ });
300
+ void flush();
301
+ });
302
+
303
+ on("message_end", (event: { message: JsonValue }, ctx) => {
304
+ const message = asRecord(event.message);
305
+ if (message?.role !== "assistant") return;
306
+ if (agentSpan) agentMessageKey = assistantMessageKey(message);
307
+ const model = stringField(message, "model");
308
+ const startTime = numberField(message, "timestamp");
309
+ const duration = numberField(message, "duration");
310
+ const errorMessage = stringField(message, "errorMessage");
311
+ const stopReason = stringField(message, "stopReason");
312
+ const span = startSpan(model ? `chat ${model}` : "chat", {
313
+ type: "generation",
314
+ parent: agentSpan,
315
+ startTime,
316
+ model,
317
+ provider: stringField(message, "provider"),
318
+ responseId: stringField(message, "responseId"),
319
+ usage: usageFields(message),
320
+ finishReason: stopReason,
321
+ timeToFirstChunkMs: numberField(message, "ttft"),
322
+ output: textContent(message.content),
323
+ error: stopReason === "error" ? failureError(stopReason, errorMessage) : undefined,
324
+ attributes: spanAttributes(ctx),
325
+ });
326
+ for (const block of Array.isArray(message.content) ? message.content : []) {
327
+ const record = asRecord(block);
328
+ const id = stringField(record, "id");
329
+ if (record?.type === "toolCall" && id !== undefined) {
330
+ chatSpanByToolCall.set(id, span);
331
+ }
332
+ }
333
+ span.end({
334
+ endTime:
335
+ startTime !== undefined && duration !== undefined ? startTime + duration : undefined,
336
+ });
337
+ });
338
+
339
+ on(
340
+ "tool_execution_start",
341
+ (event: { toolCallId: string; toolName: string; args: JsonValue }, ctx) => {
342
+ const span = startSpan(`execute_tool ${event.toolName}`, {
343
+ type: "tool",
344
+ parent: chatSpanByToolCall.get(event.toolCallId) ?? agentSpan,
345
+ toolName: event.toolName,
346
+ toolCallId: event.toolCallId,
347
+ input: event.args,
348
+ attributes: spanAttributes(ctx),
349
+ });
350
+ chatSpanByToolCall.delete(event.toolCallId);
351
+ toolSpans.set(event.toolCallId, span);
352
+ },
353
+ );
354
+
355
+ on(
356
+ "tool_execution_end",
357
+ (
358
+ event: { toolCallId: string; toolName: string; result: JsonValue; isError: boolean },
359
+ _ctx,
360
+ ) => {
361
+ const span = toolSpans.get(event.toolCallId);
362
+ if (!span) return;
363
+ toolSpans.delete(event.toolCallId);
364
+ const resultText = textContent(asRecord(event.result)?.content);
365
+ span.end({
366
+ output: event.result,
367
+ error: event.isError ? failureError("ToolExecutionError", resultText) : undefined,
368
+ });
369
+ },
370
+ );
371
+
372
+ on("turn_start", (event: { turnIndex: number }, ctx) => {
373
+ emit("turn_start", ctx, "debug", "Turn started", { "omp.turn.index": event.turnIndex });
374
+ });
375
+
376
+ on("turn_end", (event: { turnIndex: number }, ctx) => {
377
+ emit("turn_end", ctx, "debug", "Turn completed", { "omp.turn.index": event.turnIndex });
378
+ });
379
+
380
+ on("session_start", (_event, ctx) => {
381
+ const model = asRecord(ctx.model);
382
+ emit("session_start", ctx, "info", "Session started", {
383
+ "gen_ai.request.model": stringField(model, "id"),
384
+ "gen_ai.provider.name": stringField(model, "provider"),
385
+ "omp.cwd": ctx.cwd,
386
+ });
387
+ });
388
+
389
+ on("session_switch", (_event, ctx) => {
390
+ emit("session_switch", ctx, "info", "Session switched");
391
+ });
392
+
393
+ on("session_branch", (_event, ctx) => {
394
+ emit("session_branch", ctx, "info", "Session branched");
395
+ });
396
+
397
+ on("session_compact", (_event, ctx) => {
398
+ emit("session_compact", ctx, "info", "Session compacted");
399
+ });
400
+
401
+ on("auto_compaction_start", (event: { reason: string; action: string }, ctx) => {
402
+ emit("auto_compaction_start", ctx, "info", `Auto-compaction started (${event.reason})`, {
403
+ "omp.compaction.reason": event.reason,
404
+ "omp.compaction.action": event.action,
405
+ });
406
+ });
407
+
408
+ on(
409
+ "auto_compaction_end",
410
+ (
411
+ event: {
412
+ action: string;
413
+ aborted: boolean;
414
+ willRetry: boolean;
415
+ errorMessage?: string;
416
+ skipped?: boolean;
417
+ },
418
+ ctx,
419
+ ) => {
420
+ const record = asRecord(event);
421
+ const errorMessage = stringField(record, "errorMessage");
422
+ emit(
423
+ "auto_compaction_end",
424
+ ctx,
425
+ errorMessage ? "error" : "info",
426
+ errorMessage ? `Auto-compaction failed: ${errorMessage}` : "Auto-compaction completed",
427
+ {
428
+ "omp.compaction.action": event.action,
429
+ "omp.compaction.aborted": booleanField(record, "aborted"),
430
+ "omp.compaction.skipped": booleanField(record, "skipped"),
431
+ },
432
+ );
433
+ },
434
+ );
435
+
436
+ on(
437
+ "auto_retry_start",
438
+ (
439
+ event: { attempt: number; maxAttempts: number; delayMs: number; errorMessage: string },
440
+ ctx,
441
+ ) => {
442
+ emit(
443
+ "auto_retry_start",
444
+ ctx,
445
+ "warn",
446
+ `Auto-retry attempt ${event.attempt}/${event.maxAttempts}: ${event.errorMessage}`,
447
+ {
448
+ "omp.retry.attempt": event.attempt,
449
+ "omp.retry.max_attempts": event.maxAttempts,
450
+ "omp.retry.delay_ms": event.delayMs,
451
+ },
452
+ );
453
+ },
454
+ );
455
+
456
+ on(
457
+ "auto_retry_end",
458
+ (event: { success: boolean; attempt: number; finalError?: string }, ctx) => {
459
+ const finalError = stringField(asRecord(event), "finalError");
460
+ emit(
461
+ "auto_retry_end",
462
+ ctx,
463
+ event.success ? "info" : "error",
464
+ event.success
465
+ ? `Auto-retry succeeded after ${event.attempt} attempt(s)`
466
+ : `Auto-retry failed: ${finalError ?? "unknown error"}`,
467
+ { "omp.retry.attempt": event.attempt },
468
+ );
469
+ },
470
+ );
471
+
472
+ register("session_shutdown", async (_event: JsonValue, ctx: TelemetryDevExtensionContext) => {
473
+ try {
474
+ if (agentSpan) endAgentSpan({ finishReason: "incomplete" });
475
+ emit("session_shutdown", ctx, "info", "Session shutdown");
476
+ await flush();
477
+ } catch (error) {
478
+ reportError(onError, error);
479
+ }
480
+ });
481
+ };
482
+ }
package/src/index.ts ADDED
@@ -0,0 +1,8 @@
1
+ export type { TelemetryDevOmpOptions } from "./config.ts";
2
+ export {
3
+ telemetryDevExtension,
4
+ type TelemetryDevExtension,
5
+ type TelemetryDevExtensionContext,
6
+ type TelemetryDevExtensionHost,
7
+ type TelemetryDevExtensionOptions,
8
+ } from "./extension.ts";
@@ -0,0 +1,8 @@
1
+ import { telemetryDevExtension } from "./extension.ts";
2
+
3
+ /**
4
+ * Ready-to-load omp extension entry configured from `TELEMETRY_DEV_*`
5
+ * environment variables. Referenced by this package's `omp.extensions`
6
+ * manifest so `@telemetry-dev/omp` works as an installed omp package.
7
+ */
8
+ export default telemetryDevExtension();