@narumitw/pi-langfuse 0.50.6 → 0.51.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 +82 -4
- package/dist/chunks/chunk-IASLJKKB.js +316 -0
- package/dist/chunks/chunk-IASLJKKB.js.map +7 -0
- package/dist/chunks/runtime-6MIKLWNJ.js +352 -0
- package/dist/chunks/runtime-6MIKLWNJ.js.map +7 -0
- package/dist/config.d.ts +33 -0
- package/dist/git.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1551 -0
- package/dist/index.js.map +7 -0
- package/dist/index.ts +588 -479
- package/dist/index.ts.map +4 -4
- package/dist/langfuse.d.ts +13 -0
- package/dist/pi-session.d.ts +43 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/runtime-core.d.ts +26 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/sanitizer.d.ts +2 -0
- package/dist/tracing.d.ts +202 -0
- package/package.json +11 -3
- package/src/git.ts +60 -0
- package/src/index.ts +9 -0
- package/src/langfuse.ts +88 -292
- package/src/pi-session.ts +570 -0
- package/src/public-api.ts +17 -0
- package/src/runtime-core.ts +113 -0
- package/src/runtime.ts +234 -51
- package/src/sanitizer.ts +10 -7
- package/src/tracing.ts +52 -14
- package/dist/chunks/runtime-X75FRK2E.js +0 -215
- package/dist/chunks/runtime-X75FRK2E.js.map +0 -7
package/dist/index.ts
CHANGED
|
@@ -1,232 +1,65 @@
|
|
|
1
1
|
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
2
|
// @ts-nocheck -- the generated entry uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_BASE_URL,
|
|
5
|
+
createLangfuseRuntimeFromBackend,
|
|
6
|
+
getLangfuseRuntimeInternal,
|
|
7
|
+
loadLangfuseConfig,
|
|
8
|
+
normalizeLangfuseConfig,
|
|
9
|
+
writeLangfuseConfig
|
|
10
|
+
} from "./chunks/chunk-IASLJKKB.js";
|
|
3
11
|
|
|
4
|
-
// src/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function langfuseConfigPath() {
|
|
12
|
-
return join(getAgentDir(), CONFIG_FILE_NAME);
|
|
13
|
-
}
|
|
14
|
-
var configSaveQueues = /* @__PURE__ */ new Map();
|
|
15
|
-
function writeLangfuseConfig(patch, path = langfuseConfigPath()) {
|
|
16
|
-
const previous = configSaveQueues.get(path) ?? Promise.resolve();
|
|
17
|
-
const operation = previous.then(() => writeLangfuseConfigNow(patch, path));
|
|
18
|
-
const tail = operation.then(
|
|
19
|
-
() => void 0,
|
|
20
|
-
() => void 0
|
|
21
|
-
);
|
|
22
|
-
configSaveQueues.set(path, tail);
|
|
23
|
-
void tail.then(() => {
|
|
24
|
-
if (configSaveQueues.get(path) === tail) configSaveQueues.delete(path);
|
|
25
|
-
});
|
|
26
|
-
return operation;
|
|
27
|
-
}
|
|
28
|
-
async function writeLangfuseConfigNow(patch, path) {
|
|
29
|
-
const currentDocument = await readLangfuseDocumentForUpdate(path);
|
|
30
|
-
const normalized = normalizeLangfuseConfig({ ...currentDocument, ...patch });
|
|
31
|
-
if (!normalized.ok) throw new Error(normalized.reason);
|
|
32
|
-
const {
|
|
33
|
-
publicKey: _publicKey,
|
|
34
|
-
secretKey: _secretKey,
|
|
35
|
-
baseUrl: _baseUrl,
|
|
36
|
-
environment: _environment,
|
|
37
|
-
release: _release,
|
|
38
|
-
userId: _userId,
|
|
39
|
-
captureContent: _captureContent,
|
|
40
|
-
...unknownFields
|
|
41
|
-
} = currentDocument;
|
|
42
|
-
const nextDocument = { ...unknownFields, ...normalized.config };
|
|
43
|
-
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
44
|
-
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
45
|
-
try {
|
|
46
|
-
await writeFile(tempPath, `${JSON.stringify(nextDocument, null, 2)}
|
|
47
|
-
`, {
|
|
48
|
-
encoding: "utf8",
|
|
49
|
-
mode: 384
|
|
50
|
-
});
|
|
51
|
-
await chmod(tempPath, 384);
|
|
52
|
-
await rename(tempPath, path);
|
|
53
|
-
return normalized.config;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
await rm(tempPath, { force: true }).catch(() => void 0);
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async function readLangfuseDocumentForUpdate(path) {
|
|
60
|
-
let parsed;
|
|
61
|
-
try {
|
|
62
|
-
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
65
|
-
throw new Error(
|
|
66
|
-
`Cannot update ${CONFIG_FILE_NAME} because the existing file is malformed or unreadable; repair it first.`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
const normalized = normalizeLangfuseConfig(parsed);
|
|
70
|
-
if (!normalized.ok) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`Cannot update ${CONFIG_FILE_NAME} because the existing file has invalid recognized fields; repair it first.`
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return { ...parsed };
|
|
76
|
-
}
|
|
77
|
-
async function loadLangfuseConfig(path = langfuseConfigPath()) {
|
|
78
|
-
await configSaveQueues.get(path);
|
|
79
|
-
const warnings = [];
|
|
80
|
-
const permissionFailure = await ensurePrivatePermissions(path, warnings);
|
|
81
|
-
if (permissionFailure) {
|
|
82
|
-
return {
|
|
83
|
-
ok: false,
|
|
84
|
-
path,
|
|
85
|
-
warnings,
|
|
86
|
-
reason: `Refusing to load credentials: ${permissionFailure}`
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
let text;
|
|
90
|
-
try {
|
|
91
|
-
text = await readFile(path, "utf8");
|
|
92
|
-
} catch (error) {
|
|
93
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
94
|
-
return { ok: false, path, warnings, reason: `Configuration file not found: ${path}` };
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
ok: false,
|
|
98
|
-
path,
|
|
99
|
-
warnings,
|
|
100
|
-
reason: `Failed to read ${path}: ${formatError(error)}`
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
let parsed;
|
|
104
|
-
try {
|
|
105
|
-
parsed = JSON.parse(text);
|
|
106
|
-
} catch {
|
|
107
|
-
return {
|
|
108
|
-
ok: false,
|
|
109
|
-
path,
|
|
110
|
-
warnings,
|
|
111
|
-
reason: `Failed to parse ${path} as JSON; repair the file before retrying.`
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
const normalized = normalizeLangfuseConfig(parsed);
|
|
115
|
-
if (!normalized.ok) return { ok: false, path, warnings, reason: normalized.reason };
|
|
116
|
-
return { ok: true, config: normalized.config, path, warnings };
|
|
117
|
-
}
|
|
118
|
-
function normalizeLangfuseConfig(value) {
|
|
119
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
120
|
-
return { ok: false, reason: "pi-langfuse.json must contain a JSON object." };
|
|
121
|
-
}
|
|
122
|
-
const input = value;
|
|
123
|
-
const publicKey = normalizeString(input.publicKey);
|
|
124
|
-
const secretKey = normalizeString(input.secretKey);
|
|
125
|
-
if (!publicKey) {
|
|
126
|
-
return { ok: false, reason: "pi-langfuse.json requires a literal publicKey string." };
|
|
127
|
-
}
|
|
128
|
-
if (!secretKey) {
|
|
129
|
-
return { ok: false, reason: "pi-langfuse.json requires a literal secretKey string." };
|
|
130
|
-
}
|
|
131
|
-
if (isInterpolation(publicKey)) {
|
|
132
|
-
return {
|
|
133
|
-
ok: false,
|
|
134
|
-
reason: "pi-langfuse.json publicKey must be literal; environment and command interpolation are not supported."
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (isInterpolation(secretKey)) {
|
|
138
|
-
return {
|
|
139
|
-
ok: false,
|
|
140
|
-
reason: "pi-langfuse.json secretKey must be literal; environment and command interpolation are not supported."
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
const rawBaseUrl = input.baseUrl === void 0 ? DEFAULT_BASE_URL : normalizeString(input.baseUrl);
|
|
144
|
-
if (!rawBaseUrl) {
|
|
145
|
-
return { ok: false, reason: "pi-langfuse.json baseUrl must be a non-empty string." };
|
|
146
|
-
}
|
|
147
|
-
const baseUrl = normalizeBaseUrl(rawBaseUrl);
|
|
148
|
-
if (!baseUrl) {
|
|
149
|
-
return {
|
|
150
|
-
ok: false,
|
|
151
|
-
reason: "pi-langfuse.json baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment."
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
if (input.captureContent !== void 0 && typeof input.captureContent !== "boolean") {
|
|
155
|
-
return { ok: false, reason: "pi-langfuse.json captureContent must be a boolean." };
|
|
156
|
-
}
|
|
157
|
-
const environment = optionalString(input.environment, "environment");
|
|
158
|
-
if (!environment.ok) return environment;
|
|
159
|
-
if (environment.value && (environment.value.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment.value))) {
|
|
12
|
+
// src/git.ts
|
|
13
|
+
var GIT_LOOKUP_TIMEOUT_MS = 1e3;
|
|
14
|
+
var MAX_GIT_BRANCH_LENGTH = 256;
|
|
15
|
+
async function resolveGitMetadata(exec, cwd) {
|
|
16
|
+
const [branchResult, commit] = await Promise.all([resolveGitBranch(exec, cwd), resolveGitCommit(exec, cwd)]);
|
|
17
|
+
if (!branchResult.resolved) return void 0;
|
|
18
|
+
if (branchResult.branch) {
|
|
160
19
|
return {
|
|
161
|
-
|
|
162
|
-
|
|
20
|
+
branch: branchResult.branch,
|
|
21
|
+
...commit ? { commit } : {},
|
|
22
|
+
detached: false
|
|
163
23
|
};
|
|
164
24
|
}
|
|
165
|
-
|
|
166
|
-
if (!release.ok) return release;
|
|
167
|
-
const userId = optionalString(input.userId, "userId");
|
|
168
|
-
if (!userId.ok) return userId;
|
|
169
|
-
if (userId.value && userId.value.length > 200) {
|
|
170
|
-
return { ok: false, reason: "pi-langfuse.json userId must be at most 200 characters." };
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
ok: true,
|
|
174
|
-
config: {
|
|
175
|
-
publicKey,
|
|
176
|
-
secretKey,
|
|
177
|
-
baseUrl,
|
|
178
|
-
...environment.value ? { environment: environment.value } : {},
|
|
179
|
-
...release.value ? { release: release.value } : {},
|
|
180
|
-
...userId.value ? { userId: userId.value } : {},
|
|
181
|
-
captureContent: input.captureContent ?? true
|
|
182
|
-
}
|
|
183
|
-
};
|
|
25
|
+
return commit ? { commit, detached: true } : void 0;
|
|
184
26
|
}
|
|
185
|
-
async function
|
|
27
|
+
async function resolveGitBranch(exec, cwd) {
|
|
186
28
|
try {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
} catch
|
|
197
|
-
|
|
198
|
-
const failure = `Failed to enforce 0600 permissions for ${path}: ${formatError(error)}`;
|
|
199
|
-
warnings.push(failure);
|
|
200
|
-
return failure;
|
|
29
|
+
const result = await exec("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], {
|
|
30
|
+
cwd,
|
|
31
|
+
timeout: GIT_LOOKUP_TIMEOUT_MS
|
|
32
|
+
});
|
|
33
|
+
if (result.killed) return { resolved: false };
|
|
34
|
+
if (result.code === 1) return { resolved: true };
|
|
35
|
+
if (result.code !== 0) return { resolved: false };
|
|
36
|
+
const branch = normalizeGitBranch(result.stdout);
|
|
37
|
+
return branch ? { resolved: true, branch } : { resolved: false };
|
|
38
|
+
} catch {
|
|
39
|
+
return { resolved: false };
|
|
201
40
|
}
|
|
202
|
-
return void 0;
|
|
203
41
|
}
|
|
204
|
-
function
|
|
42
|
+
async function resolveGitCommit(exec, cwd) {
|
|
205
43
|
try {
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
44
|
+
const result = await exec("git", ["rev-parse", "--verify", "--short=12", "HEAD"], {
|
|
45
|
+
cwd,
|
|
46
|
+
timeout: GIT_LOOKUP_TIMEOUT_MS
|
|
47
|
+
});
|
|
48
|
+
if (result.code !== 0 || result.killed) return void 0;
|
|
49
|
+
const commit = result.stdout.trim();
|
|
50
|
+
return /^[0-9a-f]{4,64}$/iu.test(commit) ? commit.toLowerCase() : void 0;
|
|
210
51
|
} catch {
|
|
211
52
|
return void 0;
|
|
212
53
|
}
|
|
213
54
|
}
|
|
214
|
-
function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
function isInterpolation(value) {
|
|
223
|
-
return value.startsWith("$") || value.startsWith("!");
|
|
224
|
-
}
|
|
225
|
-
function isNodeError(error) {
|
|
226
|
-
return error instanceof Error && "code" in error;
|
|
227
|
-
}
|
|
228
|
-
function formatError(error) {
|
|
229
|
-
return error instanceof Error ? error.message : String(error);
|
|
55
|
+
function normalizeGitBranch(value) {
|
|
56
|
+
const branch = value.trim();
|
|
57
|
+
if (!branch || branch.length > MAX_GIT_BRANCH_LENGTH) return void 0;
|
|
58
|
+
for (const character of branch) {
|
|
59
|
+
const code = character.codePointAt(0) ?? 0;
|
|
60
|
+
if (code <= 31 || code === 127) return void 0;
|
|
61
|
+
}
|
|
62
|
+
return branch;
|
|
230
63
|
}
|
|
231
64
|
|
|
232
65
|
// src/sanitizer.ts
|
|
@@ -249,9 +82,7 @@ function sanitize(value, active, depth, budget) {
|
|
|
249
82
|
return consume(value, budget);
|
|
250
83
|
}
|
|
251
84
|
if (typeof value === "string") {
|
|
252
|
-
|
|
253
|
-
const redacted = bounded.replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
|
|
254
|
-
return consume(redacted, budget);
|
|
85
|
+
return consume(sanitizeString(value, Math.min(MAX_STRING_LENGTH, budget.remaining)), budget);
|
|
255
86
|
}
|
|
256
87
|
if (typeof value === "bigint") return consume(value.toString(), budget);
|
|
257
88
|
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
|
|
@@ -301,12 +132,13 @@ function sanitize(value, active, depth, budget) {
|
|
|
301
132
|
omitted = true;
|
|
302
133
|
break;
|
|
303
134
|
}
|
|
304
|
-
|
|
135
|
+
const sanitizedKey = sanitizeString(key, keyBudget);
|
|
136
|
+
budget.remaining -= byteLength(sanitizedKey) + 4;
|
|
305
137
|
const property = readProperty(record, key);
|
|
306
138
|
if (redactData && key === "data") {
|
|
307
|
-
output[
|
|
308
|
-
} else if (!property.ok) output[
|
|
309
|
-
else output[
|
|
139
|
+
output[sanitizedKey] = consume("[base64 omitted]", budget);
|
|
140
|
+
} else if (!property.ok) output[sanitizedKey] = consume("[unreadable property]", budget);
|
|
141
|
+
else output[sanitizedKey] = sanitize(property.value, active, depth + 1, budget);
|
|
310
142
|
processed += 1;
|
|
311
143
|
}
|
|
312
144
|
} catch {
|
|
@@ -338,6 +170,9 @@ function consume(value, budget) {
|
|
|
338
170
|
budget.remaining -= size;
|
|
339
171
|
return value;
|
|
340
172
|
}
|
|
173
|
+
function sanitizeString(value, maxBytes) {
|
|
174
|
+
return truncateString(value, maxBytes).replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
|
|
175
|
+
}
|
|
341
176
|
function truncateString(value, maxBytes) {
|
|
342
177
|
const boundedPrefix = value.slice(0, maxBytes);
|
|
343
178
|
if (boundedPrefix.length === value.length && byteLength(boundedPrefix) <= maxBytes) {
|
|
@@ -432,6 +267,7 @@ var TraceRecorder = class {
|
|
|
432
267
|
...hasItems(input.images) ? { images: input.images } : {}
|
|
433
268
|
});
|
|
434
269
|
const metadata = {
|
|
270
|
+
...customTraceMetadata(this.context.metadata),
|
|
435
271
|
"pi.cwd": this.context.cwd,
|
|
436
272
|
...input.git?.branch ? { "pi.git.branch": input.git.branch } : {},
|
|
437
273
|
...input.git?.commit ? { "pi.git.commit": input.git.commit } : {},
|
|
@@ -439,6 +275,7 @@ var TraceRecorder = class {
|
|
|
439
275
|
"pi.mode": this.context.mode,
|
|
440
276
|
...input.model?.id ? { "pi.model": input.model.id } : {},
|
|
441
277
|
...input.model?.provider ? { "pi.provider": input.model.provider } : {},
|
|
278
|
+
...input.requestId ? { "pi.request.id": input.requestId } : {},
|
|
442
279
|
"pi.session.id": this.context.sessionId,
|
|
443
280
|
"pi.trace.schema_version": Number(TRACE_SCHEMA_VERSION),
|
|
444
281
|
...snapshotMetadata("start", input.snapshot)
|
|
@@ -447,14 +284,21 @@ var TraceRecorder = class {
|
|
|
447
284
|
const gitTag = input.git?.branch ? `branch:${input.git.branch}` : input.git?.detached ? "git:detached" : void 0;
|
|
448
285
|
this.root = this.startObservation("pi.agent", attributes, { asType: "agent" });
|
|
449
286
|
this.root.updateTrace?.({
|
|
450
|
-
name:
|
|
287
|
+
name: normalizeTraceName(this.context.traceName),
|
|
451
288
|
sessionId: this.context.sessionId,
|
|
452
289
|
...this.context.userId ? { userId: this.context.userId } : {},
|
|
453
290
|
version: TRACE_SCHEMA_VERSION,
|
|
454
291
|
input: traceInput,
|
|
455
292
|
metadata,
|
|
456
|
-
tags:
|
|
293
|
+
tags: traceTags(this.context.tags, gitTag)
|
|
457
294
|
});
|
|
295
|
+
const traceId = this.root.traceId;
|
|
296
|
+
if (traceId && this.context.onTraceId) {
|
|
297
|
+
try {
|
|
298
|
+
void Promise.resolve(this.context.onTraceId(traceId)).catch(() => void 0);
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
}
|
|
458
302
|
}
|
|
459
303
|
beginAttempt(input = {}) {
|
|
460
304
|
if (!this.root) return;
|
|
@@ -544,7 +388,11 @@ var TraceRecorder = class {
|
|
|
544
388
|
...Object.keys(requestMetadata).length > 0 ? { metadata: requestMetadata } : {},
|
|
545
389
|
version: TRACE_SCHEMA_VERSION
|
|
546
390
|
},
|
|
547
|
-
{
|
|
391
|
+
{
|
|
392
|
+
asType: "generation",
|
|
393
|
+
parent: this.turn ?? this.attempt ?? this.root,
|
|
394
|
+
...input.startedAt === void 0 ? {} : { startTime: new Date(input.startedAt) }
|
|
395
|
+
}
|
|
548
396
|
);
|
|
549
397
|
this.generation = {
|
|
550
398
|
observation,
|
|
@@ -746,12 +594,8 @@ var TraceRecorder = class {
|
|
|
746
594
|
interrupt(statusMessage, snapshot) {
|
|
747
595
|
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
748
596
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
752
|
-
async shutdown(snapshot) {
|
|
753
|
-
this.closeActiveTrace("Pi shut down before the active trace settled.", snapshot, "interrupted");
|
|
754
|
-
await this.backend.shutdown();
|
|
597
|
+
dispose(statusMessage = "Pi session ended before the active trace settled.", snapshot) {
|
|
598
|
+
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
755
599
|
}
|
|
756
600
|
closeActiveTrace(statusMessage, snapshot, forcedOutcome) {
|
|
757
601
|
this.closeTurn(statusMessage ?? "Pi turn ended when the agent run settled.");
|
|
@@ -874,6 +718,24 @@ var TraceRecorder = class {
|
|
|
874
718
|
return this.context.captureContent ? sanitizeTraceValue(value) : CONTENT_DISABLED;
|
|
875
719
|
}
|
|
876
720
|
};
|
|
721
|
+
function normalizeTraceName(value) {
|
|
722
|
+
return typeof value === "string" && value.trim() ? value.trim() : "pi.trace";
|
|
723
|
+
}
|
|
724
|
+
function traceTags(customTags, gitTag) {
|
|
725
|
+
const tags = ["pi", ...gitTag ? [gitTag] : [], ...customTags ?? []];
|
|
726
|
+
return [...new Set(tags.map(sanitizeTraceTag).filter((tag) => tag !== void 0))];
|
|
727
|
+
}
|
|
728
|
+
function sanitizeTraceTag(value) {
|
|
729
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
730
|
+
const sanitized = sanitizeTraceValue(value.trim());
|
|
731
|
+
return typeof sanitized === "string" && sanitized.trim() ? sanitized.trim() : void 0;
|
|
732
|
+
}
|
|
733
|
+
function customTraceMetadata(metadata) {
|
|
734
|
+
if (!metadata) return {};
|
|
735
|
+
const sanitized = sanitizeTraceValue(metadata);
|
|
736
|
+
if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) return {};
|
|
737
|
+
return Object.fromEntries(Object.entries(sanitized).filter(([key]) => !key.startsWith("pi.")));
|
|
738
|
+
}
|
|
877
739
|
function generationHttpMetadata(state) {
|
|
878
740
|
const finalStatus = state.statuses.at(-1);
|
|
879
741
|
return {
|
|
@@ -986,227 +848,258 @@ function emptyCounters() {
|
|
|
986
848
|
};
|
|
987
849
|
}
|
|
988
850
|
|
|
989
|
-
// src/
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
function createLangfuseExtension(dependencies = {}) {
|
|
995
|
-
const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
|
|
996
|
-
const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
|
|
997
|
-
const createBackend = dependencies.createBackend ?? (async (config) => {
|
|
998
|
-
const { createProductionBackend } = await import("./chunks/runtime-X75FRK2E.js");
|
|
999
|
-
return createProductionBackend(config);
|
|
851
|
+
// src/pi-session.ts
|
|
852
|
+
function createPiLangfuseSession(runtime, options = {}) {
|
|
853
|
+
const sessionOptions = snapshotSessionOptions(options);
|
|
854
|
+
return createPiLangfuseSessionController({
|
|
855
|
+
resolveSession: async () => ({ runtime, options: sessionOptions })
|
|
1000
856
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
857
|
+
}
|
|
858
|
+
function createPiLangfuseSessionController(options) {
|
|
859
|
+
let binding;
|
|
860
|
+
let runtimeForShutdown;
|
|
861
|
+
let ownerRegistration;
|
|
862
|
+
let pendingInitialization;
|
|
863
|
+
const outstandingInitializations = /* @__PURE__ */ new Set();
|
|
864
|
+
let disposePromise;
|
|
865
|
+
let disposed = false;
|
|
866
|
+
let sessionGeneration = 0;
|
|
867
|
+
let requestId;
|
|
868
|
+
let requestIdRevision = 0;
|
|
869
|
+
let nextAttemptReason;
|
|
870
|
+
let lastSnapshot;
|
|
871
|
+
let pendingProviderGeneration;
|
|
872
|
+
const controller = {
|
|
873
|
+
extension(pi) {
|
|
874
|
+
if (disposed) throw new Error("A disposed Pi Langfuse session controller cannot be bound.");
|
|
875
|
+
registerHooks(pi);
|
|
876
|
+
},
|
|
877
|
+
setRequestId(value) {
|
|
878
|
+
requestId = normalizeOptionalString(value);
|
|
879
|
+
requestIdRevision += 1;
|
|
880
|
+
},
|
|
881
|
+
dispose() {
|
|
882
|
+
if (disposePromise) return disposePromise;
|
|
883
|
+
disposed = true;
|
|
884
|
+
const initializations = invalidateAllInitializations("disposed");
|
|
885
|
+
ownerRegistration = void 0;
|
|
886
|
+
sessionGeneration += 1;
|
|
887
|
+
closeBinding(binding, "Pi Langfuse session controller was disposed.", lastSnapshot);
|
|
888
|
+
runtimeForShutdown = void 0;
|
|
889
|
+
disposePromise = joinInitializations(initializations);
|
|
890
|
+
return disposePromise;
|
|
891
|
+
},
|
|
892
|
+
get active() {
|
|
893
|
+
return binding !== void 0;
|
|
894
|
+
},
|
|
895
|
+
get runtime() {
|
|
896
|
+
return binding?.runtime;
|
|
897
|
+
},
|
|
898
|
+
async flush() {
|
|
899
|
+
const runtime = binding?.runtime;
|
|
900
|
+
if (!runtime) throw new Error("Langfuse tracing is not enabled for this session.");
|
|
901
|
+
await runtime.flush();
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
function closeBinding(target, statusMessage, snapshot) {
|
|
905
|
+
if (!target) return;
|
|
906
|
+
const wasCurrent = binding === target;
|
|
907
|
+
if (wasCurrent) {
|
|
908
|
+
binding = void 0;
|
|
909
|
+
pendingProviderGeneration = void 0;
|
|
910
|
+
}
|
|
911
|
+
target.releaseRuntime();
|
|
912
|
+
target.recorder.dispose(statusMessage, snapshot);
|
|
913
|
+
if (wasCurrent) nextAttemptReason = void 0;
|
|
914
|
+
}
|
|
915
|
+
function invalidatePendingInitialization(reason) {
|
|
916
|
+
if (!pendingInitialization) return;
|
|
917
|
+
pendingInitialization.current = false;
|
|
918
|
+
pendingInitialization.reason = reason;
|
|
919
|
+
pendingInitialization = void 0;
|
|
920
|
+
}
|
|
921
|
+
function invalidateAllInitializations(reason) {
|
|
922
|
+
const initializations = [...outstandingInitializations];
|
|
923
|
+
for (const initialization of initializations) {
|
|
924
|
+
initialization.current = false;
|
|
925
|
+
initialization.reason = reason;
|
|
926
|
+
}
|
|
927
|
+
pendingInitialization = void 0;
|
|
928
|
+
return initializations;
|
|
929
|
+
}
|
|
930
|
+
function createPendingInitialization() {
|
|
931
|
+
let complete;
|
|
932
|
+
const completion = new Promise((resolve) => {
|
|
933
|
+
complete = resolve;
|
|
934
|
+
});
|
|
935
|
+
const initialization = { current: true, completion, complete };
|
|
936
|
+
outstandingInitializations.add(initialization);
|
|
937
|
+
return initialization;
|
|
938
|
+
}
|
|
939
|
+
async function joinInitializations(initializations) {
|
|
940
|
+
const results = await Promise.all(initializations.map(({ completion }) => completion));
|
|
941
|
+
const errors = results.flatMap((result) => result.ok ? [] : [result.error]);
|
|
942
|
+
if (errors.length === 1) throw errors[0];
|
|
943
|
+
if (errors.length > 1) throw new AggregateError(errors, "Pi Langfuse session initialization cleanup failed.");
|
|
944
|
+
}
|
|
945
|
+
async function initializeSession(registration, initialization, ctx) {
|
|
946
|
+
const generation = ++sessionGeneration;
|
|
947
|
+
closeBinding(binding, "Pi session was replaced before shutdown completed.", lastSnapshot);
|
|
948
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
949
|
+
nextAttemptReason = void 0;
|
|
950
|
+
options.onSessionStart?.();
|
|
951
|
+
const isCurrent = () => !disposed && initialization.current && ownerRegistration === registration && generation === sessionGeneration;
|
|
952
|
+
let resolved;
|
|
953
|
+
try {
|
|
954
|
+
resolved = await options.resolveSession(ctx, isCurrent);
|
|
955
|
+
} catch (error) {
|
|
956
|
+
if (isCurrent()) options.onInitializationError?.(error, ctx);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
if (!isCurrent()) {
|
|
960
|
+
const reason = initialization.reason ?? "replaced";
|
|
961
|
+
if (resolved && options.shutdownRuntimeOnQuit && reason !== "quit" && reason !== "disposed" && (!runtimeForShutdown || runtimeForShutdown.closed)) {
|
|
962
|
+
runtimeForShutdown = resolved.runtime;
|
|
1017
963
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
{
|
|
1033
|
-
id: "configure",
|
|
1034
|
-
label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
1035
|
-
action: "configure"
|
|
1036
|
-
},
|
|
1037
|
-
{ id: "help", label: HELP_ACTION, action: "help" }
|
|
1038
|
-
],
|
|
1039
|
-
hint: "close"
|
|
1040
|
-
})
|
|
1041
|
-
},
|
|
1042
|
-
actions: {
|
|
1043
|
-
flush: async () => {
|
|
1044
|
-
const menuRecorder = recorder;
|
|
1045
|
-
const menuConfig = activeConfig;
|
|
1046
|
-
if (!menuRecorder) {
|
|
1047
|
-
ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
|
|
1048
|
-
return { kind: "close" };
|
|
1049
|
-
}
|
|
1050
|
-
try {
|
|
1051
|
-
await menuRecorder.flush();
|
|
1052
|
-
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1053
|
-
ctx.ui.notify("Langfuse traces flushed for this session.", "info");
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1056
|
-
ctx.ui.notify(`Langfuse flush failed: ${formatError2(error, menuConfig)}`, "error");
|
|
1057
|
-
}
|
|
1058
|
-
return { kind: "close" };
|
|
1059
|
-
},
|
|
1060
|
-
configure: async () => {
|
|
1061
|
-
const loaded = await loadConfig(configPath);
|
|
1062
|
-
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1063
|
-
configPath = loaded.path;
|
|
1064
|
-
const next = await promptForConfig(
|
|
1065
|
-
ctx,
|
|
1066
|
-
loaded.ok ? loaded.config : void 0,
|
|
1067
|
-
() => menuGeneration === sessionGeneration
|
|
1068
|
-
);
|
|
1069
|
-
if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1070
|
-
try {
|
|
1071
|
-
const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
|
|
1072
|
-
configWriteQueue = write.then(
|
|
1073
|
-
() => void 0,
|
|
1074
|
-
() => void 0
|
|
1075
|
-
);
|
|
1076
|
-
await write;
|
|
1077
|
-
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1078
|
-
hasStoredConfig = true;
|
|
1079
|
-
configurationNotice = "Saved; restart each Pi process to use it in subsequent sessions.";
|
|
1080
|
-
ctx.ui.notify(
|
|
1081
|
-
`Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
|
|
1082
|
-
"info"
|
|
1083
|
-
);
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1086
|
-
ctx.ui.notify(`Failed to save Langfuse config: ${formatError2(error, next)}`, "error");
|
|
1087
|
-
}
|
|
1088
|
-
return { kind: "close" };
|
|
1089
|
-
},
|
|
1090
|
-
help: async () => {
|
|
1091
|
-
ctx.ui.notify(formatHelp(configPath), "info");
|
|
1092
|
-
return { kind: "close" };
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
});
|
|
1096
|
-
await runMenu(ctx, menu, {
|
|
1097
|
-
getState: () => void 0,
|
|
1098
|
-
signal: menuSignal,
|
|
1099
|
-
isCurrent
|
|
964
|
+
await resolved?.releaseIfStale?.(reason);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!resolved) {
|
|
968
|
+
options.onSessionUnavailable?.();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
try {
|
|
972
|
+
const internal = getLangfuseRuntimeInternal(resolved.runtime);
|
|
973
|
+
const recorderOptions = createRecorderOptions(ctx, resolved.options);
|
|
974
|
+
const recorder = new TraceRecorder(internal.backend, recorderOptions);
|
|
975
|
+
const nextBinding = {};
|
|
976
|
+
const releaseRuntime = internal.registerSession((statusMessage) => {
|
|
977
|
+
closeBinding(nextBinding, statusMessage, lastSnapshot);
|
|
1100
978
|
});
|
|
979
|
+
Object.assign(nextBinding, { registration, recorder, runtime: resolved.runtime, releaseRuntime });
|
|
980
|
+
if (!isCurrent()) {
|
|
981
|
+
releaseRuntime();
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
binding = nextBinding;
|
|
985
|
+
if (options.shutdownRuntimeOnQuit) runtimeForShutdown = resolved.runtime;
|
|
986
|
+
options.onSessionReady?.(resolved);
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (isCurrent()) options.onInitializationError?.(error, ctx);
|
|
1101
989
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
990
|
+
}
|
|
991
|
+
function registerHooks(pi) {
|
|
992
|
+
const registration = {};
|
|
993
|
+
const activeRecorder = () => activeRecorderFor(registration);
|
|
1106
994
|
pi.on("session_start", async (_event, ctx) => {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
hasStoredConfig = false;
|
|
1115
|
-
configurationNotice = void 0;
|
|
1116
|
-
nextAttemptReason = void 0;
|
|
1117
|
-
await configWriteQueue;
|
|
1118
|
-
if (generation !== sessionGeneration) return;
|
|
1119
|
-
const result = await loadConfig();
|
|
1120
|
-
if (generation !== sessionGeneration) return;
|
|
1121
|
-
configPath = result.path;
|
|
1122
|
-
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
1123
|
-
if (!result.ok) {
|
|
1124
|
-
initializationError = formatConfigError(result);
|
|
1125
|
-
ctx.ui.notify(initializationError, "warning");
|
|
995
|
+
if (disposed) return;
|
|
996
|
+
pendingProviderGeneration = void 0;
|
|
997
|
+
if (ownerRegistration && ownerRegistration !== registration) {
|
|
998
|
+
options.onInitializationError?.(
|
|
999
|
+
new Error("A Pi Langfuse session controller cannot manage multiple active Pi sessions."),
|
|
1000
|
+
ctx
|
|
1001
|
+
);
|
|
1126
1002
|
return;
|
|
1127
1003
|
}
|
|
1128
|
-
|
|
1004
|
+
invalidatePendingInitialization("replaced");
|
|
1005
|
+
ownerRegistration = registration;
|
|
1006
|
+
const initialization = createPendingInitialization();
|
|
1007
|
+
pendingInitialization = initialization;
|
|
1008
|
+
let result = { ok: true };
|
|
1129
1009
|
try {
|
|
1130
|
-
|
|
1131
|
-
if (generation !== sessionGeneration) {
|
|
1132
|
-
await backend.shutdown().catch(() => void 0);
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
activeConfig = result.config;
|
|
1136
|
-
recorder = new TraceRecorder(backend, {
|
|
1137
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
1138
|
-
...result.config.userId ? { userId: result.config.userId } : {},
|
|
1139
|
-
cwd: ctx.cwd,
|
|
1140
|
-
mode: ctx.mode,
|
|
1141
|
-
captureContent: result.config.captureContent
|
|
1142
|
-
});
|
|
1010
|
+
await initializeSession(registration, initialization, ctx);
|
|
1143
1011
|
} catch (error) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1012
|
+
result = { ok: false, error };
|
|
1013
|
+
throw error;
|
|
1014
|
+
} finally {
|
|
1015
|
+
initialization.complete(result);
|
|
1016
|
+
outstandingInitializations.delete(initialization);
|
|
1017
|
+
if (pendingInitialization === initialization) pendingInitialization = void 0;
|
|
1147
1018
|
}
|
|
1148
1019
|
});
|
|
1149
1020
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1021
|
+
pendingProviderGeneration = void 0;
|
|
1022
|
+
const active = ownerRegistration === registration ? binding : void 0;
|
|
1023
|
+
if (!active || active.runtime.closed) return;
|
|
1150
1024
|
nextAttemptReason = void 0;
|
|
1151
|
-
const
|
|
1152
|
-
if (
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1025
|
+
const git = await (options.resolveGitMetadata ? options.resolveGitMetadata(pi, ctx.cwd) : resolveGitMetadata((command, args, execOptions) => pi.exec(command, args, execOptions), ctx.cwd)).catch(() => void 0);
|
|
1026
|
+
if (ownerRegistration !== registration || binding !== active || active.runtime.closed) return;
|
|
1027
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1028
|
+
startRootTrace((nextRequestId) => {
|
|
1029
|
+
active.recorder.beginAgent({
|
|
1030
|
+
prompt: event.prompt,
|
|
1031
|
+
images: event.images,
|
|
1032
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
|
|
1033
|
+
git,
|
|
1034
|
+
snapshot: lastSnapshot,
|
|
1035
|
+
...nextRequestId ? { requestId: nextRequestId } : {}
|
|
1036
|
+
});
|
|
1161
1037
|
});
|
|
1162
1038
|
});
|
|
1163
1039
|
pi.on("agent_start", () => {
|
|
1040
|
+
const recorder = activeRecorder();
|
|
1164
1041
|
if (!recorder) return;
|
|
1165
1042
|
recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : void 0);
|
|
1166
1043
|
nextAttemptReason = void 0;
|
|
1167
1044
|
});
|
|
1168
1045
|
pi.on("turn_start", (event, ctx) => {
|
|
1046
|
+
const recorder = activeRecorder();
|
|
1169
1047
|
if (!recorder) return;
|
|
1170
|
-
|
|
1048
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1049
|
+
ensureActiveRun(recorder, ctx, startRootTrace);
|
|
1171
1050
|
recorder.beginTurn(event.turnIndex);
|
|
1172
1051
|
});
|
|
1173
1052
|
pi.on("before_provider_request", (event, ctx) => {
|
|
1174
|
-
if (!
|
|
1175
|
-
|
|
1176
|
-
|
|
1053
|
+
if (!activeRecorder()) return;
|
|
1054
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1055
|
+
pendingProviderGeneration = {
|
|
1056
|
+
startedAt: Date.now(),
|
|
1177
1057
|
payload: event.payload,
|
|
1178
|
-
payloadStage: "before_provider_request",
|
|
1179
1058
|
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
|
|
1180
|
-
thinkingLevel: pi.getThinkingLevel()
|
|
1181
|
-
|
|
1059
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1060
|
+
responses: []
|
|
1061
|
+
};
|
|
1182
1062
|
});
|
|
1183
1063
|
pi.on("after_provider_response", (event) => {
|
|
1184
|
-
|
|
1064
|
+
pendingProviderGeneration?.responses.push({ status: event.status, headers: event.headers });
|
|
1065
|
+
});
|
|
1066
|
+
pi.on("message_start", (event, ctx) => {
|
|
1067
|
+
if (event.message.role === "assistant") claimPendingProviderGeneration(ctx, activeRecorder());
|
|
1185
1068
|
});
|
|
1186
|
-
pi.on("message_update", (event) => {
|
|
1187
|
-
if (isRealOutputDelta(event.assistantMessageEvent))
|
|
1069
|
+
pi.on("message_update", (event, ctx) => {
|
|
1070
|
+
if (!isRealOutputDelta(event.assistantMessageEvent)) return;
|
|
1071
|
+
const recorder = activeRecorder();
|
|
1072
|
+
claimPendingProviderGeneration(ctx, recorder);
|
|
1073
|
+
recorder?.markGenerationFirstOutput();
|
|
1188
1074
|
});
|
|
1189
|
-
pi.on("message_end", (event) => {
|
|
1190
|
-
if (event.message.role
|
|
1075
|
+
pi.on("message_end", (event, ctx) => {
|
|
1076
|
+
if (event.message.role !== "assistant") return;
|
|
1077
|
+
const recorder = activeRecorder();
|
|
1078
|
+
claimPendingProviderGeneration(ctx, recorder);
|
|
1079
|
+
recorder?.markGenerationEnd();
|
|
1191
1080
|
});
|
|
1192
|
-
pi.on("turn_end", (event) => {
|
|
1193
|
-
|
|
1194
|
-
recorder
|
|
1081
|
+
pi.on("turn_end", (event, ctx) => {
|
|
1082
|
+
const recorder = activeRecorder();
|
|
1083
|
+
if (!recorder) return;
|
|
1084
|
+
if (event.message.role === "assistant") claimPendingProviderGeneration(ctx, recorder);
|
|
1085
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1086
|
+
if (event.message.role === "assistant") recorder.finishAssistant(event.message);
|
|
1087
|
+
recorder.finishTurn(event.turnIndex, {
|
|
1195
1088
|
message: event.message,
|
|
1196
1089
|
toolResultCount: event.toolResults.length
|
|
1197
1090
|
});
|
|
1198
1091
|
});
|
|
1199
1092
|
pi.on("tool_execution_start", (event) => {
|
|
1200
|
-
|
|
1093
|
+
activeRecorder()?.beginTool(event.toolCallId, event.toolName, event.args);
|
|
1201
1094
|
});
|
|
1202
1095
|
pi.on("tool_execution_update", (event) => {
|
|
1203
|
-
|
|
1096
|
+
activeRecorder()?.recordToolProgress(event.toolCallId);
|
|
1204
1097
|
});
|
|
1205
1098
|
pi.on("tool_result", (event) => {
|
|
1206
|
-
|
|
1099
|
+
activeRecorder()?.recordToolInput(event.toolCallId, event.input);
|
|
1207
1100
|
});
|
|
1208
1101
|
pi.on("tool_execution_end", (event) => {
|
|
1209
|
-
|
|
1102
|
+
activeRecorder()?.finishTool(event.toolCallId, {
|
|
1210
1103
|
content: event.result.content,
|
|
1211
1104
|
details: event.result.details,
|
|
1212
1105
|
isError: event.isError
|
|
@@ -1214,10 +1107,10 @@ function createLangfuseExtension(dependencies = {}) {
|
|
|
1214
1107
|
});
|
|
1215
1108
|
pi.on("agent_end", (event) => {
|
|
1216
1109
|
const message = findLastAssistant(event.messages);
|
|
1217
|
-
|
|
1110
|
+
activeRecorder()?.finishAttempt(message);
|
|
1218
1111
|
});
|
|
1219
1112
|
pi.on("session_before_compact", (event) => {
|
|
1220
|
-
|
|
1113
|
+
activeRecorder()?.beginCompaction({
|
|
1221
1114
|
reason: event.reason,
|
|
1222
1115
|
willRetry: event.willRetry,
|
|
1223
1116
|
tokensBefore: event.preparation.tokensBefore,
|
|
@@ -1228,6 +1121,7 @@ function createLangfuseExtension(dependencies = {}) {
|
|
|
1228
1121
|
});
|
|
1229
1122
|
});
|
|
1230
1123
|
pi.on("session_compact", (event) => {
|
|
1124
|
+
const recorder = activeRecorder();
|
|
1231
1125
|
const entry = event.compactionEntry;
|
|
1232
1126
|
recorder?.finishCompaction({
|
|
1233
1127
|
reason: event.reason,
|
|
@@ -1240,39 +1134,102 @@ function createLangfuseExtension(dependencies = {}) {
|
|
|
1240
1134
|
if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
|
|
1241
1135
|
});
|
|
1242
1136
|
pi.on("agent_settled", (_event, ctx) => {
|
|
1243
|
-
recorder
|
|
1137
|
+
const recorder = activeRecorder();
|
|
1138
|
+
if (!recorder) return;
|
|
1139
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1140
|
+
recorder.settle(lastSnapshot);
|
|
1141
|
+
pendingProviderGeneration = void 0;
|
|
1244
1142
|
nextAttemptReason = void 0;
|
|
1245
1143
|
});
|
|
1246
1144
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
1145
|
+
if (ownerRegistration !== registration) return;
|
|
1146
|
+
const initializations = invalidateAllInitializations(event.reason);
|
|
1147
|
+
ownerRegistration = void 0;
|
|
1247
1148
|
const generation = ++sessionGeneration;
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1149
|
+
options.onSessionShutdown?.();
|
|
1150
|
+
const active = binding?.registration === registration ? binding : void 0;
|
|
1151
|
+
const runtime = active?.runtime ?? runtimeForShutdown;
|
|
1152
|
+
lastSnapshot = active ? contextSnapshot(ctx) : lastSnapshot;
|
|
1153
|
+
closeBinding(
|
|
1154
|
+
active,
|
|
1155
|
+
event.reason === "quit" ? "Pi shut down before the active trace settled." : `Pi session ended before settlement (${event.reason}).`,
|
|
1156
|
+
lastSnapshot
|
|
1157
|
+
);
|
|
1158
|
+
let beforeDisposeFailure;
|
|
1159
|
+
try {
|
|
1160
|
+
await options.beforeSessionDispose?.();
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
beforeDisposeFailure = { error };
|
|
1163
|
+
}
|
|
1257
1164
|
try {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1165
|
+
await joinInitializations(initializations);
|
|
1166
|
+
if (disposed || generation !== sessionGeneration) return;
|
|
1167
|
+
if (beforeDisposeFailure) throw beforeDisposeFailure.error;
|
|
1168
|
+
if (runtime) {
|
|
1169
|
+
if (event.reason === "quit" && options.shutdownRuntimeOnQuit) await runtime.shutdown();
|
|
1170
|
+
else if (options.flushOnReplacement) await runtime.flush();
|
|
1262
1171
|
}
|
|
1263
1172
|
} catch (error) {
|
|
1264
|
-
if (generation
|
|
1265
|
-
ctx.ui.notify(`Langfuse shutdown export failed: ${formatError2(error, shutdownConfig)}`, "error");
|
|
1173
|
+
if (!disposed && generation === sessionGeneration) options.onShutdownError?.(error, ctx);
|
|
1266
1174
|
}
|
|
1267
1175
|
});
|
|
1176
|
+
}
|
|
1177
|
+
function claimPendingProviderGeneration(ctx, recorder) {
|
|
1178
|
+
const pending = pendingProviderGeneration;
|
|
1179
|
+
if (!pending || !recorder) return;
|
|
1180
|
+
pendingProviderGeneration = void 0;
|
|
1181
|
+
ensureActiveRun(recorder, ctx, startRootTrace);
|
|
1182
|
+
recorder.beginGeneration({
|
|
1183
|
+
startedAt: pending.startedAt,
|
|
1184
|
+
payload: pending.payload,
|
|
1185
|
+
payloadStage: "before_provider_request",
|
|
1186
|
+
model: pending.model,
|
|
1187
|
+
thinkingLevel: pending.thinkingLevel
|
|
1188
|
+
});
|
|
1189
|
+
for (const response of pending.responses) recorder.recordProviderResponse(response.status, response.headers);
|
|
1190
|
+
}
|
|
1191
|
+
function startRootTrace(start) {
|
|
1192
|
+
const revision = requestIdRevision;
|
|
1193
|
+
const nextRequestId = requestId;
|
|
1194
|
+
start(nextRequestId);
|
|
1195
|
+
if (requestIdRevision === revision) requestId = void 0;
|
|
1196
|
+
}
|
|
1197
|
+
function activeRecorderFor(registration) {
|
|
1198
|
+
return ownerRegistration === registration && binding?.registration === registration && !binding.runtime.closed ? binding.recorder : void 0;
|
|
1199
|
+
}
|
|
1200
|
+
return controller;
|
|
1201
|
+
}
|
|
1202
|
+
function createRecorderOptions(ctx, options = {}) {
|
|
1203
|
+
const userId = normalizeUserId(options.userId);
|
|
1204
|
+
return {
|
|
1205
|
+
sessionId: normalizeOptionalString(options.sessionId) ?? ctx.sessionManager.getSessionId(),
|
|
1206
|
+
...userId ? { userId } : {},
|
|
1207
|
+
cwd: ctx.cwd,
|
|
1208
|
+
mode: ctx.mode,
|
|
1209
|
+
captureContent: options.captureContent ?? true,
|
|
1210
|
+
...normalizeOptionalString(options.traceName) ? { traceName: normalizeOptionalString(options.traceName) } : {},
|
|
1211
|
+
...options.tags ? { tags: [...options.tags] } : {},
|
|
1212
|
+
...options.metadata ? { metadata: { ...options.metadata } } : {},
|
|
1213
|
+
...options.onTraceId ? { onTraceId: options.onTraceId } : {}
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function snapshotSessionOptions(options) {
|
|
1217
|
+
normalizeUserId(options.userId);
|
|
1218
|
+
return {
|
|
1219
|
+
...options,
|
|
1220
|
+
...options.tags ? { tags: [...options.tags] } : {},
|
|
1221
|
+
...options.metadata ? { metadata: { ...options.metadata } } : {}
|
|
1268
1222
|
};
|
|
1269
1223
|
}
|
|
1270
|
-
function ensureActiveRun(recorder, ctx) {
|
|
1224
|
+
function ensureActiveRun(recorder, ctx, startRootTrace) {
|
|
1271
1225
|
if (!recorder.hasActiveTrace()) {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1226
|
+
startRootTrace((nextRequestId) => {
|
|
1227
|
+
recorder.beginAgent({
|
|
1228
|
+
prompt: "[automatic continuation]",
|
|
1229
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
|
|
1230
|
+
snapshot: contextSnapshot(ctx),
|
|
1231
|
+
...nextRequestId ? { requestId: nextRequestId } : {}
|
|
1232
|
+
});
|
|
1276
1233
|
});
|
|
1277
1234
|
}
|
|
1278
1235
|
if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
|
|
@@ -1293,56 +1250,200 @@ function findLastAssistant(messages) {
|
|
|
1293
1250
|
function isRealOutputDelta(event) {
|
|
1294
1251
|
return (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") && typeof event.delta === "string" && event.delta.length > 0;
|
|
1295
1252
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
if (!branchResult.resolved) return void 0;
|
|
1301
|
-
if (branchResult.branch) {
|
|
1302
|
-
return {
|
|
1303
|
-
branch: branchResult.branch,
|
|
1304
|
-
...commit ? { commit } : {},
|
|
1305
|
-
detached: false
|
|
1306
|
-
};
|
|
1307
|
-
}
|
|
1308
|
-
return commit ? { commit, detached: true } : void 0;
|
|
1253
|
+
function normalizeUserId(value) {
|
|
1254
|
+
const userId = normalizeOptionalString(value);
|
|
1255
|
+
if (userId && userId.length > 200) throw new Error("Langfuse userId must be at most 200 characters.");
|
|
1256
|
+
return userId;
|
|
1309
1257
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
timeout: GIT_LOOKUP_TIMEOUT_MS
|
|
1315
|
-
});
|
|
1316
|
-
if (result.killed) return { resolved: false };
|
|
1317
|
-
if (result.code === 1) return { resolved: true };
|
|
1318
|
-
if (result.code !== 0) return { resolved: false };
|
|
1319
|
-
const branch = normalizeGitBranch(result.stdout);
|
|
1320
|
-
return branch ? { resolved: true, branch } : { resolved: false };
|
|
1321
|
-
} catch {
|
|
1322
|
-
return { resolved: false };
|
|
1323
|
-
}
|
|
1258
|
+
function normalizeOptionalString(value) {
|
|
1259
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
1260
|
+
const sanitized = sanitizeTraceValue(value.trim());
|
|
1261
|
+
return typeof sanitized === "string" && sanitized.trim() ? sanitized.trim() : void 0;
|
|
1324
1262
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1263
|
+
|
|
1264
|
+
// src/langfuse.ts
|
|
1265
|
+
var FLUSH_ACTION = "Flush completed traces for this session";
|
|
1266
|
+
var SET_UP_ACTION = "Set up Langfuse for this Pi agent directory (restart required)";
|
|
1267
|
+
var UPDATE_ACTION = "Update Langfuse for this Pi agent directory (restart required)";
|
|
1268
|
+
var HELP_ACTION = "Show setup and privacy help";
|
|
1269
|
+
function createLangfuseExtension(dependencies = {}) {
|
|
1270
|
+
const loadConfig = dependencies.loadConfig ?? loadLangfuseConfig;
|
|
1271
|
+
const writeConfig = dependencies.writeConfig ?? writeLangfuseConfig;
|
|
1272
|
+
const resolveInjectedGitMetadata = dependencies.resolveGitMetadata;
|
|
1273
|
+
return function langfuse(pi) {
|
|
1274
|
+
let activeConfig;
|
|
1275
|
+
let loadingConfig;
|
|
1276
|
+
let runtimeConfigForShutdown;
|
|
1277
|
+
let shutdownConfig;
|
|
1278
|
+
let configPath;
|
|
1279
|
+
let initializationError;
|
|
1280
|
+
let hasStoredConfig = false;
|
|
1281
|
+
let configurationNotice;
|
|
1282
|
+
let sessionGeneration = 0;
|
|
1283
|
+
let menuController = new AbortController();
|
|
1284
|
+
let configWriteQueue = Promise.resolve();
|
|
1285
|
+
const tracing = createPiLangfuseSessionController({
|
|
1286
|
+
onSessionStart() {
|
|
1287
|
+
sessionGeneration += 1;
|
|
1288
|
+
menuController.abort(new DOMException("Langfuse session replaced", "AbortError"));
|
|
1289
|
+
menuController = new AbortController();
|
|
1290
|
+
activeConfig = void 0;
|
|
1291
|
+
loadingConfig = void 0;
|
|
1292
|
+
shutdownConfig = void 0;
|
|
1293
|
+
configPath = void 0;
|
|
1294
|
+
initializationError = void 0;
|
|
1295
|
+
hasStoredConfig = false;
|
|
1296
|
+
configurationNotice = void 0;
|
|
1297
|
+
},
|
|
1298
|
+
onSessionShutdown() {
|
|
1299
|
+
sessionGeneration += 1;
|
|
1300
|
+
menuController.abort(new DOMException("Langfuse session shut down", "AbortError"));
|
|
1301
|
+
shutdownConfig = activeConfig ?? runtimeConfigForShutdown ?? loadingConfig;
|
|
1302
|
+
activeConfig = void 0;
|
|
1303
|
+
},
|
|
1304
|
+
async resolveSession(ctx, isCurrent) {
|
|
1305
|
+
await configWriteQueue;
|
|
1306
|
+
if (!isCurrent()) return void 0;
|
|
1307
|
+
const result = await loadConfig();
|
|
1308
|
+
if (!isCurrent()) return void 0;
|
|
1309
|
+
configPath = result.path;
|
|
1310
|
+
for (const warning of result.warnings) ctx.ui.notify(warning, "warning");
|
|
1311
|
+
if (!result.ok) {
|
|
1312
|
+
initializationError = formatConfigError(result);
|
|
1313
|
+
ctx.ui.notify(initializationError, "warning");
|
|
1314
|
+
return void 0;
|
|
1315
|
+
}
|
|
1316
|
+
hasStoredConfig = true;
|
|
1317
|
+
loadingConfig = result.config;
|
|
1318
|
+
const createBackend = dependencies.createBackend;
|
|
1319
|
+
const ownsRuntime = createBackend !== void 0;
|
|
1320
|
+
const runtime = createBackend ? createLangfuseRuntimeFromBackend(await createBackend(result.config)) : await (await import("./chunks/runtime-6MIKLWNJ.js")).createLangfuseRuntime({ config: result.config, env: false });
|
|
1321
|
+
if (isCurrent() || !runtimeConfigForShutdown) runtimeConfigForShutdown = result.config;
|
|
1322
|
+
if (isCurrent()) {
|
|
1323
|
+
activeConfig = result.config;
|
|
1324
|
+
loadingConfig = void 0;
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
runtime,
|
|
1328
|
+
releaseIfStale: async (reason) => {
|
|
1329
|
+
if (ownsRuntime || reason === "quit") await runtime.shutdown();
|
|
1330
|
+
},
|
|
1331
|
+
options: {
|
|
1332
|
+
...result.config.userId ? { userId: result.config.userId } : {},
|
|
1333
|
+
captureContent: result.config.captureContent
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
},
|
|
1337
|
+
...resolveInjectedGitMetadata ? { resolveGitMetadata: (_pi, cwd) => resolveInjectedGitMetadata(cwd) } : {},
|
|
1338
|
+
onInitializationError(error, ctx) {
|
|
1339
|
+
const config = loadingConfig;
|
|
1340
|
+
loadingConfig = void 0;
|
|
1341
|
+
initializationError = `Langfuse tracing could not start: ${formatError(error, config)}`;
|
|
1342
|
+
ctx.ui.notify(initializationError, "warning");
|
|
1343
|
+
},
|
|
1344
|
+
beforeSessionDispose: async () => {
|
|
1345
|
+
await configWriteQueue;
|
|
1346
|
+
},
|
|
1347
|
+
onShutdownError(error, ctx) {
|
|
1348
|
+
ctx.ui.notify(`Langfuse shutdown export failed: ${formatError(error, shutdownConfig)}`, "error");
|
|
1349
|
+
},
|
|
1350
|
+
flushOnReplacement: true,
|
|
1351
|
+
shutdownRuntimeOnQuit: true
|
|
1330
1352
|
});
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1353
|
+
async function showLangfuseMenu(ctx) {
|
|
1354
|
+
if (!ctx.hasUI) {
|
|
1355
|
+
ctx.ui.notify(formatNonInteractiveStatus(activeConfig, configPath, initializationError), "warning");
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const menuGeneration = sessionGeneration;
|
|
1359
|
+
const menuSignal = menuController.signal;
|
|
1360
|
+
const isCurrent = () => menuGeneration === sessionGeneration && !menuSignal.aborted;
|
|
1361
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
1362
|
+
if (!isCurrent()) return;
|
|
1363
|
+
const menu = defineMenu({
|
|
1364
|
+
start: "main",
|
|
1365
|
+
screens: {
|
|
1366
|
+
main: () => ({
|
|
1367
|
+
kind: "actions",
|
|
1368
|
+
title: "Langfuse",
|
|
1369
|
+
lines: formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice).split("\n").slice(1),
|
|
1370
|
+
items: [
|
|
1371
|
+
...tracing.active ? [{ id: "flush", label: FLUSH_ACTION, action: "flush" }] : [],
|
|
1372
|
+
{
|
|
1373
|
+
id: "configure",
|
|
1374
|
+
label: hasStoredConfig ? UPDATE_ACTION : SET_UP_ACTION,
|
|
1375
|
+
action: "configure"
|
|
1376
|
+
},
|
|
1377
|
+
{ id: "help", label: HELP_ACTION, action: "help" }
|
|
1378
|
+
],
|
|
1379
|
+
hint: "close"
|
|
1380
|
+
})
|
|
1381
|
+
},
|
|
1382
|
+
actions: {
|
|
1383
|
+
flush: async () => {
|
|
1384
|
+
const menuConfig = activeConfig;
|
|
1385
|
+
if (!tracing.active) {
|
|
1386
|
+
ctx.ui.notify("Langfuse tracing is not enabled for this session.", "warning");
|
|
1387
|
+
return { kind: "close" };
|
|
1388
|
+
}
|
|
1389
|
+
try {
|
|
1390
|
+
await tracing.flush();
|
|
1391
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1392
|
+
ctx.ui.notify("Langfuse traces flushed for this session.", "info");
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1395
|
+
ctx.ui.notify(`Langfuse flush failed: ${formatError(error, menuConfig)}`, "error");
|
|
1396
|
+
}
|
|
1397
|
+
return { kind: "close" };
|
|
1398
|
+
},
|
|
1399
|
+
configure: async () => {
|
|
1400
|
+
const loaded = await loadConfig(configPath);
|
|
1401
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1402
|
+
configPath = loaded.path;
|
|
1403
|
+
const next = await promptForConfig(
|
|
1404
|
+
ctx,
|
|
1405
|
+
loaded.ok ? loaded.config : void 0,
|
|
1406
|
+
() => menuGeneration === sessionGeneration
|
|
1407
|
+
);
|
|
1408
|
+
if (!next || menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1409
|
+
try {
|
|
1410
|
+
const write = configWriteQueue.then(() => writeConfig(next, loaded.path));
|
|
1411
|
+
configWriteQueue = write.then(
|
|
1412
|
+
() => void 0,
|
|
1413
|
+
() => void 0
|
|
1414
|
+
);
|
|
1415
|
+
await write;
|
|
1416
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1417
|
+
hasStoredConfig = true;
|
|
1418
|
+
configurationNotice = "Saved; restart each Pi process to use it in subsequent sessions.";
|
|
1419
|
+
ctx.ui.notify(
|
|
1420
|
+
`Saved Langfuse config to ${loaded.path} for this Pi agent directory. Restart each Pi process to apply it to subsequent sessions.`,
|
|
1421
|
+
"info"
|
|
1422
|
+
);
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
if (menuGeneration !== sessionGeneration) return { kind: "close" };
|
|
1425
|
+
ctx.ui.notify(`Failed to save Langfuse config: ${formatError(error, next)}`, "error");
|
|
1426
|
+
}
|
|
1427
|
+
return { kind: "close" };
|
|
1428
|
+
},
|
|
1429
|
+
help: async () => {
|
|
1430
|
+
ctx.ui.notify(formatHelp(configPath), "info");
|
|
1431
|
+
return { kind: "close" };
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1435
|
+
await runMenu(ctx, menu, {
|
|
1436
|
+
getState: () => void 0,
|
|
1437
|
+
signal: menuSignal,
|
|
1438
|
+
isCurrent
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
pi.registerCommand("langfuse", {
|
|
1442
|
+
description: "Open interactive Langfuse tracing controls",
|
|
1443
|
+
handler: async (_args, ctx) => showLangfuseMenu(ctx)
|
|
1444
|
+
});
|
|
1445
|
+
tracing.extension(pi);
|
|
1446
|
+
};
|
|
1346
1447
|
}
|
|
1347
1448
|
async function promptForConfig(ctx, current, isCurrent) {
|
|
1348
1449
|
const secretKey = await ctx.ui.input("Langfuse secret key (leave blank to keep existing):");
|
|
@@ -1427,7 +1528,7 @@ function formatConfigError(result) {
|
|
|
1427
1528
|
const setupHint = result.reason.startsWith("Configuration file not found:") ? " Run /langfuse and choose Set up Langfuse." : "";
|
|
1428
1529
|
return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
|
|
1429
1530
|
}
|
|
1430
|
-
function
|
|
1531
|
+
function formatError(error, config) {
|
|
1431
1532
|
let message = error instanceof Error ? error.message : String(error);
|
|
1432
1533
|
const secrets = [config?.publicKey, config?.secretKey].filter((secret) => Boolean(secret)).sort((left, right) => right.length - left.length);
|
|
1433
1534
|
for (const secret of secrets) {
|
|
@@ -1436,7 +1537,15 @@ function formatError2(error, config) {
|
|
|
1436
1537
|
return message;
|
|
1437
1538
|
}
|
|
1438
1539
|
var langfuse_default = createLangfuseExtension();
|
|
1540
|
+
|
|
1541
|
+
// src/public-api.ts
|
|
1542
|
+
async function createLangfuseRuntime(options = {}) {
|
|
1543
|
+
const runtime = await import("./chunks/runtime-6MIKLWNJ.js");
|
|
1544
|
+
return runtime.createLangfuseRuntime(options);
|
|
1545
|
+
}
|
|
1439
1546
|
export {
|
|
1547
|
+
createLangfuseRuntime,
|
|
1548
|
+
createPiLangfuseSession,
|
|
1440
1549
|
langfuse_default as default
|
|
1441
1550
|
};
|
|
1442
1551
|
//# sourceMappingURL=index.ts.map
|