pi-langfuse 1.1.0 → 1.3.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/index.ts +75 -1005
- package/package.json +1 -1
- package/src/config.ts +102 -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
|
@@ -7,1007 +7,39 @@
|
|
|
7
7
|
* - one tool observation per tool call, keyed by toolCallId
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
import { resolve, dirname, basename } from "node:path";
|
|
12
|
-
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { basename } from "node:path";
|
|
13
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const config = JSON.parse(content) as Config;
|
|
34
|
-
if (config.publicKey && config.secretKey) {
|
|
35
|
-
return {
|
|
36
|
-
publicKey: config.publicKey,
|
|
37
|
-
secretKey: config.secretKey,
|
|
38
|
-
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
} catch (e) {
|
|
42
|
-
console.warn("📊 Langfuse: Failed to load config.json", e);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function loadConfigFromEnv(): Config | null {
|
|
50
|
-
const publicKey = process.env.LANGFUSE_PUBLIC_KEY || "";
|
|
51
|
-
const secretKey = process.env.LANGFUSE_SECRET_KEY || "";
|
|
52
|
-
if (!publicKey || !secretKey) {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
publicKey,
|
|
58
|
-
secretKey,
|
|
59
|
-
host: process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function saveConfig(config: Config) {
|
|
64
|
-
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ============================================
|
|
68
|
-
// Langfuse SDK facade (lazy-loaded)
|
|
69
|
-
// ============================================
|
|
70
|
-
|
|
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;
|
|
82
|
-
}
|
|
83
|
-
|
|
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;
|
|
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;
|
|
106
|
-
};
|
|
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;
|
|
132
|
-
let config: Config | null = loadConfigFromFile() ?? loadConfigFromEnv();
|
|
133
|
-
let setupAttemptedThisSession = false;
|
|
134
|
-
|
|
135
|
-
async function getRuntime(): Promise<LangfuseRuntime> {
|
|
136
|
-
if (!config) {
|
|
137
|
-
throw new Error("Langfuse config is not set");
|
|
138
|
-
}
|
|
139
|
-
|
|
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({
|
|
149
|
-
publicKey: config.publicKey,
|
|
150
|
-
secretKey: config.secretKey,
|
|
151
|
-
baseUrl: config.host,
|
|
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;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// ============================================
|
|
191
|
-
// State
|
|
192
|
-
// ============================================
|
|
193
|
-
|
|
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;
|
|
205
|
-
}
|
|
206
|
-
|
|
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>>;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
let currentSessionId = "";
|
|
221
|
-
let currentModel = "";
|
|
222
|
-
let currentProvider = "";
|
|
223
|
-
let agentState: AgentState | null = null;
|
|
224
|
-
|
|
225
|
-
// Evaluation tracking state
|
|
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
|
-
}
|
|
279
|
-
|
|
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>());
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function safeSerialize(value: unknown, maxLength = MAX_TOOL_PAYLOAD_LENGTH): string {
|
|
320
|
-
try {
|
|
321
|
-
return truncate(JSON.stringify(shapePayload(value, { maxString: maxLength }), null, 2), maxLength);
|
|
322
|
-
} catch {
|
|
323
|
-
return `[unserializable ${typeof value}]`;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
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)) {
|
|
333
|
-
return undefined;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const text = content
|
|
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)
|
|
343
|
-
.join("\n");
|
|
344
|
-
|
|
345
|
-
if (!text) {
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return maxLength ? truncate(text, maxLength) : text;
|
|
350
|
-
}
|
|
351
|
-
|
|
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;
|
|
517
|
-
toolCallCount = 0;
|
|
518
|
-
errorCount = 0;
|
|
519
|
-
turnCount = 0;
|
|
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
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async function ensureConfig(ctx: any): Promise<boolean> {
|
|
552
|
-
if (config) {
|
|
553
|
-
return true;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (setupAttemptedThisSession) {
|
|
557
|
-
return false;
|
|
558
|
-
}
|
|
559
|
-
setupAttemptedThisSession = true;
|
|
560
|
-
|
|
561
|
-
if (!ctx.hasUI) {
|
|
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.");
|
|
563
|
-
return false;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
ctx.ui.notify("Langfuse setup required. Enter your API keys to enable tracing.", "info");
|
|
567
|
-
|
|
568
|
-
const publicKey = (await ctx.ui.input("Langfuse public key:", "pk-lf-..."))?.trim();
|
|
569
|
-
if (!publicKey) {
|
|
570
|
-
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const secretKey = (await ctx.ui.input("Langfuse secret key:", "sk-lf-..."))?.trim();
|
|
575
|
-
if (!secretKey) {
|
|
576
|
-
ctx.ui.notify("Langfuse setup cancelled.", "warning");
|
|
577
|
-
return false;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
const hostInput = (await ctx.ui.input("Langfuse host:", DEFAULT_LANGFUSE_HOST))?.trim();
|
|
581
|
-
config = {
|
|
582
|
-
publicKey,
|
|
583
|
-
secretKey,
|
|
584
|
-
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
try {
|
|
588
|
-
saveConfig(config);
|
|
589
|
-
ctx.ui.notify(`Langfuse config saved to ${CONFIG_PATH}`, "info");
|
|
590
|
-
return true;
|
|
591
|
-
} catch (error) {
|
|
592
|
-
console.warn("📊 Langfuse: Failed to save config.json", error);
|
|
593
|
-
ctx.ui.notify("Failed to save Langfuse config.json. Check extension directory permissions.", "error");
|
|
594
|
-
config = null;
|
|
595
|
-
return false;
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
async function promptForConfig(ctx: any): Promise<boolean> {
|
|
600
|
-
setupAttemptedThisSession = false;
|
|
601
|
-
config = null;
|
|
602
|
-
await shutdownRuntime();
|
|
603
|
-
return ensureConfig(ctx);
|
|
604
|
-
}
|
|
605
|
-
|
|
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
|
-
},
|
|
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
|
-
}
|
|
1002
|
-
}
|
|
13
|
+
import { state, resetRunState } from "./src/state.js";
|
|
14
|
+
import { ensureConfig, promptForConfig, loadConfigFromEnv, loadConfigFromFile } 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";
|
|
1003
31
|
|
|
1004
32
|
// ============================================
|
|
1005
33
|
// Extension
|
|
1006
34
|
// ============================================
|
|
1007
35
|
|
|
1008
36
|
export default async function (pi: ExtensionAPI) {
|
|
1009
|
-
if (config) {
|
|
1010
|
-
|
|
37
|
+
if (!state.config) {
|
|
38
|
+
state.config = loadConfigFromEnv() || loadConfigFromFile();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (state.config) {
|
|
42
|
+
console.log("📊 Langfuse: Tracing enabled →", state.config.host);
|
|
1011
43
|
} else {
|
|
1012
44
|
console.log("📊 Langfuse: Waiting for first-run setup");
|
|
1013
45
|
}
|
|
@@ -1020,18 +52,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1020
52
|
});
|
|
1021
53
|
|
|
1022
54
|
pi.on("session_start", async (_event, ctx) => {
|
|
1023
|
-
setupAttemptedThisSession = false;
|
|
55
|
+
state.setupAttemptedThisSession = false;
|
|
1024
56
|
await ensureConfig(ctx);
|
|
1025
57
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1026
58
|
if (sessionFile) {
|
|
1027
|
-
currentSessionId = basename(sessionFile, ".jsonl");
|
|
59
|
+
state.currentSessionId = basename(sessionFile, ".jsonl");
|
|
1028
60
|
}
|
|
1029
61
|
resetRunState();
|
|
1030
62
|
});
|
|
1031
63
|
|
|
1032
64
|
pi.on("model_select", async (event) => {
|
|
1033
|
-
currentModel = event.model?.id || "";
|
|
1034
|
-
currentProvider = event.model?.provider || "";
|
|
65
|
+
state.currentModel = event.model?.id || "";
|
|
66
|
+
state.currentProvider = event.model?.provider || "";
|
|
1035
67
|
});
|
|
1036
68
|
|
|
1037
69
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -1039,11 +71,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1039
71
|
});
|
|
1040
72
|
|
|
1041
73
|
pi.on("agent_start", async (event, ctx) => {
|
|
1042
|
-
if (!agentState?.root) {
|
|
74
|
+
if (!state.agentState?.root) {
|
|
1043
75
|
await startAgentRun(event, ctx);
|
|
1044
76
|
}
|
|
1045
77
|
});
|
|
1046
78
|
|
|
79
|
+
pi.on("turn_start", async (event) => {
|
|
80
|
+
await startTurnObservation(event);
|
|
81
|
+
});
|
|
82
|
+
|
|
1047
83
|
pi.on("before_provider_request", async (event) => {
|
|
1048
84
|
await startGeneration(event);
|
|
1049
85
|
});
|
|
@@ -1053,9 +89,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1053
89
|
});
|
|
1054
90
|
|
|
1055
91
|
pi.on("message_update", async (event) => {
|
|
92
|
+
recordTTFT(event);
|
|
1056
93
|
const message = getMessageFromEvent(event);
|
|
1057
|
-
if (message?.role === "assistant" && agentState) {
|
|
1058
|
-
agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
94
|
+
if (message?.role === "assistant" && state.agentState) {
|
|
95
|
+
state.agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
1059
96
|
}
|
|
1060
97
|
});
|
|
1061
98
|
|
|
@@ -1080,12 +117,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1080
117
|
});
|
|
1081
118
|
|
|
1082
119
|
pi.on("turn_end", async (event) => {
|
|
1083
|
-
turnCount++;
|
|
120
|
+
state.turnCount++;
|
|
1084
121
|
const message = getMessageFromEvent(event);
|
|
1085
122
|
if (message?.role === "assistant") {
|
|
1086
123
|
await createFallbackGenerationFromTurn(event, message);
|
|
1087
124
|
await finishGenerationFromMessage(event);
|
|
1088
125
|
}
|
|
126
|
+
finishTurnObservation(event);
|
|
1089
127
|
});
|
|
1090
128
|
|
|
1091
129
|
pi.on("agent_end", async (event) => {
|
|
@@ -1093,12 +131,44 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1093
131
|
await shutdownRuntime();
|
|
1094
132
|
});
|
|
1095
133
|
|
|
1096
|
-
|
|
1097
|
-
if (agentState?.root) {
|
|
1098
|
-
closeDanglingObservations(
|
|
1099
|
-
agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
134
|
+
const handleSessionInterruption = (reason: string) => {
|
|
135
|
+
if (state.agentState?.root) {
|
|
136
|
+
closeDanglingObservations(reason);
|
|
137
|
+
state.agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
1100
138
|
}
|
|
1101
139
|
resetRunState();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
pi.on("session_before_switch", async () => {
|
|
143
|
+
handleSessionInterruption("Session switched");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
pi.on("session_before_fork", async () => {
|
|
147
|
+
handleSessionInterruption("Session forked");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
pi.on("session_compact", async (event) => {
|
|
151
|
+
if (state.agentState?.root) {
|
|
152
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
153
|
+
try {
|
|
154
|
+
const observation = parent.startObservation ? parent.startObservation(
|
|
155
|
+
"session_compact",
|
|
156
|
+
{
|
|
157
|
+
level: "DEFAULT",
|
|
158
|
+
statusMessage: "Context was compacted",
|
|
159
|
+
metadata: { ...event }
|
|
160
|
+
},
|
|
161
|
+
{ asType: "span" }
|
|
162
|
+
) : undefined;
|
|
163
|
+
observation?.end();
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
pi.on("session_shutdown", async () => {
|
|
171
|
+
handleSessionInterruption("Session shutdown before agent completed");
|
|
1102
172
|
await shutdownRuntime();
|
|
1103
173
|
});
|
|
1104
174
|
}
|