@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 +90 -37
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -0
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +329 -52
- package/dist/extension.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/langfuse.d.ts +3 -24
- package/dist/langfuse.d.ts.map +1 -1
- package/dist/langfuse.js +34 -287
- package/dist/langfuse.js.map +1 -1
- package/dist/otel.d.ts +3 -0
- package/dist/otel.d.ts.map +1 -1
- package/dist/otel.js +21 -0
- package/dist/otel.js.map +1 -1
- package/package.json +24 -2
- package/preview.png +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @amaster.ai/pi-telemetry
|
|
2
2
|
|
|
3
|
+

|
|
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/
|
|
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
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
68
|
+
### Config Fields
|
|
22
69
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
76
|
+
### Langfuse Config
|
|
33
77
|
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
import { createOtelRuntimeEventExporterFromEnv } from "@amaster.ai/pi-telemetry/otel";
|
|
87
|
+
### OTEL Config
|
|
38
88
|
|
|
39
|
-
|
|
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
|
-
|
|
98
|
+
When the endpoint does not end with `/v1/traces`, the exporter appends `/v1/traces`.
|
|
43
99
|
|
|
44
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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.
|
|
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`.
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|
package/dist/extension.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"
|
|
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 {
|
|
4
|
-
import {
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
|
17
|
-
|
|
18
|
-
let
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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('
|
|
33
|
-
|
|
34
|
-
|
|
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('
|
|
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 =
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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:
|
|
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:
|
|
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
|
-
...(
|
|
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:
|
|
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:
|
|
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:
|
|
314
|
+
sessionId: localSessionId,
|
|
315
|
+
conversationId: localSessionId,
|
|
316
|
+
...(isSubagent ? { childSessionId: localSessionId } : {}),
|
|
113
317
|
llmGenerationId: `gen-${llmGenerationCounter}`,
|
|
114
|
-
status:
|
|
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
|
-
|
|
117
|
-
|
|
391
|
+
details: {
|
|
392
|
+
eventType: 'session_compact',
|
|
393
|
+
fromExtension: evt.fromExtension ?? false,
|
|
394
|
+
},
|
|
118
395
|
});
|
|
119
396
|
});
|
|
120
397
|
pi.on('session_shutdown', async () => {
|