@narumitw/pi-langfuse 0.49.3 → 0.50.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/README.md +6 -5
- package/package.json +9 -6
- package/src/config.ts +8 -0
- package/src/langfuse.ts +1 -0
- package/src/runtime.ts +27 -9
- package/src/tracing.ts +22 -6
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ pi -e npm:@narumitw/pi-langfuse
|
|
|
35
35
|
Try a local checkout:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
pi -e ./
|
|
38
|
+
pi -e ./packages/pi-langfuse
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
The Langfuse v4 SDK requires Node.js 20 or newer.
|
|
@@ -61,13 +61,14 @@ You can also create the file manually:
|
|
|
61
61
|
"baseUrl": "https://us.cloud.langfuse.com",
|
|
62
62
|
"environment": "development",
|
|
63
63
|
"release": "local",
|
|
64
|
+
"userId": "your-user-id",
|
|
64
65
|
"captureContent": true
|
|
65
66
|
}
|
|
66
67
|
```
|
|
67
68
|
|
|
68
69
|
`publicKey` and `secretKey` are required literal strings. Environment-variable and command interpolation are intentionally unsupported. `baseUrl` defaults to `https://us.cloud.langfuse.com`; regional and self-hosted HTTP or HTTPS endpoints are supported. Prefer HTTPS because HTTP sends Langfuse credentials and trace content without transport encryption.
|
|
69
70
|
|
|
70
|
-
`environment` and `
|
|
71
|
+
`environment`, `release`, and `userId` are optional Langfuse trace attributes. An environment must match Langfuse's contract: at most 40 lowercase letters, numbers, hyphens, or underscores, and it cannot start with `langfuse`. `userId` populates the Langfuse user dimension, which is what the Sessions and Traces views group by; Langfuse accepts at most 200 characters, and leaving it unset reports no user. Set `captureContent` to `false` to trace timing, model, usage, cost, status, and bounded diagnostic metadata without sending prompts, provider-request snapshots, responses, or tool content.
|
|
71
72
|
|
|
72
73
|
The extension automatically restricts an existing config file to mode `0600` and refuses to load credentials if that protection cannot be enforced. You can also set it explicitly:
|
|
73
74
|
|
|
@@ -75,7 +76,7 @@ The extension automatically restricts an existing config file to mode `0600` and
|
|
|
75
76
|
chmod 600 ~/.pi/agent/pi-langfuse.json
|
|
76
77
|
```
|
|
77
78
|
|
|
78
|
-
Restart Pi after changing credentials, endpoint, environment, release, or `captureContent`. The isolated OpenTelemetry tracer provider is initialized once per Pi process and selected only for Langfuse; it does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
|
|
79
|
+
Restart Pi after changing credentials, endpoint, environment, release, or `captureContent`. Changes to `userId` apply to new sessions without restarting Pi. The isolated OpenTelemetry tracer provider is initialized once per Pi process and selected only for Langfuse; it does not replace Pi's process-global provider or send Langfuse observations to another extension's exporter.
|
|
79
80
|
|
|
80
81
|
## 🔭 What is traced
|
|
81
82
|
|
|
@@ -166,14 +167,14 @@ includes current tracing state and the manual configuration path.
|
|
|
166
167
|
|
|
167
168
|
With content capture enabled, traces can contain user prompts, model responses, tool arguments, and tool results. These may include source code, file contents, shell output, or other sensitive project data. Review your Langfuse retention and access controls before enabling this extension.
|
|
168
169
|
|
|
169
|
-
Git branch names, commit ids, working directory, session/leaf ids, model identity, usage/cost, aggregate counts, and allowlisted response-header values are metadata. They remain exported when `captureContent` is `false`; branch names and diagnostic header values can themselves contain operational details.
|
|
170
|
+
Git branch names, commit ids, working directory, session/leaf ids, the configured `userId`, model identity, usage/cost, aggregate counts, and allowlisted response-header values are metadata. They remain exported when `captureContent` is `false`; branch names and diagnostic header values can themselves contain operational details, and a `userId` may itself be personally identifying if you set it to an email address or a real name. Choose a pseudonymous value when that matters, or leave `userId` unset to export no user at all.
|
|
170
171
|
|
|
171
172
|
The built-in mask specifically protects Langfuse credentials; it is not a general secret scanner. Set `"captureContent": false` in `pi-langfuse.json` when prompts, provider-request snapshots, responses, and tool content must remain local. Compaction summaries, tool partial results, opaque continuation signatures, authorization headers, cookies, and unapproved response headers are never exported in either mode.
|
|
172
173
|
|
|
173
174
|
## 🗂️ Package layout
|
|
174
175
|
|
|
175
176
|
```txt
|
|
176
|
-
|
|
177
|
+
packages/pi-langfuse/
|
|
177
178
|
├── src/
|
|
178
179
|
│ ├── index.ts # Pi package entrypoint
|
|
179
180
|
│ ├── langfuse.ts # Pi lifecycle integration and slash command
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-langfuse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "Pi extension that traces LLM generations and tool activity to Langfuse.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"./src/index.ts"
|
|
24
24
|
]
|
|
25
25
|
},
|
|
26
|
+
"piExtension": {
|
|
27
|
+
"lifecycle": "stable"
|
|
28
|
+
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"check": "biome check . && npm run typecheck",
|
|
28
31
|
"format": "biome check --write .",
|
|
@@ -32,15 +35,15 @@
|
|
|
32
35
|
"@earendil-works/pi-coding-agent": "*"
|
|
33
36
|
},
|
|
34
37
|
"devDependencies": {
|
|
35
|
-
"@biomejs/biome": "2.5.
|
|
36
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
37
|
-
"@types/node": "26.
|
|
38
|
+
"@biomejs/biome": "2.5.8",
|
|
39
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
40
|
+
"@types/node": "26.2.0",
|
|
38
41
|
"typescript": "7.0.2"
|
|
39
42
|
},
|
|
40
43
|
"dependencies": {
|
|
41
44
|
"@langfuse/otel": "^5.10.0",
|
|
42
45
|
"@langfuse/tracing": "^5.10.0",
|
|
43
|
-
"@narumitw/pi-tui-kit": "^0.
|
|
46
|
+
"@narumitw/pi-tui-kit": "^0.55.0",
|
|
44
47
|
"@opentelemetry/api": "^1.9.1",
|
|
45
48
|
"@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
|
|
46
49
|
"@opentelemetry/sdk-trace-base": "^2.10.0",
|
|
@@ -49,6 +52,6 @@
|
|
|
49
52
|
"repository": {
|
|
50
53
|
"type": "git",
|
|
51
54
|
"url": "https://github.com/narumiruna/pi-extensions",
|
|
52
|
-
"directory": "
|
|
55
|
+
"directory": "packages/pi-langfuse"
|
|
53
56
|
}
|
|
54
57
|
}
|
package/src/config.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface LangfuseConfig {
|
|
|
12
12
|
baseUrl: string;
|
|
13
13
|
environment?: string;
|
|
14
14
|
release?: string;
|
|
15
|
+
userId?: string;
|
|
15
16
|
captureContent: boolean;
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -61,6 +62,7 @@ async function writeLangfuseConfigNow(
|
|
|
61
62
|
baseUrl: _baseUrl,
|
|
62
63
|
environment: _environment,
|
|
63
64
|
release: _release,
|
|
65
|
+
userId: _userId,
|
|
64
66
|
captureContent: _captureContent,
|
|
65
67
|
...unknownFields
|
|
66
68
|
} = currentDocument;
|
|
@@ -209,6 +211,11 @@ export function normalizeLangfuseConfig(
|
|
|
209
211
|
}
|
|
210
212
|
const release = optionalString(input.release, "release");
|
|
211
213
|
if (!release.ok) return release;
|
|
214
|
+
const userId = optionalString(input.userId, "userId");
|
|
215
|
+
if (!userId.ok) return userId;
|
|
216
|
+
if (userId.value && userId.value.length > 200) {
|
|
217
|
+
return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
|
|
218
|
+
}
|
|
212
219
|
|
|
213
220
|
return {
|
|
214
221
|
ok: true,
|
|
@@ -218,6 +225,7 @@ export function normalizeLangfuseConfig(
|
|
|
218
225
|
baseUrl,
|
|
219
226
|
...(environment.value ? { environment: environment.value } : {}),
|
|
220
227
|
...(release.value ? { release: release.value } : {}),
|
|
228
|
+
...(userId.value ? { userId: userId.value } : {}),
|
|
221
229
|
captureContent: input.captureContent ?? true,
|
|
222
230
|
},
|
|
223
231
|
};
|
package/src/langfuse.ts
CHANGED
|
@@ -201,6 +201,7 @@ export function createLangfuseExtension(
|
|
|
201
201
|
activeConfig = result.config;
|
|
202
202
|
recorder = new TraceRecorder(backend, {
|
|
203
203
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
204
|
+
...(result.config.userId ? { userId: result.config.userId } : {}),
|
|
204
205
|
cwd: ctx.cwd,
|
|
205
206
|
mode: ctx.mode,
|
|
206
207
|
captureContent: result.config.captureContent,
|
package/src/runtime.ts
CHANGED
|
@@ -39,21 +39,23 @@ class ProductionObservation implements Observation {
|
|
|
39
39
|
constructor(readonly native: LangfuseObservation) {}
|
|
40
40
|
|
|
41
41
|
update(attributes: ObservationAttributes): Observation {
|
|
42
|
-
|
|
42
|
+
const { sessionId, userId, ...observationAttributes } = attributes;
|
|
43
|
+
this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
|
|
44
|
+
applySessionId(this.native, sessionId);
|
|
45
|
+
applyUserId(this.native, userId);
|
|
43
46
|
return this;
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
updateTrace(attributes: ObservationAttributes): Observation {
|
|
47
|
-
const { input, output, metadata, name, sessionId, tags, version } = attributes;
|
|
50
|
+
const { input, output, metadata, name, sessionId, userId, tags, version } = attributes;
|
|
48
51
|
if (input !== undefined || output !== undefined) {
|
|
49
52
|
this.native.setTraceIO({ input, output });
|
|
50
53
|
}
|
|
51
54
|
if (name !== undefined) {
|
|
52
55
|
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_NAME, name);
|
|
53
56
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
+
applySessionId(this.native, sessionId);
|
|
58
|
+
applyUserId(this.native, userId);
|
|
57
59
|
if (tags !== undefined) {
|
|
58
60
|
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_TAGS, tags);
|
|
59
61
|
}
|
|
@@ -86,11 +88,15 @@ class ProductionTraceBackend implements TraceBackend {
|
|
|
86
88
|
attributes: ObservationAttributes,
|
|
87
89
|
options: { asType: ObservationType; parent?: Observation },
|
|
88
90
|
): Observation {
|
|
91
|
+
const { sessionId, userId, ...observationAttributes } = attributes;
|
|
89
92
|
const parent = options.parent;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
const native =
|
|
94
|
+
parent instanceof ProductionObservation
|
|
95
|
+
? startChild(parent.native, name, observationAttributes, options.asType)
|
|
96
|
+
: startRoot(name, observationAttributes, options.asType);
|
|
97
|
+
applySessionId(native, sessionId);
|
|
98
|
+
applyUserId(native, userId);
|
|
99
|
+
return new ProductionObservation(native);
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
async forceFlush(): Promise<void> {
|
|
@@ -177,6 +183,18 @@ const defaultFactories: RuntimeFactories = {
|
|
|
177
183
|
selectProvider: setLangfuseTracerProvider,
|
|
178
184
|
};
|
|
179
185
|
|
|
186
|
+
function applySessionId(observation: LangfuseObservation, sessionId: string | undefined): void {
|
|
187
|
+
if (sessionId !== undefined) {
|
|
188
|
+
observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function applyUserId(observation: LangfuseObservation, userId: string | undefined): void {
|
|
193
|
+
if (userId !== undefined) {
|
|
194
|
+
observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_USER_ID, userId);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
180
198
|
function startRoot(
|
|
181
199
|
name: string,
|
|
182
200
|
attributes: ObservationAttributes,
|
package/src/tracing.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface ObservationAttributes {
|
|
|
40
40
|
version?: string;
|
|
41
41
|
name?: string;
|
|
42
42
|
sessionId?: string;
|
|
43
|
+
userId?: string;
|
|
43
44
|
tags?: string[];
|
|
44
45
|
}
|
|
45
46
|
|
|
@@ -63,6 +64,7 @@ export interface TraceBackend {
|
|
|
63
64
|
|
|
64
65
|
interface RecorderContext {
|
|
65
66
|
sessionId: string;
|
|
67
|
+
userId?: string;
|
|
66
68
|
cwd: string;
|
|
67
69
|
mode: string;
|
|
68
70
|
captureContent: boolean;
|
|
@@ -234,6 +236,19 @@ export class TraceRecorder {
|
|
|
234
236
|
private readonly context: RecorderContext,
|
|
235
237
|
) {}
|
|
236
238
|
|
|
239
|
+
private startObservation(
|
|
240
|
+
name: string,
|
|
241
|
+
attributes: ObservationAttributes,
|
|
242
|
+
options: { asType: ObservationType; parent?: Observation },
|
|
243
|
+
): Observation {
|
|
244
|
+
const { sessionId, userId } = this.context;
|
|
245
|
+
return this.backend.start(
|
|
246
|
+
name,
|
|
247
|
+
{ ...attributes, sessionId, ...(userId ? { userId } : {}) },
|
|
248
|
+
options,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
237
252
|
hasActiveTrace(): boolean {
|
|
238
253
|
return this.root !== undefined;
|
|
239
254
|
}
|
|
@@ -275,10 +290,11 @@ export class TraceRecorder {
|
|
|
275
290
|
? "git:detached"
|
|
276
291
|
: undefined;
|
|
277
292
|
|
|
278
|
-
this.root = this.
|
|
293
|
+
this.root = this.startObservation("pi.agent", attributes, { asType: "agent" });
|
|
279
294
|
this.root.updateTrace?.({
|
|
280
295
|
name: "pi.trace",
|
|
281
296
|
sessionId: this.context.sessionId,
|
|
297
|
+
...(this.context.userId ? { userId: this.context.userId } : {}),
|
|
282
298
|
version: TRACE_SCHEMA_VERSION,
|
|
283
299
|
input: traceInput,
|
|
284
300
|
metadata,
|
|
@@ -296,7 +312,7 @@ export class TraceRecorder {
|
|
|
296
312
|
this.lastAssistant = undefined;
|
|
297
313
|
this.unresolvedToolErrors = 0;
|
|
298
314
|
this.attemptIndex = index;
|
|
299
|
-
this.attempt = this.
|
|
315
|
+
this.attempt = this.startObservation(
|
|
300
316
|
"pi.attempt",
|
|
301
317
|
{
|
|
302
318
|
metadata: {
|
|
@@ -325,7 +341,7 @@ export class TraceRecorder {
|
|
|
325
341
|
if (this.turn) this.closeTurn("Interrupted by the next Pi turn.", "ERROR");
|
|
326
342
|
this.counters.turns += 1;
|
|
327
343
|
this.turnIndex = turnIndex;
|
|
328
|
-
this.turn = this.
|
|
344
|
+
this.turn = this.startObservation(
|
|
329
345
|
"pi.turn",
|
|
330
346
|
{ metadata: { "pi.turn.index": turnIndex }, version: TRACE_SCHEMA_VERSION },
|
|
331
347
|
{ asType: "span", parent: this.attempt ?? this.root },
|
|
@@ -372,7 +388,7 @@ export class TraceRecorder {
|
|
|
372
388
|
...(input.model?.api ? { "pi.request.api": input.model.api } : {}),
|
|
373
389
|
...(input.thinkingLevel ? { "pi.request.thinking_level": input.thinkingLevel } : {}),
|
|
374
390
|
};
|
|
375
|
-
const observation = this.
|
|
391
|
+
const observation = this.startObservation(
|
|
376
392
|
"pi.llm",
|
|
377
393
|
{
|
|
378
394
|
...(input.payload !== undefined ? { input: this.capture(input.payload) } : {}),
|
|
@@ -559,7 +575,7 @@ export class TraceRecorder {
|
|
|
559
575
|
this.closeCompaction("Interrupted by another Pi compaction.");
|
|
560
576
|
this.counters.compactions += 1;
|
|
561
577
|
this.compaction = {
|
|
562
|
-
observation: this.
|
|
578
|
+
observation: this.startObservation(
|
|
563
579
|
"pi.compaction",
|
|
564
580
|
{
|
|
565
581
|
metadata: {
|
|
@@ -711,7 +727,7 @@ export class TraceRecorder {
|
|
|
711
727
|
}
|
|
712
728
|
|
|
713
729
|
private startToolObservation(toolCallId: string, toolName: string, args?: unknown): Observation {
|
|
714
|
-
return this.
|
|
730
|
+
return this.startObservation(
|
|
715
731
|
`pi.tool.${toolName}`,
|
|
716
732
|
{
|
|
717
733
|
...(args !== undefined ? { input: this.capture(args) } : {}),
|