pi-langfuse 1.4.5 → 1.4.7
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/package.json +4 -2
- package/src/commands.ts +48 -25
- package/src/langfuse.ts +62 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-langfuse",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.7",
|
|
4
4
|
"description": "Langfuse extension for Pi coding agent",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"tsconfig.json"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
|
-
"typecheck": "tsc --noEmit"
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "tsx --test test/*.test.ts"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"pi-package",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"node": ">=22"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
64
|
+
"tsx": "^4.19.0",
|
|
63
65
|
"typescript": "^6.0.3"
|
|
64
66
|
}
|
|
65
67
|
}
|
package/src/commands.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface CommandContextLike {
|
|
|
13
13
|
hasUI?: boolean;
|
|
14
14
|
ui?: {
|
|
15
15
|
notify?: (message: string, level?: "info" | "warning" | "error") => void;
|
|
16
|
+
select?: (title: string, options: string[]) => Promise<string | undefined>;
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -147,6 +148,36 @@ function hasActiveAgentObservation() {
|
|
|
147
148
|
return false;
|
|
148
149
|
}
|
|
149
150
|
|
|
151
|
+
function savePrivacyPreset(
|
|
152
|
+
requestedPreset: PrivacyPreset,
|
|
153
|
+
ctx: CommandContextLike,
|
|
154
|
+
configPath: string,
|
|
155
|
+
): boolean {
|
|
156
|
+
const existing = readPersistedConfig(configPath);
|
|
157
|
+
const loaded = state.config ?? loadConfig(process.env, configPath);
|
|
158
|
+
|
|
159
|
+
const publicKey = existing.publicKey ?? loaded?.publicKey;
|
|
160
|
+
const secretKey = existing.secretKey ?? loaded?.secretKey;
|
|
161
|
+
const host = existing.host ?? loaded?.host;
|
|
162
|
+
|
|
163
|
+
if (!publicKey || !secretKey || !host) {
|
|
164
|
+
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup before changing privacy settings.", "warning");
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextConfig = {
|
|
169
|
+
publicKey: String(publicKey),
|
|
170
|
+
secretKey: String(secretKey),
|
|
171
|
+
host: String(host),
|
|
172
|
+
privacyPreset: requestedPreset,
|
|
173
|
+
};
|
|
174
|
+
saveConfig(nextConfig, configPath);
|
|
175
|
+
state.config = loadConfig(process.env, configPath);
|
|
176
|
+
|
|
177
|
+
notify(ctx, `Langfuse privacy preset saved: ${requestedPreset}\n${describePolicy(state.config?.capturePolicy ?? createCapturePolicy())}`);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
150
181
|
export async function handleLangfusePrivacyCommand(
|
|
151
182
|
args: string,
|
|
152
183
|
ctx: CommandContextLike,
|
|
@@ -163,6 +194,22 @@ export async function handleLangfusePrivacyCommand(
|
|
|
163
194
|
if (!requestedPreset) {
|
|
164
195
|
state.config = state.config ?? loadConfig(process.env, configPath);
|
|
165
196
|
const policy = state.config?.capturePolicy ?? createCapturePolicy();
|
|
197
|
+
if (ctx.hasUI && ctx.ui?.select) {
|
|
198
|
+
const currentPreset = inferPreset(policy);
|
|
199
|
+
const selectedPreset = await ctx.ui.select(
|
|
200
|
+
`Langfuse privacy preset (current: ${currentPreset})`,
|
|
201
|
+
[...PRIVACY_PRESETS],
|
|
202
|
+
);
|
|
203
|
+
if (!selectedPreset) {
|
|
204
|
+
notify(ctx, `Current Langfuse privacy preset: ${currentPreset}\n${describePolicy(policy)}`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (!isPrivacyPreset(selectedPreset)) {
|
|
208
|
+
notify(ctx, `Unknown privacy preset '${selectedPreset}'. Use one of: ${PRIVACY_PRESETS.join(", ")}.`, "warning");
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return savePrivacyPreset(selectedPreset, ctx, configPath);
|
|
212
|
+
}
|
|
166
213
|
notify(ctx, `Current Langfuse privacy preset: ${inferPreset(policy)}\n${describePolicy(policy)}`);
|
|
167
214
|
return true;
|
|
168
215
|
}
|
|
@@ -176,29 +223,7 @@ export async function handleLangfusePrivacyCommand(
|
|
|
176
223
|
return false;
|
|
177
224
|
}
|
|
178
225
|
|
|
179
|
-
|
|
180
|
-
const loaded = state.config ?? loadConfig(process.env, configPath);
|
|
181
|
-
|
|
182
|
-
const publicKey = existing.publicKey ?? loaded?.publicKey;
|
|
183
|
-
const secretKey = existing.secretKey ?? loaded?.secretKey;
|
|
184
|
-
const host = existing.host ?? loaded?.host;
|
|
185
|
-
|
|
186
|
-
if (!publicKey || !secretKey || !host) {
|
|
187
|
-
notify(ctx, "Langfuse is not configured yet. Run /langfuse-setup before changing privacy settings.", "warning");
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const nextConfig = {
|
|
192
|
-
publicKey: String(publicKey),
|
|
193
|
-
secretKey: String(secretKey),
|
|
194
|
-
host: String(host),
|
|
195
|
-
privacyPreset: requestedPreset,
|
|
196
|
-
};
|
|
197
|
-
saveConfig(nextConfig, configPath);
|
|
198
|
-
state.config = loadConfig(process.env, configPath);
|
|
199
|
-
|
|
200
|
-
notify(ctx, `Langfuse privacy preset saved: ${requestedPreset}\n${describePolicy(state.config?.capturePolicy ?? createCapturePolicy())}`);
|
|
201
|
-
return true;
|
|
226
|
+
return savePrivacyPreset(requestedPreset, ctx, configPath);
|
|
202
227
|
}
|
|
203
228
|
|
|
204
229
|
export async function handleLangfuseTestCommand(
|
|
@@ -245,8 +270,6 @@ export async function handleLangfuseTestCommand(
|
|
|
245
270
|
return observation;
|
|
246
271
|
},
|
|
247
272
|
);
|
|
248
|
-
await rt.tracerProvider?.forceFlush?.();
|
|
249
|
-
await rt.scoreClient.flush?.();
|
|
250
273
|
notify(ctx, `Langfuse test succeeded. Test trace sent to ${state.config?.host}.`);
|
|
251
274
|
return true;
|
|
252
275
|
} catch (error) {
|
package/src/langfuse.ts
CHANGED
|
@@ -45,6 +45,9 @@ interface RestFallbackStore {
|
|
|
45
45
|
|
|
46
46
|
const OTEL_VISIBILITY_TIMEOUT_MS = 1_500;
|
|
47
47
|
const OTEL_VISIBILITY_POLL_INTERVAL_MS = 200;
|
|
48
|
+
const DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS = 2_000;
|
|
49
|
+
|
|
50
|
+
let shutdownStepTimeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS;
|
|
48
51
|
|
|
49
52
|
function nowIso() {
|
|
50
53
|
return new Date().toISOString();
|
|
@@ -54,6 +57,29 @@ function delay(ms: number) {
|
|
|
54
57
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
async function withTimeout<T>(label: string, operation: Promise<T> | undefined): Promise<T | undefined> {
|
|
61
|
+
if (!operation) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
66
|
+
try {
|
|
67
|
+
return await Promise.race([
|
|
68
|
+
operation,
|
|
69
|
+
new Promise<undefined>((resolve) => {
|
|
70
|
+
timeout = setTimeout(() => {
|
|
71
|
+
console.log(`📊 Langfuse: ${label} timed out after ${shutdownStepTimeoutMs}ms`);
|
|
72
|
+
resolve(undefined);
|
|
73
|
+
}, shutdownStepTimeoutMs);
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
} finally {
|
|
77
|
+
if (timeout) {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
57
83
|
function toIso(value: unknown): string | undefined {
|
|
58
84
|
if (!value) {
|
|
59
85
|
return undefined;
|
|
@@ -185,7 +211,10 @@ async function traceExists(rt: LangfuseRuntime, traceId: string): Promise<boolea
|
|
|
185
211
|
if (!getTrace) {
|
|
186
212
|
return false;
|
|
187
213
|
}
|
|
188
|
-
await getTrace(traceId);
|
|
214
|
+
const trace = await withTimeout("Trace visibility check", getTrace(traceId));
|
|
215
|
+
if (!trace) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
189
218
|
return true;
|
|
190
219
|
} catch {
|
|
191
220
|
return false;
|
|
@@ -272,21 +301,34 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
272
301
|
});
|
|
273
302
|
}
|
|
274
303
|
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
const ingestionBatch = rt.scoreClient.api?.ingestion?.batch;
|
|
305
|
+
if (!ingestionBatch) {
|
|
306
|
+
console.log("📊 Langfuse: REST fallback ingestion is unavailable");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const response = await withTimeout(
|
|
311
|
+
"REST fallback ingestion",
|
|
312
|
+
ingestionBatch({
|
|
313
|
+
batch,
|
|
314
|
+
metadata: {
|
|
315
|
+
source: "pi-langfuse",
|
|
316
|
+
fallback: "rest-ingestion",
|
|
317
|
+
reason: "otel-trace-not-visible-after-flush",
|
|
318
|
+
},
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (!response) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
283
325
|
|
|
284
326
|
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
285
327
|
const errors = Array.isArray(responseBody?.errors) ? responseBody.errors : [];
|
|
286
328
|
if (errors.length > 0) {
|
|
287
329
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
288
330
|
} else {
|
|
289
|
-
console.
|
|
331
|
+
console.log(`📊 Langfuse: OTel trace ${trace.id} was not visible; wrote fallback trace via REST ingestion`);
|
|
290
332
|
}
|
|
291
333
|
}
|
|
292
334
|
|
|
@@ -356,11 +398,11 @@ function doShutdownRuntime(): Promise<void> {
|
|
|
356
398
|
runtime = null;
|
|
357
399
|
|
|
358
400
|
try {
|
|
359
|
-
await rt.tracerProvider?.forceFlush?.();
|
|
401
|
+
await withTimeout("OTel force flush", rt.tracerProvider?.forceFlush?.());
|
|
360
402
|
await fallbackToRestIngestion(rt);
|
|
361
|
-
await rt.scoreClient.flush?.();
|
|
362
|
-
await rt.scoreClient.shutdown?.();
|
|
363
|
-
await rt.tracerProvider?.shutdown?.();
|
|
403
|
+
await withTimeout("Langfuse score flush", rt.scoreClient.flush?.());
|
|
404
|
+
await withTimeout("Langfuse client shutdown", rt.scoreClient.shutdown?.());
|
|
405
|
+
await withTimeout("OTel tracer shutdown", rt.tracerProvider?.shutdown?.());
|
|
364
406
|
} catch (e) {
|
|
365
407
|
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
366
408
|
} finally {
|
|
@@ -400,6 +442,12 @@ export async function forceShutdownRuntime(): Promise<void> {
|
|
|
400
442
|
await doShutdownRuntime();
|
|
401
443
|
}
|
|
402
444
|
|
|
445
|
+
export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS): void {
|
|
446
|
+
runtime = rt;
|
|
447
|
+
shutdownStepTimeoutMs = timeoutMs;
|
|
448
|
+
activeSessions.clear();
|
|
449
|
+
}
|
|
450
|
+
|
|
403
451
|
export async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
404
452
|
try {
|
|
405
453
|
const rt = await getRuntime();
|