@narumitw/pi-langfuse 0.18.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -35
- package/package.json +5 -5
- package/src/config.ts +10 -0
- package/src/langfuse.ts +339 -96
- package/src/sanitizer.ts +147 -0
- package/src/tracing.ts +651 -204
package/src/langfuse.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionCommandContext,
|
|
4
|
+
ExtensionContext,
|
|
4
5
|
} from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import {
|
|
6
7
|
DEFAULT_BASE_URL,
|
|
@@ -10,20 +11,24 @@ import {
|
|
|
10
11
|
normalizeLangfuseConfig,
|
|
11
12
|
writeLangfuseConfig,
|
|
12
13
|
} from "./config.js";
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
type ContextSnapshot,
|
|
16
|
+
type GitMetadata,
|
|
17
|
+
type TraceBackend,
|
|
18
|
+
TraceRecorder,
|
|
19
|
+
} from "./tracing.js";
|
|
14
20
|
|
|
15
21
|
interface ExtensionDependencies {
|
|
16
22
|
loadConfig(path?: string): Promise<LangfuseConfigResult>;
|
|
17
23
|
writeConfig(config: LangfuseConfig, path?: string): Promise<LangfuseConfig>;
|
|
18
24
|
createBackend(config: LangfuseConfig): Promise<TraceBackend>;
|
|
25
|
+
resolveGitMetadata(cwd: string): Promise<GitMetadata | undefined>;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ value: "init", label: "init", description: "Interactively create or update config" },
|
|
26
|
-
];
|
|
28
|
+
const FLUSH_ACTION = "Flush completed traces for this session";
|
|
29
|
+
const SET_UP_ACTION = "Set up Langfuse for this Pi agent directory (restart required)";
|
|
30
|
+
const UPDATE_ACTION = "Update Langfuse for this Pi agent directory (restart required)";
|
|
31
|
+
const HELP_ACTION = "Show setup and privacy help";
|
|
27
32
|
|
|
28
33
|
export function createLangfuseExtension(
|
|
29
34
|
dependencies: Partial<ExtensionDependencies> = {},
|
|
@@ -38,97 +43,97 @@ export function createLangfuseExtension(
|
|
|
38
43
|
});
|
|
39
44
|
|
|
40
45
|
return function langfuse(pi: ExtensionAPI) {
|
|
46
|
+
const resolveGit =
|
|
47
|
+
dependencies.resolveGitMetadata ??
|
|
48
|
+
((cwd: string) =>
|
|
49
|
+
resolveGitMetadata((command, args, options) => pi.exec(command, args, options), cwd));
|
|
41
50
|
let recorder: TraceRecorder | undefined;
|
|
42
51
|
let activeConfig: LangfuseConfig | undefined;
|
|
43
52
|
let configPath: string | undefined;
|
|
44
53
|
let initializationError: string | undefined;
|
|
54
|
+
let hasStoredConfig = false;
|
|
55
|
+
let configurationNotice: string | undefined;
|
|
56
|
+
let sessionGeneration = 0;
|
|
57
|
+
let nextAttemptReason: string | undefined;
|
|
45
58
|
|
|
46
59
|
pi.registerCommand("langfuse", {
|
|
47
|
-
description: "
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (/\s/.test(normalized)) return null;
|
|
51
|
-
const matches = COMMAND_COMPLETIONS.filter((item) => item.value.startsWith(normalized));
|
|
52
|
-
return matches.length > 0 ? matches : null;
|
|
53
|
-
},
|
|
54
|
-
handler: async (args, ctx) => {
|
|
55
|
-
const action = args.trim().toLowerCase() || "status";
|
|
56
|
-
if (action === "status") {
|
|
57
|
-
if (activeConfig) {
|
|
58
|
-
ctx.ui.notify(
|
|
59
|
-
[
|
|
60
|
-
"Langfuse tracing is enabled.",
|
|
61
|
-
`Endpoint: ${activeConfig.baseUrl}`,
|
|
62
|
-
`Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
|
|
63
|
-
`Configuration: ${configPath ?? "unknown"}`,
|
|
64
|
-
].join("\n"),
|
|
65
|
-
"info",
|
|
66
|
-
);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
60
|
+
description: "Open interactive Langfuse tracing controls",
|
|
61
|
+
handler: async (_args, ctx) => {
|
|
62
|
+
if (!ctx.hasUI) {
|
|
69
63
|
ctx.ui.notify(
|
|
70
|
-
initializationError
|
|
71
|
-
"Langfuse tracing is disabled because its credentials are not configured.",
|
|
64
|
+
formatNonInteractiveStatus(activeConfig, configPath, initializationError),
|
|
72
65
|
"warning",
|
|
73
66
|
);
|
|
74
67
|
return;
|
|
75
68
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
|
|
70
|
+
const menuGeneration = sessionGeneration;
|
|
71
|
+
const menuRecorder = recorder;
|
|
72
|
+
const menuConfig = activeConfig;
|
|
73
|
+
const choice = await ctx.ui.select(
|
|
74
|
+
formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice),
|
|
75
|
+
[
|
|
76
|
+
...(menuRecorder ? [FLUSH_ACTION] : []),
|
|
77
|
+
hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
78
|
+
HELP_ACTION,
|
|
79
|
+
],
|
|
80
|
+
);
|
|
81
|
+
if (!choice || menuGeneration !== sessionGeneration) return;
|
|
82
|
+
|
|
83
|
+
if (choice === FLUSH_ACTION) {
|
|
84
|
+
if (!menuRecorder) {
|
|
85
|
+
ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
|
|
79
86
|
return;
|
|
80
87
|
}
|
|
81
88
|
try {
|
|
82
|
-
await
|
|
83
|
-
|
|
89
|
+
await menuRecorder.flush();
|
|
90
|
+
if (menuGeneration !== sessionGeneration) return;
|
|
91
|
+
ctx.ui.notify("Langfuse traces flushed for this session.", "info");
|
|
84
92
|
} catch (error) {
|
|
85
|
-
|
|
93
|
+
if (menuGeneration !== sessionGeneration) return;
|
|
94
|
+
ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
|
|
86
95
|
}
|
|
87
96
|
return;
|
|
88
97
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ctx.ui.notify(
|
|
92
|
-
"/langfuse init requires interactive UI. Edit pi-langfuse.json manually.",
|
|
93
|
-
"warning",
|
|
94
|
-
);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
98
|
+
|
|
99
|
+
if (choice === SET_UP_ACTION || choice === UPDATE_ACTION) {
|
|
97
100
|
const loaded = await loadConfig(configPath);
|
|
101
|
+
if (menuGeneration !== sessionGeneration) return;
|
|
98
102
|
configPath = loaded.path;
|
|
99
103
|
const next = await promptForConfig(ctx, loaded.ok ? loaded.config : undefined);
|
|
100
|
-
if (!next) return;
|
|
104
|
+
if (!next || menuGeneration !== sessionGeneration) return;
|
|
101
105
|
try {
|
|
102
106
|
await writeConfig(next, loaded.path);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
if (menuGeneration !== sessionGeneration) return;
|
|
108
|
+
hasStoredConfig = true;
|
|
109
|
+
configurationNotice =
|
|
110
|
+
"Saved; restart each Pi process to use it in subsequent sessions.";
|
|
111
|
+
ctx.ui.notify(
|
|
112
|
+
`Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
|
|
113
|
+
"info",
|
|
114
|
+
);
|
|
106
115
|
} catch (error) {
|
|
107
|
-
|
|
116
|
+
if (menuGeneration !== sessionGeneration) return;
|
|
117
|
+
ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
|
|
108
118
|
}
|
|
109
119
|
return;
|
|
110
120
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"Usage: /langfuse [status|flush|help|init]",
|
|
115
|
-
"status: show tracing state without credentials",
|
|
116
|
-
"flush: wait for completed traces to export",
|
|
117
|
-
"init: interactively create or update the private config",
|
|
118
|
-
].join("\n"),
|
|
119
|
-
"info",
|
|
120
|
-
);
|
|
121
|
-
return;
|
|
121
|
+
|
|
122
|
+
if (choice === HELP_ACTION) {
|
|
123
|
+
ctx.ui.notify(formatHelp(configPath), "info");
|
|
122
124
|
}
|
|
123
|
-
ctx.ui.notify("Usage: /langfuse [status|flush|help|init]", "warning");
|
|
124
125
|
},
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
pi.on("session_start", async (_event, ctx) => {
|
|
129
|
+
sessionGeneration += 1;
|
|
128
130
|
recorder = undefined;
|
|
129
131
|
activeConfig = undefined;
|
|
130
132
|
configPath = undefined;
|
|
131
133
|
initializationError = undefined;
|
|
134
|
+
hasStoredConfig = false;
|
|
135
|
+
configurationNotice = undefined;
|
|
136
|
+
nextAttemptReason = undefined;
|
|
132
137
|
|
|
133
138
|
const result = await loadConfig();
|
|
134
139
|
configPath = result.path;
|
|
@@ -139,6 +144,7 @@ export function createLangfuseExtension(
|
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
hasStoredConfig = true;
|
|
142
148
|
try {
|
|
143
149
|
const backend = await createBackend(result.config);
|
|
144
150
|
activeConfig = result.config;
|
|
@@ -149,43 +155,59 @@ export function createLangfuseExtension(
|
|
|
149
155
|
captureContent: result.config.captureContent,
|
|
150
156
|
});
|
|
151
157
|
} catch (error) {
|
|
152
|
-
initializationError = `Langfuse tracing could not start: ${formatError(error)}`;
|
|
158
|
+
initializationError = `Langfuse tracing could not start: ${formatError(error, result.config)}`;
|
|
153
159
|
ctx.ui.notify(initializationError, "warning");
|
|
154
160
|
}
|
|
155
161
|
});
|
|
156
162
|
|
|
157
|
-
pi.on("before_agent_start", (event, ctx) => {
|
|
158
|
-
|
|
163
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
164
|
+
nextAttemptReason = undefined;
|
|
165
|
+
const activeRecorder = recorder;
|
|
166
|
+
if (!activeRecorder) return;
|
|
167
|
+
const git = await resolveGit(ctx.cwd).catch(() => undefined);
|
|
168
|
+
if (recorder !== activeRecorder) return;
|
|
169
|
+
activeRecorder.beginAgent({
|
|
159
170
|
prompt: event.prompt,
|
|
160
171
|
images: event.images,
|
|
161
|
-
model: ctx.model
|
|
172
|
+
model: ctx.model
|
|
173
|
+
? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
|
|
174
|
+
: undefined,
|
|
175
|
+
git,
|
|
176
|
+
snapshot: contextSnapshot(ctx),
|
|
162
177
|
});
|
|
163
178
|
});
|
|
164
179
|
|
|
180
|
+
pi.on("agent_start", () => {
|
|
181
|
+
if (!recorder) return;
|
|
182
|
+
recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : undefined);
|
|
183
|
+
nextAttemptReason = undefined;
|
|
184
|
+
});
|
|
185
|
+
|
|
165
186
|
pi.on("turn_start", (event, ctx) => {
|
|
166
187
|
if (!recorder) return;
|
|
167
|
-
|
|
168
|
-
recorder.beginAgent({
|
|
169
|
-
prompt: "[automatic continuation]",
|
|
170
|
-
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
188
|
+
ensureActiveRun(recorder, ctx);
|
|
173
189
|
recorder.beginTurn(event.turnIndex);
|
|
174
190
|
});
|
|
175
191
|
|
|
176
|
-
pi.on("before_provider_request", (
|
|
192
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
177
193
|
if (!recorder) return;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
194
|
+
ensureActiveRun(recorder, ctx);
|
|
195
|
+
recorder.beginGeneration({
|
|
196
|
+
payload: event.payload,
|
|
197
|
+
payloadStage: "before_provider_request",
|
|
198
|
+
model: ctx.model
|
|
199
|
+
? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
|
|
200
|
+
: undefined,
|
|
201
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
202
|
+
});
|
|
185
203
|
});
|
|
186
204
|
|
|
187
205
|
pi.on("after_provider_response", (event) => {
|
|
188
|
-
recorder?.recordProviderResponse(event.status);
|
|
206
|
+
recorder?.recordProviderResponse(event.status, event.headers);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
pi.on("message_update", (event) => {
|
|
210
|
+
if (isRealOutputDelta(event.assistantMessageEvent)) recorder?.markGenerationFirstOutput();
|
|
189
211
|
});
|
|
190
212
|
|
|
191
213
|
pi.on("message_end", (event) => {
|
|
@@ -201,11 +223,15 @@ export function createLangfuseExtension(
|
|
|
201
223
|
});
|
|
202
224
|
|
|
203
225
|
pi.on("tool_execution_start", (event) => {
|
|
204
|
-
recorder?.beginTool(event.toolCallId, event.toolName);
|
|
226
|
+
recorder?.beginTool(event.toolCallId, event.toolName, event.args);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
pi.on("tool_execution_update", (event) => {
|
|
230
|
+
recorder?.recordToolProgress(event.toolCallId);
|
|
205
231
|
});
|
|
206
232
|
|
|
207
233
|
pi.on("tool_result", (event) => {
|
|
208
|
-
recorder?.
|
|
234
|
+
recorder?.recordToolInput(event.toolCallId, event.input);
|
|
209
235
|
});
|
|
210
236
|
|
|
211
237
|
pi.on("tool_execution_end", (event) => {
|
|
@@ -217,33 +243,177 @@ export function createLangfuseExtension(
|
|
|
217
243
|
});
|
|
218
244
|
|
|
219
245
|
pi.on("agent_end", (event) => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
246
|
+
const message = findLastAssistant(event.messages);
|
|
247
|
+
recorder?.finishAttempt(message);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
pi.on("session_before_compact", (event) => {
|
|
251
|
+
recorder?.beginCompaction({
|
|
252
|
+
reason: event.reason,
|
|
253
|
+
willRetry: event.willRetry,
|
|
254
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
255
|
+
messagesToSummarize: event.preparation.messagesToSummarize.length,
|
|
256
|
+
turnPrefixMessages: event.preparation.turnPrefixMessages.length,
|
|
257
|
+
branchEntries: event.branchEntries.length,
|
|
258
|
+
isSplitTurn: event.preparation.isSplitTurn,
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
pi.on("session_compact", (event) => {
|
|
263
|
+
const entry = event.compactionEntry as typeof event.compactionEntry & {
|
|
264
|
+
usage?: Parameters<TraceRecorder["finishCompaction"]>[0]["usage"];
|
|
265
|
+
};
|
|
266
|
+
recorder?.finishCompaction({
|
|
267
|
+
reason: event.reason,
|
|
268
|
+
willRetry: event.willRetry,
|
|
269
|
+
fromExtension: event.fromExtension,
|
|
270
|
+
tokensBefore: entry.tokensBefore,
|
|
271
|
+
details: entry.details,
|
|
272
|
+
usage: entry.usage,
|
|
273
|
+
});
|
|
274
|
+
if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
278
|
+
recorder?.settle(contextSnapshot(ctx));
|
|
279
|
+
nextAttemptReason = undefined;
|
|
227
280
|
});
|
|
228
281
|
|
|
229
282
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
283
|
+
sessionGeneration += 1;
|
|
230
284
|
const activeRecorder = recorder;
|
|
285
|
+
const shutdownConfig = activeConfig;
|
|
231
286
|
recorder = undefined;
|
|
232
287
|
activeConfig = undefined;
|
|
233
288
|
if (!activeRecorder) return;
|
|
234
289
|
try {
|
|
235
|
-
|
|
290
|
+
const snapshot = contextSnapshot(ctx);
|
|
291
|
+
if (event.reason === "quit") await activeRecorder.shutdown(snapshot);
|
|
236
292
|
else {
|
|
237
|
-
activeRecorder.
|
|
293
|
+
activeRecorder.interrupt(
|
|
294
|
+
`Pi session ended before settlement (${event.reason}).`,
|
|
295
|
+
snapshot,
|
|
296
|
+
);
|
|
238
297
|
await activeRecorder.flush();
|
|
239
298
|
}
|
|
240
299
|
} catch (error) {
|
|
241
|
-
ctx.ui.notify(
|
|
300
|
+
ctx.ui.notify(
|
|
301
|
+
`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`,
|
|
302
|
+
"error",
|
|
303
|
+
);
|
|
242
304
|
}
|
|
243
305
|
});
|
|
244
306
|
};
|
|
245
307
|
}
|
|
246
308
|
|
|
309
|
+
function ensureActiveRun(recorder: TraceRecorder, ctx: ExtensionContext): void {
|
|
310
|
+
if (!recorder.hasActiveTrace()) {
|
|
311
|
+
recorder.beginAgent({
|
|
312
|
+
prompt: "[automatic continuation]",
|
|
313
|
+
model: ctx.model
|
|
314
|
+
? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api }
|
|
315
|
+
: undefined,
|
|
316
|
+
snapshot: contextSnapshot(ctx),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function contextSnapshot(ctx: ExtensionContext): ContextSnapshot {
|
|
323
|
+
return {
|
|
324
|
+
leafId:
|
|
325
|
+
typeof ctx.sessionManager.getLeafId === "function"
|
|
326
|
+
? ctx.sessionManager.getLeafId()
|
|
327
|
+
: undefined,
|
|
328
|
+
contextUsage: ctx.getContextUsage(),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function findLastAssistant<T extends { role?: string }>(messages: readonly T[]): T | undefined {
|
|
333
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
334
|
+
const message = messages[index];
|
|
335
|
+
if (message?.role === "assistant") return message;
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function isRealOutputDelta(event: { type: string; delta?: unknown }): boolean {
|
|
341
|
+
return (
|
|
342
|
+
(event.type === "text_delta" ||
|
|
343
|
+
event.type === "thinking_delta" ||
|
|
344
|
+
event.type === "toolcall_delta") &&
|
|
345
|
+
typeof event.delta === "string" &&
|
|
346
|
+
event.delta.length > 0
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const GIT_LOOKUP_TIMEOUT_MS = 1_000;
|
|
351
|
+
const MAX_GIT_BRANCH_LENGTH = 256;
|
|
352
|
+
|
|
353
|
+
type GitExecutor = ExtensionAPI["exec"];
|
|
354
|
+
|
|
355
|
+
export async function resolveGitMetadata(
|
|
356
|
+
exec: GitExecutor,
|
|
357
|
+
cwd: string,
|
|
358
|
+
): Promise<GitMetadata | undefined> {
|
|
359
|
+
const [branchResult, commit] = await Promise.all([
|
|
360
|
+
resolveGitBranch(exec, cwd),
|
|
361
|
+
resolveGitCommit(exec, cwd),
|
|
362
|
+
]);
|
|
363
|
+
if (!branchResult.resolved) return undefined;
|
|
364
|
+
if (branchResult.branch) {
|
|
365
|
+
return {
|
|
366
|
+
branch: branchResult.branch,
|
|
367
|
+
...(commit ? { commit } : {}),
|
|
368
|
+
detached: false,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return commit ? { commit, detached: true } : undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function resolveGitBranch(
|
|
375
|
+
exec: GitExecutor,
|
|
376
|
+
cwd: string,
|
|
377
|
+
): Promise<{ resolved: boolean; branch?: string }> {
|
|
378
|
+
try {
|
|
379
|
+
const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
380
|
+
cwd,
|
|
381
|
+
timeout: GIT_LOOKUP_TIMEOUT_MS,
|
|
382
|
+
});
|
|
383
|
+
if (result.killed) return { resolved: false };
|
|
384
|
+
if (result.code === 1) return { resolved: true };
|
|
385
|
+
if (result.code !== 0) return { resolved: false };
|
|
386
|
+
const branch = normalizeGitBranch(result.stdout);
|
|
387
|
+
return branch ? { resolved: true, branch } : { resolved: false };
|
|
388
|
+
} catch {
|
|
389
|
+
return { resolved: false };
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function resolveGitCommit(exec: GitExecutor, cwd: string): Promise<string | undefined> {
|
|
394
|
+
try {
|
|
395
|
+
const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
|
|
396
|
+
cwd,
|
|
397
|
+
timeout: GIT_LOOKUP_TIMEOUT_MS,
|
|
398
|
+
});
|
|
399
|
+
if (result.code !== 0 || result.killed) return undefined;
|
|
400
|
+
const commit = result.stdout.trim();
|
|
401
|
+
return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : undefined;
|
|
402
|
+
} catch {
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function normalizeGitBranch(value: string): string | undefined {
|
|
408
|
+
const branch = value.trim();
|
|
409
|
+
if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return undefined;
|
|
410
|
+
for (const character of branch) {
|
|
411
|
+
const code = character.codePointAt(0) ?? 0;
|
|
412
|
+
if (code <= 31 || code === 127) return undefined;
|
|
413
|
+
}
|
|
414
|
+
return branch;
|
|
415
|
+
}
|
|
416
|
+
|
|
247
417
|
async function promptForConfig(
|
|
248
418
|
ctx: ExtensionCommandContext,
|
|
249
419
|
current: LangfuseConfig | undefined,
|
|
@@ -281,15 +451,88 @@ async function promptForConfig(
|
|
|
281
451
|
return normalized.config;
|
|
282
452
|
}
|
|
283
453
|
|
|
454
|
+
function formatMenuTitle(
|
|
455
|
+
activeConfig: LangfuseConfig | undefined,
|
|
456
|
+
configPath: string | undefined,
|
|
457
|
+
initializationError: string | undefined,
|
|
458
|
+
configurationNotice: string | undefined,
|
|
459
|
+
): string {
|
|
460
|
+
const lines = [
|
|
461
|
+
"Langfuse",
|
|
462
|
+
"",
|
|
463
|
+
"Current session:",
|
|
464
|
+
` Tracing: ${activeConfig ? "enabled" : "disabled"}`,
|
|
465
|
+
];
|
|
466
|
+
if (activeConfig) {
|
|
467
|
+
lines.push(
|
|
468
|
+
` Endpoint: ${activeConfig.baseUrl}`,
|
|
469
|
+
` Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
|
|
470
|
+
);
|
|
471
|
+
} else if (configurationNotice) {
|
|
472
|
+
lines.push(" State: tracing remains disabled until Pi restarts.");
|
|
473
|
+
} else if (initializationError) {
|
|
474
|
+
lines.push(` Reason: ${initializationError}`);
|
|
475
|
+
}
|
|
476
|
+
lines.push(
|
|
477
|
+
"",
|
|
478
|
+
"Agent-directory configuration:",
|
|
479
|
+
` Configuration: ${configPath ?? "unknown"}`,
|
|
480
|
+
" Scope: this Pi agent directory; restart each Pi process to apply changes.",
|
|
481
|
+
);
|
|
482
|
+
if (configurationNotice) lines.push(` Pending: ${configurationNotice}`);
|
|
483
|
+
lines.push("", "What do you want to do?");
|
|
484
|
+
return lines.join("\n");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function formatNonInteractiveStatus(
|
|
488
|
+
activeConfig: LangfuseConfig | undefined,
|
|
489
|
+
configPath: string | undefined,
|
|
490
|
+
initializationError: string | undefined,
|
|
491
|
+
): string {
|
|
492
|
+
return [
|
|
493
|
+
"/langfuse requires interactive UI.",
|
|
494
|
+
`Current session tracing: ${activeConfig ? "enabled" : "disabled"}`,
|
|
495
|
+
...(activeConfig
|
|
496
|
+
? [
|
|
497
|
+
`Endpoint: ${activeConfig.baseUrl}`,
|
|
498
|
+
`Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`,
|
|
499
|
+
]
|
|
500
|
+
: initializationError
|
|
501
|
+
? [`Reason: ${initializationError}`]
|
|
502
|
+
: []),
|
|
503
|
+
`Configuration: ${configPath ?? "unknown"}`,
|
|
504
|
+
"Edit the private config manually, then restart each Pi process to apply it to subsequent sessions.",
|
|
505
|
+
].join("\n");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function formatHelp(configPath: string | undefined): string {
|
|
509
|
+
return [
|
|
510
|
+
"Langfuse setup and privacy:",
|
|
511
|
+
"Run /langfuse in interactive Pi to manage tracing for the current session.",
|
|
512
|
+
`Configuration: ${configPath ?? "pi-langfuse.json"}`,
|
|
513
|
+
"The file belongs to this Pi agent directory; restart each Pi process after changing it.",
|
|
514
|
+
"Trace content may contain prompts, responses, tool arguments, tool results, and source code.",
|
|
515
|
+
"Git branch, commit, cwd, model, and usage remain in metadata when content capture is disabled.",
|
|
516
|
+
'Use "captureContent": false in the private config to export metadata only.',
|
|
517
|
+
].join("\n");
|
|
518
|
+
}
|
|
519
|
+
|
|
284
520
|
function formatConfigError(result: Extract<LangfuseConfigResult, { ok: false }>): string {
|
|
285
521
|
const setupHint = result.reason.startsWith("Configuration file not found:")
|
|
286
|
-
? " Run /langfuse
|
|
522
|
+
? " Run /langfuse and choose Set up Langfuse."
|
|
287
523
|
: "";
|
|
288
524
|
return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
|
|
289
525
|
}
|
|
290
526
|
|
|
291
|
-
function formatError(error: unknown): string {
|
|
292
|
-
|
|
527
|
+
function formatError(error: unknown, config?: LangfuseConfig): string {
|
|
528
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
529
|
+
const secrets = [config?.publicKey, config?.secretKey]
|
|
530
|
+
.filter((secret): secret is string => Boolean(secret))
|
|
531
|
+
.sort((left, right) => right.length - left.length);
|
|
532
|
+
for (const secret of secrets) {
|
|
533
|
+
message = message.split(secret).join("[LANGFUSE_KEY_REDACTED]");
|
|
534
|
+
}
|
|
535
|
+
return message;
|
|
293
536
|
}
|
|
294
537
|
|
|
295
538
|
export default createLangfuseExtension();
|