@amaster.ai/pi-telemetry 0.1.1-beta.0 → 0.1.1

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
@@ -1,5 +1,7 @@
1
1
  # @amaster.ai/pi-telemetry
2
2
 
3
+ ![pi-telemetry preview](https://raw.githubusercontent.com/TGYD-helige/pi/master/packages/pi-telemetry/preview.png)
4
+
3
5
  Runtime telemetry contracts and exporters for pi.
4
6
 
5
7
  The root package exposes stable exporter contracts plus no-op and composite exporters. Provider-specific implementations live behind explicit subpath entry points so applications can depend on the smallest public surface they need.
@@ -7,55 +9,106 @@ The root package exposes stable exporter contracts plus no-op and composite expo
7
9
  ## Entry Points
8
10
 
9
11
  - `@amaster.ai/pi-telemetry`: stable contracts, `NoopRuntimeEventExporter`, and `CompositeRuntimeEventExporter`.
10
- - `@amaster.ai/pi-telemetry/langfuse`: Langfuse SDK and ingestion API exporters.
12
+ - `@amaster.ai/pi-telemetry/config`: `TelemetryConfig` type, `resolveConfig`, and `loadConfigFromFile`.
13
+ - `@amaster.ai/pi-telemetry/langfuse`: Langfuse SDK exporter.
11
14
  - `@amaster.ai/pi-telemetry/otel`: generic OTLP/HTTP traces exporter.
12
15
 
13
- ## Langfuse
14
-
15
- ```ts
16
- import { createRuntimeEventExporterFromEnv } from "@amaster.ai/pi-telemetry/langfuse";
17
-
18
- const exporter = createRuntimeEventExporterFromEnv(process.env);
16
+ ## Events
17
+
18
+ The extension hooks into the following Pi lifecycle events:
19
+
20
+ | Event | Telemetry action |
21
+ |-------|-----------------|
22
+ | `session_start` | Initialize exporters from config |
23
+ | `input` | Start a new trace (traceId boundary = user input) |
24
+ | `turn_start` | Begin a generation span |
25
+ | `before_provider_request` | Record model input |
26
+ | `after_provider_response` | Record model output, usage, latency |
27
+ | `turn_end` | End generation span |
28
+ | `tool_execution_start` | Begin tool span |
29
+ | `tool_execution_end` | End tool span with result |
30
+ | `message_end` | Publish accumulated trace to exporters |
31
+ | `model_select` | Record model switch events |
32
+ | `session_compact` | Record context compaction events |
33
+ | `session_shutdown` | Flush and shutdown exporters |
34
+
35
+ ### Trace lifecycle
36
+
37
+ Traces are scoped to user input boundaries (not individual turns). A single user message may trigger multiple LLM turns and tool calls — all grouped under one trace. The trace is published on `message_end`.
38
+
39
+ ## Configuration
40
+
41
+ Configuration is read from `.pi/settings.json` under the `"pi-telemetry"` key. Project-level settings (`.pi/settings.json` in the working directory) take priority over user-level settings (`~/.pi/agent/settings.json`).
42
+
43
+ ```json
44
+ {
45
+ "pi-telemetry": {
46
+ "serviceName": "my-service",
47
+ "serviceVersion": "1.0.0",
48
+ "includePayloads": true,
49
+ "langfuse": {
50
+ "enabled": true,
51
+ "publicKey": "pk-lf-...",
52
+ "secretKey": "sk-lf-...",
53
+ "baseUrl": "https://cloud.langfuse.com",
54
+ "flushAt": 20,
55
+ "flushIntervalMs": 5000
56
+ },
57
+ "otel": {
58
+ "enabled": true,
59
+ "endpoint": "https://otel-collector.example.com",
60
+ "headers": { "Authorization": "Bearer ..." },
61
+ "flushAt": 20,
62
+ "flushIntervalMs": 5000
63
+ }
64
+ }
65
+ }
19
66
  ```
20
67
 
21
- Telemetry is disabled unless credentials are present. Supported environment variables include:
68
+ ### Config Fields
22
69
 
23
- - `LANGFUSE_ENABLED`
24
- - `LANGFUSE_PUBLIC_KEY`
25
- - `LANGFUSE_SECRET_KEY`
26
- - `LANGFUSE_BASE_URL`
27
- - `LANGFUSE_TRANSPORT`
28
- - `TELEMETRY_SERVICE_NAME`
29
- - `TELEMETRY_SERVICE_VERSION`
30
- - `TELEMETRY_INCLUDE_PAYLOADS`
70
+ | Field | Type | Default | Description |
71
+ |-------|------|---------|-------------|
72
+ | `serviceName` | `string` | `"pi-server"` | Service name for traces |
73
+ | `serviceVersion` | `string` | — | Service version for traces |
74
+ | `includePayloads` | `boolean` | `true` | Include chat payloads, tool args, LLM I/O |
31
75
 
32
- Set `TELEMETRY_INCLUDE_PAYLOADS=false` to remove chat payloads, tool args, and LLM input/output from exported telemetry. For finer control, construct a Langfuse exporter directly and pass `redactEvent`.
76
+ ### Langfuse Config
33
77
 
34
- ## Generic OTEL
78
+ | Field | Type | Default | Description |
79
+ |-------|------|---------|-------------|
80
+ | `enabled` | `boolean` | `false` | Enable Langfuse exporter |
81
+ | `publicKey` | `string` | — | Langfuse public API key |
82
+ | `secretKey` | `string` | — | Langfuse secret API key |
83
+ | `baseUrl` | `string` | `"https://cloud.langfuse.com"` | Langfuse server URL |
84
+ | `flushAt` | `number` | `20` | Batch size before flush |
85
+ | `flushIntervalMs` | `number` | `5000` | Flush interval in ms |
35
86
 
36
- ```ts
37
- import { createOtelRuntimeEventExporterFromEnv } from "@amaster.ai/pi-telemetry/otel";
87
+ ### OTEL Config
38
88
 
39
- const exporter = createOtelRuntimeEventExporterFromEnv(process.env);
40
- ```
89
+ | Field | Type | Default | Description |
90
+ |-------|------|---------|-------------|
91
+ | `enabled` | `boolean` | `false` | Enable OTEL exporter |
92
+ | `endpoint` | `string` | — | OTLP traces endpoint |
93
+ | `headers` | `Record<string, string>` | — | Request headers |
94
+ | `flushAt` | `number` | `20` | Batch size before flush |
95
+ | `flushIntervalMs` | `number` | `5000` | Flush interval in ms |
96
+ | `errorLabel` | `string` | — | Custom label for error messages |
41
97
 
42
- Supported generic OTEL environment variables include:
98
+ When the endpoint does not end with `/v1/traces`, the exporter appends `/v1/traces`.
43
99
 
44
- - `OTEL_EXPORTER_OTLP_ENDPOINT`
45
- - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
46
- - `OTEL_EXPORTER_OTLP_HEADERS`
47
- - `OTEL_EXPORTER_OTLP_TRACES_HEADERS`
48
- - `OTEL_BSP_MAX_EXPORT_BATCH_SIZE`
49
- - `OTEL_BSP_SCHEDULE_DELAY`
50
- - `OTEL_SERVICE_NAME`
51
- - `OTEL_RESOURCE_ATTRIBUTES`
52
- - `OTEL_SDK_DISABLED`
53
- - `TELEMETRY_INCLUDE_PAYLOADS`
54
- - `TELEMETRY_SERVICE_VERSION`
100
+ ## Programmatic Usage
55
101
 
56
- When the endpoint does not end with `/v1/traces`, the exporter appends `/v1/traces`.
57
- Use this entry point for any OTLP/HTTP collector, including Langfuse's OTEL endpoint.
102
+ ```ts
103
+ import { loadConfigFromFile, resolveConfig } from "@amaster.ai/pi-telemetry/config";
104
+ import { createLangfuseExporter } from "@amaster.ai/pi-telemetry/langfuse";
105
+ import { createOtelExporter } from "@amaster.ai/pi-telemetry/otel";
106
+
107
+ const config = resolveConfig(loadConfigFromFile());
108
+ const langfuse = createLangfuseExporter(config);
109
+ const otel = createOtelExporter(config);
110
+ ```
58
111
 
59
112
  ## Privacy
60
113
 
61
- Runtime events may include user prompts, assistant responses, tool arguments, tool outputs, and model inputs/outputs. Keep telemetry disabled by default in downstream applications unless users explicitly configure an exporter.
114
+ Runtime events may include user prompts, assistant responses, tool arguments, tool outputs, and model inputs/outputs. Set `includePayloads: false` to strip these from exported telemetry. For finer control, construct an exporter directly and pass `redactEvent`.
@@ -0,0 +1,27 @@
1
+ import { type PiSettingsOptions } from '@amaster.ai/pi-shared/settings';
2
+ export interface LangfuseConfig {
3
+ enabled?: boolean;
4
+ publicKey?: string;
5
+ secretKey?: string;
6
+ baseUrl?: string;
7
+ flushAt?: number;
8
+ flushIntervalMs?: number;
9
+ }
10
+ export interface OtelConfig {
11
+ enabled?: boolean;
12
+ endpoint?: string;
13
+ headers?: Record<string, string>;
14
+ flushAt?: number;
15
+ flushIntervalMs?: number;
16
+ errorLabel?: string;
17
+ }
18
+ export interface TelemetryConfig {
19
+ serviceName?: string;
20
+ serviceVersion?: string;
21
+ includePayloads?: boolean;
22
+ langfuse?: LangfuseConfig;
23
+ otel?: OtelConfig;
24
+ }
25
+ export declare function resolveConfig(config?: TelemetryConfig): TelemetryConfig;
26
+ export declare function loadConfigFromFile(options?: PiSettingsOptions): TelemetryConfig;
27
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGxF,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAOD,wBAAgB,aAAa,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,eAAe,CAEvE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,eAAe,CAE/E"}
package/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ import { loadPiSettings } from '@amaster.ai/pi-shared/settings';
2
+ import { getAgentDir } from '@earendil-works/pi-coding-agent';
3
+ const DEFAULTS = {
4
+ serviceName: 'pi-server',
5
+ includePayloads: true,
6
+ };
7
+ export function resolveConfig(config) {
8
+ return { ...DEFAULTS, ...config };
9
+ }
10
+ export function loadConfigFromFile(options) {
11
+ return loadPiSettings('pi-telemetry', { agentDir: getAgentDir(), ...options });
12
+ }
13
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAA0B,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AA6B9D,MAAM,QAAQ,GAAoB;IAChC,WAAW,EAAE,WAAW;IACxB,eAAe,EAAE,IAAI;CACtB,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,MAAwB;IACpD,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,OAA2B;IAC5D,OAAO,cAAc,CAAkB,cAAc,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;AAClG,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAmBpE,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAwHjE"}
1
+ {"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAoB,MAAM,iCAAiC,CAAC;AAqKtF,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAsQjE"}
package/dist/extension.js CHANGED
@@ -1,26 +1,169 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { loadConfigFromFile, resolveConfig } from './config.js';
2
3
  import { CompositeRuntimeEventExporter, NoopRuntimeEventExporter, } from './index.js';
3
- import { createRuntimeEventExporterFromEnv } from './langfuse.js';
4
- import { createOtelRuntimeEventExporterFromEnv } from './otel.js';
5
- function extractModelConfig(payload) {
6
- if (payload && typeof payload === 'object' && 'model' in payload) {
7
- const model = payload.model;
8
- if (typeof model === 'string') {
9
- return { provider: 'unknown', model };
4
+ import { createLangfuseExporter } from './langfuse.js';
5
+ import { createOtelExporter } from './otel.js';
6
+ function modelConfigFromCtx(ctx) {
7
+ const model = ctx.model;
8
+ if (!model) {
9
+ return { provider: 'unknown', model: 'unknown' };
10
+ }
11
+ return {
12
+ provider: model.provider ?? 'unknown',
13
+ model: model.id ?? model.name ?? 'unknown',
14
+ };
15
+ }
16
+ function extractOutput(message) {
17
+ if (!message || typeof message !== 'object')
18
+ return undefined;
19
+ const msg = message;
20
+ if (msg.role !== 'assistant' || !Array.isArray(msg.content))
21
+ return undefined;
22
+ const texts = [];
23
+ for (const block of msg.content) {
24
+ if (block &&
25
+ typeof block === 'object' &&
26
+ 'type' in block &&
27
+ block.type === 'text' &&
28
+ 'text' in block) {
29
+ texts.push(String(block.text));
30
+ }
31
+ }
32
+ return texts.length > 0 ? texts.join('\n') : undefined;
33
+ }
34
+ function simplifyContent(content) {
35
+ if (typeof content === 'string')
36
+ return content;
37
+ if (!Array.isArray(content) || content.length === 0)
38
+ return content;
39
+ const allText = content.every((b) => b && typeof b === 'object' && 'type' in b && b.type === 'text');
40
+ if (allText) {
41
+ const texts = content.map((b) => String(b.text));
42
+ return texts.join('\n');
43
+ }
44
+ return content;
45
+ }
46
+ function toolEventDetails(result) {
47
+ const rawDetails = result && typeof result === 'object' && !Array.isArray(result)
48
+ ? result.details
49
+ : undefined;
50
+ const details = sanitizeToolDetails(rawDetails);
51
+ const output = summarizeToolResultOutput(result, rawDetails);
52
+ if (output !== undefined && details.output === undefined) {
53
+ details.output = output;
54
+ }
55
+ return Object.keys(details).length > 0 ? details : undefined;
56
+ }
57
+ function sanitizeToolDetails(value) {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
59
+ return {};
60
+ }
61
+ const sanitized = {};
62
+ for (const [key, raw] of Object.entries(value)) {
63
+ if (key === 'fullOutput' || key === 'fullOutputMimeType') {
64
+ continue;
10
65
  }
66
+ sanitized[key] = toTelemetryValue(raw);
67
+ }
68
+ return sanitized;
69
+ }
70
+ function summarizeToolResultOutput(result, details) {
71
+ if (result === undefined || shouldSuppressToolOutput(details)) {
72
+ return undefined;
73
+ }
74
+ if (typeof result === 'string') {
75
+ return summarizeString(result);
76
+ }
77
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
78
+ return toTelemetryValue(result);
79
+ }
80
+ const resultRecord = result;
81
+ if (resultRecord.output !== undefined) {
82
+ return toTelemetryValue(resultRecord.output);
83
+ }
84
+ const text = textContentFromToolResult(resultRecord);
85
+ if (text) {
86
+ return summarizeString(text);
87
+ }
88
+ return resultRecord.content !== undefined ? toTelemetryValue(resultRecord.content) : undefined;
89
+ }
90
+ function textContentFromToolResult(result) {
91
+ if (!Array.isArray(result.content)) {
92
+ return undefined;
93
+ }
94
+ const text = result.content
95
+ .filter((item) => item && typeof item === 'object' && !Array.isArray(item))
96
+ .map((item) => {
97
+ const record = item;
98
+ return typeof record.text === 'string' ? record.text : undefined;
99
+ })
100
+ .filter(Boolean)
101
+ .join('\n');
102
+ return text || undefined;
103
+ }
104
+ function shouldSuppressToolOutput(details) {
105
+ return Boolean(details &&
106
+ typeof details === 'object' &&
107
+ !Array.isArray(details) &&
108
+ details.outputSuppressed === true);
109
+ }
110
+ function toTelemetryValue(value) {
111
+ if (value === undefined) {
112
+ return undefined;
113
+ }
114
+ if (value === null || typeof value === 'number' || typeof value === 'boolean') {
115
+ return value;
116
+ }
117
+ if (typeof value === 'string') {
118
+ return summarizeString(value);
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return value.map(toTelemetryValue).filter((item) => item !== undefined);
122
+ }
123
+ if (value && typeof value === 'object') {
124
+ return Object.fromEntries(Object.entries(value).map(([key, raw]) => [key, toTelemetryValue(raw)]));
11
125
  }
12
- return { provider: 'unknown', model: 'unknown' };
126
+ return String(value);
127
+ }
128
+ function summarizeString(value) {
129
+ return value.length > 500
130
+ ? `${value.slice(0, 500)}... [truncated ${value.length - 500} chars]`
131
+ : value;
132
+ }
133
+ function mapUsage(usage) {
134
+ const result = {};
135
+ if (typeof usage.input === 'number')
136
+ result.input = usage.input;
137
+ if (typeof usage.output === 'number')
138
+ result.output = usage.output;
139
+ if (typeof usage.cacheRead === 'number')
140
+ result.cacheRead = usage.cacheRead;
141
+ if (typeof usage.cacheWrite === 'number')
142
+ result.cacheWrite = usage.cacheWrite;
143
+ if (typeof usage.totalTokens === 'number')
144
+ result.totalTokens = usage.totalTokens;
145
+ if (usage.cost != null && typeof usage.cost === 'object')
146
+ result.cost = usage.cost;
147
+ return result;
13
148
  }
14
149
  export default function telemetryExtension(pi) {
150
+ const inheritedTraceId = process.env.PI_TELEMETRY_TRACE_ID;
151
+ const inheritedSessionId = process.env.PI_TELEMETRY_SESSION_ID;
152
+ const ownerPid = process.env.PI_TELEMETRY_OWNER_PID;
153
+ const isSubagent = Boolean(inheritedTraceId && ownerPid && ownerPid !== String(process.pid));
15
154
  let exporter = new NoopRuntimeEventExporter();
16
- const sessionId = randomUUID();
17
- let currentTraceId;
18
- let turnStartTime;
155
+ const localSessionId = randomUUID();
156
+ const sessionId = isSubagent && inheritedSessionId ? inheritedSessionId : localSessionId;
157
+ let currentTraceId = isSubagent ? inheritedTraceId : undefined;
158
+ let traceStartTime;
159
+ let tracePublished = false;
19
160
  let llmGenerationCounter = 0;
20
- pi.on('session_start', async () => {
21
- const env = process.env;
22
- const langfuse = createRuntimeEventExporterFromEnv(env);
23
- const otel = createOtelRuntimeEventExporterFromEnv(env);
161
+ let pendingInput;
162
+ let lastModelConfig = { provider: 'unknown', model: 'unknown' };
163
+ pi.on('session_start', async (_event, ctx) => {
164
+ const config = resolveConfig(loadConfigFromFile({ cwd: ctx.cwd }));
165
+ const langfuse = createLangfuseExporter(config);
166
+ const otel = createOtelExporter(config);
24
167
  const active = [langfuse, otel].filter((e) => !(e instanceof NoopRuntimeEventExporter));
25
168
  if (active.length > 1) {
26
169
  exporter = new CompositeRuntimeEventExporter(active);
@@ -29,33 +172,82 @@ export default function telemetryExtension(pi) {
29
172
  exporter = active[0];
30
173
  }
31
174
  });
32
- pi.on('turn_start', async (event) => {
33
- currentTraceId = randomUUID().replace(/-/g, '');
34
- turnStartTime = event.timestamp;
175
+ pi.on('input', async (event) => {
176
+ pendingInput = event.text;
177
+ if (!isSubagent) {
178
+ currentTraceId = randomUUID().replace(/-/g, '');
179
+ process.env.PI_TELEMETRY_TRACE_ID = currentTraceId;
180
+ process.env.PI_TELEMETRY_SESSION_ID = sessionId;
181
+ process.env.PI_TELEMETRY_OWNER_PID = String(process.pid);
182
+ }
183
+ traceStartTime = undefined;
184
+ tracePublished = false;
35
185
  llmGenerationCounter = 0;
36
- await exporter.publish({
37
- id: randomUUID(),
38
- traceId: currentTraceId,
39
- type: 'chat_turn_started',
40
- sessionId,
41
- createdAt: new Date(event.timestamp).toISOString(),
42
- });
43
186
  });
44
- pi.on('turn_end', async () => {
187
+ pi.on('turn_start', async (event) => {
188
+ if (!currentTraceId) {
189
+ currentTraceId = randomUUID().replace(/-/g, '');
190
+ llmGenerationCounter = 0;
191
+ tracePublished = false;
192
+ }
193
+ if (!traceStartTime) {
194
+ traceStartTime = event.timestamp;
195
+ }
196
+ if (!tracePublished) {
197
+ tracePublished = true;
198
+ if (isSubagent) {
199
+ await exporter.publish({
200
+ id: randomUUID(),
201
+ traceId: currentTraceId,
202
+ type: 'subagent_started',
203
+ sessionId,
204
+ childSessionId: localSessionId,
205
+ createdAt: new Date(event.timestamp).toISOString(),
206
+ ...(pendingInput !== undefined ? { details: { input: pendingInput } } : {}),
207
+ });
208
+ }
209
+ else {
210
+ await exporter.publish({
211
+ id: randomUUID(),
212
+ traceId: currentTraceId,
213
+ type: 'chat_turn_started',
214
+ sessionId,
215
+ createdAt: new Date(event.timestamp).toISOString(),
216
+ ...(pendingInput !== undefined ? { details: { input: pendingInput } } : {}),
217
+ });
218
+ }
219
+ pendingInput = undefined;
220
+ }
221
+ });
222
+ pi.on('turn_end', async (event) => {
45
223
  if (!currentTraceId)
46
224
  return;
47
225
  const now = Date.now();
48
- const durationMs = turnStartTime ? now - turnStartTime : undefined;
49
- await exporter.publish({
50
- id: randomUUID(),
51
- traceId: currentTraceId,
52
- type: 'chat_turn_completed',
53
- sessionId,
54
- createdAt: new Date(now).toISOString(),
55
- ...(durationMs !== undefined ? { durationMs } : {}),
56
- });
57
- currentTraceId = undefined;
58
- turnStartTime = undefined;
226
+ const durationMs = traceStartTime ? now - traceStartTime : undefined;
227
+ const output = extractOutput(event.message);
228
+ if (isSubagent) {
229
+ await exporter.publish({
230
+ id: randomUUID(),
231
+ traceId: currentTraceId,
232
+ type: 'subagent_completed',
233
+ sessionId,
234
+ childSessionId: localSessionId,
235
+ createdAt: new Date(now).toISOString(),
236
+ ...(durationMs !== undefined ? { durationMs } : {}),
237
+ ...(output !== undefined ? { details: { output } } : {}),
238
+ });
239
+ }
240
+ else {
241
+ await exporter.publish({
242
+ id: randomUUID(),
243
+ traceId: currentTraceId,
244
+ type: 'chat_turn_completed',
245
+ sessionId,
246
+ createdAt: new Date(now).toISOString(),
247
+ ...(durationMs !== undefined ? { durationMs } : {}),
248
+ ...(output !== undefined ? { details: { output } } : {}),
249
+ });
250
+ }
59
251
  });
60
252
  pi.on('tool_execution_start', async (event) => {
61
253
  if (!currentTraceId)
@@ -63,8 +255,9 @@ export default function telemetryExtension(pi) {
63
255
  await exporter.publish({
64
256
  id: randomUUID(),
65
257
  traceId: currentTraceId,
66
- sessionId,
67
- conversationId: sessionId,
258
+ sessionId: localSessionId,
259
+ conversationId: localSessionId,
260
+ ...(isSubagent ? { childSessionId: localSessionId } : {}),
68
261
  toolCallId: event.toolCallId,
69
262
  toolName: event.toolName,
70
263
  status: 'started',
@@ -75,46 +268,130 @@ export default function telemetryExtension(pi) {
75
268
  pi.on('tool_execution_end', async (event) => {
76
269
  if (!currentTraceId)
77
270
  return;
271
+ const details = toolEventDetails(event.result);
78
272
  await exporter.publish({
79
273
  id: randomUUID(),
80
274
  traceId: currentTraceId,
81
- sessionId,
82
- conversationId: sessionId,
275
+ sessionId: localSessionId,
276
+ conversationId: localSessionId,
277
+ ...(isSubagent ? { childSessionId: localSessionId } : {}),
83
278
  toolCallId: event.toolCallId,
84
279
  toolName: event.toolName,
85
280
  status: event.isError ? 'failed' : 'completed',
86
281
  createdAt: new Date().toISOString(),
87
- ...(event.isError ? { error: String(event.result) } : {}),
282
+ ...(details ? { details } : {}),
283
+ ...(event.isError
284
+ ? { error: typeof event.result === 'string' ? event.result : JSON.stringify(event.result) }
285
+ : {}),
88
286
  });
89
287
  });
90
- pi.on('before_provider_request', async (event) => {
288
+ pi.on('before_provider_request', async (event, ctx) => {
91
289
  if (!currentTraceId)
92
290
  return;
93
291
  llmGenerationCounter++;
292
+ lastModelConfig = modelConfigFromCtx(ctx);
94
293
  await exporter.publish({
95
294
  id: randomUUID(),
96
295
  traceId: currentTraceId,
97
- sessionId,
98
- conversationId: sessionId,
296
+ sessionId: localSessionId,
297
+ conversationId: localSessionId,
298
+ ...(isSubagent ? { childSessionId: localSessionId } : {}),
99
299
  llmGenerationId: `gen-${llmGenerationCounter}`,
100
300
  status: 'started',
101
301
  createdAt: new Date().toISOString(),
102
- model: extractModelConfig(event.payload),
302
+ model: lastModelConfig,
303
+ input: event.payload,
103
304
  });
104
305
  });
105
- pi.on('after_provider_response', async (event) => {
306
+ pi.on('after_provider_response', async (event, ctx) => {
106
307
  if (!currentTraceId)
107
308
  return;
309
+ if (event.status < 400)
310
+ return;
108
311
  await exporter.publish({
109
312
  id: randomUUID(),
110
313
  traceId: currentTraceId,
111
- sessionId,
112
- conversationId: sessionId,
314
+ sessionId: localSessionId,
315
+ conversationId: localSessionId,
316
+ ...(isSubagent ? { childSessionId: localSessionId } : {}),
113
317
  llmGenerationId: `gen-${llmGenerationCounter}`,
114
- status: event.status >= 400 ? 'failed' : 'completed',
318
+ status: 'failed',
319
+ createdAt: new Date().toISOString(),
320
+ model: modelConfigFromCtx(ctx),
321
+ error: `HTTP ${event.status}`,
322
+ });
323
+ });
324
+ pi.on('message_end', async (event) => {
325
+ if (!currentTraceId)
326
+ return;
327
+ const msg = event.message;
328
+ if (msg.role !== 'assistant')
329
+ return;
330
+ const content = simplifyContent(msg.content) ?? extractOutput(event.message);
331
+ const usage = msg.usage;
332
+ const mapped = usage ? mapUsage(usage) : undefined;
333
+ const output = {};
334
+ if (content !== undefined)
335
+ output.content = content;
336
+ if (usage !== undefined)
337
+ output.usage = usage;
338
+ await exporter.publish({
339
+ id: randomUUID(),
340
+ traceId: currentTraceId,
341
+ sessionId: localSessionId,
342
+ conversationId: localSessionId,
343
+ ...(isSubagent ? { childSessionId: localSessionId } : {}),
344
+ llmGenerationId: `gen-${llmGenerationCounter}`,
345
+ status: 'completed',
346
+ createdAt: new Date().toISOString(),
347
+ model: lastModelConfig,
348
+ ...(Object.keys(output).length > 0 ? { output } : {}),
349
+ ...(mapped ? { usage: mapped } : {}),
350
+ ...(typeof msg.responseId === 'string' ? { responseId: msg.responseId } : {}),
351
+ ...(typeof msg.stopReason === 'string' ? { stopReason: msg.stopReason } : {}),
352
+ });
353
+ });
354
+ pi.on('model_select', async (event) => {
355
+ if (!currentTraceId)
356
+ return;
357
+ const evt = event;
358
+ const model = evt.model;
359
+ const previousModel = evt.previousModel;
360
+ await exporter.publish({
361
+ id: randomUUID(),
362
+ traceId: currentTraceId,
363
+ type: 'chat_turn_steered',
364
+ sessionId,
365
+ createdAt: new Date().toISOString(),
366
+ details: {
367
+ eventType: 'model_switch',
368
+ from: previousModel
369
+ ? {
370
+ provider: String(previousModel.provider ?? 'unknown'),
371
+ model: String(previousModel.id ?? 'unknown'),
372
+ }
373
+ : null,
374
+ to: model
375
+ ? { provider: String(model.provider ?? 'unknown'), model: String(model.id ?? 'unknown') }
376
+ : null,
377
+ source: String(evt.source ?? 'unknown'),
378
+ },
379
+ });
380
+ });
381
+ pi.on('session_compact', async (event) => {
382
+ if (!currentTraceId)
383
+ return;
384
+ const evt = event;
385
+ await exporter.publish({
386
+ id: randomUUID(),
387
+ traceId: currentTraceId,
388
+ type: 'chat_turn_steered',
389
+ sessionId,
115
390
  createdAt: new Date().toISOString(),
116
- model: extractModelConfig(undefined),
117
- ...(event.status >= 400 ? { error: `HTTP ${event.status}` } : {}),
391
+ details: {
392
+ eventType: 'session_compact',
393
+ fromExtension: evt.fromExtension ?? false,
394
+ },
118
395
  });
119
396
  });
120
397
  pi.on('session_shutdown', async () => {