@narumitw/pi-langfuse 0.50.5 → 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 +125 -62
- 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 +594 -521
- 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 +63 -59
- package/src/config.ts +211 -232
- package/src/git.ts +60 -0
- package/src/index.ts +9 -0
- package/src/langfuse.ts +307 -563
- 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 +393 -227
- package/src/sanitizer.ts +115 -117
- package/src/tracing.ts +828 -843
- package/dist/chunks/runtime-247EMSG3.js +0 -218
- package/dist/chunks/runtime-247EMSG3.js.map +0 -7
package/dist/index.js
ADDED
|
@@ -0,0 +1,1551 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
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";
|
|
11
|
+
|
|
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) {
|
|
19
|
+
return {
|
|
20
|
+
branch: branchResult.branch,
|
|
21
|
+
...commit ? { commit } : {},
|
|
22
|
+
detached: false
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return commit ? { commit, detached: true } : void 0;
|
|
26
|
+
}
|
|
27
|
+
async function resolveGitBranch(exec, cwd) {
|
|
28
|
+
try {
|
|
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 };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function resolveGitCommit(exec, cwd) {
|
|
43
|
+
try {
|
|
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;
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
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;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/sanitizer.ts
|
|
66
|
+
var MAX_CAPTURE_BYTES = 64 * 1024;
|
|
67
|
+
var MAX_STRING_LENGTH = 5e4;
|
|
68
|
+
var MAX_COLLECTION_LENGTH = 200;
|
|
69
|
+
var MAX_DEPTH = 12;
|
|
70
|
+
var TRUNCATED = "[truncated: content budget exceeded]";
|
|
71
|
+
var BASE64_DATA_URI = /data:[^,\s"'`]*;base64,[^\s"'`]*/gi;
|
|
72
|
+
var BASE64_DATA_URI_OMITTED = "[base64 data URI omitted]";
|
|
73
|
+
var OPAQUE_SIGNATURE_KEYS = /* @__PURE__ */ new Set(["thinkingSignature", "textSignature", "thoughtSignature"]);
|
|
74
|
+
function sanitizeTraceValue(value) {
|
|
75
|
+
const budget = { remaining: MAX_CAPTURE_BYTES };
|
|
76
|
+
const sanitized = sanitize(value, /* @__PURE__ */ new WeakSet(), 0, budget);
|
|
77
|
+
return serializedBytes(sanitized) <= MAX_CAPTURE_BYTES ? sanitized : TRUNCATED;
|
|
78
|
+
}
|
|
79
|
+
function sanitize(value, active, depth, budget) {
|
|
80
|
+
if (budget.remaining <= byteLength(TRUNCATED)) return TRUNCATED;
|
|
81
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
82
|
+
return consume(value, budget);
|
|
83
|
+
}
|
|
84
|
+
if (typeof value === "string") {
|
|
85
|
+
return consume(sanitizeString(value, Math.min(MAX_STRING_LENGTH, budget.remaining)), budget);
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === "bigint") return consume(value.toString(), budget);
|
|
88
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
|
|
89
|
+
return void 0;
|
|
90
|
+
}
|
|
91
|
+
if (value instanceof Date) {
|
|
92
|
+
return consume(Number.isNaN(value.getTime()) ? "[invalid date]" : value.toISOString(), budget);
|
|
93
|
+
}
|
|
94
|
+
if (value instanceof Error) {
|
|
95
|
+
return sanitize({ name: value.name, message: value.message }, active, depth, budget);
|
|
96
|
+
}
|
|
97
|
+
if (depth >= MAX_DEPTH) return consume("[maximum depth reached]", budget);
|
|
98
|
+
if (active.has(value)) return consume("[circular]", budget);
|
|
99
|
+
active.add(value);
|
|
100
|
+
let result;
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
const items = [];
|
|
103
|
+
for (const item of value.slice(0, MAX_COLLECTION_LENGTH)) {
|
|
104
|
+
if (budget.remaining <= byteLength(TRUNCATED)) break;
|
|
105
|
+
items.push(sanitize(item, active, depth + 1, budget));
|
|
106
|
+
}
|
|
107
|
+
if (items.length < value.length) items.push(`[${value.length - items.length} items omitted]`);
|
|
108
|
+
result = items;
|
|
109
|
+
} else {
|
|
110
|
+
const record = value;
|
|
111
|
+
const output = {};
|
|
112
|
+
const objectType = readProperty(record, "type").value;
|
|
113
|
+
const redactData = objectType === "image" || objectType === "base64";
|
|
114
|
+
let visited = 0;
|
|
115
|
+
let processed = 0;
|
|
116
|
+
let omitted = false;
|
|
117
|
+
try {
|
|
118
|
+
for (const key in record) {
|
|
119
|
+
if (visited >= MAX_COLLECTION_LENGTH || budget.remaining <= byteLength(TRUNCATED)) {
|
|
120
|
+
omitted = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
visited += 1;
|
|
124
|
+
if (!Object.hasOwn(record, key) || OPAQUE_SIGNATURE_KEYS.has(key)) continue;
|
|
125
|
+
const keyBudget = Math.min(MAX_STRING_LENGTH, Math.max(0, budget.remaining - 4));
|
|
126
|
+
if (key.length > keyBudget) {
|
|
127
|
+
omitted = true;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
const keyBytes = byteLength(key);
|
|
131
|
+
if (keyBytes > keyBudget) {
|
|
132
|
+
omitted = true;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const sanitizedKey = sanitizeString(key, keyBudget);
|
|
136
|
+
budget.remaining -= byteLength(sanitizedKey) + 4;
|
|
137
|
+
const property = readProperty(record, key);
|
|
138
|
+
if (redactData && key === "data") {
|
|
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);
|
|
142
|
+
processed += 1;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
if (processed === 0) {
|
|
146
|
+
active.delete(value);
|
|
147
|
+
return consume("[unreadable object]", budget);
|
|
148
|
+
}
|
|
149
|
+
omitted = true;
|
|
150
|
+
}
|
|
151
|
+
if (omitted) output.$truncated = "additional object entries omitted";
|
|
152
|
+
result = output;
|
|
153
|
+
}
|
|
154
|
+
active.delete(value);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
function readProperty(record, key) {
|
|
158
|
+
try {
|
|
159
|
+
return { ok: true, value: record[key] };
|
|
160
|
+
} catch {
|
|
161
|
+
return { ok: false };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function consume(value, budget) {
|
|
165
|
+
const size = serializedBytes(value);
|
|
166
|
+
if (size > budget.remaining) {
|
|
167
|
+
budget.remaining -= byteLength(TRUNCATED);
|
|
168
|
+
return TRUNCATED;
|
|
169
|
+
}
|
|
170
|
+
budget.remaining -= size;
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function sanitizeString(value, maxBytes) {
|
|
174
|
+
return truncateString(value, maxBytes).replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
|
|
175
|
+
}
|
|
176
|
+
function truncateString(value, maxBytes) {
|
|
177
|
+
const boundedPrefix = value.slice(0, maxBytes);
|
|
178
|
+
if (boundedPrefix.length === value.length && byteLength(boundedPrefix) <= maxBytes) {
|
|
179
|
+
return boundedPrefix;
|
|
180
|
+
}
|
|
181
|
+
const suffix = "\u2026 [truncated]";
|
|
182
|
+
const target = Math.max(0, maxBytes - byteLength(suffix) - 2);
|
|
183
|
+
let bytes = 0;
|
|
184
|
+
let output = "";
|
|
185
|
+
for (const character of boundedPrefix) {
|
|
186
|
+
const size = byteLength(character);
|
|
187
|
+
if (bytes + size > target) break;
|
|
188
|
+
output += character;
|
|
189
|
+
bytes += size;
|
|
190
|
+
}
|
|
191
|
+
return `${output}${suffix}`;
|
|
192
|
+
}
|
|
193
|
+
function serializedBytes(value) {
|
|
194
|
+
return byteLength(JSON.stringify(value) ?? "null");
|
|
195
|
+
}
|
|
196
|
+
function byteLength(value) {
|
|
197
|
+
return Buffer.byteLength(value, "utf8");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/tracing.ts
|
|
201
|
+
var TRACE_SCHEMA_VERSION = "2";
|
|
202
|
+
var MAX_HEADER_VALUE_LENGTH = 1024;
|
|
203
|
+
var CONTENT_DISABLED = "[content capture disabled]";
|
|
204
|
+
var ALLOWED_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
205
|
+
"anthropic-ratelimit-requests-limit",
|
|
206
|
+
"anthropic-ratelimit-requests-remaining",
|
|
207
|
+
"anthropic-ratelimit-requests-reset",
|
|
208
|
+
"anthropic-ratelimit-tokens-limit",
|
|
209
|
+
"anthropic-ratelimit-tokens-remaining",
|
|
210
|
+
"anthropic-ratelimit-tokens-reset",
|
|
211
|
+
"cf-ray",
|
|
212
|
+
"openai-request-id",
|
|
213
|
+
"request-id",
|
|
214
|
+
"retry-after",
|
|
215
|
+
"x-amzn-requestid",
|
|
216
|
+
"x-request-id",
|
|
217
|
+
"x-ratelimit-limit-requests",
|
|
218
|
+
"x-ratelimit-limit-tokens",
|
|
219
|
+
"x-ratelimit-remaining-requests",
|
|
220
|
+
"x-ratelimit-remaining-tokens",
|
|
221
|
+
"x-ratelimit-reset-requests",
|
|
222
|
+
"x-ratelimit-reset-tokens"
|
|
223
|
+
]);
|
|
224
|
+
var TraceRecorder = class {
|
|
225
|
+
constructor(backend, context) {
|
|
226
|
+
this.backend = backend;
|
|
227
|
+
this.context = context;
|
|
228
|
+
}
|
|
229
|
+
backend;
|
|
230
|
+
context;
|
|
231
|
+
root;
|
|
232
|
+
attempt;
|
|
233
|
+
attemptIndex;
|
|
234
|
+
turn;
|
|
235
|
+
turnIndex;
|
|
236
|
+
generation;
|
|
237
|
+
tools = /* @__PURE__ */ new Map();
|
|
238
|
+
duplicateToolIds = /* @__PURE__ */ new Set();
|
|
239
|
+
compaction;
|
|
240
|
+
lastOutput;
|
|
241
|
+
lastAssistant;
|
|
242
|
+
lastAttemptOutcome;
|
|
243
|
+
unresolvedToolErrors = 0;
|
|
244
|
+
startSnapshot;
|
|
245
|
+
counters = emptyCounters();
|
|
246
|
+
startObservation(name, attributes, options) {
|
|
247
|
+
const { sessionId, userId } = this.context;
|
|
248
|
+
return this.backend.start(name, { ...attributes, sessionId, ...userId ? { userId } : {} }, options);
|
|
249
|
+
}
|
|
250
|
+
hasActiveTrace() {
|
|
251
|
+
return this.root !== void 0;
|
|
252
|
+
}
|
|
253
|
+
hasActiveAttempt() {
|
|
254
|
+
return this.attempt !== void 0;
|
|
255
|
+
}
|
|
256
|
+
beginAgent(input) {
|
|
257
|
+
if (this.root) this.closeActiveTrace("Interrupted by a new Pi run.", void 0, "interrupted");
|
|
258
|
+
this.lastOutput = void 0;
|
|
259
|
+
this.lastAssistant = void 0;
|
|
260
|
+
this.lastAttemptOutcome = void 0;
|
|
261
|
+
this.unresolvedToolErrors = 0;
|
|
262
|
+
this.duplicateToolIds.clear();
|
|
263
|
+
this.startSnapshot = input.snapshot;
|
|
264
|
+
this.counters = emptyCounters();
|
|
265
|
+
const traceInput = this.capture({
|
|
266
|
+
prompt: input.prompt,
|
|
267
|
+
...hasItems(input.images) ? { images: input.images } : {}
|
|
268
|
+
});
|
|
269
|
+
const metadata = {
|
|
270
|
+
...customTraceMetadata(this.context.metadata),
|
|
271
|
+
"pi.cwd": this.context.cwd,
|
|
272
|
+
...input.git?.branch ? { "pi.git.branch": input.git.branch } : {},
|
|
273
|
+
...input.git?.commit ? { "pi.git.commit": input.git.commit } : {},
|
|
274
|
+
...input.git ? { "pi.git.detached": input.git.detached } : {},
|
|
275
|
+
"pi.mode": this.context.mode,
|
|
276
|
+
...input.model?.id ? { "pi.model": input.model.id } : {},
|
|
277
|
+
...input.model?.provider ? { "pi.provider": input.model.provider } : {},
|
|
278
|
+
...input.requestId ? { "pi.request.id": input.requestId } : {},
|
|
279
|
+
"pi.session.id": this.context.sessionId,
|
|
280
|
+
"pi.trace.schema_version": Number(TRACE_SCHEMA_VERSION),
|
|
281
|
+
...snapshotMetadata("start", input.snapshot)
|
|
282
|
+
};
|
|
283
|
+
const attributes = { input: traceInput, metadata, version: TRACE_SCHEMA_VERSION };
|
|
284
|
+
const gitTag = input.git?.branch ? `branch:${input.git.branch}` : input.git?.detached ? "git:detached" : void 0;
|
|
285
|
+
this.root = this.startObservation("pi.agent", attributes, { asType: "agent" });
|
|
286
|
+
this.root.updateTrace?.({
|
|
287
|
+
name: normalizeTraceName(this.context.traceName),
|
|
288
|
+
sessionId: this.context.sessionId,
|
|
289
|
+
...this.context.userId ? { userId: this.context.userId } : {},
|
|
290
|
+
version: TRACE_SCHEMA_VERSION,
|
|
291
|
+
input: traceInput,
|
|
292
|
+
metadata,
|
|
293
|
+
tags: traceTags(this.context.tags, gitTag)
|
|
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
|
+
}
|
|
302
|
+
}
|
|
303
|
+
beginAttempt(input = {}) {
|
|
304
|
+
if (!this.root) return;
|
|
305
|
+
if (this.attempt) {
|
|
306
|
+
this.closeAttempt(void 0, "Interrupted by the next Pi attempt.", "interrupted");
|
|
307
|
+
}
|
|
308
|
+
const index = this.counters.attempts;
|
|
309
|
+
this.counters.attempts += 1;
|
|
310
|
+
this.lastAssistant = void 0;
|
|
311
|
+
this.unresolvedToolErrors = 0;
|
|
312
|
+
this.attemptIndex = index;
|
|
313
|
+
this.attempt = this.startObservation(
|
|
314
|
+
"pi.attempt",
|
|
315
|
+
{
|
|
316
|
+
metadata: {
|
|
317
|
+
"pi.attempt.index": index,
|
|
318
|
+
...input.reason ? { "pi.attempt.reason": input.reason } : {}
|
|
319
|
+
},
|
|
320
|
+
version: TRACE_SCHEMA_VERSION
|
|
321
|
+
},
|
|
322
|
+
{ asType: "span", parent: this.root }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
finishAttempt(message) {
|
|
326
|
+
if (message?.role === "assistant") this.finishAssistant(message);
|
|
327
|
+
if (!this.attempt) return;
|
|
328
|
+
const outcome = this.unresolvedToolErrors > 0 ? "error" : classifyOutcome(message);
|
|
329
|
+
this.closeAttempt(
|
|
330
|
+
message,
|
|
331
|
+
this.unresolvedToolErrors > 0 ? "Pi attempt ended after a tool failure." : void 0,
|
|
332
|
+
outcome
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
beginTurn(turnIndex) {
|
|
336
|
+
if (!this.root) return;
|
|
337
|
+
if (this.turn) this.closeTurn("Interrupted by the next Pi turn.", "ERROR");
|
|
338
|
+
this.counters.turns += 1;
|
|
339
|
+
this.turnIndex = turnIndex;
|
|
340
|
+
this.turn = this.startObservation(
|
|
341
|
+
"pi.turn",
|
|
342
|
+
{ metadata: { "pi.turn.index": turnIndex }, version: TRACE_SCHEMA_VERSION },
|
|
343
|
+
{ asType: "span", parent: this.attempt ?? this.root }
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
finishTurn(turnIndex, result) {
|
|
347
|
+
if (!this.turn) return;
|
|
348
|
+
this.closeGeneration("Turn ended without a finalized assistant message.");
|
|
349
|
+
this.closeTools("Tool span ended when the Pi turn finished.");
|
|
350
|
+
const mismatched = this.turnIndex !== turnIndex;
|
|
351
|
+
const outcome = classifyOutcome(result.message);
|
|
352
|
+
this.turn.update({
|
|
353
|
+
metadata: {
|
|
354
|
+
"pi.turn.index": this.turnIndex ?? turnIndex,
|
|
355
|
+
"pi.turn.tool_result_count": result.toolResultCount,
|
|
356
|
+
...result.message.stopReason ? { "pi.turn.stop_reason": result.message.stopReason } : {}
|
|
357
|
+
},
|
|
358
|
+
...mismatched && outcome === "success" ? {
|
|
359
|
+
level: "WARNING",
|
|
360
|
+
statusMessage: `Turn ${turnIndex} ended while turn ${this.turnIndex} was active.`
|
|
361
|
+
} : severityForOutcome(outcome, result.message.errorMessage)
|
|
362
|
+
});
|
|
363
|
+
this.turn.end();
|
|
364
|
+
this.turn = void 0;
|
|
365
|
+
this.turnIndex = void 0;
|
|
366
|
+
}
|
|
367
|
+
beginGeneration(input = {}) {
|
|
368
|
+
if (!this.root) return;
|
|
369
|
+
this.closeGeneration("Interrupted by the next provider request.");
|
|
370
|
+
if (this.unresolvedToolErrors > 0) {
|
|
371
|
+
this.counters.recoveredErrors += this.unresolvedToolErrors;
|
|
372
|
+
this.unresolvedToolErrors = 0;
|
|
373
|
+
}
|
|
374
|
+
this.counters.generations += 1;
|
|
375
|
+
const requestMetadata = {
|
|
376
|
+
...input.payloadStage ? { "pi.request.payload_stage": input.payloadStage } : {},
|
|
377
|
+
...input.model?.provider ? { "pi.request.provider": input.model.provider } : {},
|
|
378
|
+
...input.model?.id ? { "pi.request.model": input.model.id } : {},
|
|
379
|
+
...input.model?.api ? { "pi.request.api": input.model.api } : {},
|
|
380
|
+
...input.thinkingLevel ? { "pi.request.thinking_level": input.thinkingLevel } : {}
|
|
381
|
+
};
|
|
382
|
+
const observation = this.startObservation(
|
|
383
|
+
"pi.llm",
|
|
384
|
+
{
|
|
385
|
+
...input.payload !== void 0 ? { input: this.capture(input.payload) } : {},
|
|
386
|
+
...input.model?.id ? { model: input.model.id } : {},
|
|
387
|
+
...input.thinkingLevel ? { modelParameters: { thinking_level: input.thinkingLevel } } : {},
|
|
388
|
+
...Object.keys(requestMetadata).length > 0 ? { metadata: requestMetadata } : {},
|
|
389
|
+
version: TRACE_SCHEMA_VERSION
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
asType: "generation",
|
|
393
|
+
parent: this.turn ?? this.attempt ?? this.root,
|
|
394
|
+
...input.startedAt === void 0 ? {} : { startTime: new Date(input.startedAt) }
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
this.generation = {
|
|
398
|
+
observation,
|
|
399
|
+
requestMetadata,
|
|
400
|
+
statuses: [],
|
|
401
|
+
headers: {}
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
recordProviderResponse(status, headers = {}) {
|
|
405
|
+
if (!this.generation) return;
|
|
406
|
+
this.generation.statuses.push(status);
|
|
407
|
+
Object.assign(this.generation.headers, allowlistedResponseHeaders(headers));
|
|
408
|
+
}
|
|
409
|
+
markGenerationFirstOutput(timestamp = Date.now()) {
|
|
410
|
+
if (this.generation && this.generation.completionStartTime === void 0) {
|
|
411
|
+
this.generation.completionStartTime = timestamp;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
markGenerationEnd(endTime = Date.now()) {
|
|
415
|
+
if (this.generation && this.generation.endTime === void 0) {
|
|
416
|
+
this.generation.endTime = endTime;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
finishAssistant(message) {
|
|
420
|
+
if (message.role !== "assistant") return;
|
|
421
|
+
this.lastAssistant = message;
|
|
422
|
+
this.lastOutput = this.capture(message.content);
|
|
423
|
+
if (!this.generation) return;
|
|
424
|
+
const state = this.generation;
|
|
425
|
+
const usageDetails = numericRecord({
|
|
426
|
+
input: message.usage?.input,
|
|
427
|
+
output: message.usage?.output,
|
|
428
|
+
cache_read_input_tokens: message.usage?.cacheRead,
|
|
429
|
+
cache_creation_input_tokens: message.usage?.cacheWrite,
|
|
430
|
+
total: message.usage?.totalTokens
|
|
431
|
+
});
|
|
432
|
+
const costDetails = positiveNumericRecord({
|
|
433
|
+
input: message.usage?.cost?.input,
|
|
434
|
+
output: message.usage?.cost?.output,
|
|
435
|
+
cache_read: message.usage?.cost?.cacheRead,
|
|
436
|
+
cache_write: message.usage?.cost?.cacheWrite,
|
|
437
|
+
total: message.usage?.cost?.total
|
|
438
|
+
});
|
|
439
|
+
const outcome = classifyOutcome(message);
|
|
440
|
+
const responseModel = message.responseModel ?? message.model;
|
|
441
|
+
const httpMetadata = generationHttpMetadata(state);
|
|
442
|
+
const recoveredStatuses = state.statuses.filter((status) => status >= 400).length;
|
|
443
|
+
if (outcome === "success" && recoveredStatuses > 0) {
|
|
444
|
+
this.counters.recoveredErrors += recoveredStatuses;
|
|
445
|
+
}
|
|
446
|
+
state.observation.update({
|
|
447
|
+
output: this.lastOutput,
|
|
448
|
+
...state.completionStartTime !== void 0 ? { completionStartTime: new Date(state.completionStartTime) } : {},
|
|
449
|
+
...responseModel ? { model: responseModel } : {},
|
|
450
|
+
...Object.keys(usageDetails).length > 0 ? { usageDetails } : {},
|
|
451
|
+
...Object.keys(costDetails).length > 0 ? { costDetails } : {},
|
|
452
|
+
metadata: {
|
|
453
|
+
...state.requestMetadata,
|
|
454
|
+
...httpMetadata,
|
|
455
|
+
...message.provider ? { "pi.provider": message.provider, "pi.response.provider": message.provider } : {},
|
|
456
|
+
...message.api ? { "pi.response.api": message.api } : {},
|
|
457
|
+
...responseModel ? { "pi.response.model": responseModel } : {},
|
|
458
|
+
...message.responseId ? { "pi.response.id": message.responseId } : {},
|
|
459
|
+
...message.responseModel && message.model && message.responseModel !== message.model ? { "pi.requested_model": message.model } : {},
|
|
460
|
+
...message.stopReason ? { "pi.stop_reason": message.stopReason } : {},
|
|
461
|
+
...finiteNumber(message.usage?.reasoning) ? { "pi.usage.reasoning_tokens": message.usage.reasoning } : {},
|
|
462
|
+
...finiteNumber(message.usage?.cacheWrite1h) ? { "pi.usage.cache_write_1h_tokens": message.usage.cacheWrite1h } : {}
|
|
463
|
+
},
|
|
464
|
+
...severityForOutcome(outcome, message.errorMessage)
|
|
465
|
+
});
|
|
466
|
+
state.observation.end(state.endTime);
|
|
467
|
+
this.generation = void 0;
|
|
468
|
+
}
|
|
469
|
+
beginTool(toolCallId, toolName, args, startedAt = Date.now()) {
|
|
470
|
+
if (!this.root) return;
|
|
471
|
+
this.counters.tools += 1;
|
|
472
|
+
if (this.duplicateToolIds.has(toolCallId)) {
|
|
473
|
+
this.counters.toolErrors += 1;
|
|
474
|
+
const duplicate = this.startToolObservation(toolCallId, toolName, args);
|
|
475
|
+
duplicate.update({
|
|
476
|
+
level: "ERROR",
|
|
477
|
+
statusMessage: "Duplicate tool call ID cannot be correlated safely."
|
|
478
|
+
});
|
|
479
|
+
duplicate.end();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const existing = this.tools.get(toolCallId);
|
|
483
|
+
if (existing) {
|
|
484
|
+
existing.observation.update({
|
|
485
|
+
level: "ERROR",
|
|
486
|
+
statusMessage: "Duplicate tool execution started."
|
|
487
|
+
});
|
|
488
|
+
existing.observation.end();
|
|
489
|
+
this.tools.delete(toolCallId);
|
|
490
|
+
this.counters.toolErrors += 2;
|
|
491
|
+
const duplicate = this.startToolObservation(toolCallId, toolName, args);
|
|
492
|
+
duplicate.update({
|
|
493
|
+
level: "ERROR",
|
|
494
|
+
statusMessage: "Duplicate tool call ID cannot be correlated safely."
|
|
495
|
+
});
|
|
496
|
+
duplicate.end();
|
|
497
|
+
this.duplicateToolIds.add(toolCallId);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
this.tools.set(toolCallId, {
|
|
501
|
+
observation: this.startToolObservation(toolCallId, toolName, args),
|
|
502
|
+
startedAt,
|
|
503
|
+
progressUpdates: 0
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
recordToolInput(toolCallId, input) {
|
|
507
|
+
if (input === void 0 || this.duplicateToolIds.has(toolCallId)) return;
|
|
508
|
+
this.tools.get(toolCallId)?.observation.update({ input: this.capture(input) });
|
|
509
|
+
}
|
|
510
|
+
recordToolProgress(toolCallId, timestamp = Date.now()) {
|
|
511
|
+
const tool = this.tools.get(toolCallId);
|
|
512
|
+
if (!tool) return;
|
|
513
|
+
tool.progressUpdates += 1;
|
|
514
|
+
tool.firstProgressAt ??= timestamp;
|
|
515
|
+
}
|
|
516
|
+
finishTool(toolCallId, result) {
|
|
517
|
+
if (this.duplicateToolIds.has(toolCallId)) return;
|
|
518
|
+
const tool = this.tools.get(toolCallId);
|
|
519
|
+
if (!tool) return;
|
|
520
|
+
if (result.isError) {
|
|
521
|
+
this.counters.toolErrors += 1;
|
|
522
|
+
this.unresolvedToolErrors += 1;
|
|
523
|
+
}
|
|
524
|
+
tool.observation.update({
|
|
525
|
+
output: this.capture({ content: result.content, details: result.details }),
|
|
526
|
+
metadata: {
|
|
527
|
+
"pi.tool.progress_update_count": tool.progressUpdates,
|
|
528
|
+
...tool.firstProgressAt !== void 0 ? {
|
|
529
|
+
"pi.tool.time_to_first_progress_ms": Math.max(0, tool.firstProgressAt - tool.startedAt)
|
|
530
|
+
} : {}
|
|
531
|
+
},
|
|
532
|
+
...result.isError ? { level: "ERROR", statusMessage: "Pi tool execution failed." } : {}
|
|
533
|
+
});
|
|
534
|
+
tool.observation.end();
|
|
535
|
+
this.tools.delete(toolCallId);
|
|
536
|
+
}
|
|
537
|
+
beginCompaction(input) {
|
|
538
|
+
if (!this.root) return;
|
|
539
|
+
this.closeCompaction("Interrupted by another Pi compaction.");
|
|
540
|
+
this.counters.compactions += 1;
|
|
541
|
+
this.compaction = {
|
|
542
|
+
observation: this.startObservation(
|
|
543
|
+
"pi.compaction",
|
|
544
|
+
{
|
|
545
|
+
metadata: {
|
|
546
|
+
"pi.compaction.reason": input.reason,
|
|
547
|
+
"pi.compaction.will_retry": input.willRetry,
|
|
548
|
+
...finiteNumber(input.tokensBefore) ? { "pi.compaction.tokens_before": input.tokensBefore } : {},
|
|
549
|
+
"pi.compaction.messages_to_summarize": input.messagesToSummarize,
|
|
550
|
+
"pi.compaction.turn_prefix_messages": input.turnPrefixMessages,
|
|
551
|
+
"pi.compaction.branch_entries": input.branchEntries,
|
|
552
|
+
"pi.compaction.is_split_turn": input.isSplitTurn
|
|
553
|
+
},
|
|
554
|
+
version: TRACE_SCHEMA_VERSION
|
|
555
|
+
},
|
|
556
|
+
{ asType: "span", parent: this.root }
|
|
557
|
+
)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
finishCompaction(input) {
|
|
561
|
+
if (!this.compaction) return;
|
|
562
|
+
const details = structuralCompactionDetails(input.details);
|
|
563
|
+
const usage = numericRecord({
|
|
564
|
+
input: input.usage?.input,
|
|
565
|
+
output: input.usage?.output,
|
|
566
|
+
cache_read: input.usage?.cacheRead,
|
|
567
|
+
cache_write: input.usage?.cacheWrite,
|
|
568
|
+
total: input.usage?.totalTokens
|
|
569
|
+
});
|
|
570
|
+
const cost = positiveNumericRecord({
|
|
571
|
+
input: input.usage?.cost?.input,
|
|
572
|
+
output: input.usage?.cost?.output,
|
|
573
|
+
cache_read: input.usage?.cost?.cacheRead,
|
|
574
|
+
cache_write: input.usage?.cost?.cacheWrite,
|
|
575
|
+
total: input.usage?.cost?.total
|
|
576
|
+
});
|
|
577
|
+
this.compaction.observation.update({
|
|
578
|
+
metadata: {
|
|
579
|
+
"pi.compaction.reason": input.reason,
|
|
580
|
+
"pi.compaction.will_retry": input.willRetry,
|
|
581
|
+
"pi.compaction.from_extension": input.fromExtension,
|
|
582
|
+
...finiteNumber(input.tokensBefore) ? { "pi.compaction.tokens_before": input.tokensBefore } : {},
|
|
583
|
+
...details,
|
|
584
|
+
...prefixRecord("pi.compaction.usage", usage),
|
|
585
|
+
...prefixRecord("pi.compaction.cost", cost)
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
this.compaction.observation.end();
|
|
589
|
+
this.compaction = void 0;
|
|
590
|
+
}
|
|
591
|
+
settle(snapshot) {
|
|
592
|
+
this.closeActiveTrace(void 0, snapshot);
|
|
593
|
+
}
|
|
594
|
+
interrupt(statusMessage, snapshot) {
|
|
595
|
+
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
596
|
+
}
|
|
597
|
+
dispose(statusMessage = "Pi session ended before the active trace settled.", snapshot) {
|
|
598
|
+
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
599
|
+
}
|
|
600
|
+
closeActiveTrace(statusMessage, snapshot, forcedOutcome) {
|
|
601
|
+
this.closeTurn(statusMessage ?? "Pi turn ended when the agent run settled.");
|
|
602
|
+
this.closeAttempt(void 0, statusMessage, forcedOutcome ?? "interrupted");
|
|
603
|
+
this.closeCompaction(statusMessage ?? "Pi compaction ended when the agent run settled.");
|
|
604
|
+
if (!this.root) return;
|
|
605
|
+
const baseOutcome = forcedOutcome ?? this.lastAttemptOutcome ?? classifyOutcome(this.lastAssistant);
|
|
606
|
+
const recoveredErrorCount = this.counters.recoveredErrors + (baseOutcome === "success" ? this.counters.failedAttempts : 0);
|
|
607
|
+
const traceOutcome = baseOutcome === "success" && recoveredErrorCount > 0 ? "recovered_success" : baseOutcome;
|
|
608
|
+
const finalMetadata = {
|
|
609
|
+
"pi.trace.schema_version": Number(TRACE_SCHEMA_VERSION),
|
|
610
|
+
"pi.trace.outcome": traceOutcome,
|
|
611
|
+
...this.lastAssistant?.stopReason ? { "pi.trace.stop_reason": this.lastAssistant.stopReason } : {},
|
|
612
|
+
"pi.trace.attempt_count": this.counters.attempts,
|
|
613
|
+
"pi.trace.turn_count": this.counters.turns,
|
|
614
|
+
"pi.trace.generation_count": this.counters.generations,
|
|
615
|
+
"pi.trace.tool_count": this.counters.tools,
|
|
616
|
+
"pi.trace.tool_error_count": this.counters.toolErrors,
|
|
617
|
+
"pi.trace.compaction_count": this.counters.compactions,
|
|
618
|
+
"pi.trace.recovered_error_count": recoveredErrorCount,
|
|
619
|
+
...snapshotMetadata("start", this.startSnapshot),
|
|
620
|
+
...snapshotMetadata("end", snapshot)
|
|
621
|
+
};
|
|
622
|
+
const finalAttributes = {
|
|
623
|
+
...this.lastOutput !== void 0 ? { output: this.lastOutput } : {},
|
|
624
|
+
metadata: finalMetadata,
|
|
625
|
+
...severityForOutcome(baseOutcome, statusMessage ?? this.lastAssistant?.errorMessage)
|
|
626
|
+
};
|
|
627
|
+
this.root.update(finalAttributes);
|
|
628
|
+
this.root.updateTrace?.({
|
|
629
|
+
...this.lastOutput !== void 0 ? { output: this.lastOutput } : {},
|
|
630
|
+
metadata: finalMetadata,
|
|
631
|
+
version: TRACE_SCHEMA_VERSION
|
|
632
|
+
});
|
|
633
|
+
this.root.end();
|
|
634
|
+
this.root = void 0;
|
|
635
|
+
this.lastOutput = void 0;
|
|
636
|
+
this.lastAssistant = void 0;
|
|
637
|
+
this.lastAttemptOutcome = void 0;
|
|
638
|
+
this.unresolvedToolErrors = 0;
|
|
639
|
+
this.duplicateToolIds.clear();
|
|
640
|
+
this.startSnapshot = void 0;
|
|
641
|
+
}
|
|
642
|
+
closeAttempt(message, statusMessage, forcedOutcome) {
|
|
643
|
+
if (!this.attempt) return;
|
|
644
|
+
this.closeTurn(statusMessage ?? "Pi turn ended when the attempt finished.");
|
|
645
|
+
const outcome = forcedOutcome ?? classifyOutcome(message);
|
|
646
|
+
if (outcome === "error") this.counters.failedAttempts += 1;
|
|
647
|
+
this.lastAttemptOutcome = outcome;
|
|
648
|
+
this.attempt.update({
|
|
649
|
+
...message?.content !== void 0 ? { output: this.capture(message.content) } : {},
|
|
650
|
+
metadata: {
|
|
651
|
+
"pi.attempt.index": this.attemptIndex ?? Math.max(0, this.counters.attempts - 1),
|
|
652
|
+
"pi.attempt.outcome": outcome,
|
|
653
|
+
...message?.stopReason ? { "pi.attempt.stop_reason": message.stopReason } : {}
|
|
654
|
+
},
|
|
655
|
+
...severityForOutcome(outcome, statusMessage ?? message?.errorMessage)
|
|
656
|
+
});
|
|
657
|
+
this.attempt.end();
|
|
658
|
+
this.attempt = void 0;
|
|
659
|
+
this.attemptIndex = void 0;
|
|
660
|
+
this.unresolvedToolErrors = 0;
|
|
661
|
+
}
|
|
662
|
+
startToolObservation(toolCallId, toolName, args) {
|
|
663
|
+
return this.startObservation(
|
|
664
|
+
`pi.tool.${toolName}`,
|
|
665
|
+
{
|
|
666
|
+
...args !== void 0 ? { input: this.capture(args) } : {},
|
|
667
|
+
metadata: { "pi.tool.call_id": toolCallId, "pi.tool.name": toolName },
|
|
668
|
+
version: TRACE_SCHEMA_VERSION
|
|
669
|
+
},
|
|
670
|
+
{ asType: "tool", parent: this.turn ?? this.attempt ?? this.root }
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
closeTurn(statusMessage, generationLevel = "WARNING") {
|
|
674
|
+
this.closeGeneration(statusMessage, generationLevel);
|
|
675
|
+
this.closeTools(statusMessage);
|
|
676
|
+
if (!this.turn) return;
|
|
677
|
+
this.turn.update({ level: "WARNING", statusMessage });
|
|
678
|
+
this.turn.end();
|
|
679
|
+
this.turn = void 0;
|
|
680
|
+
this.turnIndex = void 0;
|
|
681
|
+
}
|
|
682
|
+
closeTools(statusMessage) {
|
|
683
|
+
for (const tool of this.tools.values()) {
|
|
684
|
+
tool.observation.update({
|
|
685
|
+
level: "WARNING",
|
|
686
|
+
statusMessage,
|
|
687
|
+
metadata: {
|
|
688
|
+
"pi.tool.progress_update_count": tool.progressUpdates,
|
|
689
|
+
...tool.firstProgressAt !== void 0 ? {
|
|
690
|
+
"pi.tool.time_to_first_progress_ms": Math.max(0, tool.firstProgressAt - tool.startedAt)
|
|
691
|
+
} : {}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
tool.observation.end();
|
|
695
|
+
}
|
|
696
|
+
this.tools.clear();
|
|
697
|
+
}
|
|
698
|
+
closeGeneration(statusMessage, level = "ERROR") {
|
|
699
|
+
if (!this.generation) return;
|
|
700
|
+
this.generation.observation.update({
|
|
701
|
+
level,
|
|
702
|
+
statusMessage,
|
|
703
|
+
metadata: {
|
|
704
|
+
...this.generation.requestMetadata,
|
|
705
|
+
...generationHttpMetadata(this.generation)
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
this.generation.observation.end(this.generation.endTime);
|
|
709
|
+
this.generation = void 0;
|
|
710
|
+
}
|
|
711
|
+
closeCompaction(statusMessage) {
|
|
712
|
+
if (!this.compaction) return;
|
|
713
|
+
this.compaction.observation.update({ level: "WARNING", statusMessage });
|
|
714
|
+
this.compaction.observation.end();
|
|
715
|
+
this.compaction = void 0;
|
|
716
|
+
}
|
|
717
|
+
capture(value) {
|
|
718
|
+
return this.context.captureContent ? sanitizeTraceValue(value) : CONTENT_DISABLED;
|
|
719
|
+
}
|
|
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
|
+
}
|
|
739
|
+
function generationHttpMetadata(state) {
|
|
740
|
+
const finalStatus = state.statuses.at(-1);
|
|
741
|
+
return {
|
|
742
|
+
...state.statuses.length > 0 ? {
|
|
743
|
+
"http.response.status_codes": [...state.statuses],
|
|
744
|
+
"http.response.status_code": finalStatus,
|
|
745
|
+
"http.response.attempt_count": state.statuses.length,
|
|
746
|
+
"http.response.retry_count": Math.max(0, state.statuses.length - 1)
|
|
747
|
+
} : {},
|
|
748
|
+
...Object.keys(state.headers).length > 0 ? { "http.response.headers": { ...state.headers } } : {}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function allowlistedResponseHeaders(headers) {
|
|
752
|
+
const output = {};
|
|
753
|
+
for (const [rawName, rawValue] of Object.entries(headers)) {
|
|
754
|
+
const name = rawName.toLowerCase();
|
|
755
|
+
if (!ALLOWED_RESPONSE_HEADERS.has(name) || typeof rawValue !== "string") continue;
|
|
756
|
+
output[name] = truncateString2(rawValue, MAX_HEADER_VALUE_LENGTH);
|
|
757
|
+
}
|
|
758
|
+
return output;
|
|
759
|
+
}
|
|
760
|
+
function structuralCompactionDetails(details) {
|
|
761
|
+
if (!details || typeof details !== "object" || Array.isArray(details)) return {};
|
|
762
|
+
const record = details;
|
|
763
|
+
return {
|
|
764
|
+
...Array.isArray(record.readFiles) ? { "pi.compaction.read_file_count": record.readFiles.length } : {},
|
|
765
|
+
...Array.isArray(record.modifiedFiles) ? { "pi.compaction.modified_file_count": record.modifiedFiles.length } : {}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
function snapshotMetadata(prefix, snapshot) {
|
|
769
|
+
if (!snapshot) return {};
|
|
770
|
+
return {
|
|
771
|
+
...snapshot.leafId ? { [`pi.trace.${prefix}_leaf_id`]: snapshot.leafId } : {},
|
|
772
|
+
...snapshot.contextUsage ? {
|
|
773
|
+
...finiteNumber(snapshot.contextUsage.tokens) ? { [`pi.trace.${prefix}_context_tokens`]: snapshot.contextUsage.tokens } : {},
|
|
774
|
+
[`pi.trace.${prefix}_context_window`]: snapshot.contextUsage.contextWindow,
|
|
775
|
+
...finiteNumber(snapshot.contextUsage.percent) ? { [`pi.trace.${prefix}_context_percent`]: snapshot.contextUsage.percent } : {}
|
|
776
|
+
} : {}
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function classifyOutcome(message) {
|
|
780
|
+
if (message?.role !== "assistant") return "interrupted";
|
|
781
|
+
if (message.stopReason === "error" || message.errorMessage) return "error";
|
|
782
|
+
if (message.stopReason === "aborted") return "aborted";
|
|
783
|
+
if (message.stopReason === "length") return "length";
|
|
784
|
+
return "success";
|
|
785
|
+
}
|
|
786
|
+
function severityForOutcome(outcome, statusMessage) {
|
|
787
|
+
if (outcome === "success") return {};
|
|
788
|
+
if (outcome === "error") {
|
|
789
|
+
return {
|
|
790
|
+
level: "ERROR",
|
|
791
|
+
statusMessage: statusMessage ?? "The Pi operation failed."
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
level: "WARNING",
|
|
796
|
+
statusMessage: statusMessage ?? (outcome === "aborted" ? "The Pi operation was aborted." : outcome === "length" ? "The model reached its output limit." : "The Pi operation was interrupted.")
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
function prefixRecord(prefix, values) {
|
|
800
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [`${prefix}.${key}`, value]));
|
|
801
|
+
}
|
|
802
|
+
function truncateString2(value, maxBytes) {
|
|
803
|
+
const boundedPrefix = value.slice(0, maxBytes);
|
|
804
|
+
if (boundedPrefix.length === value.length && byteLength2(boundedPrefix) <= maxBytes) {
|
|
805
|
+
return boundedPrefix;
|
|
806
|
+
}
|
|
807
|
+
const suffix = "\u2026 [truncated]";
|
|
808
|
+
const target = Math.max(0, maxBytes - byteLength2(suffix) - 2);
|
|
809
|
+
let bytes = 0;
|
|
810
|
+
let output = "";
|
|
811
|
+
for (const character of boundedPrefix) {
|
|
812
|
+
const size = byteLength2(character);
|
|
813
|
+
if (bytes + size > target) break;
|
|
814
|
+
output += character;
|
|
815
|
+
bytes += size;
|
|
816
|
+
}
|
|
817
|
+
return `${output}${suffix}`;
|
|
818
|
+
}
|
|
819
|
+
function byteLength2(value) {
|
|
820
|
+
return Buffer.byteLength(value, "utf8");
|
|
821
|
+
}
|
|
822
|
+
function numericRecord(values) {
|
|
823
|
+
return Object.fromEntries(
|
|
824
|
+
Object.entries(values).filter((entry) => finiteNumber(entry[1]))
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
function positiveNumericRecord(values) {
|
|
828
|
+
return Object.fromEntries(
|
|
829
|
+
Object.entries(values).filter((entry) => finiteNumber(entry[1]) && entry[1] > 0)
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
function finiteNumber(value) {
|
|
833
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
834
|
+
}
|
|
835
|
+
function hasItems(value) {
|
|
836
|
+
return Array.isArray(value) ? value.length > 0 : value !== void 0;
|
|
837
|
+
}
|
|
838
|
+
function emptyCounters() {
|
|
839
|
+
return {
|
|
840
|
+
attempts: 0,
|
|
841
|
+
turns: 0,
|
|
842
|
+
generations: 0,
|
|
843
|
+
tools: 0,
|
|
844
|
+
toolErrors: 0,
|
|
845
|
+
compactions: 0,
|
|
846
|
+
recoveredErrors: 0,
|
|
847
|
+
failedAttempts: 0
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/pi-session.ts
|
|
852
|
+
function createPiLangfuseSession(runtime, options = {}) {
|
|
853
|
+
const sessionOptions = snapshotSessionOptions(options);
|
|
854
|
+
return createPiLangfuseSessionController({
|
|
855
|
+
resolveSession: async () => ({ runtime, options: sessionOptions })
|
|
856
|
+
});
|
|
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;
|
|
963
|
+
}
|
|
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);
|
|
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);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function registerHooks(pi) {
|
|
992
|
+
const registration = {};
|
|
993
|
+
const activeRecorder = () => activeRecorderFor(registration);
|
|
994
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
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
|
+
);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
invalidatePendingInitialization("replaced");
|
|
1005
|
+
ownerRegistration = registration;
|
|
1006
|
+
const initialization = createPendingInitialization();
|
|
1007
|
+
pendingInitialization = initialization;
|
|
1008
|
+
let result = { ok: true };
|
|
1009
|
+
try {
|
|
1010
|
+
await initializeSession(registration, initialization, ctx);
|
|
1011
|
+
} catch (error) {
|
|
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;
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
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;
|
|
1024
|
+
nextAttemptReason = void 0;
|
|
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
|
+
});
|
|
1037
|
+
});
|
|
1038
|
+
});
|
|
1039
|
+
pi.on("agent_start", () => {
|
|
1040
|
+
const recorder = activeRecorder();
|
|
1041
|
+
if (!recorder) return;
|
|
1042
|
+
recorder.beginAttempt(nextAttemptReason ? { reason: nextAttemptReason } : void 0);
|
|
1043
|
+
nextAttemptReason = void 0;
|
|
1044
|
+
});
|
|
1045
|
+
pi.on("turn_start", (event, ctx) => {
|
|
1046
|
+
const recorder = activeRecorder();
|
|
1047
|
+
if (!recorder) return;
|
|
1048
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1049
|
+
ensureActiveRun(recorder, ctx, startRootTrace);
|
|
1050
|
+
recorder.beginTurn(event.turnIndex);
|
|
1051
|
+
});
|
|
1052
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
1053
|
+
if (!activeRecorder()) return;
|
|
1054
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1055
|
+
pendingProviderGeneration = {
|
|
1056
|
+
startedAt: Date.now(),
|
|
1057
|
+
payload: event.payload,
|
|
1058
|
+
model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id, api: ctx.model.api } : void 0,
|
|
1059
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
1060
|
+
responses: []
|
|
1061
|
+
};
|
|
1062
|
+
});
|
|
1063
|
+
pi.on("after_provider_response", (event) => {
|
|
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());
|
|
1068
|
+
});
|
|
1069
|
+
pi.on("message_update", (event, ctx) => {
|
|
1070
|
+
if (!isRealOutputDelta(event.assistantMessageEvent)) return;
|
|
1071
|
+
const recorder = activeRecorder();
|
|
1072
|
+
claimPendingProviderGeneration(ctx, recorder);
|
|
1073
|
+
recorder?.markGenerationFirstOutput();
|
|
1074
|
+
});
|
|
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();
|
|
1080
|
+
});
|
|
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, {
|
|
1088
|
+
message: event.message,
|
|
1089
|
+
toolResultCount: event.toolResults.length
|
|
1090
|
+
});
|
|
1091
|
+
});
|
|
1092
|
+
pi.on("tool_execution_start", (event) => {
|
|
1093
|
+
activeRecorder()?.beginTool(event.toolCallId, event.toolName, event.args);
|
|
1094
|
+
});
|
|
1095
|
+
pi.on("tool_execution_update", (event) => {
|
|
1096
|
+
activeRecorder()?.recordToolProgress(event.toolCallId);
|
|
1097
|
+
});
|
|
1098
|
+
pi.on("tool_result", (event) => {
|
|
1099
|
+
activeRecorder()?.recordToolInput(event.toolCallId, event.input);
|
|
1100
|
+
});
|
|
1101
|
+
pi.on("tool_execution_end", (event) => {
|
|
1102
|
+
activeRecorder()?.finishTool(event.toolCallId, {
|
|
1103
|
+
content: event.result.content,
|
|
1104
|
+
details: event.result.details,
|
|
1105
|
+
isError: event.isError
|
|
1106
|
+
});
|
|
1107
|
+
});
|
|
1108
|
+
pi.on("agent_end", (event) => {
|
|
1109
|
+
const message = findLastAssistant(event.messages);
|
|
1110
|
+
activeRecorder()?.finishAttempt(message);
|
|
1111
|
+
});
|
|
1112
|
+
pi.on("session_before_compact", (event) => {
|
|
1113
|
+
activeRecorder()?.beginCompaction({
|
|
1114
|
+
reason: event.reason,
|
|
1115
|
+
willRetry: event.willRetry,
|
|
1116
|
+
tokensBefore: event.preparation.tokensBefore,
|
|
1117
|
+
messagesToSummarize: event.preparation.messagesToSummarize.length,
|
|
1118
|
+
turnPrefixMessages: event.preparation.turnPrefixMessages.length,
|
|
1119
|
+
branchEntries: event.branchEntries.length,
|
|
1120
|
+
isSplitTurn: event.preparation.isSplitTurn
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
pi.on("session_compact", (event) => {
|
|
1124
|
+
const recorder = activeRecorder();
|
|
1125
|
+
const entry = event.compactionEntry;
|
|
1126
|
+
recorder?.finishCompaction({
|
|
1127
|
+
reason: event.reason,
|
|
1128
|
+
willRetry: event.willRetry,
|
|
1129
|
+
fromExtension: event.fromExtension,
|
|
1130
|
+
tokensBefore: entry.tokensBefore,
|
|
1131
|
+
details: entry.details,
|
|
1132
|
+
usage: entry.usage
|
|
1133
|
+
});
|
|
1134
|
+
if (event.willRetry && recorder?.hasActiveTrace()) nextAttemptReason = "post_compaction";
|
|
1135
|
+
});
|
|
1136
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
1137
|
+
const recorder = activeRecorder();
|
|
1138
|
+
if (!recorder) return;
|
|
1139
|
+
lastSnapshot = contextSnapshot(ctx);
|
|
1140
|
+
recorder.settle(lastSnapshot);
|
|
1141
|
+
pendingProviderGeneration = void 0;
|
|
1142
|
+
nextAttemptReason = void 0;
|
|
1143
|
+
});
|
|
1144
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1145
|
+
if (ownerRegistration !== registration) return;
|
|
1146
|
+
const initializations = invalidateAllInitializations(event.reason);
|
|
1147
|
+
ownerRegistration = void 0;
|
|
1148
|
+
const generation = ++sessionGeneration;
|
|
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
|
+
}
|
|
1164
|
+
try {
|
|
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();
|
|
1171
|
+
}
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
if (!disposed && generation === sessionGeneration) options.onShutdownError?.(error, ctx);
|
|
1174
|
+
}
|
|
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 } } : {}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function ensureActiveRun(recorder, ctx, startRootTrace) {
|
|
1225
|
+
if (!recorder.hasActiveTrace()) {
|
|
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
|
+
});
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
if (!recorder.hasActiveAttempt()) recorder.beginAttempt();
|
|
1236
|
+
}
|
|
1237
|
+
function contextSnapshot(ctx) {
|
|
1238
|
+
return {
|
|
1239
|
+
leafId: typeof ctx.sessionManager.getLeafId === "function" ? ctx.sessionManager.getLeafId() : void 0,
|
|
1240
|
+
contextUsage: ctx.getContextUsage()
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
function findLastAssistant(messages) {
|
|
1244
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1245
|
+
const message = messages[index];
|
|
1246
|
+
if (message?.role === "assistant") return message;
|
|
1247
|
+
}
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
function isRealOutputDelta(event) {
|
|
1251
|
+
return (event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") && typeof event.delta === "string" && event.delta.length > 0;
|
|
1252
|
+
}
|
|
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;
|
|
1257
|
+
}
|
|
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;
|
|
1262
|
+
}
|
|
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
|
|
1352
|
+
});
|
|
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
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
async function promptForConfig(ctx, current, isCurrent) {
|
|
1449
|
+
const secretKey = await ctx.ui.input("Langfuse secret key (leave blank to keep existing):");
|
|
1450
|
+
if (!isCurrent()) return void 0;
|
|
1451
|
+
if (secretKey === void 0) {
|
|
1452
|
+
ctx.ui.notify("Cancelled", "info");
|
|
1453
|
+
return void 0;
|
|
1454
|
+
}
|
|
1455
|
+
const publicKey = await ctx.ui.input("Langfuse public key (leave blank to keep existing):");
|
|
1456
|
+
if (!isCurrent()) return void 0;
|
|
1457
|
+
if (publicKey === void 0) {
|
|
1458
|
+
ctx.ui.notify("Cancelled", "info");
|
|
1459
|
+
return void 0;
|
|
1460
|
+
}
|
|
1461
|
+
const baseUrl = await ctx.ui.input(
|
|
1462
|
+
`Langfuse base URL (leave blank for default ${DEFAULT_BASE_URL}):`,
|
|
1463
|
+
DEFAULT_BASE_URL
|
|
1464
|
+
);
|
|
1465
|
+
if (!isCurrent()) return void 0;
|
|
1466
|
+
if (baseUrl === void 0) {
|
|
1467
|
+
ctx.ui.notify("Cancelled", "info");
|
|
1468
|
+
return void 0;
|
|
1469
|
+
}
|
|
1470
|
+
const patch = {
|
|
1471
|
+
...publicKey.trim() ? { publicKey: publicKey.trim() } : {},
|
|
1472
|
+
...secretKey.trim() ? { secretKey: secretKey.trim() } : {},
|
|
1473
|
+
baseUrl: baseUrl.trim() || DEFAULT_BASE_URL
|
|
1474
|
+
};
|
|
1475
|
+
const normalized = normalizeLangfuseConfig({ ...current, ...patch });
|
|
1476
|
+
if (!normalized.ok) {
|
|
1477
|
+
ctx.ui.notify(`Invalid Langfuse config: ${normalized.reason}`, "error");
|
|
1478
|
+
return void 0;
|
|
1479
|
+
}
|
|
1480
|
+
return patch;
|
|
1481
|
+
}
|
|
1482
|
+
function formatMenuTitle(activeConfig, configPath, initializationError, configurationNotice) {
|
|
1483
|
+
const lines = ["Langfuse", "", "Current session:", ` Tracing: ${activeConfig ? "enabled" : "disabled"}`];
|
|
1484
|
+
if (activeConfig) {
|
|
1485
|
+
lines.push(
|
|
1486
|
+
` Endpoint: ${activeConfig.baseUrl}`,
|
|
1487
|
+
` Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`
|
|
1488
|
+
);
|
|
1489
|
+
} else if (configurationNotice) {
|
|
1490
|
+
lines.push(" State: tracing remains disabled until Pi restarts.");
|
|
1491
|
+
} else if (initializationError) {
|
|
1492
|
+
lines.push(` Reason: ${initializationError}`);
|
|
1493
|
+
}
|
|
1494
|
+
lines.push(
|
|
1495
|
+
"",
|
|
1496
|
+
"Agent-directory configuration:",
|
|
1497
|
+
` Configuration: ${configPath ?? "unknown"}`,
|
|
1498
|
+
" Scope: this Pi agent directory; restart each Pi process to apply changes."
|
|
1499
|
+
);
|
|
1500
|
+
if (configurationNotice) lines.push(` Pending: ${configurationNotice}`);
|
|
1501
|
+
lines.push("", "What do you want to do?");
|
|
1502
|
+
return lines.join("\n");
|
|
1503
|
+
}
|
|
1504
|
+
function formatNonInteractiveStatus(activeConfig, configPath, initializationError) {
|
|
1505
|
+
return [
|
|
1506
|
+
"/langfuse requires interactive UI.",
|
|
1507
|
+
`Current session tracing: ${activeConfig ? "enabled" : "disabled"}`,
|
|
1508
|
+
...activeConfig ? [
|
|
1509
|
+
`Endpoint: ${activeConfig.baseUrl}`,
|
|
1510
|
+
`Content capture: ${activeConfig.captureContent ? "enabled" : "disabled"}`
|
|
1511
|
+
] : initializationError ? [`Reason: ${initializationError}`] : [],
|
|
1512
|
+
`Configuration: ${configPath ?? "unknown"}`,
|
|
1513
|
+
"Edit the private config manually, then restart each Pi process to apply it to subsequent sessions."
|
|
1514
|
+
].join("\n");
|
|
1515
|
+
}
|
|
1516
|
+
function formatHelp(configPath) {
|
|
1517
|
+
return [
|
|
1518
|
+
"Langfuse setup and privacy:",
|
|
1519
|
+
"Run /langfuse in interactive Pi to manage tracing for the current session.",
|
|
1520
|
+
`Configuration: ${configPath ?? "pi-langfuse.json"}`,
|
|
1521
|
+
"The file belongs to this Pi agent directory; restart each Pi process after changing it.",
|
|
1522
|
+
"Trace content may contain prompts, responses, tool arguments, tool results, and source code.",
|
|
1523
|
+
"Git branch, commit, cwd, model, and usage remain in metadata when content capture is disabled.",
|
|
1524
|
+
'Use "captureContent": false in the private config to export metadata only.'
|
|
1525
|
+
].join("\n");
|
|
1526
|
+
}
|
|
1527
|
+
function formatConfigError(result) {
|
|
1528
|
+
const setupHint = result.reason.startsWith("Configuration file not found:") ? " Run /langfuse and choose Set up Langfuse." : "";
|
|
1529
|
+
return `Langfuse tracing is disabled: ${result.reason}${setupHint}`;
|
|
1530
|
+
}
|
|
1531
|
+
function formatError(error, config) {
|
|
1532
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1533
|
+
const secrets = [config?.publicKey, config?.secretKey].filter((secret) => Boolean(secret)).sort((left, right) => right.length - left.length);
|
|
1534
|
+
for (const secret of secrets) {
|
|
1535
|
+
message = message.split(secret).join("[LANGFUSE_KEY_REDACTED]");
|
|
1536
|
+
}
|
|
1537
|
+
return message;
|
|
1538
|
+
}
|
|
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
|
+
}
|
|
1546
|
+
export {
|
|
1547
|
+
createLangfuseRuntime,
|
|
1548
|
+
createPiLangfuseSession,
|
|
1549
|
+
langfuse_default as default
|
|
1550
|
+
};
|
|
1551
|
+
//# sourceMappingURL=index.js.map
|