pi-langfuse 1.5.7 → 1.5.9
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 +8 -0
- package/README_CN.md +8 -0
- package/index.ts +5 -5
- package/package.json +1 -1
- package/src/langfuse.ts +283 -50
- package/src/types.ts +25 -0
package/README.md
CHANGED
|
@@ -77,6 +77,14 @@ export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # optional; LANGFUSE_HOST
|
|
|
77
77
|
|
|
78
78
|
Saved config takes precedence. Environment variables are only used when `~/.pi/agent/pi-langfuse/config.json` is missing or incomplete.
|
|
79
79
|
|
|
80
|
+
For short-lived SDK hosts, set the bounded final score-delivery attempt during shutdown:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export PI_LANGFUSE_SCORE_SHUTDOWN_TIMEOUT=2 # seconds; defaults to 2 seconds
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The extension attempts queued trace-level scores before other shutdown telemetry work. This value cannot extend the overall shutdown deadline.
|
|
87
|
+
|
|
80
88
|
Privacy controls can also be set through environment variables:
|
|
81
89
|
|
|
82
90
|
```bash
|
package/README_CN.md
CHANGED
|
@@ -77,6 +77,14 @@ export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 可选;也支持 LANG
|
|
|
77
77
|
|
|
78
78
|
保存的配置优先级更高。只有当 `~/.pi/agent/pi-langfuse/config.json` 缺失或不完整时,扩展才会使用环境变量。
|
|
79
79
|
|
|
80
|
+
对于短生命周期的 SDK 宿主,可设置关闭时最终分数发送尝试的上限:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
export PI_LANGFUSE_SCORE_SHUTDOWN_TIMEOUT=2 # 单位为秒;默认 2 秒
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
扩展会在其他关闭遥测工作之前尝试发送已排队的 trace 级分数。该值不会延长总关闭超时。
|
|
87
|
+
|
|
80
88
|
隐私采集策略也可以通过环境变量设置:
|
|
81
89
|
|
|
82
90
|
```bash
|
package/index.ts
CHANGED
|
@@ -159,11 +159,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
159
159
|
pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
|
|
160
160
|
await finishAgentRun(event);
|
|
161
161
|
const sessionId = state.currentSessionId;
|
|
162
|
-
|
|
163
|
-
shutdownRuntime(sessionId)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
162
|
+
try {
|
|
163
|
+
await shutdownRuntime(sessionId);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn("📊 Langfuse: Shutdown failed", error);
|
|
166
|
+
}
|
|
167
167
|
}));
|
|
168
168
|
|
|
169
169
|
const handleSessionInterruption = (reason: string) => {
|
package/package.json
CHANGED
package/src/langfuse.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LangfuseRuntime, LangfuseScoreClient } from "./types.js";
|
|
1
|
+
import type { LangfuseRuntime, LangfuseScoreClient, PendingScore } from "./types.js";
|
|
2
2
|
import { state } from "./state.js";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
@@ -60,6 +60,12 @@ interface RestFallbackStore {
|
|
|
60
60
|
const OTEL_VISIBILITY_TIMEOUT_MS = 1_500;
|
|
61
61
|
const OTEL_VISIBILITY_POLL_INTERVAL_MS = 200;
|
|
62
62
|
const DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS = 2_000;
|
|
63
|
+
const DEFAULT_SCORE_SHUTDOWN_TIMEOUT_MS = 2_000;
|
|
64
|
+
const DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS = 5;
|
|
65
|
+
const DEFAULT_SCORE_FLUSH_AT = 10;
|
|
66
|
+
const DEFAULT_SCORE_FLUSH_INTERVAL_MS = 1_000;
|
|
67
|
+
const MAX_SCORE_QUEUE_SIZE = 100_000;
|
|
68
|
+
const MAX_SCORE_BATCH_SIZE = 100;
|
|
63
69
|
|
|
64
70
|
let shutdownStepTimeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS;
|
|
65
71
|
|
|
@@ -67,8 +73,34 @@ function nowIso() {
|
|
|
67
73
|
return new Date().toISOString();
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
function
|
|
71
|
-
|
|
76
|
+
function resolvePositiveEnvNumber(name: string, fallback: number, integer = false): number {
|
|
77
|
+
const parsed = Number(process.env[name]);
|
|
78
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
return integer ? Math.floor(parsed) : parsed;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getScoreShutdownTimeoutMs(): number {
|
|
85
|
+
return resolvePositiveEnvNumber(
|
|
86
|
+
"PI_LANGFUSE_SCORE_SHUTDOWN_TIMEOUT",
|
|
87
|
+
DEFAULT_SCORE_SHUTDOWN_TIMEOUT_MS / 1_000,
|
|
88
|
+
) * 1_000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function delay(ms: number, signal?: AbortSignal) {
|
|
92
|
+
return new Promise<void>((resolve, reject) => {
|
|
93
|
+
if (signal?.aborted) {
|
|
94
|
+
reject(signal.reason);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const timeout = setTimeout(resolve, ms);
|
|
99
|
+
signal?.addEventListener("abort", () => {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
reject(signal.reason);
|
|
102
|
+
}, { once: true });
|
|
103
|
+
});
|
|
72
104
|
}
|
|
73
105
|
|
|
74
106
|
function debugLog(message: string) {
|
|
@@ -107,7 +139,14 @@ export function getLastRuntimeError(): { scope: string; message: string; timesta
|
|
|
107
139
|
return lastRuntimeError;
|
|
108
140
|
}
|
|
109
141
|
|
|
110
|
-
async function
|
|
142
|
+
async function withShutdownDeadline<T>(label: string, startOperation: () => Promise<T> | undefined, deadline: number): Promise<T | undefined> {
|
|
143
|
+
const remainingMs = deadline - Date.now();
|
|
144
|
+
if (remainingMs <= 0) {
|
|
145
|
+
debugLog(`📊 Langfuse: Skipped ${label}; shutdown deadline elapsed`);
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const operation = startOperation();
|
|
111
150
|
if (!operation) {
|
|
112
151
|
return undefined;
|
|
113
152
|
}
|
|
@@ -118,9 +157,9 @@ async function withTimeout<T>(label: string, operation: Promise<T> | undefined):
|
|
|
118
157
|
operation,
|
|
119
158
|
new Promise<undefined>((resolve) => {
|
|
120
159
|
timeout = setTimeout(() => {
|
|
121
|
-
debugLog(`📊 Langfuse: ${label} timed out
|
|
160
|
+
debugLog(`📊 Langfuse: ${label} timed out; shutdown deadline elapsed`);
|
|
122
161
|
resolve(undefined);
|
|
123
|
-
},
|
|
162
|
+
}, remainingMs);
|
|
124
163
|
}),
|
|
125
164
|
]);
|
|
126
165
|
} finally {
|
|
@@ -130,6 +169,142 @@ async function withTimeout<T>(label: string, operation: Promise<T> | undefined):
|
|
|
130
169
|
}
|
|
131
170
|
}
|
|
132
171
|
|
|
172
|
+
function getRuntimeConfig(rt: LangfuseRuntime) {
|
|
173
|
+
return rt.runtimeConfig ?? state.config;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function ingestionHeaders(rt: LangfuseRuntime): Record<string, string> {
|
|
177
|
+
const config = getRuntimeConfig(rt);
|
|
178
|
+
if (!config) {
|
|
179
|
+
throw new Error("Langfuse runtime config is unavailable");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const auth = Buffer.from(`${config.publicKey}:${config.secretKey}`).toString("base64");
|
|
183
|
+
return {
|
|
184
|
+
Authorization: `Basic ${auth}`,
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function ingestBatch(rt: LangfuseRuntime, batch: unknown[], signal: AbortSignal): Promise<unknown[]> {
|
|
190
|
+
const config = getRuntimeConfig(rt);
|
|
191
|
+
if (!config) {
|
|
192
|
+
throw new Error("Langfuse runtime config is unavailable");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const response = await fetch(`${config.host.replace(/\/$/, "")}/api/public/ingestion`, {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: ingestionHeaders(rt),
|
|
198
|
+
body: JSON.stringify({ batch }),
|
|
199
|
+
signal,
|
|
200
|
+
});
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
throw new Error(`Langfuse ingestion failed with HTTP ${response.status}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const text = await response.text();
|
|
206
|
+
if (!text) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const responseBody = JSON.parse(text) as { errors?: unknown[] };
|
|
211
|
+
return Array.isArray(responseBody.errors) ? responseBody.errors : [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function flushPendingScores(rt: LangfuseRuntime, signal: AbortSignal): Promise<void> {
|
|
215
|
+
const pendingScores = rt.pendingScores;
|
|
216
|
+
if (!pendingScores || pendingScores.length === 0) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
while (pendingScores.length > 0) {
|
|
221
|
+
const scores = pendingScores.slice(0, MAX_SCORE_BATCH_SIZE);
|
|
222
|
+
try {
|
|
223
|
+
const errors = await ingestBatch(
|
|
224
|
+
rt,
|
|
225
|
+
scores.map((score) => ({
|
|
226
|
+
type: "score-create",
|
|
227
|
+
id: randomUUID(),
|
|
228
|
+
timestamp: nowIso(),
|
|
229
|
+
body: score,
|
|
230
|
+
})),
|
|
231
|
+
signal,
|
|
232
|
+
);
|
|
233
|
+
pendingScores.splice(0, scores.length);
|
|
234
|
+
if (errors.length > 0) {
|
|
235
|
+
rememberRuntimeError("score ingestion", new Error(JSON.stringify(errors)));
|
|
236
|
+
console.warn("📊 Langfuse: Score ingestion reported errors", errors);
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if ((error as { name?: string }).name !== "AbortError") {
|
|
240
|
+
rememberRuntimeError("score ingestion", error);
|
|
241
|
+
console.warn("📊 Langfuse: Failed to flush scores", error);
|
|
242
|
+
}
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function clearScoreFlushTimer(rt: LangfuseRuntime) {
|
|
249
|
+
if (rt.scoreFlushTimer) {
|
|
250
|
+
clearTimeout(rt.scoreFlushTimer);
|
|
251
|
+
rt.scoreFlushTimer = undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function scheduleScoreFlush(rt: LangfuseRuntime) {
|
|
256
|
+
if (
|
|
257
|
+
rt.scoreFlushStopped
|
|
258
|
+
|| rt.scoreFlushTimer
|
|
259
|
+
|| rt.scoreFlushPromise
|
|
260
|
+
|| !rt.pendingScores?.length
|
|
261
|
+
) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
rt.scoreFlushTimer = setTimeout(() => {
|
|
266
|
+
rt.scoreFlushTimer = undefined;
|
|
267
|
+
void startScoreFlush(rt);
|
|
268
|
+
}, rt.scoreFlushIntervalMs ?? DEFAULT_SCORE_FLUSH_INTERVAL_MS);
|
|
269
|
+
rt.scoreFlushTimer.unref?.();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function startScoreFlush(rt: LangfuseRuntime): Promise<void> {
|
|
273
|
+
if (rt.scoreFlushPromise) {
|
|
274
|
+
return rt.scoreFlushPromise;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
clearScoreFlushTimer(rt);
|
|
278
|
+
const controller = new AbortController();
|
|
279
|
+
const timeout = setTimeout(
|
|
280
|
+
() => controller.abort(new DOMException("Langfuse score request timed out", "AbortError")),
|
|
281
|
+
rt.scoreRequestTimeoutMs ?? DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS * 1_000,
|
|
282
|
+
);
|
|
283
|
+
timeout.unref?.();
|
|
284
|
+
rt.scoreFlushController = controller;
|
|
285
|
+
|
|
286
|
+
const promise = flushPendingScores(rt, controller.signal).finally(() => {
|
|
287
|
+
clearTimeout(timeout);
|
|
288
|
+
if (rt.scoreFlushPromise === promise) {
|
|
289
|
+
rt.scoreFlushPromise = undefined;
|
|
290
|
+
}
|
|
291
|
+
if (rt.scoreFlushController === controller) {
|
|
292
|
+
rt.scoreFlushController = undefined;
|
|
293
|
+
}
|
|
294
|
+
scheduleScoreFlush(rt);
|
|
295
|
+
});
|
|
296
|
+
rt.scoreFlushPromise = promise;
|
|
297
|
+
return promise;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function stopScoreFlush(rt: LangfuseRuntime) {
|
|
301
|
+
rt.scoreFlushStopped = true;
|
|
302
|
+
clearScoreFlushTimer(rt);
|
|
303
|
+
rt.scoreFlushController?.abort(
|
|
304
|
+
new DOMException("Langfuse score flushing stopped", "AbortError"),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
133
308
|
function toIso(value: unknown): string | undefined {
|
|
134
309
|
if (!value) {
|
|
135
310
|
return undefined;
|
|
@@ -258,27 +433,40 @@ function wrapObservation(
|
|
|
258
433
|
};
|
|
259
434
|
}
|
|
260
435
|
|
|
261
|
-
async function traceExists(rt: LangfuseRuntime, traceId: string): Promise<boolean> {
|
|
436
|
+
async function traceExists(rt: LangfuseRuntime, traceId: string, signal: AbortSignal): Promise<boolean> {
|
|
437
|
+
const config = getRuntimeConfig(rt);
|
|
438
|
+
if (!config) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
|
|
262
442
|
try {
|
|
263
|
-
const
|
|
264
|
-
|
|
443
|
+
const response = await fetch(
|
|
444
|
+
`${config.host.replace(/\/$/, "")}/api/public/traces/${encodeURIComponent(traceId)}`,
|
|
445
|
+
{
|
|
446
|
+
headers: ingestionHeaders(rt),
|
|
447
|
+
signal,
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
if (response.status === 404) {
|
|
265
451
|
return false;
|
|
266
452
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
return false;
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
throw new Error(`Langfuse trace visibility check failed with HTTP ${response.status}`);
|
|
270
455
|
}
|
|
271
456
|
return true;
|
|
272
|
-
} catch {
|
|
457
|
+
} catch (error) {
|
|
458
|
+
if (signal.aborted) {
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
273
461
|
return false;
|
|
274
462
|
}
|
|
275
463
|
}
|
|
276
464
|
|
|
277
|
-
async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string): Promise<boolean> {
|
|
465
|
+
async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string, signal: AbortSignal): Promise<boolean> {
|
|
278
466
|
const deadline = Date.now() + OTEL_VISIBILITY_TIMEOUT_MS;
|
|
279
467
|
|
|
280
468
|
while (true) {
|
|
281
|
-
if (await traceExists(rt, traceId)) {
|
|
469
|
+
if (await traceExists(rt, traceId, signal)) {
|
|
282
470
|
return true;
|
|
283
471
|
}
|
|
284
472
|
|
|
@@ -287,7 +475,7 @@ async function waitForTraceVisibility(rt: LangfuseRuntime, traceId: string): Pro
|
|
|
287
475
|
return false;
|
|
288
476
|
}
|
|
289
477
|
|
|
290
|
-
await delay(Math.min(OTEL_VISIBILITY_POLL_INTERVAL_MS, remainingMs));
|
|
478
|
+
await delay(Math.min(OTEL_VISIBILITY_POLL_INTERVAL_MS, remainingMs), signal);
|
|
291
479
|
}
|
|
292
480
|
}
|
|
293
481
|
|
|
@@ -295,14 +483,14 @@ function eventTimestamp(record: { endTime?: string; startTime?: string; timestam
|
|
|
295
483
|
return record.endTime ?? record.startTime ?? record.timestamp ?? nowIso();
|
|
296
484
|
}
|
|
297
485
|
|
|
298
|
-
async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
486
|
+
async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal) {
|
|
299
487
|
const store = rt.restFallback as RestFallbackStore | undefined;
|
|
300
488
|
if (!store?.trace || store.attempted) {
|
|
301
489
|
return;
|
|
302
490
|
}
|
|
303
491
|
store.attempted = true;
|
|
304
492
|
|
|
305
|
-
if (await waitForTraceVisibility(rt, store.trace.id)) {
|
|
493
|
+
if (await waitForTraceVisibility(rt, store.trace.id, signal)) {
|
|
306
494
|
return;
|
|
307
495
|
}
|
|
308
496
|
|
|
@@ -355,31 +543,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
355
543
|
});
|
|
356
544
|
}
|
|
357
545
|
|
|
358
|
-
const
|
|
359
|
-
if (!ingestionApi?.batch) {
|
|
360
|
-
debugLog("📊 Langfuse: REST fallback ingestion is unavailable");
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const response = await withTimeout(
|
|
365
|
-
"REST fallback ingestion",
|
|
366
|
-
ingestionApi.batch({
|
|
367
|
-
batch,
|
|
368
|
-
metadata: {
|
|
369
|
-
source: "pi-langfuse",
|
|
370
|
-
fallback: "rest-ingestion",
|
|
371
|
-
reason: "otel-trace-not-visible-after-flush",
|
|
372
|
-
},
|
|
373
|
-
}),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
if (!response) {
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
381
|
-
const responseErrors = responseBody?.errors;
|
|
382
|
-
const errors = Array.isArray(responseErrors) ? responseErrors : [];
|
|
546
|
+
const errors = await ingestBatch(rt, batch, signal);
|
|
383
547
|
if (errors.length > 0) {
|
|
384
548
|
rememberRuntimeError("REST fallback ingestion", new Error(JSON.stringify(errors)));
|
|
385
549
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
@@ -426,6 +590,11 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
426
590
|
|
|
427
591
|
try {
|
|
428
592
|
ensureOtelContextManager(context, AsyncHooksContextManager);
|
|
593
|
+
const scoreFlushAt = resolvePositiveEnvNumber("LANGFUSE_FLUSH_AT", DEFAULT_SCORE_FLUSH_AT, true);
|
|
594
|
+
const scoreFlushIntervalMs =
|
|
595
|
+
resolvePositiveEnvNumber("LANGFUSE_FLUSH_INTERVAL", DEFAULT_SCORE_FLUSH_INTERVAL_MS / 1_000) * 1_000;
|
|
596
|
+
const scoreRequestTimeoutMs =
|
|
597
|
+
resolvePositiveEnvNumber("LANGFUSE_TIMEOUT", DEFAULT_LANGFUSE_REQUEST_TIMEOUT_SECONDS) * 1_000;
|
|
429
598
|
const spanProcessor = new LangfuseSpanProcessor({
|
|
430
599
|
publicKey: state.config.publicKey,
|
|
431
600
|
secretKey: state.config.secretKey,
|
|
@@ -449,6 +618,16 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
449
618
|
tracerProvider,
|
|
450
619
|
clearTracerProvider: () => tracing.setLangfuseTracerProvider(null),
|
|
451
620
|
restFallback,
|
|
621
|
+
pendingScores: [],
|
|
622
|
+
scoreFlushAt,
|
|
623
|
+
scoreFlushIntervalMs,
|
|
624
|
+
scoreRequestTimeoutMs,
|
|
625
|
+
scoreFlushStopped: false,
|
|
626
|
+
runtimeConfig: {
|
|
627
|
+
publicKey: state.config.publicKey,
|
|
628
|
+
secretKey: state.config.secretKey,
|
|
629
|
+
host: state.config.host,
|
|
630
|
+
},
|
|
452
631
|
};
|
|
453
632
|
lastRuntimeError = null;
|
|
454
633
|
} catch (e) {
|
|
@@ -468,17 +647,44 @@ function doShutdownRuntime(): Promise<void> {
|
|
|
468
647
|
|
|
469
648
|
const rt = runtime;
|
|
470
649
|
runtime = null;
|
|
650
|
+
const deadline = Date.now() + shutdownStepTimeoutMs;
|
|
651
|
+
const controller = new AbortController();
|
|
652
|
+
const abortTimeout = setTimeout(() => controller.abort(), shutdownStepTimeoutMs);
|
|
653
|
+
const scoreController = new AbortController();
|
|
654
|
+
const scoreAbortTimeout = setTimeout(
|
|
655
|
+
() => scoreController.abort(),
|
|
656
|
+
Math.min(getScoreShutdownTimeoutMs(), shutdownStepTimeoutMs),
|
|
657
|
+
);
|
|
658
|
+
scoreAbortTimeout.unref?.();
|
|
659
|
+
stopScoreFlush(rt);
|
|
471
660
|
|
|
472
661
|
try {
|
|
473
|
-
await
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
662
|
+
await withShutdownDeadline(
|
|
663
|
+
"Active score flush",
|
|
664
|
+
() => rt.scoreFlushPromise,
|
|
665
|
+
deadline,
|
|
666
|
+
);
|
|
667
|
+
await flushPendingScores(rt, scoreController.signal);
|
|
668
|
+
await withShutdownDeadline("OTel force flush", () => rt.tracerProvider?.forceFlush?.(), deadline);
|
|
669
|
+
await withShutdownDeadline(
|
|
670
|
+
"REST fallback ingestion",
|
|
671
|
+
() => fallbackToRestIngestion(rt, controller.signal),
|
|
672
|
+
deadline,
|
|
673
|
+
);
|
|
674
|
+
await withShutdownDeadline("Langfuse score flush", () => rt.scoreClient.flush?.(), deadline);
|
|
675
|
+
await withShutdownDeadline("Langfuse client shutdown", () => rt.scoreClient.shutdown?.(), deadline);
|
|
676
|
+
await withShutdownDeadline("OTel tracer shutdown", () => rt.tracerProvider?.shutdown?.(), deadline);
|
|
478
677
|
} catch (e) {
|
|
479
678
|
rememberRuntimeError("runtime shutdown", e);
|
|
480
679
|
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
481
680
|
} finally {
|
|
681
|
+
clearTimeout(abortTimeout);
|
|
682
|
+
clearTimeout(scoreAbortTimeout);
|
|
683
|
+
scoreController.abort();
|
|
684
|
+
clearScoreFlushTimer(rt);
|
|
685
|
+
rt.scoreFlushController?.abort();
|
|
686
|
+
rt.scoreFlushController = undefined;
|
|
687
|
+
rt.scoreFlushPromise = undefined;
|
|
482
688
|
if (!runtime) {
|
|
483
689
|
rt.clearTracerProvider?.();
|
|
484
690
|
}
|
|
@@ -516,7 +722,13 @@ export async function forceShutdownRuntime(): Promise<void> {
|
|
|
516
722
|
}
|
|
517
723
|
|
|
518
724
|
export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFAULT_SHUTDOWN_STEP_TIMEOUT_MS): void {
|
|
725
|
+
if (runtime && runtime !== rt) {
|
|
726
|
+
stopScoreFlush(runtime);
|
|
727
|
+
}
|
|
519
728
|
runtime = rt;
|
|
729
|
+
if (rt) {
|
|
730
|
+
rt.scoreFlushStopped = false;
|
|
731
|
+
}
|
|
520
732
|
shutdownStepTimeoutMs = timeoutMs;
|
|
521
733
|
activeSessions.clear();
|
|
522
734
|
}
|
|
@@ -524,14 +736,35 @@ export function __setRuntimeForTest(rt: LangfuseRuntime | null, timeoutMs = DEFA
|
|
|
524
736
|
export async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
525
737
|
try {
|
|
526
738
|
const rt = await getRuntime();
|
|
527
|
-
|
|
739
|
+
const score: PendingScore = {
|
|
528
740
|
name,
|
|
529
741
|
value,
|
|
530
742
|
dataType: name === "session_had_errors" || name === "tool_is_error" ? "BOOLEAN" : "NUMERIC",
|
|
531
743
|
traceId: options.traceId,
|
|
532
744
|
observationId: options.observationId,
|
|
533
745
|
sessionId: options.traceId ? undefined : state.currentSessionId || undefined,
|
|
534
|
-
|
|
746
|
+
...(process.env.LANGFUSE_TRACING_ENVIRONMENT
|
|
747
|
+
? { environment: process.env.LANGFUSE_TRACING_ENVIRONMENT }
|
|
748
|
+
: {}),
|
|
749
|
+
};
|
|
750
|
+
if (!rt.pendingScores) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (rt.pendingScores.length >= MAX_SCORE_QUEUE_SIZE) {
|
|
754
|
+
const error = new Error(
|
|
755
|
+
`Langfuse score queue is full (${MAX_SCORE_QUEUE_SIZE}); dropping score`,
|
|
756
|
+
);
|
|
757
|
+
rememberRuntimeError("score queue", error);
|
|
758
|
+
console.warn(`📊 Langfuse: ${error.message}`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
rt.pendingScores.push(score);
|
|
763
|
+
if (rt.pendingScores.length >= (rt.scoreFlushAt ?? DEFAULT_SCORE_FLUSH_AT)) {
|
|
764
|
+
void startScoreFlush(rt);
|
|
765
|
+
} else {
|
|
766
|
+
scheduleScoreFlush(rt);
|
|
767
|
+
}
|
|
535
768
|
} catch (e) {
|
|
536
769
|
rememberRuntimeError(`score ${name}`, e);
|
|
537
770
|
console.warn(`📊 Langfuse: Failed to send score ${name}`, e);
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,22 @@ export interface LangfuseScoreClient {
|
|
|
59
59
|
shutdown?: () => Promise<void>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export interface PendingScore {
|
|
63
|
+
traceId?: string;
|
|
64
|
+
sessionId?: string;
|
|
65
|
+
observationId?: string;
|
|
66
|
+
name: string;
|
|
67
|
+
value: number;
|
|
68
|
+
dataType?: "NUMERIC" | "BOOLEAN";
|
|
69
|
+
environment?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LangfuseRuntimeConfig {
|
|
73
|
+
publicKey: string;
|
|
74
|
+
secretKey: string;
|
|
75
|
+
host: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
export interface LangfuseRuntime {
|
|
63
79
|
startObservation: (
|
|
64
80
|
name: string,
|
|
@@ -79,6 +95,15 @@ export interface LangfuseRuntime {
|
|
|
79
95
|
tracerProvider?: { forceFlush?: () => Promise<void>; shutdown?: () => Promise<void> };
|
|
80
96
|
clearTracerProvider?: () => void;
|
|
81
97
|
restFallback?: unknown;
|
|
98
|
+
pendingScores?: PendingScore[];
|
|
99
|
+
scoreFlushAt?: number;
|
|
100
|
+
scoreFlushIntervalMs?: number;
|
|
101
|
+
scoreRequestTimeoutMs?: number;
|
|
102
|
+
scoreFlushTimer?: NodeJS.Timeout;
|
|
103
|
+
scoreFlushPromise?: Promise<void>;
|
|
104
|
+
scoreFlushController?: AbortController;
|
|
105
|
+
scoreFlushStopped?: boolean;
|
|
106
|
+
runtimeConfig?: LangfuseRuntimeConfig;
|
|
82
107
|
}
|
|
83
108
|
|
|
84
109
|
export interface GenerationState {
|