pi-langfuse 1.1.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/index.ts +71 -1005
- package/package.json +1 -1
- 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
|
@@ -7,1007 +7,35 @@
|
|
|
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 } 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
|
-
console.log("📊 Langfuse: Tracing enabled →", config.host);
|
|
37
|
+
if (state.config) {
|
|
38
|
+
console.log("📊 Langfuse: Tracing enabled →", state.config.host);
|
|
1011
39
|
} else {
|
|
1012
40
|
console.log("📊 Langfuse: Waiting for first-run setup");
|
|
1013
41
|
}
|
|
@@ -1020,18 +48,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1020
48
|
});
|
|
1021
49
|
|
|
1022
50
|
pi.on("session_start", async (_event, ctx) => {
|
|
1023
|
-
setupAttemptedThisSession = false;
|
|
51
|
+
state.setupAttemptedThisSession = false;
|
|
1024
52
|
await ensureConfig(ctx);
|
|
1025
53
|
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1026
54
|
if (sessionFile) {
|
|
1027
|
-
currentSessionId = basename(sessionFile, ".jsonl");
|
|
55
|
+
state.currentSessionId = basename(sessionFile, ".jsonl");
|
|
1028
56
|
}
|
|
1029
57
|
resetRunState();
|
|
1030
58
|
});
|
|
1031
59
|
|
|
1032
60
|
pi.on("model_select", async (event) => {
|
|
1033
|
-
currentModel = event.model?.id || "";
|
|
1034
|
-
currentProvider = event.model?.provider || "";
|
|
61
|
+
state.currentModel = event.model?.id || "";
|
|
62
|
+
state.currentProvider = event.model?.provider || "";
|
|
1035
63
|
});
|
|
1036
64
|
|
|
1037
65
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -1039,11 +67,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1039
67
|
});
|
|
1040
68
|
|
|
1041
69
|
pi.on("agent_start", async (event, ctx) => {
|
|
1042
|
-
if (!agentState?.root) {
|
|
70
|
+
if (!state.agentState?.root) {
|
|
1043
71
|
await startAgentRun(event, ctx);
|
|
1044
72
|
}
|
|
1045
73
|
});
|
|
1046
74
|
|
|
75
|
+
pi.on("turn_start", async (event) => {
|
|
76
|
+
await startTurnObservation(event);
|
|
77
|
+
});
|
|
78
|
+
|
|
1047
79
|
pi.on("before_provider_request", async (event) => {
|
|
1048
80
|
await startGeneration(event);
|
|
1049
81
|
});
|
|
@@ -1053,9 +85,10 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1053
85
|
});
|
|
1054
86
|
|
|
1055
87
|
pi.on("message_update", async (event) => {
|
|
88
|
+
recordTTFT(event);
|
|
1056
89
|
const message = getMessageFromEvent(event);
|
|
1057
|
-
if (message?.role === "assistant" && agentState) {
|
|
1058
|
-
agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
90
|
+
if (message?.role === "assistant" && state.agentState) {
|
|
91
|
+
state.agentState.latestAssistantOutput = extractAssistantOutput(message);
|
|
1059
92
|
}
|
|
1060
93
|
});
|
|
1061
94
|
|
|
@@ -1080,12 +113,13 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1080
113
|
});
|
|
1081
114
|
|
|
1082
115
|
pi.on("turn_end", async (event) => {
|
|
1083
|
-
turnCount++;
|
|
116
|
+
state.turnCount++;
|
|
1084
117
|
const message = getMessageFromEvent(event);
|
|
1085
118
|
if (message?.role === "assistant") {
|
|
1086
119
|
await createFallbackGenerationFromTurn(event, message);
|
|
1087
120
|
await finishGenerationFromMessage(event);
|
|
1088
121
|
}
|
|
122
|
+
finishTurnObservation(event);
|
|
1089
123
|
});
|
|
1090
124
|
|
|
1091
125
|
pi.on("agent_end", async (event) => {
|
|
@@ -1093,12 +127,44 @@ export default async function (pi: ExtensionAPI) {
|
|
|
1093
127
|
await shutdownRuntime();
|
|
1094
128
|
});
|
|
1095
129
|
|
|
1096
|
-
|
|
1097
|
-
if (agentState?.root) {
|
|
1098
|
-
closeDanglingObservations(
|
|
1099
|
-
agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
130
|
+
const handleSessionInterruption = (reason: string) => {
|
|
131
|
+
if (state.agentState?.root) {
|
|
132
|
+
closeDanglingObservations(reason);
|
|
133
|
+
state.agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
|
|
1100
134
|
}
|
|
1101
135
|
resetRunState();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
pi.on("session_before_switch", async () => {
|
|
139
|
+
handleSessionInterruption("Session switched");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
pi.on("session_before_fork", async () => {
|
|
143
|
+
handleSessionInterruption("Session forked");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
pi.on("session_compact", async (event) => {
|
|
147
|
+
if (state.agentState?.root) {
|
|
148
|
+
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
149
|
+
try {
|
|
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();
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// ignore
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
pi.on("session_shutdown", async () => {
|
|
167
|
+
handleSessionInterruption("Session shutdown before agent completed");
|
|
1102
168
|
await shutdownRuntime();
|
|
1103
169
|
});
|
|
1104
170
|
}
|