@telemetry-dev/pi 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -54,45 +54,54 @@ options.
54
54
 
55
55
  ## Trace shape
56
56
 
57
- A typical prompt produces this hierarchy:
57
+ A typical session produces this hierarchy:
58
58
 
59
59
  ```text
60
- invoke_agent
61
- ├── chat {model} (assistant message)
62
- ├── execute_tool {toolName} (tool calls issued by that message)
63
- └── execute_tool {toolName}
64
- └── chat {model} (final assistant message)
60
+ session
61
+ └── invoke_agent
62
+ ├── chat {model} (assistant message)
63
+ ├── execute_tool {toolName} (tool calls from that message)
64
+ └── execute_tool {toolName}
65
+ └── chat {model} (final assistant message)
65
66
  ```
66
67
 
67
- - one `invoke_agent` span for the full agent loop, including the prompt and final assistant text;
68
- - one nested `chat {model}` span per completed assistant message, with provider, requested and
69
- response model, response id, finish reason, usage, and pi's estimated USD cost;
70
- - one `execute_tool {toolName}` span per tool execution, nested under the `chat` span whose tool
71
- call issued it (tool call ids are matched against the assistant message's tool-call blocks;
72
- executions with no matching message fall back to the `invoke_agent` span), including the tool
73
- call id, arguments, result, and error state;
68
+ - one `session` wrapper span for the full pi session;
69
+ - one `invoke_agent` span for each prompt, with the prompt and the final assistant content;
70
+ - one nested `chat {model}` span for each completed assistant message, with the provider, models,
71
+ response id, finish reason, usage, pi cost, provider request, and all returned content blocks;
72
+ - one `execute_tool {toolName}` span for each tool execution, under the `chat` span that issued the
73
+ call. If no message matches the tool call id, the span is under the `invoke_agent` span. The span
74
+ includes the tool call id, arguments, result, and error state;
74
75
  - lifecycle logs for sessions, turns, compaction, and model changes.
75
76
 
76
- Every span and log reads `ctx.sessionManager.getSessionId()` when its event fires and records it as
77
- `gen_ai.conversation.id`. This keeps `/new`, `/resume`, `/fork`, and `/reload` events attached to the
78
- new session rather than a cached id. Logs also carry `gen_ai.agent.name` and the raw pi event type as
79
- `eventName`.
77
+ Each span and log reads `ctx.sessionManager.getSessionId()` when its event occurs. The span or log
78
+ records the value as `gen_ai.conversation.id`. This keeps `/new`, `/resume`, `/fork`, and `/reload`
79
+ events with the new session. Logs also have `gen_ai.agent.name` and the pi event type as `eventName`.
80
80
 
81
- `agent_end` closes any unfinished tool spans and starts an asynchronous flush. `session_shutdown`
82
- closes an unfinished agent loop and awaits a final flush before pi tears down or switches sessions.
83
- It never shuts down the SDK during a pi session change.
81
+ `agent_end` records the last run result and starts an asynchronous flush. `agent_settled` closes
82
+ unfinished tool spans and the prompt span. `session_shutdown` closes an unfinished prompt and the
83
+ session span, then waits for the final flush. It does not shut down the SDK during a pi session change.
84
84
 
85
85
  ## Content capture
86
86
 
87
- Prompt input, assistant text output, tool arguments, and tool results follow the telemetry.dev SDK
88
- `captureInput` and `captureOutput` settings. Both default to `true`. Use `mask` to redact values before
89
- capture and `maxAttributeLength` to bound serialized attributes.
87
+ The prompt includes its image blocks. Chat input records the provider-formatted request from
88
+ `before_provider_request`, after all `context` handlers and pi's model-message conversion.
89
+ This conversion removes `!!` shell output and custom-message `details` from the model input.
90
+ System instructions come from the effective `ctx.getSystemPrompt()` at the request boundary, after all
91
+ `before_agent_start` handlers. Chat output contains all returned content blocks. The integration also
92
+ records tool arguments and tool results.
90
93
 
91
- Thinking and tool-call content blocks are not copied into assistant output; only text blocks are
92
- joined. Usage metadata still includes reasoning-token counts when pi reports them.
94
+ These values obey the telemetry.dev SDK `captureInput` and `captureOutput` settings, which default to
95
+ `true`. Use `mask` to remove values before capture. Use `maxAttributeLength` to limit serialized
96
+ attributes.
93
97
 
94
98
  ## Limitations
95
99
 
100
+ - Chat input uses each provider's request shape, not pi's raw session messages. Pi's public hooks do
101
+ not expose the final wire payload. A later `before_provider_request` handler can replace the request
102
+ after capture. Custom providers must call pi's `onPayload` callback for request and system
103
+ instruction capture. Without that callback, those fields are absent. Prompt, output, and tool
104
+ capture still work.
96
105
  - Pi does not expose per-request TTFT or duration fields. Chat duration is measured from
97
106
  `message_start` to `message_end` using local wall-clock time, and TTFT is not recorded.
98
107
  - `tool_call` and `tool_result` are intentionally not intercepted. In pi, an uncaught `tool_call`
@@ -7,7 +7,7 @@ import { ClientOverrides, TelemetryOptions } from "@telemetry-dev/sdk";
7
7
  * integration never touches the global provider — every span is created
8
8
  * through the SDK's own tracer with explicit parenting.
9
9
  */
10
- type TelemetryDevPiOptions = Omit<TelemetryOptions, "registerGlobal">;
10
+ type TelemetryDevPiOptions = Omit<TelemetryOptions, "registerGlobal" | "sdkName">;
11
11
  //#endregion
12
12
  //#region src/extension.d.ts
13
13
  /** Options for {@link telemetryDevExtension}. */
@@ -31,6 +31,8 @@ interface TelemetryDevExtensionContext {
31
31
  sessionManager: {
32
32
  getSessionId(): string | undefined;
33
33
  };
34
+ /** Effective system prompt after all before_agent_start handlers. */
35
+ getSystemPrompt(): string;
34
36
  }
35
37
  /**
36
38
  * Structural stand-in for the host `ExtensionAPI` passed to extension
@@ -46,9 +48,12 @@ type TelemetryDevExtension = (pi: TelemetryDevExtensionHost) => void;
46
48
  * Creates a pi extension factory that exports agent loops, model calls, tool
47
49
  * executions, and lifecycle logs to telemetry.dev.
48
50
  *
49
- * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
50
- * child span per assistant message and one `execute_tool {tool}` child span per
51
- * tool execution. Logs join the same session via `gen_ai.conversation.id`.
51
+ * Trace shape: one trace per pi session with a `session` root span. Each user
52
+ * prompt is an `invoke_agent` child, with one `chat {model}` child span per
53
+ * assistant message and one `execute_tool {tool}` child span per tool
54
+ * execution. Payloads include the prompt and its images, the provider-formatted
55
+ * request, and every returned content block (thinking, text, tool calls).
56
+ * Logs join the same session through `gen_ai.conversation.id`.
52
57
  */
53
58
  declare function telemetryDevExtension(options?: TelemetryDevExtensionOptions, overrides?: ClientOverrides): TelemetryDevExtension;
54
59
  //#endregion
@@ -8,6 +8,7 @@ function ensureInit(options = {}, overrides) {
8
8
  init({
9
9
  ...options,
10
10
  serviceName: options.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "pi",
11
+ sdkName: "@telemetry-dev/pi",
11
12
  registerGlobal: false
12
13
  }, overrides);
13
14
  }
@@ -70,9 +71,12 @@ function failureError(name, message) {
70
71
  * Creates a pi extension factory that exports agent loops, model calls, tool
71
72
  * executions, and lifecycle logs to telemetry.dev.
72
73
  *
73
- * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
74
- * child span per assistant message and one `execute_tool {tool}` child span per
75
- * tool execution. Logs join the same session via `gen_ai.conversation.id`.
74
+ * Trace shape: one trace per pi session with a `session` root span. Each user
75
+ * prompt is an `invoke_agent` child, with one `chat {model}` child span per
76
+ * assistant message and one `execute_tool {tool}` child span per tool
77
+ * execution. Payloads include the prompt and its images, the provider-formatted
78
+ * request, and every returned content block (thinking, text, tool calls).
79
+ * Logs join the same session through `gen_ai.conversation.id`.
76
80
  */
77
81
  function telemetryDevExtension(options = {}, overrides) {
78
82
  const { agentName = "pi", ...sdkOptions } = options;
@@ -87,9 +91,13 @@ function telemetryDevExtension(options = {}, overrides) {
87
91
  function register(event, handler) {
88
92
  pi.on(event, handler);
89
93
  }
94
+ /** Root of the one trace per pi session; every prompt span nests under it. */
95
+ let sessionSpan;
90
96
  let agentSpan;
91
97
  let assistantStartedAt;
92
98
  let pendingPrompt;
99
+ let systemPrompt;
100
+ let request;
93
101
  /** Latest agent_end outcome, applied to the prompt span when the run settles. */
94
102
  let lastRunResult;
95
103
  /** Chat span that issued each pending tool call, so tool spans nest under it. */
@@ -125,6 +133,10 @@ function telemetryDevExtension(options = {}, overrides) {
125
133
  agentSpan?.end(fields);
126
134
  agentSpan = void 0;
127
135
  }
136
+ function endSessionSpan() {
137
+ sessionSpan?.end();
138
+ sessionSpan = void 0;
139
+ }
128
140
  function on(event, handler) {
129
141
  register(event, (payload, ctx) => {
130
142
  try {
@@ -136,15 +148,29 @@ function telemetryDevExtension(options = {}, overrides) {
136
148
  }
137
149
  register("before_agent_start", (event) => {
138
150
  try {
139
- pendingPrompt = event.prompt?.constructor === String ? event.prompt : void 0;
151
+ const prompt = event.prompt?.constructor === String ? event.prompt : void 0;
152
+ pendingPrompt = Array.isArray(event.images) && event.images.length > 0 ? [{
153
+ type: "text",
154
+ text: prompt ?? ""
155
+ }, ...event.images] : prompt;
140
156
  } catch (error) {
141
157
  reportError(onError, error);
142
158
  }
143
159
  });
160
+ on("before_provider_request", (event, ctx) => {
161
+ request = event.payload;
162
+ systemPrompt = ctx.getSystemPrompt();
163
+ });
144
164
  on("agent_start", (_event, ctx) => {
145
165
  if (agentSpan) return;
166
+ sessionSpan ??= startSpan("session", {
167
+ type: "agent",
168
+ agentName,
169
+ attributes: spanAttributes(ctx)
170
+ });
146
171
  agentSpan = startSpan("invoke_agent", {
147
172
  type: "agent",
173
+ parent: sessionSpan,
148
174
  agentName,
149
175
  input: pendingPrompt,
150
176
  attributes: spanAttributes(ctx)
@@ -161,7 +187,7 @@ function telemetryDevExtension(options = {}, overrides) {
161
187
  const stopReason = stringField(lastAssistant, "stopReason");
162
188
  const errorMessage = stringField(lastAssistant, "errorMessage");
163
189
  lastRunResult = {
164
- output: textContent(lastAssistant?.content),
190
+ output: lastAssistant?.content,
165
191
  finishReason: stopReason,
166
192
  error: stopReason === "error" ? failureError(stopReason, errorMessage) : void 0
167
193
  };
@@ -196,10 +222,14 @@ function telemetryDevExtension(options = {}, overrides) {
196
222
  usage: usageFields(message),
197
223
  costUsd: numberField(asRecord(asRecord(message.usage)?.cost), "total"),
198
224
  finishReason: stopReason,
199
- output: textContent(message.content),
225
+ systemInstructions: systemPrompt,
226
+ input: request,
227
+ output: message.content,
200
228
  error: stopReason === "error" ? failureError(stopReason, errorMessage) : void 0,
201
229
  attributes: spanAttributes(ctx)
202
230
  });
231
+ request = void 0;
232
+ systemPrompt = void 0;
203
233
  for (const block of Array.isArray(message.content) ? message.content : []) {
204
234
  const record = asRecord(block);
205
235
  if (record?.type === "toolCall" && record.id?.constructor === String) chatSpanByToolCall.set(record.id, span);
@@ -235,6 +265,7 @@ function telemetryDevExtension(options = {}, overrides) {
235
265
  emit("turn_end", ctx, "debug", "Turn completed", { "pi.turn.index": event.turnIndex });
236
266
  });
237
267
  on("session_start", (event, ctx) => {
268
+ endSessionSpan();
238
269
  const model = asRecord(ctx.model);
239
270
  emit("session_start", ctx, "info", "Session started", {
240
271
  "gen_ai.request.model": stringField(model, "id"),
@@ -262,6 +293,7 @@ function telemetryDevExtension(options = {}, overrides) {
262
293
  endAgentSpan(lastRunResult ?? { finishReason: "incomplete" });
263
294
  lastRunResult = void 0;
264
295
  }
296
+ endSessionSpan();
265
297
  emit("session_shutdown", ctx, "info", "Session shutdown", { "pi.session.reason": event.reason });
266
298
  await flush();
267
299
  } catch (error) {
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
- import { a as telemetryDevExtension, i as TelemetryDevExtensionOptions, n as TelemetryDevExtensionContext, o as TelemetryDevPiOptions, r as TelemetryDevExtensionHost, t as TelemetryDevExtension } from "./extension-CZQ_PvAB.mjs";
1
+ import { a as telemetryDevExtension, i as TelemetryDevExtensionOptions, n as TelemetryDevExtensionContext, o as TelemetryDevPiOptions, r as TelemetryDevExtensionHost, t as TelemetryDevExtension } from "./extension-DhhRAe5R.mjs";
2
2
  export { type TelemetryDevExtension, type TelemetryDevExtensionContext, type TelemetryDevExtensionHost, type TelemetryDevExtensionOptions, type TelemetryDevPiOptions, telemetryDevExtension };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as telemetryDevExtension } from "./extension-DLbX-piL.mjs";
1
+ import { t as telemetryDevExtension } from "./extension-V9WCbPBU.mjs";
2
2
  export { telemetryDevExtension };
@@ -1,4 +1,4 @@
1
- import { t as TelemetryDevExtension } from "./extension-CZQ_PvAB.mjs";
1
+ import { t as TelemetryDevExtension } from "./extension-DhhRAe5R.mjs";
2
2
 
3
3
  //#region src/register.d.ts
4
4
  /**
package/dist/register.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as telemetryDevExtension } from "./extension-DLbX-piL.mjs";
1
+ import { t as telemetryDevExtension } from "./extension-V9WCbPBU.mjs";
2
2
  //#region src/register.ts
3
3
  /**
4
4
  * Ready-to-load pi extension entry configured from `TELEMETRY_DEV_*`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/pi",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Pi coding-agent telemetry integration for telemetry.dev.",
5
5
  "keywords": [
6
6
  "agents",
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@opentelemetry/api": "^1.9.1",
46
- "@telemetry-dev/sdk": "^0.1.0"
46
+ "@telemetry-dev/sdk": "^0.1.2"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@earendil-works/pi-coding-agent": "0.82.1",
package/src/config.ts CHANGED
@@ -6,7 +6,7 @@ import { init, shutdown, type ClientOverrides, type TelemetryOptions } from "@te
6
6
  * integration never touches the global provider — every span is created
7
7
  * through the SDK's own tracer with explicit parenting.
8
8
  */
9
- export type TelemetryDevPiOptions = Omit<TelemetryOptions, "registerGlobal">;
9
+ export type TelemetryDevPiOptions = Omit<TelemetryOptions, "registerGlobal" | "sdkName">;
10
10
 
11
11
  export type { ClientOverrides };
12
12
 
@@ -20,6 +20,7 @@ export function ensureInit(options: TelemetryDevPiOptions = {}, overrides?: Clie
20
20
  {
21
21
  ...options,
22
22
  serviceName: options.serviceName ?? process.env.OTEL_SERVICE_NAME ?? "pi",
23
+ sdkName: "@telemetry-dev/pi",
23
24
  // Enforce the guarantee above at runtime for untyped (plain JS) callers.
24
25
  registerGlobal: false,
25
26
  },
package/src/extension.ts CHANGED
@@ -30,6 +30,8 @@ export interface TelemetryDevExtensionContext {
30
30
  model: unknown;
31
31
  /** Read-only session manager exposing the session id. */
32
32
  sessionManager: { getSessionId(): string | undefined };
33
+ /** Effective system prompt after all before_agent_start handlers. */
34
+ getSystemPrompt(): string;
33
35
  }
34
36
 
35
37
  /**
@@ -116,9 +118,12 @@ function failureError(name: string, message: string | undefined): Error {
116
118
  * Creates a pi extension factory that exports agent loops, model calls, tool
117
119
  * executions, and lifecycle logs to telemetry.dev.
118
120
  *
119
- * Trace shape per user prompt: one `invoke_agent` span, with one `chat {model}`
120
- * child span per assistant message and one `execute_tool {tool}` child span per
121
- * tool execution. Logs join the same session via `gen_ai.conversation.id`.
121
+ * Trace shape: one trace per pi session with a `session` root span. Each user
122
+ * prompt is an `invoke_agent` child, with one `chat {model}` child span per
123
+ * assistant message and one `execute_tool {tool}` child span per tool
124
+ * execution. Payloads include the prompt and its images, the provider-formatted
125
+ * request, and every returned content block (thinking, text, tool calls).
126
+ * Logs join the same session through `gen_ai.conversation.id`.
122
127
  */
123
128
  export function telemetryDevExtension(
124
129
  options: TelemetryDevExtensionOptions = {},
@@ -142,9 +147,13 @@ export function telemetryDevExtension(
142
147
  pi.on(event as never, handler as never);
143
148
  }
144
149
 
150
+ /** Root of the one trace per pi session; every prompt span nests under it. */
151
+ let sessionSpan: SpanHandle | undefined;
145
152
  let agentSpan: SpanHandle | undefined;
146
153
  let assistantStartedAt: number | undefined;
147
- let pendingPrompt: string | undefined;
154
+ let pendingPrompt: JsonValue | undefined;
155
+ let systemPrompt: string | undefined;
156
+ let request: unknown;
148
157
  /** Latest agent_end outcome, applied to the prompt span when the run settles. */
149
158
  let lastRunResult: Parameters<SpanHandle["end"]>[0];
150
159
  /** Chat span that issued each pending tool call, so tool spans nest under it. */
@@ -187,6 +196,11 @@ export function telemetryDevExtension(
187
196
  agentSpan = undefined;
188
197
  }
189
198
 
199
+ function endSessionSpan(): void {
200
+ sessionSpan?.end();
201
+ sessionSpan = undefined;
202
+ }
203
+
190
204
  function on<Event>(
191
205
  event: string,
192
206
  handler: (event: Event, ctx: TelemetryDevExtensionContext) => void,
@@ -200,20 +214,36 @@ export function telemetryDevExtension(
200
214
  });
201
215
  }
202
216
 
203
- register("before_agent_start", (event: { prompt?: JsonValue }) => {
217
+ register("before_agent_start", (event: { prompt?: JsonValue; images?: JsonValue[] }) => {
204
218
  try {
205
- pendingPrompt = event.prompt?.constructor === String ? event.prompt : undefined;
219
+ const prompt = event.prompt?.constructor === String ? event.prompt : undefined;
220
+ pendingPrompt =
221
+ Array.isArray(event.images) && event.images.length > 0
222
+ ? [{ type: "text", text: prompt ?? "" }, ...event.images]
223
+ : prompt;
206
224
  } catch (error) {
207
225
  reportError(onError, error);
208
226
  }
209
227
  });
210
228
 
229
+ // All context handlers and host conversion run before this hook.
230
+ on("before_provider_request", (event: { payload: unknown }, ctx) => {
231
+ request = event.payload;
232
+ systemPrompt = ctx.getSystemPrompt();
233
+ });
234
+
211
235
  on("agent_start", (_event, ctx) => {
212
236
  // Automatic retries, compaction, and queued continuations re-enter the
213
237
  // agent loop before the run settles; keep the original prompt span open.
214
238
  if (agentSpan) return;
239
+ sessionSpan ??= startSpan("session", {
240
+ type: "agent",
241
+ agentName,
242
+ attributes: spanAttributes(ctx),
243
+ });
215
244
  agentSpan = startSpan("invoke_agent", {
216
245
  type: "agent",
246
+ parent: sessionSpan,
217
247
  agentName,
218
248
  input: pendingPrompt,
219
249
  attributes: spanAttributes(ctx),
@@ -234,7 +264,7 @@ export function telemetryDevExtension(
234
264
  // failure and start another run before the prompt settles. Record the
235
265
  // outcome and close the span at agent_settled.
236
266
  lastRunResult = {
237
- output: textContent(lastAssistant?.content),
267
+ output: lastAssistant?.content,
238
268
  finishReason: stopReason,
239
269
  error: stopReason === "error" ? failureError(stopReason, errorMessage) : undefined,
240
270
  };
@@ -273,10 +303,14 @@ export function telemetryDevExtension(
273
303
  usage: usageFields(message),
274
304
  costUsd: numberField(asRecord(asRecord(message.usage)?.cost), "total"),
275
305
  finishReason: stopReason,
276
- output: textContent(message.content),
306
+ systemInstructions: systemPrompt,
307
+ input: request,
308
+ output: message.content,
277
309
  error: stopReason === "error" ? failureError(stopReason, errorMessage) : undefined,
278
310
  attributes: spanAttributes(ctx),
279
311
  });
312
+ request = undefined;
313
+ systemPrompt = undefined;
280
314
  for (const block of Array.isArray(message.content) ? message.content : []) {
281
315
  const record = asRecord(block);
282
316
  if (record?.type === "toolCall" && record.id?.constructor === String) {
@@ -328,6 +362,8 @@ export function telemetryDevExtension(
328
362
  });
329
363
 
330
364
  on("session_start", (event: { reason: string; previousSessionFile?: string }, ctx) => {
365
+ // A new, resumed, forked, or switched session is a new trace.
366
+ endSessionSpan();
331
367
  const model = asRecord(ctx.model);
332
368
  emit("session_start", ctx, "info", "Session started", {
333
369
  "gen_ai.request.model": stringField(model, "id"),
@@ -363,6 +399,7 @@ export function telemetryDevExtension(
363
399
  endAgentSpan(lastRunResult ?? { finishReason: "incomplete" });
364
400
  lastRunResult = undefined;
365
401
  }
402
+ endSessionSpan();
366
403
  emit("session_shutdown", ctx, "info", "Session shutdown", {
367
404
  "pi.session.reason": event.reason,
368
405
  });