pi-langfuse 1.0.0 → 1.2.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/.trae/documents/optimize_langfuse_reporting.md +68 -0
- package/.trae/documents/pi-langfuse-refactor.md +78 -0
- package/AGENTS.md +33 -42
- package/README.md +270 -59
- package/README_CN.md +273 -61
- package/image.png +0 -0
- package/index.ts +109 -491
- package/package.json +18 -3
- package/src/config.ts +98 -0
- package/src/constants.ts +15 -0
- package/src/handlers/agent.ts +136 -0
- package/src/handlers/generation.ts +239 -0
- package/src/handlers/tool.ts +126 -0
- package/src/handlers/turn.ts +53 -0
- package/src/langfuse.ts +75 -0
- package/src/state.ts +38 -0
- package/src/types.ts +94 -0
- package/src/utils.ts +273 -0
- package/tsconfig.json +1 -0
package/index.ts
CHANGED
|
@@ -1,281 +1,41 @@
|
|
|
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
|
-
import {
|
|
17
|
-
import { resolve, dirname, basename } from "node:path";
|
|
18
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { basename } from "node:path";
|
|
19
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const config = JSON.parse(content) as Config;
|
|
40
|
-
if (config.publicKey && config.secretKey) {
|
|
41
|
-
return {
|
|
42
|
-
publicKey: config.publicKey,
|
|
43
|
-
secretKey: config.secretKey,
|
|
44
|
-
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
} catch (e) {
|
|
48
|
-
console.warn("📊 Langfuse: Failed to load config.json", e);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function loadConfigFromEnv(): Config | null {
|
|
56
|
-
const publicKey = process.env.LANGFUSE_PUBLIC_KEY || "";
|
|
57
|
-
const secretKey = process.env.LANGFUSE_SECRET_KEY || "";
|
|
58
|
-
if (!publicKey || !secretKey) {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
publicKey,
|
|
64
|
-
secretKey,
|
|
65
|
-
host: process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function saveConfig(config: Config) {
|
|
70
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ============================================
|
|
74
|
-
// Langfuse Client (lazy-loaded via dynamic import)
|
|
75
|
-
// ============================================
|
|
76
|
-
|
|
77
|
-
interface LangfuseSpan {
|
|
78
|
-
id: string;
|
|
79
|
-
end(body?: { metadata?: Record<string, unknown>; isError?: boolean; output?: unknown }): void;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
interface LangfuseGeneration {
|
|
83
|
-
id: string;
|
|
84
|
-
end(body?: {
|
|
85
|
-
metadata?: Record<string, unknown>;
|
|
86
|
-
usage?: unknown;
|
|
87
|
-
output?: unknown;
|
|
88
|
-
costDetails?: unknown;
|
|
89
|
-
}): void;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface LangfuseClient {
|
|
93
|
-
trace(body?: { name: string; metadata?: Record<string, unknown>; input?: unknown; output?: unknown; sessionId?: string }): {
|
|
94
|
-
id: string;
|
|
95
|
-
update(body?: { metadata?: Record<string, unknown>; output?: unknown; input?: unknown }): void;
|
|
96
|
-
};
|
|
97
|
-
span(body: { name: string; traceId: string; metadata?: Record<string, unknown>; input?: unknown }): LangfuseSpan;
|
|
98
|
-
generation(body: {
|
|
99
|
-
name: string;
|
|
100
|
-
traceId: string;
|
|
101
|
-
metadata?: Record<string, unknown>;
|
|
102
|
-
input?: unknown;
|
|
103
|
-
output?: unknown;
|
|
104
|
-
usage?: unknown;
|
|
105
|
-
model?: string;
|
|
106
|
-
costDetails?: unknown;
|
|
107
|
-
}): LangfuseGeneration;
|
|
108
|
-
score(body: { name: string; value: number; traceId?: string; observationId?: string }): void;
|
|
109
|
-
shutdownAsync(): Promise<void>;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let client: LangfuseClient | null = null;
|
|
113
|
-
let config: Config | null = loadConfigFromFile() ?? loadConfigFromEnv();
|
|
114
|
-
let setupAttemptedThisSession = false;
|
|
115
|
-
|
|
116
|
-
async function getClient(): Promise<LangfuseClient> {
|
|
117
|
-
if (!config) {
|
|
118
|
-
throw new Error("Langfuse config is not set");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!client) {
|
|
122
|
-
const lib = await import(`${EXT_DIR}/node_modules/langfuse/lib/index.mjs`) as {
|
|
123
|
-
Langfuse: new (options: { publicKey: string; secretKey?: string; baseUrl?: string }) => LangfuseClient;
|
|
124
|
-
};
|
|
125
|
-
client = new lib.Langfuse({
|
|
126
|
-
publicKey: config.publicKey,
|
|
127
|
-
secretKey: config.secretKey,
|
|
128
|
-
baseUrl: config.host,
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
return client;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ============================================
|
|
135
|
-
// State
|
|
136
|
-
// ============================================
|
|
137
|
-
|
|
138
|
-
interface TraceData {
|
|
139
|
-
id: string;
|
|
140
|
-
update?: (body?: { metadata?: Record<string, unknown>; output?: unknown; input?: unknown }) => void;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface SpanData {
|
|
144
|
-
span: LangfuseSpan;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let currentTrace: TraceData | null = null;
|
|
148
|
-
let currentUserPrompt: string = "";
|
|
149
|
-
let currentSessionId: string = "";
|
|
150
|
-
let currentModel: string = "";
|
|
151
|
-
let currentProvider: string = "";
|
|
152
|
-
const activeSpans: Map<string, SpanData> = new Map();
|
|
153
|
-
|
|
154
|
-
// Evaluation tracking state
|
|
155
|
-
let toolCallCount: number = 0;
|
|
156
|
-
let errorCount: number = 0;
|
|
157
|
-
let turnCount: number = 0;
|
|
158
|
-
|
|
159
|
-
function truncate(value: string, maxLength: number): string {
|
|
160
|
-
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function safeSerialize(value: unknown, maxLength: number): string {
|
|
164
|
-
try {
|
|
165
|
-
return truncate(JSON.stringify(value, null, 2), maxLength);
|
|
166
|
-
} catch {
|
|
167
|
-
return `[unserializable ${typeof value}]`;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function extractTextContent(
|
|
172
|
-
content: Array<{ type: string; text?: string; thinking?: string }> | undefined,
|
|
173
|
-
maxLength?: number,
|
|
174
|
-
): string | undefined {
|
|
175
|
-
if (!content?.length) {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const text = content
|
|
180
|
-
.filter((item) => item.type === "text" && item.text)
|
|
181
|
-
.map((item) => item.text)
|
|
182
|
-
.join("\n");
|
|
183
|
-
|
|
184
|
-
if (!text) {
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return maxLength ? truncate(text, maxLength) : text;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function resetSessionState() {
|
|
192
|
-
toolCallCount = 0;
|
|
193
|
-
errorCount = 0;
|
|
194
|
-
turnCount = 0;
|
|
195
|
-
activeSpans.clear();
|
|
196
|
-
currentTrace = null;
|
|
197
|
-
currentUserPrompt = "";
|
|
198
|
-
currentModel = "";
|
|
199
|
-
currentProvider = "";
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function ensureConfig(ctx: any): Promise<boolean> {
|
|
203
|
-
if (config) {
|
|
204
|
-
return true;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (setupAttemptedThisSession) {
|
|
208
|
-
return false;
|
|
209
|
-
}
|
|
210
|
-
setupAttemptedThisSession = true;
|
|
211
|
-
|
|
212
|
-
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 / LANGFUSE_HOST.");
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
ctx.ui.notify("Langfuse setup required. Enter your API keys to enable tracing.", "info");
|
|
218
|
-
|
|
219
|
-
const publicKey = (await ctx.ui.input("Langfuse public key:", "pk-lf-..."))?.trim();
|
|
220
|
-
if (!publicKey) {
|
|
221
|
-
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const secretKey = (await ctx.ui.input("Langfuse secret key:", "sk-lf-..."))?.trim();
|
|
226
|
-
if (!secretKey) {
|
|
227
|
-
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const hostInput = (await ctx.ui.input("Langfuse host:", DEFAULT_LANGFUSE_HOST))?.trim();
|
|
232
|
-
config = {
|
|
233
|
-
publicKey,
|
|
234
|
-
secretKey,
|
|
235
|
-
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
saveConfig(config);
|
|
240
|
-
ctx.ui.notify(`Langfuse config saved to ${CONFIG_PATH}`, "info");
|
|
241
|
-
return true;
|
|
242
|
-
} catch (error) {
|
|
243
|
-
console.warn("📊 Langfuse: Failed to save config.json", error);
|
|
244
|
-
ctx.ui.notify("Failed to save Langfuse config.json. Check extension directory permissions.", "error");
|
|
245
|
-
config = null;
|
|
246
|
-
return false;
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function promptForConfig(ctx: any): Promise<boolean> {
|
|
251
|
-
setupAttemptedThisSession = false;
|
|
252
|
-
config = null;
|
|
253
|
-
client = null;
|
|
254
|
-
return ensureConfig(ctx);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function computeEvaluationScores() {
|
|
258
|
-
const toolSuccessRate = toolCallCount > 0
|
|
259
|
-
? (toolCallCount - errorCount) / toolCallCount
|
|
260
|
-
: 1;
|
|
261
|
-
const sessionHadErrors = errorCount > 0;
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
tool_call_count: toolCallCount,
|
|
265
|
-
turn_count: turnCount,
|
|
266
|
-
total_tool_errors: errorCount,
|
|
267
|
-
tool_success_rate: toolSuccessRate,
|
|
268
|
-
session_had_errors: sessionHadErrors ? 1 : 0, // 1 for true, 0 for false
|
|
269
|
-
};
|
|
270
|
-
}
|
|
13
|
+
import { state, resetRunState } from "./src/state.js";
|
|
14
|
+
import { ensureConfig, promptForConfig } from "./src/config.js";
|
|
15
|
+
import { shutdownRuntime } from "./src/langfuse.js";
|
|
16
|
+
import { getMessageFromEvent, extractAssistantOutput } from "./src/utils.js";
|
|
17
|
+
import { startAgentRun, finishAgentRun } from "./src/handlers/agent.js";
|
|
18
|
+
import { startTurnObservation, finishTurnObservation } from "./src/handlers/turn.js";
|
|
19
|
+
import {
|
|
20
|
+
startGeneration,
|
|
21
|
+
updateGenerationMetadata,
|
|
22
|
+
finishGenerationFromMessage,
|
|
23
|
+
createFallbackGenerationFromTurn,
|
|
24
|
+
recordTTFT,
|
|
25
|
+
} from "./src/handlers/generation.js";
|
|
26
|
+
import {
|
|
27
|
+
startToolObservation,
|
|
28
|
+
finishToolObservation,
|
|
29
|
+
closeDanglingObservations,
|
|
30
|
+
} from "./src/handlers/tool.js";
|
|
271
31
|
|
|
272
32
|
// ============================================
|
|
273
33
|
// Extension
|
|
274
34
|
// ============================================
|
|
275
35
|
|
|
276
36
|
export default async function (pi: ExtensionAPI) {
|
|
277
|
-
if (config) {
|
|
278
|
-
console.log("📊 Langfuse: Tracing enabled →", config.host);
|
|
37
|
+
if (state.config) {
|
|
38
|
+
console.log("📊 Langfuse: Tracing enabled →", state.config.host);
|
|
279
39
|
} else {
|
|
280
40
|
console.log("📊 Langfuse: Waiting for first-run setup");
|
|
281
41
|
}
|
|
@@ -287,266 +47,124 @@ export default async function (pi: ExtensionAPI) {
|
|
|
287
47
|
},
|
|
288
48
|
});
|
|
289
49
|
|
|
290
|
-
// Capture session ID on session start
|
|
291
50
|
pi.on("session_start", async (_event, ctx) => {
|
|
292
|
-
setupAttemptedThisSession = false;
|
|
51
|
+
state.setupAttemptedThisSession = false;
|
|
293
52
|
await ensureConfig(ctx);
|
|
294
53
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
295
54
|
if (sessionFile) {
|
|
296
|
-
|
|
297
|
-
currentSessionId = basename(sessionFile, ".jsonl");
|
|
55
|
+
state.currentSessionId = basename(sessionFile, ".jsonl");
|
|
298
56
|
}
|
|
299
|
-
|
|
300
|
-
resetSessionState();
|
|
57
|
+
resetRunState();
|
|
301
58
|
});
|
|
302
59
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
currentProvider = event.model?.provider || '';
|
|
60
|
+
pi.on("model_select", async (event) => {
|
|
61
|
+
state.currentModel = event.model?.id || "";
|
|
62
|
+
state.currentProvider = event.model?.provider || "";
|
|
307
63
|
});
|
|
308
64
|
|
|
309
|
-
// Use before_agent_start to capture user prompt and create trace
|
|
310
65
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
66
|
+
await startAgentRun(event, ctx);
|
|
67
|
+
});
|
|
314
68
|
|
|
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);
|
|
69
|
+
pi.on("agent_start", async (event, ctx) => {
|
|
70
|
+
if (!state.agentState?.root) {
|
|
71
|
+
await startAgentRun(event, ctx);
|
|
339
72
|
}
|
|
340
73
|
});
|
|
341
74
|
|
|
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
|
-
}
|
|
75
|
+
pi.on("turn_start", async (event) => {
|
|
76
|
+
await startTurnObservation(event);
|
|
397
77
|
});
|
|
398
78
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
79
|
+
pi.on("before_provider_request", async (event) => {
|
|
80
|
+
await startGeneration(event);
|
|
81
|
+
});
|
|
402
82
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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);
|
|
83
|
+
pi.on("after_provider_response", async (event) => {
|
|
84
|
+
updateGenerationMetadata(event);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("message_update", async (event) => {
|
|
88
|
+
recordTTFT(event);
|
|
89
|
+
const message = getMessageFromEvent(event);
|
|
90
|
+
if (message?.role === "assistant" && state.agentState) {
|
|
91
|
+
state.agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
422
92
|
}
|
|
423
93
|
});
|
|
424
94
|
|
|
425
|
-
|
|
95
|
+
pi.on("message_end", async (event) => {
|
|
96
|
+
await finishGenerationFromMessage(event);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("tool_execution_start", async (event) => {
|
|
100
|
+
await startToolObservation(event);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.on("tool_call", async (event) => {
|
|
104
|
+
await startToolObservation(event);
|
|
105
|
+
});
|
|
106
|
+
|
|
426
107
|
pi.on("tool_result", async (event) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
}
|
|
108
|
+
await finishToolObservation(event);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
pi.on("tool_execution_end", async (event) => {
|
|
112
|
+
await finishToolObservation(event);
|
|
458
113
|
});
|
|
459
114
|
|
|
460
|
-
// Track turns and generations
|
|
461
115
|
pi.on("turn_end", async (event) => {
|
|
462
|
-
|
|
116
|
+
state.turnCount++;
|
|
117
|
+
const message = getMessageFromEvent(event);
|
|
118
|
+
if (message?.role === "assistant") {
|
|
119
|
+
await createFallbackGenerationFromTurn(event, message);
|
|
120
|
+
await finishGenerationFromMessage(event);
|
|
121
|
+
}
|
|
122
|
+
finishTurnObservation(event);
|
|
123
|
+
});
|
|
463
124
|
|
|
464
|
-
|
|
465
|
-
|
|
125
|
+
pi.on("agent_end", async (event) => {
|
|
126
|
+
await finishAgentRun(event);
|
|
127
|
+
await shutdownRuntime();
|
|
128
|
+
});
|
|
466
129
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
};
|
|
130
|
+
const handleSessionInterruption = (reason: string) => {
|
|
131
|
+
if (state.agentState?.root) {
|
|
132
|
+
closeDanglingObservations(reason);
|
|
133
|
+
state.agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
134
|
+
}
|
|
135
|
+
resetRunState();
|
|
136
|
+
};
|
|
484
137
|
|
|
485
|
-
|
|
486
|
-
|
|
138
|
+
pi.on("session_before_switch", async () => {
|
|
139
|
+
handleSessionInterruption("Session switched");
|
|
140
|
+
});
|
|
487
141
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const cost = usage?.cost;
|
|
142
|
+
pi.on("session_before_fork", async () => {
|
|
143
|
+
handleSessionInterruption("Session forked");
|
|
144
|
+
});
|
|
492
145
|
|
|
493
|
-
|
|
146
|
+
pi.on("session_compact", async (event) => {
|
|
147
|
+
if (state.agentState?.root) {
|
|
148
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
494
149
|
try {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
});
|
|
150
|
+
const observation = parent.startObservation ? parent.startObservation(
|
|
151
|
+
"session_compact",
|
|
152
|
+
{
|
|
153
|
+
level: "DEFAULT",
|
|
154
|
+
statusMessage: "Context was compacted",
|
|
155
|
+
metadata: { ...event }
|
|
156
|
+
},
|
|
157
|
+
{ asType: "span" }
|
|
158
|
+
) : undefined;
|
|
159
|
+
observation?.end();
|
|
529
160
|
} catch (e) {
|
|
530
|
-
|
|
161
|
+
// ignore
|
|
531
162
|
}
|
|
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.)
|
|
536
163
|
}
|
|
537
164
|
});
|
|
538
165
|
|
|
539
166
|
pi.on("session_shutdown", async () => {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
currentTrace.update({ metadata: { completed: true } });
|
|
543
|
-
}
|
|
544
|
-
currentTrace = null;
|
|
545
|
-
}
|
|
546
|
-
if (client) {
|
|
547
|
-
await client.shutdownAsync();
|
|
548
|
-
client = null;
|
|
549
|
-
}
|
|
550
|
-
resetSessionState();
|
|
167
|
+
handleSessionInterruption("Session shutdown before agent completed");
|
|
168
|
+
await shutdownRuntime();
|
|
551
169
|
});
|
|
552
170
|
}
|