pi-langfuse 1.0.0 → 1.1.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 +270 -59
- package/README_CN.md +273 -61
- package/image.png +0 -0
- package/index.ts +877 -325
- package/package.json +18 -3
package/index.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Langfuse Observability Extension for Pi Coding Agent
|
|
3
|
-
*
|
|
4
|
-
* Sends
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - tool_call_count: Total number of tool calls
|
|
9
|
-
* - turn_count: Number of turns in the session
|
|
10
|
-
* - total_tool_errors: Number of tools that returned errors
|
|
11
|
-
* - tool_success_rate: Success rate of tool calls (0-1)
|
|
12
|
-
* - session_had_errors: Boolean indicating if any tool error occurred
|
|
13
|
-
* - tool_is_error: Per-tool score indicating if that specific tool call errored
|
|
3
|
+
*
|
|
4
|
+
* Sends one complete Langfuse trace per Pi agent run:
|
|
5
|
+
* - root agent observation for the user prompt and final assistant response
|
|
6
|
+
* - one generation observation per provider request
|
|
7
|
+
* - one tool observation per tool call, keyed by toolCallId
|
|
14
8
|
*/
|
|
15
9
|
|
|
16
10
|
import { readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
@@ -62,7 +56,7 @@ function loadConfigFromEnv(): Config | null {
|
|
|
62
56
|
return {
|
|
63
57
|
publicKey,
|
|
64
58
|
secretKey,
|
|
65
|
-
host: process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
59
|
+
host: process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
66
60
|
};
|
|
67
61
|
}
|
|
68
62
|
|
|
@@ -71,114 +65,281 @@ function saveConfig(config: Config) {
|
|
|
71
65
|
}
|
|
72
66
|
|
|
73
67
|
// ============================================
|
|
74
|
-
// Langfuse
|
|
68
|
+
// Langfuse SDK facade (lazy-loaded)
|
|
75
69
|
// ============================================
|
|
76
70
|
|
|
77
|
-
interface
|
|
78
|
-
id
|
|
79
|
-
|
|
71
|
+
interface LangfuseObservation {
|
|
72
|
+
id?: string;
|
|
73
|
+
traceId?: string;
|
|
74
|
+
update(body?: ObservationUpdate): LangfuseObservation;
|
|
75
|
+
end(body?: ObservationUpdate): void;
|
|
76
|
+
startObservation?(
|
|
77
|
+
name: string,
|
|
78
|
+
body?: ObservationUpdate,
|
|
79
|
+
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
80
|
+
): LangfuseObservation;
|
|
81
|
+
setTraceIO?(body?: { input?: unknown; output?: unknown }): void;
|
|
80
82
|
}
|
|
81
83
|
|
|
82
|
-
interface
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
interface ObservationUpdate {
|
|
85
|
+
input?: unknown;
|
|
86
|
+
output?: unknown;
|
|
87
|
+
metadata?: Record<string, unknown>;
|
|
88
|
+
model?: string;
|
|
89
|
+
usageDetails?: Record<string, number>;
|
|
90
|
+
usage?: Record<string, number>;
|
|
91
|
+
costDetails?: Record<string, number>;
|
|
92
|
+
level?: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
|
|
93
|
+
statusMessage?: string;
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
interface
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
+
interface LangfuseScoreClient {
|
|
97
|
+
score?: {
|
|
98
|
+
create(body: {
|
|
99
|
+
traceId?: string;
|
|
100
|
+
sessionId?: string;
|
|
101
|
+
observationId?: string;
|
|
102
|
+
name: string;
|
|
103
|
+
value: number;
|
|
104
|
+
dataType?: "NUMERIC" | "BOOLEAN";
|
|
105
|
+
}): unknown;
|
|
96
106
|
};
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
107
|
+
flush?: () => Promise<void>;
|
|
108
|
+
shutdown?: () => Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
interface LangfuseRuntime {
|
|
112
|
+
startObservation: (
|
|
113
|
+
name: string,
|
|
114
|
+
body?: ObservationUpdate,
|
|
115
|
+
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
116
|
+
) => LangfuseObservation;
|
|
117
|
+
propagateAttributes: (
|
|
118
|
+
params: {
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
traceName?: string;
|
|
121
|
+
metadata?: Record<string, string>;
|
|
122
|
+
tags?: string[];
|
|
123
|
+
},
|
|
124
|
+
fn: () => LangfuseObservation,
|
|
125
|
+
) => LangfuseObservation;
|
|
126
|
+
scoreClient: LangfuseScoreClient;
|
|
127
|
+
spanProcessor?: { forceFlush?: () => Promise<void>; shutdown?: () => Promise<void> };
|
|
128
|
+
sdk?: { start?: () => void; shutdown?: () => Promise<void> };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let runtime: LangfuseRuntime | null = null;
|
|
113
132
|
let config: Config | null = loadConfigFromFile() ?? loadConfigFromEnv();
|
|
114
133
|
let setupAttemptedThisSession = false;
|
|
115
134
|
|
|
116
|
-
async function
|
|
135
|
+
async function getRuntime(): Promise<LangfuseRuntime> {
|
|
117
136
|
if (!config) {
|
|
118
137
|
throw new Error("Langfuse config is not set");
|
|
119
138
|
}
|
|
120
139
|
|
|
121
|
-
if (!
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
140
|
+
if (!runtime) {
|
|
141
|
+
const [{ NodeSDK }, { LangfuseSpanProcessor }, tracing, { LangfuseClient }] = await Promise.all([
|
|
142
|
+
import("@opentelemetry/sdk-node"),
|
|
143
|
+
import("@langfuse/otel"),
|
|
144
|
+
import("@langfuse/tracing"),
|
|
145
|
+
import("@langfuse/client"),
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const spanProcessor = new LangfuseSpanProcessor({
|
|
126
149
|
publicKey: config.publicKey,
|
|
127
150
|
secretKey: config.secretKey,
|
|
128
151
|
baseUrl: config.host,
|
|
129
152
|
});
|
|
153
|
+
const sdk = new NodeSDK({ spanProcessors: [spanProcessor] });
|
|
154
|
+
sdk.start();
|
|
155
|
+
|
|
156
|
+
runtime = {
|
|
157
|
+
startObservation: tracing.startObservation as unknown as LangfuseRuntime["startObservation"],
|
|
158
|
+
propagateAttributes: tracing.propagateAttributes as unknown as LangfuseRuntime["propagateAttributes"],
|
|
159
|
+
scoreClient: new LangfuseClient({
|
|
160
|
+
publicKey: config.publicKey,
|
|
161
|
+
secretKey: config.secretKey,
|
|
162
|
+
baseUrl: config.host,
|
|
163
|
+
}) as LangfuseScoreClient,
|
|
164
|
+
spanProcessor,
|
|
165
|
+
sdk,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return runtime as LangfuseRuntime;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function shutdownRuntime(): Promise<void> {
|
|
173
|
+
if (!runtime) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
await runtime.scoreClient.flush?.();
|
|
179
|
+
await runtime.scoreClient.shutdown?.();
|
|
180
|
+
await runtime.spanProcessor?.forceFlush?.();
|
|
181
|
+
await runtime.spanProcessor?.shutdown?.();
|
|
182
|
+
await runtime.sdk?.shutdown?.();
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
185
|
+
} finally {
|
|
186
|
+
runtime = null;
|
|
130
187
|
}
|
|
131
|
-
return client;
|
|
132
188
|
}
|
|
133
189
|
|
|
134
190
|
// ============================================
|
|
135
191
|
// State
|
|
136
192
|
// ============================================
|
|
137
193
|
|
|
138
|
-
interface
|
|
139
|
-
|
|
140
|
-
|
|
194
|
+
interface GenerationState {
|
|
195
|
+
observation: LangfuseObservation;
|
|
196
|
+
requestKey: string;
|
|
197
|
+
ended: boolean;
|
|
198
|
+
metadata: Record<string, unknown>;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface ToolState {
|
|
202
|
+
observation: LangfuseObservation;
|
|
203
|
+
toolName: string;
|
|
204
|
+
ended: boolean;
|
|
141
205
|
}
|
|
142
206
|
|
|
143
|
-
interface
|
|
144
|
-
|
|
207
|
+
interface AgentState {
|
|
208
|
+
root?: LangfuseObservation;
|
|
209
|
+
traceId?: string;
|
|
210
|
+
promptInput?: unknown;
|
|
211
|
+
cwd?: string;
|
|
212
|
+
generationSeq: number;
|
|
213
|
+
activeGenerations: Map<string, GenerationState>;
|
|
214
|
+
generationOrder: string[];
|
|
215
|
+
activeTools: Map<string, ToolState>;
|
|
216
|
+
latestAssistantOutput?: unknown;
|
|
217
|
+
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
145
218
|
}
|
|
146
219
|
|
|
147
|
-
let
|
|
148
|
-
let
|
|
149
|
-
let
|
|
150
|
-
let
|
|
151
|
-
let currentProvider: string = "";
|
|
152
|
-
const activeSpans: Map<string, SpanData> = new Map();
|
|
220
|
+
let currentSessionId = "";
|
|
221
|
+
let currentModel = "";
|
|
222
|
+
let currentProvider = "";
|
|
223
|
+
let agentState: AgentState | null = null;
|
|
153
224
|
|
|
154
225
|
// Evaluation tracking state
|
|
155
|
-
let toolCallCount
|
|
156
|
-
let errorCount
|
|
157
|
-
let turnCount
|
|
226
|
+
let toolCallCount = 0;
|
|
227
|
+
let errorCount = 0;
|
|
228
|
+
let turnCount = 0;
|
|
229
|
+
|
|
230
|
+
const MAX_STRING_LENGTH = 12_000;
|
|
231
|
+
const MAX_TOOL_PAYLOAD_LENGTH = 24_000;
|
|
232
|
+
const MAX_DEPTH = 6;
|
|
233
|
+
const MAX_ARRAY_ITEMS = 50;
|
|
234
|
+
const MAX_OBJECT_KEYS = 80;
|
|
235
|
+
|
|
236
|
+
function truncate(value: string, maxLength = MAX_STRING_LENGTH): string {
|
|
237
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}... [truncated]` : value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function tryParseJson(value: string): unknown {
|
|
241
|
+
const trimmed = value.trim();
|
|
242
|
+
if (!trimmed || !["{", "["].includes(trimmed[0])) {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
return JSON.parse(trimmed);
|
|
248
|
+
} catch {
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function shapePayload(value: unknown, options: { maxString?: number; depth?: number } = {}): unknown {
|
|
254
|
+
const maxString = options.maxString ?? MAX_STRING_LENGTH;
|
|
255
|
+
const depth = options.depth ?? MAX_DEPTH;
|
|
256
|
+
|
|
257
|
+
function visit(item: unknown, remainingDepth: number, seen: WeakSet<object>): unknown {
|
|
258
|
+
if (typeof item === "string") {
|
|
259
|
+
const truncated = truncate(item, maxString);
|
|
260
|
+
const parsed = tryParseJson(truncated);
|
|
261
|
+
if (parsed === truncated) {
|
|
262
|
+
return truncated;
|
|
263
|
+
}
|
|
264
|
+
return visit(parsed, remainingDepth - 1, seen);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
item === null ||
|
|
269
|
+
typeof item === "undefined" ||
|
|
270
|
+
typeof item === "number" ||
|
|
271
|
+
typeof item === "boolean"
|
|
272
|
+
) {
|
|
273
|
+
return item;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (typeof item === "bigint") {
|
|
277
|
+
return item.toString();
|
|
278
|
+
}
|
|
158
279
|
|
|
159
|
-
|
|
160
|
-
|
|
280
|
+
if (typeof item === "function" || typeof item === "symbol") {
|
|
281
|
+
return `[${typeof item}]`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (remainingDepth <= 0) {
|
|
285
|
+
return `[max depth ${depth} reached]`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (Array.isArray(item)) {
|
|
289
|
+
return item.slice(0, MAX_ARRAY_ITEMS).map((entry) => visit(entry, remainingDepth - 1, seen));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (item instanceof Error) {
|
|
293
|
+
return {
|
|
294
|
+
name: item.name,
|
|
295
|
+
message: item.message,
|
|
296
|
+
stack: item.stack ? truncate(item.stack, maxString) : undefined,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (typeof item === "object") {
|
|
301
|
+
if (seen.has(item)) {
|
|
302
|
+
return "[circular]";
|
|
303
|
+
}
|
|
304
|
+
seen.add(item);
|
|
305
|
+
|
|
306
|
+
const output: Record<string, unknown> = {};
|
|
307
|
+
for (const [key, entry] of Object.entries(item as Record<string, unknown>).slice(0, MAX_OBJECT_KEYS)) {
|
|
308
|
+
output[key] = visit(entry, remainingDepth - 1, seen);
|
|
309
|
+
}
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return String(item);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return visit(value, depth, new WeakSet<object>());
|
|
161
317
|
}
|
|
162
318
|
|
|
163
|
-
function safeSerialize(value: unknown, maxLength
|
|
319
|
+
function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGTH): string {
|
|
164
320
|
try {
|
|
165
|
-
return truncate(JSON.stringify(value, null, 2), maxLength);
|
|
321
|
+
return truncate(JSON.stringify(shapePayload(value, { maxString: maxLength }), null, 2), maxLength);
|
|
166
322
|
} catch {
|
|
167
323
|
return `[unserializable ${typeof value}]`;
|
|
168
324
|
}
|
|
169
325
|
}
|
|
170
326
|
|
|
171
|
-
function extractTextContent(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
327
|
+
function extractTextContent(content: unknown, maxLength?: number): string | undefined {
|
|
328
|
+
if (typeof content === "string") {
|
|
329
|
+
return maxLength ? truncate(content, maxLength) : content;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!Array.isArray(content)) {
|
|
176
333
|
return undefined;
|
|
177
334
|
}
|
|
178
335
|
|
|
179
336
|
const text = content
|
|
180
|
-
.
|
|
181
|
-
|
|
337
|
+
.map((item) => {
|
|
338
|
+
if (!item || typeof item !== "object") return "";
|
|
339
|
+
const block = item as { type?: string; text?: string; thinking?: string };
|
|
340
|
+
return block.type === "text" && block.text ? block.text : "";
|
|
341
|
+
})
|
|
342
|
+
.filter(Boolean)
|
|
182
343
|
.join("\n");
|
|
183
344
|
|
|
184
345
|
if (!text) {
|
|
@@ -188,15 +349,203 @@ function extractTextContent(
|
|
|
188
349
|
return maxLength ? truncate(text, maxLength) : text;
|
|
189
350
|
}
|
|
190
351
|
|
|
191
|
-
function
|
|
352
|
+
function extractToolCalls(message: Record<string, unknown>): unknown | undefined {
|
|
353
|
+
return (
|
|
354
|
+
message.toolCalls ??
|
|
355
|
+
message.tool_calls ??
|
|
356
|
+
message.function_calls ??
|
|
357
|
+
(message.content && Array.isArray(message.content)
|
|
358
|
+
? message.content.filter((block) => {
|
|
359
|
+
return block && typeof block === "object" && ["tool_use", "tool_call"].includes(String((block as { type?: string }).type));
|
|
360
|
+
})
|
|
361
|
+
: undefined)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function extractAssistantOutput(message: unknown): unknown | undefined {
|
|
366
|
+
if (!message || typeof message !== "object") {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const msg = message as Record<string, unknown>;
|
|
371
|
+
const text = extractTextContent(msg.content);
|
|
372
|
+
if (text) {
|
|
373
|
+
return text;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const toolCalls = extractToolCalls(msg);
|
|
377
|
+
if (Array.isArray(toolCalls) && toolCalls.length > 0) {
|
|
378
|
+
return { toolCalls: shapePayload(toolCalls) };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (toolCalls) {
|
|
382
|
+
return { toolCalls: shapePayload(toolCalls) };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return shapePayload(msg);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function extractFinalAssistant(messages: unknown): Record<string, unknown> | undefined {
|
|
389
|
+
if (!Array.isArray(messages)) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
return messages.filter((message) => message?.role === "assistant").pop() as Record<string, unknown> | undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getRequestKey(event: Record<string, unknown>, fallback: string): string {
|
|
396
|
+
return String(
|
|
397
|
+
event.requestId ??
|
|
398
|
+
event.providerRequestId ??
|
|
399
|
+
event.messageId ??
|
|
400
|
+
event.turnId ??
|
|
401
|
+
event.turnIndex ??
|
|
402
|
+
event.id ??
|
|
403
|
+
fallback,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function getToolCallId(event: Record<string, unknown>): string | undefined {
|
|
408
|
+
const id = event.toolCallId ?? event.id ?? event.callId ?? event.tool_use_id ?? event.toolUseId;
|
|
409
|
+
return id === undefined || id === null ? undefined : String(id);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function getToolName(event: Record<string, unknown>): string {
|
|
413
|
+
return String(
|
|
414
|
+
event.toolName ??
|
|
415
|
+
event.name ??
|
|
416
|
+
event.tool ??
|
|
417
|
+
event.functionName ??
|
|
418
|
+
(event.call && typeof event.call === "object" ? (event.call as Record<string, unknown>).name : undefined) ??
|
|
419
|
+
"tool",
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function getToolInput(event: Record<string, unknown>): unknown {
|
|
424
|
+
return (
|
|
425
|
+
event.input ??
|
|
426
|
+
event.args ??
|
|
427
|
+
event.arguments ??
|
|
428
|
+
event.params ??
|
|
429
|
+
(event.call && typeof event.call === "object" ? (event.call as Record<string, unknown>).input : undefined) ??
|
|
430
|
+
event
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function getProviderPayload(event: Record<string, unknown>): unknown {
|
|
435
|
+
return event.request ?? event.payload ?? event.body ?? event.providerPayload ?? event.messages ?? event;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function getMessageFromEvent(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
439
|
+
if (event.message && typeof event.message === "object") {
|
|
440
|
+
return event.message as Record<string, unknown>;
|
|
441
|
+
}
|
|
442
|
+
if (event.role || event.content) {
|
|
443
|
+
return event;
|
|
444
|
+
}
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function extractUsage(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
|
|
449
|
+
const usage = (messageOrEvent.usage ??
|
|
450
|
+
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
451
|
+
? (messageOrEvent.message as Record<string, unknown>).usage
|
|
452
|
+
: undefined)) as Record<string, unknown> | undefined;
|
|
453
|
+
if (!usage || typeof usage !== "object") {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const input = Number(usage.input ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens ?? 0);
|
|
458
|
+
const output = Number(usage.output ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens ?? 0);
|
|
459
|
+
const total = Number(usage.total ?? usage.totalTokens ?? usage.total_tokens ?? input + output);
|
|
460
|
+
const cacheRead = Number(usage.cacheRead ?? usage.cache_read ?? usage.cachedTokens ?? 0);
|
|
461
|
+
const cacheWrite = Number(usage.cacheWrite ?? usage.cache_write ?? 0);
|
|
462
|
+
|
|
463
|
+
return {
|
|
464
|
+
input,
|
|
465
|
+
output,
|
|
466
|
+
total,
|
|
467
|
+
...(cacheRead ? { cacheRead } : {}),
|
|
468
|
+
...(cacheWrite ? { cacheWrite } : {}),
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function extractCostDetails(messageOrEvent: Record<string, unknown>): Record<string, number> | undefined {
|
|
473
|
+
const usage = (messageOrEvent.usage ??
|
|
474
|
+
(messageOrEvent.message && typeof messageOrEvent.message === "object"
|
|
475
|
+
? (messageOrEvent.message as Record<string, unknown>).usage
|
|
476
|
+
: undefined)) as Record<string, unknown> | undefined;
|
|
477
|
+
const cost = (messageOrEvent.cost ?? usage?.cost ?? messageOrEvent.costDetails) as Record<string, unknown> | undefined;
|
|
478
|
+
if (!cost || typeof cost !== "object") {
|
|
479
|
+
return undefined;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const input = Number(cost.input ?? cost.inputCost ?? 0);
|
|
483
|
+
const output = Number(cost.output ?? cost.outputCost ?? 0);
|
|
484
|
+
const total = Number(cost.total ?? cost.totalCost ?? input + output);
|
|
485
|
+
|
|
486
|
+
return { input, output, total };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function extractResponseMetadata(event: Record<string, unknown>): Record<string, unknown> {
|
|
490
|
+
return shapePayload(
|
|
491
|
+
{
|
|
492
|
+
status: event.status ?? event.statusCode ?? event.httpStatus,
|
|
493
|
+
headers: event.headers,
|
|
494
|
+
responseHeaders: event.responseHeaders,
|
|
495
|
+
providerMetadata: event.providerMetadata ?? event.metadata,
|
|
496
|
+
requestId: event.requestId ?? event.providerRequestId,
|
|
497
|
+
},
|
|
498
|
+
{ depth: 4, maxString: 4_000 },
|
|
499
|
+
) as Record<string, unknown>;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function updateTraceIO(input?: unknown, output?: unknown) {
|
|
503
|
+
const root = agentState?.root;
|
|
504
|
+
if (!root?.setTraceIO) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
root.setTraceIO({ input, output });
|
|
510
|
+
} catch {
|
|
511
|
+
// Older SDKs may omit setTraceIO; root IO still mirrors trace IO in current Langfuse.
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function resetRunState() {
|
|
516
|
+
agentState = null;
|
|
192
517
|
toolCallCount = 0;
|
|
193
518
|
errorCount = 0;
|
|
194
519
|
turnCount = 0;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function computeEvaluationScores() {
|
|
523
|
+
const toolSuccessRate = toolCallCount > 0 ? (toolCallCount - errorCount) / toolCallCount : 1;
|
|
524
|
+
const sessionHadErrors = errorCount > 0;
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
tool_call_count: toolCallCount,
|
|
528
|
+
turn_count: turnCount,
|
|
529
|
+
total_tool_errors: errorCount,
|
|
530
|
+
tool_success_rate: toolSuccessRate,
|
|
531
|
+
session_had_errors: sessionHadErrors ? 1 : 0,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
536
|
+
try {
|
|
537
|
+
const rt = await getRuntime();
|
|
538
|
+
rt.scoreClient.score?.create({
|
|
539
|
+
name,
|
|
540
|
+
value,
|
|
541
|
+
dataType: name === "session_had_errors" || name === "tool_is_error" ? "BOOLEAN" : "NUMERIC",
|
|
542
|
+
traceId: options.traceId,
|
|
543
|
+
observationId: options.observationId,
|
|
544
|
+
sessionId: options.traceId ? undefined : currentSessionId || undefined,
|
|
545
|
+
});
|
|
546
|
+
} catch (e) {
|
|
547
|
+
console.warn(`📊 Langfuse: Failed to send score ${name}`, e);
|
|
548
|
+
}
|
|
200
549
|
}
|
|
201
550
|
|
|
202
551
|
async function ensureConfig(ctx: any): Promise<boolean> {
|
|
@@ -210,7 +559,7 @@ async function ensureConfig(ctx: any): Promise<boolean> {
|
|
|
210
559
|
setupAttemptedThisSession = true;
|
|
211
560
|
|
|
212
561
|
if (!ctx.hasUI) {
|
|
213
|
-
console.log("📊 Langfuse: Missing config. Run this extension in Pi UI to complete setup, or set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY /
|
|
562
|
+
console.log("📊 Langfuse: Missing config. Run this extension in Pi UI to complete setup, or set LANGFUSE_PUBLIC_KEY / LANGFUSE_SECRET_KEY / LANGFUSE_BASE_URL.");
|
|
214
563
|
return false;
|
|
215
564
|
}
|
|
216
565
|
|
|
@@ -250,23 +599,406 @@ async function ensureConfig(ctx: any): Promise<boolean> {
|
|
|
250
599
|
async function promptForConfig(ctx: any): Promise<boolean> {
|
|
251
600
|
setupAttemptedThisSession = false;
|
|
252
601
|
config = null;
|
|
253
|
-
|
|
602
|
+
await shutdownRuntime();
|
|
254
603
|
return ensureConfig(ctx);
|
|
255
604
|
}
|
|
256
605
|
|
|
257
|
-
function
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
606
|
+
async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
607
|
+
if (!(await ensureConfig(ctx))) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
const rt = await getRuntime();
|
|
613
|
+
const cwd = String(
|
|
614
|
+
(event.systemPromptOptions && typeof event.systemPromptOptions === "object"
|
|
615
|
+
? (event.systemPromptOptions as Record<string, unknown>).cwd
|
|
616
|
+
: undefined) ?? process.cwd(),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
if (!currentModel && ctx.model) {
|
|
620
|
+
currentModel = ctx.model.id || "";
|
|
621
|
+
currentProvider = ctx.model.provider || "";
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const promptInput = shapePayload({
|
|
625
|
+
prompt: event.prompt,
|
|
626
|
+
images: event.images,
|
|
627
|
+
context: event.context ?? event.attachments,
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
agentState = {
|
|
631
|
+
cwd,
|
|
632
|
+
promptInput,
|
|
633
|
+
generationSeq: 0,
|
|
634
|
+
activeGenerations: new Map(),
|
|
635
|
+
generationOrder: [],
|
|
636
|
+
activeTools: new Map(),
|
|
637
|
+
providerMetadataByRequest: new Map(),
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const root = rt.propagateAttributes(
|
|
641
|
+
{
|
|
642
|
+
sessionId: currentSessionId ? truncate(currentSessionId, 200) : undefined,
|
|
643
|
+
traceName: "pi-agent",
|
|
644
|
+
metadata: {
|
|
645
|
+
cwd: truncate(cwd, 200),
|
|
646
|
+
...(currentModel ? { model: truncate(currentModel, 200) } : {}),
|
|
647
|
+
...(currentProvider ? { provider: truncate(currentProvider, 200) } : {}),
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
() =>
|
|
651
|
+
rt.startObservation(
|
|
652
|
+
"pi-agent",
|
|
653
|
+
{
|
|
654
|
+
input: promptInput,
|
|
655
|
+
metadata: {
|
|
656
|
+
cwd,
|
|
657
|
+
model: currentModel || undefined,
|
|
658
|
+
provider: currentProvider || undefined,
|
|
659
|
+
sessionId: currentSessionId || undefined,
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
{ asType: "agent" },
|
|
663
|
+
),
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
agentState.root = root;
|
|
667
|
+
agentState.traceId = root.traceId;
|
|
668
|
+
updateTraceIO(promptInput, undefined);
|
|
669
|
+
} catch (e) {
|
|
670
|
+
console.warn("📊 Langfuse: Failed to create agent observation", e);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getOpenGeneration(): GenerationState | undefined {
|
|
675
|
+
if (!agentState) {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
for (let i = agentState.generationOrder.length - 1; i >= 0; i--) {
|
|
680
|
+
const key = agentState.generationOrder[i];
|
|
681
|
+
const state = agentState.activeGenerations.get(key);
|
|
682
|
+
if (state && !state.ended) {
|
|
683
|
+
return state;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function startGeneration(event: Record<string, unknown>) {
|
|
691
|
+
if (!agentState?.root) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const key = getRequestKey(event, `generation-${++agentState.generationSeq}`);
|
|
697
|
+
const payload = getProviderPayload(event);
|
|
698
|
+
const model = String(event.model ?? event.modelId ?? currentModel ?? "");
|
|
699
|
+
const provider = String(event.provider ?? currentProvider ?? "");
|
|
700
|
+
const metadata = shapePayload({
|
|
701
|
+
provider,
|
|
702
|
+
requestId: key,
|
|
703
|
+
url: event.url,
|
|
704
|
+
method: event.method,
|
|
705
|
+
}) as Record<string, unknown>;
|
|
706
|
+
|
|
707
|
+
const generation = agentState.root.startObservation
|
|
708
|
+
? agentState.root.startObservation(
|
|
709
|
+
"llm-generation",
|
|
710
|
+
{
|
|
711
|
+
input: shapePayload(payload),
|
|
712
|
+
model: model || undefined,
|
|
713
|
+
metadata,
|
|
714
|
+
},
|
|
715
|
+
{ asType: "generation" },
|
|
716
|
+
)
|
|
717
|
+
: (await getRuntime()).startObservation(
|
|
718
|
+
"llm-generation",
|
|
719
|
+
{
|
|
720
|
+
input: shapePayload(payload),
|
|
721
|
+
model: model || undefined,
|
|
722
|
+
metadata,
|
|
723
|
+
},
|
|
724
|
+
{ asType: "generation" },
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
agentState.activeGenerations.set(key, {
|
|
728
|
+
observation: generation,
|
|
729
|
+
requestKey: key,
|
|
730
|
+
ended: false,
|
|
731
|
+
metadata,
|
|
732
|
+
});
|
|
733
|
+
agentState.generationOrder.push(key);
|
|
734
|
+
} catch (e) {
|
|
735
|
+
console.warn("📊 Langfuse: Failed to start generation", e);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function updateGenerationMetadata(event: Record<string, unknown>) {
|
|
740
|
+
if (!agentState) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const key = getRequestKey(event, "");
|
|
745
|
+
const metadata = extractResponseMetadata(event);
|
|
746
|
+
if (!key) {
|
|
747
|
+
const generation = getOpenGeneration();
|
|
748
|
+
if (generation) {
|
|
749
|
+
generation.metadata = { ...generation.metadata, ...metadata };
|
|
750
|
+
generation.observation.update({ metadata: generation.metadata });
|
|
751
|
+
}
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const generation = agentState.activeGenerations.get(key) ?? getOpenGeneration();
|
|
756
|
+
if (generation) {
|
|
757
|
+
generation.metadata = { ...generation.metadata, ...metadata };
|
|
758
|
+
generation.observation.update({ metadata: generation.metadata });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function finishGenerationFromMessage(event: Record<string, unknown>) {
|
|
763
|
+
if (!agentState) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
const message = getMessageFromEvent(event);
|
|
768
|
+
if (!message || message.role !== "assistant") {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const generation = getOpenGeneration();
|
|
773
|
+
const output = extractAssistantOutput(message);
|
|
774
|
+
agentState.latestAssistantOutput = output;
|
|
775
|
+
|
|
776
|
+
if (!generation) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const usageDetails = extractUsage({ ...event, message });
|
|
781
|
+
const costDetails = extractCostDetails({ ...event, message });
|
|
782
|
+
const model = String(message.model ?? event.model ?? currentModel ?? "");
|
|
783
|
+
const update: ObservationUpdate = {
|
|
784
|
+
output,
|
|
785
|
+
model: model || undefined,
|
|
786
|
+
usageDetails,
|
|
787
|
+
costDetails,
|
|
788
|
+
metadata: {
|
|
789
|
+
...generation.metadata,
|
|
790
|
+
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
791
|
+
},
|
|
269
792
|
};
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
generation.observation.update(update).end();
|
|
796
|
+
generation.ended = true;
|
|
797
|
+
} catch (e) {
|
|
798
|
+
console.warn("📊 Langfuse: Failed to finish generation", e);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function createFallbackGenerationFromTurn(event: Record<string, unknown>, message: Record<string, unknown>) {
|
|
803
|
+
if (!agentState?.root || agentState.generationOrder.length > 0) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const usageDetails = extractUsage({ ...event, message });
|
|
809
|
+
const costDetails = extractCostDetails({ ...event, message });
|
|
810
|
+
const model = String(message.model ?? event.model ?? currentModel ?? "");
|
|
811
|
+
const generation = agentState.root.startObservation
|
|
812
|
+
? agentState.root.startObservation(
|
|
813
|
+
"llm-generation",
|
|
814
|
+
{
|
|
815
|
+
input: agentState.promptInput,
|
|
816
|
+
output: extractAssistantOutput(message),
|
|
817
|
+
model: model || undefined,
|
|
818
|
+
usageDetails,
|
|
819
|
+
costDetails,
|
|
820
|
+
metadata: {
|
|
821
|
+
provider: currentProvider || undefined,
|
|
822
|
+
sourceEvent: "turn_end",
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
{ asType: "generation" },
|
|
826
|
+
)
|
|
827
|
+
: (await getRuntime()).startObservation(
|
|
828
|
+
"llm-generation",
|
|
829
|
+
{
|
|
830
|
+
input: agentState.promptInput,
|
|
831
|
+
output: extractAssistantOutput(message),
|
|
832
|
+
model: model || undefined,
|
|
833
|
+
usageDetails,
|
|
834
|
+
costDetails,
|
|
835
|
+
metadata: {
|
|
836
|
+
provider: currentProvider || undefined,
|
|
837
|
+
sourceEvent: "turn_end",
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
{ asType: "generation" },
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
generation.end();
|
|
844
|
+
agentState.generationOrder.push("turn-end-fallback");
|
|
845
|
+
} catch (e) {
|
|
846
|
+
console.warn("📊 Langfuse: Failed to create fallback generation", e);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function startToolObservation(event: Record<string, unknown>) {
|
|
851
|
+
if (!agentState?.root) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const toolCallId = getToolCallId(event);
|
|
856
|
+
if (!toolCallId || agentState.activeTools.has(toolCallId)) {
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
try {
|
|
861
|
+
const toolName = getToolName(event);
|
|
862
|
+
const tool = agentState.root.startObservation
|
|
863
|
+
? agentState.root.startObservation(
|
|
864
|
+
toolName,
|
|
865
|
+
{
|
|
866
|
+
input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
867
|
+
metadata: { toolName, toolCallId },
|
|
868
|
+
},
|
|
869
|
+
{ asType: "tool" },
|
|
870
|
+
)
|
|
871
|
+
: (await getRuntime()).startObservation(
|
|
872
|
+
toolName,
|
|
873
|
+
{
|
|
874
|
+
input: shapePayload(getToolInput(event), { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
875
|
+
metadata: { toolName, toolCallId },
|
|
876
|
+
},
|
|
877
|
+
{ asType: "tool" },
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
toolCallCount++;
|
|
881
|
+
agentState.activeTools.set(toolCallId, { observation: tool, toolName, ended: false });
|
|
882
|
+
} catch (e) {
|
|
883
|
+
console.warn("📊 Langfuse: Failed to start tool observation", e);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
async function finishToolObservation(event: Record<string, unknown>) {
|
|
888
|
+
if (!agentState) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const toolCallId = getToolCallId(event);
|
|
893
|
+
if (!toolCallId) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const state = agentState.activeTools.get(toolCallId);
|
|
898
|
+
if (!state || state.ended) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const isError = Boolean(event.isError ?? event.error ?? event.status === "error");
|
|
903
|
+
const output =
|
|
904
|
+
extractTextContent(event.content, MAX_TOOL_PAYLOAD_LENGTH) ??
|
|
905
|
+
event.output ??
|
|
906
|
+
event.result ??
|
|
907
|
+
event.error ??
|
|
908
|
+
event.content ??
|
|
909
|
+
event;
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
state.observation
|
|
913
|
+
.update({
|
|
914
|
+
output: shapePayload(output, { maxString: MAX_TOOL_PAYLOAD_LENGTH }),
|
|
915
|
+
level: isError ? "ERROR" : "DEFAULT",
|
|
916
|
+
statusMessage: isError ? truncate(String(event.error ?? output), 1_000) : undefined,
|
|
917
|
+
metadata: {
|
|
918
|
+
toolName: state.toolName,
|
|
919
|
+
toolCallId,
|
|
920
|
+
isError,
|
|
921
|
+
},
|
|
922
|
+
})
|
|
923
|
+
.end();
|
|
924
|
+
state.ended = true;
|
|
925
|
+
|
|
926
|
+
if (isError) {
|
|
927
|
+
errorCount++;
|
|
928
|
+
await sendScore("tool_is_error", 1, {
|
|
929
|
+
traceId: agentState.traceId,
|
|
930
|
+
observationId: state.observation.id,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
} catch (e) {
|
|
934
|
+
console.warn("📊 Langfuse: Failed to finish tool observation", e);
|
|
935
|
+
} finally {
|
|
936
|
+
agentState.activeTools.delete(toolCallId);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function closeDanglingObservations(statusMessage: string) {
|
|
941
|
+
if (!agentState) {
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
for (const state of agentState.activeTools.values()) {
|
|
946
|
+
if (!state.ended) {
|
|
947
|
+
state.observation
|
|
948
|
+
.update({ level: "WARNING", statusMessage, metadata: { toolName: state.toolName, cancelled: true } })
|
|
949
|
+
.end();
|
|
950
|
+
state.ended = true;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
for (const state of agentState.activeGenerations.values()) {
|
|
955
|
+
if (!state.ended) {
|
|
956
|
+
state.observation.update({ level: "WARNING", statusMessage, metadata: { ...state.metadata, cancelled: true } }).end();
|
|
957
|
+
state.ended = true;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
agentState.activeTools.clear();
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
965
|
+
if (!agentState?.root) {
|
|
966
|
+
resetRunState();
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const lastAssistant = extractFinalAssistant(event.messages);
|
|
971
|
+
const output = lastAssistant ? extractAssistantOutput(lastAssistant) : agentState.latestAssistantOutput;
|
|
972
|
+
const scores = computeEvaluationScores();
|
|
973
|
+
|
|
974
|
+
closeDanglingObservations("Agent run ended before observation finalized");
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
agentState.root
|
|
978
|
+
.update({
|
|
979
|
+
output,
|
|
980
|
+
metadata: {
|
|
981
|
+
cwd: agentState.cwd,
|
|
982
|
+
completed: true,
|
|
983
|
+
model: currentModel || undefined,
|
|
984
|
+
provider: currentProvider || undefined,
|
|
985
|
+
totalTools: toolCallCount,
|
|
986
|
+
...scores,
|
|
987
|
+
},
|
|
988
|
+
})
|
|
989
|
+
.end();
|
|
990
|
+
updateTraceIO(agentState.promptInput, output);
|
|
991
|
+
|
|
992
|
+
await sendScore("tool_call_count", scores.tool_call_count, { traceId: agentState.traceId });
|
|
993
|
+
await sendScore("turn_count", scores.turn_count, { traceId: agentState.traceId });
|
|
994
|
+
await sendScore("total_tool_errors", scores.total_tool_errors, { traceId: agentState.traceId });
|
|
995
|
+
await sendScore("tool_success_rate", scores.tool_success_rate, { traceId: agentState.traceId });
|
|
996
|
+
await sendScore("session_had_errors", scores.session_had_errors, { traceId: agentState.traceId });
|
|
997
|
+
} catch (e) {
|
|
998
|
+
console.warn("📊 Langfuse: Failed to finish agent observation", e);
|
|
999
|
+
} finally {
|
|
1000
|
+
resetRunState();
|
|
1001
|
+
}
|
|
270
1002
|
}
|
|
271
1003
|
|
|
272
1004
|
// ============================================
|
|
@@ -287,266 +1019,86 @@ export default async function (pi: ExtensionAPI) {
|
|
|
287
1019
|
},
|
|
288
1020
|
});
|
|
289
1021
|
|
|
290
|
-
// Capture session ID on session start
|
|
291
1022
|
pi.on("session_start", async (_event, ctx) => {
|
|
292
1023
|
setupAttemptedThisSession = false;
|
|
293
1024
|
await ensureConfig(ctx);
|
|
294
1025
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
295
1026
|
if (sessionFile) {
|
|
296
|
-
// Extract session ID from file path (format: 2026-04-25T13-23-54-756Z_<uuid>.jsonl)
|
|
297
1027
|
currentSessionId = basename(sessionFile, ".jsonl");
|
|
298
1028
|
}
|
|
299
|
-
|
|
300
|
-
resetSessionState();
|
|
1029
|
+
resetRunState();
|
|
301
1030
|
});
|
|
302
1031
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
currentProvider = event.model?.provider || '';
|
|
1032
|
+
pi.on("model_select", async (event) => {
|
|
1033
|
+
currentModel = event.model?.id || "";
|
|
1034
|
+
currentProvider = event.model?.provider || "";
|
|
307
1035
|
});
|
|
308
1036
|
|
|
309
|
-
// Use before_agent_start to capture user prompt and create trace
|
|
310
1037
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
1038
|
+
await startAgentRun(event, ctx);
|
|
1039
|
+
});
|
|
314
1040
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
currentUserPrompt = event.prompt;
|
|
319
|
-
|
|
320
|
-
// Fallback to ctx.model if not captured via model_select
|
|
321
|
-
if (!currentModel && ctx.model) {
|
|
322
|
-
currentModel = ctx.model.id || '';
|
|
323
|
-
currentProvider = ctx.model.provider || '';
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Create trace with user input and session ID
|
|
327
|
-
currentTrace = lf.trace({
|
|
328
|
-
name: "pi-agent",
|
|
329
|
-
input: event.prompt,
|
|
330
|
-
metadata: {
|
|
331
|
-
cwd,
|
|
332
|
-
model: currentModel,
|
|
333
|
-
provider: currentProvider
|
|
334
|
-
},
|
|
335
|
-
sessionId: currentSessionId || undefined
|
|
336
|
-
});
|
|
337
|
-
} catch (e) {
|
|
338
|
-
console.warn("📊 Langfuse: Failed to create trace", e);
|
|
1041
|
+
pi.on("agent_start", async (event, ctx) => {
|
|
1042
|
+
if (!agentState?.root) {
|
|
1043
|
+
await startAgentRun(event, ctx);
|
|
339
1044
|
}
|
|
340
1045
|
});
|
|
341
1046
|
|
|
342
|
-
pi.on("
|
|
343
|
-
|
|
344
|
-
// Get final response/output
|
|
345
|
-
const eventData = event as unknown as {
|
|
346
|
-
messages?: Array<{
|
|
347
|
-
role: string;
|
|
348
|
-
content: Array<{ type: string; text?: string; thinking?: string }>;
|
|
349
|
-
}>;
|
|
350
|
-
};
|
|
351
|
-
const messages = eventData.messages || [];
|
|
352
|
-
const lastAssistant = messages.filter(m => m.role === "assistant").pop();
|
|
353
|
-
|
|
354
|
-
// Extract text from content array (filter out thinking blocks)
|
|
355
|
-
const output = extractTextContent(lastAssistant?.content);
|
|
356
|
-
|
|
357
|
-
// Compute evaluation scores
|
|
358
|
-
const scores = computeEvaluationScores();
|
|
359
|
-
|
|
360
|
-
currentTrace.update?.({
|
|
361
|
-
output: output || undefined,
|
|
362
|
-
metadata: {
|
|
363
|
-
completed: true,
|
|
364
|
-
totalTools: toolCallCount,
|
|
365
|
-
model: currentModel,
|
|
366
|
-
provider: currentProvider,
|
|
367
|
-
...scores
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Send evaluation scores to Langfuse
|
|
372
|
-
try {
|
|
373
|
-
const lf = await getClient();
|
|
374
|
-
|
|
375
|
-
// Trace-level evaluation scores
|
|
376
|
-
lf.score({ name: "tool_call_count", value: scores.tool_call_count, traceId: currentTrace.id });
|
|
377
|
-
lf.score({ name: "turn_count", value: scores.turn_count, traceId: currentTrace.id });
|
|
378
|
-
lf.score({ name: "total_tool_errors", value: scores.total_tool_errors, traceId: currentTrace.id });
|
|
379
|
-
lf.score({ name: "tool_success_rate", value: scores.tool_success_rate, traceId: currentTrace.id });
|
|
380
|
-
lf.score({ name: "session_had_errors", value: scores.session_had_errors, traceId: currentTrace.id });
|
|
381
|
-
|
|
382
|
-
console.log("📊 Langfuse: Evaluation scores sent:", scores);
|
|
383
|
-
} catch (e) {
|
|
384
|
-
console.warn("📊 Langfuse: Failed to send evaluation scores", e);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
currentTrace = null;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Reset for next session
|
|
391
|
-
resetSessionState();
|
|
392
|
-
|
|
393
|
-
if (client) {
|
|
394
|
-
await client.shutdownAsync();
|
|
395
|
-
client = null;
|
|
396
|
-
}
|
|
1047
|
+
pi.on("before_provider_request", async (event) => {
|
|
1048
|
+
await startGeneration(event);
|
|
397
1049
|
});
|
|
398
1050
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
1051
|
+
pi.on("after_provider_response", async (event) => {
|
|
1052
|
+
updateGenerationMetadata(event);
|
|
1053
|
+
});
|
|
402
1054
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
toolCallCount++;
|
|
408
|
-
|
|
409
|
-
// Format input nicely
|
|
410
|
-
const inputStr = event.input ? safeSerialize(event.input, 1000) : "";
|
|
411
|
-
|
|
412
|
-
const span = lf.span({
|
|
413
|
-
name: `tool:${event.toolName}`,
|
|
414
|
-
traceId: currentTrace.id,
|
|
415
|
-
input: inputStr,
|
|
416
|
-
metadata: { tool: event.toolName }
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
activeSpans.set(event.toolCallId, { span });
|
|
420
|
-
} catch (e) {
|
|
421
|
-
console.warn("📊 Langfuse: Failed to create span", e);
|
|
1055
|
+
pi.on("message_update", async (event) => {
|
|
1056
|
+
const message = getMessageFromEvent(event);
|
|
1057
|
+
if (message?.role === "assistant" && agentState) {
|
|
1058
|
+
agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
422
1059
|
}
|
|
423
1060
|
});
|
|
424
1061
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const spanData = activeSpans.get(event.toolCallId);
|
|
428
|
-
if (spanData) {
|
|
429
|
-
const { span } = spanData;
|
|
430
|
-
|
|
431
|
-
// Format output nicely
|
|
432
|
-
const outputStr = extractTextContent(event.content, 2000);
|
|
433
|
-
|
|
434
|
-
span.end({
|
|
435
|
-
isError: event.isError,
|
|
436
|
-
output: outputStr || undefined
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
// Track errors and send per-tool score
|
|
440
|
-
if (event.isError) {
|
|
441
|
-
errorCount++;
|
|
442
|
-
try {
|
|
443
|
-
const lf = await getClient();
|
|
444
|
-
// Per-tool error score (observation level)
|
|
445
|
-
lf.score({
|
|
446
|
-
name: "tool_is_error",
|
|
447
|
-
value: 1,
|
|
448
|
-
traceId: currentTrace?.id,
|
|
449
|
-
observationId: span.id,
|
|
450
|
-
});
|
|
451
|
-
} catch (e) {
|
|
452
|
-
console.warn("📊 Langfuse: Failed to send tool error score", e);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
activeSpans.delete(event.toolCallId);
|
|
457
|
-
}
|
|
1062
|
+
pi.on("message_end", async (event) => {
|
|
1063
|
+
await finishGenerationFromMessage(event);
|
|
458
1064
|
});
|
|
459
1065
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1066
|
+
pi.on("tool_execution_start", async (event) => {
|
|
1067
|
+
await startToolObservation(event);
|
|
1068
|
+
});
|
|
463
1069
|
|
|
464
|
-
|
|
465
|
-
|
|
1070
|
+
pi.on("tool_call", async (event) => {
|
|
1071
|
+
await startToolObservation(event);
|
|
1072
|
+
});
|
|
466
1073
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
content: Array<{ type: string; text?: string }>;
|
|
471
|
-
model?: string;
|
|
472
|
-
cost?: { input: number; output: number; total: number };
|
|
473
|
-
usage?: {
|
|
474
|
-
input: number;
|
|
475
|
-
output: number;
|
|
476
|
-
cacheRead: number;
|
|
477
|
-
cacheWrite: number;
|
|
478
|
-
totalTokens: number;
|
|
479
|
-
cost?: { input: number; output: number; total: number };
|
|
480
|
-
};
|
|
481
|
-
};
|
|
482
|
-
toolResults?: Array<{ toolName: string; toolCallId: string }>;
|
|
483
|
-
};
|
|
1074
|
+
pi.on("tool_result", async (event) => {
|
|
1075
|
+
await finishToolObservation(event);
|
|
1076
|
+
});
|
|
484
1077
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const lf = await getClient();
|
|
496
|
-
|
|
497
|
-
// Extract output text
|
|
498
|
-
const outputText = extractTextContent(message.content)?.slice(0, 1000) || "";
|
|
499
|
-
|
|
500
|
-
// Create generation for the LLM response with model info
|
|
501
|
-
// Note: usage and costDetails go in the generation observation, NOT as scores
|
|
502
|
-
const gen = lf.generation({
|
|
503
|
-
name: "llm-response",
|
|
504
|
-
traceId: currentTrace.id,
|
|
505
|
-
input: currentUserPrompt.slice(0, 500),
|
|
506
|
-
output: outputText,
|
|
507
|
-
model: modelId,
|
|
508
|
-
metadata: {
|
|
509
|
-
provider: provider,
|
|
510
|
-
inputTokens: usage.input || 0,
|
|
511
|
-
outputTokens: usage.output || 0,
|
|
512
|
-
cachedTokens: usage.cacheRead || 0,
|
|
513
|
-
},
|
|
514
|
-
usage: {
|
|
515
|
-
input: usage.input || 0,
|
|
516
|
-
output: usage.output || 0,
|
|
517
|
-
total: usage.totalTokens || (usage.input || 0) + (usage.output || 0)
|
|
518
|
-
},
|
|
519
|
-
costDetails: cost ? { total: cost.total, input: cost.input, output: cost.output } : undefined
|
|
520
|
-
});
|
|
521
|
-
gen.end({
|
|
522
|
-
costDetails: cost ? { total: cost.total, input: cost.input, output: cost.output } : undefined,
|
|
523
|
-
usage: {
|
|
524
|
-
input: usage.input || 0,
|
|
525
|
-
output: usage.output || 0,
|
|
526
|
-
total: usage.totalTokens || (usage.input || 0) + (usage.output || 0)
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
} catch (e) {
|
|
530
|
-
console.warn("📊 Langfuse: Failed to create generation", e);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// NOTE: We no longer send token counts or cost as scores
|
|
534
|
-
// Those belong in usage/costDetails on the generation observation
|
|
535
|
-
// Scores are for EVALUATION metrics (success rates, error counts, etc.)
|
|
1078
|
+
pi.on("tool_execution_end", async (event) => {
|
|
1079
|
+
await finishToolObservation(event);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
pi.on("turn_end", async (event) => {
|
|
1083
|
+
turnCount++;
|
|
1084
|
+
const message = getMessageFromEvent(event);
|
|
1085
|
+
if (message?.role === "assistant") {
|
|
1086
|
+
await createFallbackGenerationFromTurn(event, message);
|
|
1087
|
+
await finishGenerationFromMessage(event);
|
|
536
1088
|
}
|
|
537
1089
|
});
|
|
538
1090
|
|
|
1091
|
+
pi.on("agent_end", async (event) => {
|
|
1092
|
+
await finishAgentRun(event);
|
|
1093
|
+
await shutdownRuntime();
|
|
1094
|
+
});
|
|
1095
|
+
|
|
539
1096
|
pi.on("session_shutdown", async () => {
|
|
540
|
-
if (
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
currentTrace = null;
|
|
545
|
-
}
|
|
546
|
-
if (client) {
|
|
547
|
-
await client.shutdownAsync();
|
|
548
|
-
client = null;
|
|
1097
|
+
if (agentState?.root) {
|
|
1098
|
+
closeDanglingObservations("Session shutdown before agent completed");
|
|
1099
|
+
agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
549
1100
|
}
|
|
550
|
-
|
|
1101
|
+
resetRunState();
|
|
1102
|
+
await shutdownRuntime();
|
|
551
1103
|
});
|
|
552
1104
|
}
|