@narumitw/pi-langfuse 0.51.2 → 0.52.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 +46 -3
- package/dist/chunks/chunk-N7WSLB5Q.ts +88 -0
- package/dist/chunks/chunk-N7WSLB5Q.ts.map +7 -0
- package/dist/chunks/{runtime-6ZHLVC3A.ts → runtime-GLRAF7FE.ts} +2 -2
- package/dist/chunks/runtime-GLRAF7FE.ts.map +7 -0
- package/dist/config.d.ts +1 -1
- package/dist/durable-projection.d.ts +9 -0
- package/dist/durable-recorder.d.ts +49 -0
- package/dist/durable.d.ts +25 -0
- package/dist/durable.js +638 -0
- package/dist/durable.js.map +7 -0
- package/dist/index.js +248 -159
- package/dist/index.js.map +4 -4
- package/dist/index.ts +244 -16
- package/dist/index.ts.map +4 -4
- package/dist/library-chunks/chunk-B64FXCNE.js +84 -0
- package/dist/library-chunks/chunk-B64FXCNE.js.map +7 -0
- package/dist/library-chunks/chunk-ETFOPFEP.js +10 -0
- package/dist/library-chunks/chunk-ETFOPFEP.js.map +7 -0
- package/dist/library-chunks/chunk-JPFN6GGS.js +153 -0
- package/dist/library-chunks/chunk-JPFN6GGS.js.map +7 -0
- package/dist/library-chunks/{runtime-A6SQ6VEH.js → runtime-6KYOZVOE.js} +5 -3
- package/dist/library-chunks/runtime-6KYOZVOE.js.map +7 -0
- package/dist/public-api.d.ts +1 -3
- package/dist/runtime-api.d.ts +3 -0
- package/dist/runtime-defaults.d.ts +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/sanitizer.d.ts +1 -1
- package/docs/durable.md +126 -0
- package/package.json +22 -2
- package/src/config.ts +4 -1
- package/src/durable-projection.ts +106 -0
- package/src/durable-recorder.ts +493 -0
- package/src/durable.ts +195 -0
- package/src/public-api.ts +6 -12
- package/src/runtime-api.ts +8 -0
- package/src/runtime-core.ts +2 -1
- package/src/runtime-defaults.ts +1 -0
- package/src/runtime.ts +2 -1
- package/src/sanitizer.ts +16 -4
- package/dist/chunks/chunk-DBQAWNKG.ts +0 -316
- package/dist/chunks/chunk-DBQAWNKG.ts.map +0 -7
- package/dist/chunks/runtime-6ZHLVC3A.ts.map +0 -7
- package/dist/library-chunks/chunk-SAJ76IYU.js +0 -316
- package/dist/library-chunks/chunk-SAJ76IYU.js.map +0 -7
- package/dist/library-chunks/runtime-A6SQ6VEH.js.map +0 -7
package/dist/durable.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
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
|
+
createLangfuseRuntime,
|
|
5
|
+
sanitizeTraceValue
|
|
6
|
+
} from "./library-chunks/chunk-JPFN6GGS.js";
|
|
7
|
+
import {
|
|
8
|
+
createLangfuseRuntimeFromBackend,
|
|
9
|
+
getLangfuseRuntimeInternal
|
|
10
|
+
} from "./library-chunks/chunk-B64FXCNE.js";
|
|
11
|
+
|
|
12
|
+
// src/durable.ts
|
|
13
|
+
import { withCancel } from "@earendil-works/chord/context";
|
|
14
|
+
import {
|
|
15
|
+
watchEvents
|
|
16
|
+
} from "@earendil-works/pi-durable";
|
|
17
|
+
|
|
18
|
+
// src/durable-projection.ts
|
|
19
|
+
var MAX_PROJECTED_ITEMS = 199;
|
|
20
|
+
function durableAttributes(attributes) {
|
|
21
|
+
const copy = { ...attributes };
|
|
22
|
+
if (attributes.metadata) {
|
|
23
|
+
const value = sanitizeTraceValue(attributes.metadata, true);
|
|
24
|
+
copy.metadata = value && typeof value === "object" && !Array.isArray(value) ? value : { "pi.durable.metadata_omitted": true };
|
|
25
|
+
}
|
|
26
|
+
for (const key of ["name", "model", "sessionId", "userId", "statusMessage", "version"]) {
|
|
27
|
+
if (typeof copy[key] === "string") copy[key] = String(sanitizeTraceValue(copy[key], true));
|
|
28
|
+
}
|
|
29
|
+
return copy;
|
|
30
|
+
}
|
|
31
|
+
function durableDiagnosticCount(entry) {
|
|
32
|
+
const data = entry?.data;
|
|
33
|
+
return data && typeof data === "object" && !Array.isArray(data) && "diagnostics" in data && Array.isArray(data.diagnostics) ? data.diagnostics.length : 0;
|
|
34
|
+
}
|
|
35
|
+
function durableContent(messages, capture, diagnosticCount = 0) {
|
|
36
|
+
if (!capture) return "[content capture disabled]";
|
|
37
|
+
const projected = messages?.slice(0, MAX_PROJECTED_ITEMS).map((message) => {
|
|
38
|
+
if (message.role === "toolResult" && message.isError)
|
|
39
|
+
return { role: message.role, content: "[tool error content omitted]" };
|
|
40
|
+
if (message.role === "assistant" && message.stopReason === "error")
|
|
41
|
+
return { role: message.role, content: "[assistant error content omitted]" };
|
|
42
|
+
const omitDiagnosticBlock = message.role === "toolResult" && diagnosticCount > 0 && message.content.at(-1)?.type === "text";
|
|
43
|
+
const blockCount = message.content.length - (omitDiagnosticBlock ? 1 : 0);
|
|
44
|
+
return {
|
|
45
|
+
role: message.role,
|
|
46
|
+
content: typeof message.content === "string" ? message.content : message.content.slice(0, Math.min(MAX_PROJECTED_ITEMS, blockCount)).map((block) => {
|
|
47
|
+
switch (block.type) {
|
|
48
|
+
case "text":
|
|
49
|
+
return { type: "text", text: block.text };
|
|
50
|
+
case "thinking":
|
|
51
|
+
return { type: "thinking", thinking: block.thinking };
|
|
52
|
+
case "toolCall":
|
|
53
|
+
return { type: "toolCall", id: block.id, name: block.name, arguments: block.arguments };
|
|
54
|
+
case "image":
|
|
55
|
+
return { type: "image", mimeType: block.mimeType, data: "[base64 omitted]" };
|
|
56
|
+
default:
|
|
57
|
+
return { type: "unsupported", content: "[block omitted]" };
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
...Array.isArray(message.content) && blockCount > MAX_PROJECTED_ITEMS ? { blocksOmitted: blockCount - MAX_PROJECTED_ITEMS } : {},
|
|
61
|
+
...omitDiagnosticBlock ? { diagnosticsOmitted: diagnosticCount } : {}
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
if (messages && messages.length > MAX_PROJECTED_ITEMS)
|
|
65
|
+
projected?.push({ messagesOmitted: messages.length - MAX_PROJECTED_ITEMS });
|
|
66
|
+
return sanitizeTraceValue(projected, true);
|
|
67
|
+
}
|
|
68
|
+
function durableUsage(message) {
|
|
69
|
+
if (message.role !== "assistant" && message.role !== "toolResult" || !message.usage) return {};
|
|
70
|
+
const usageDetails = {};
|
|
71
|
+
const costDetails = {};
|
|
72
|
+
for (const [key, field] of [
|
|
73
|
+
["input", "input"],
|
|
74
|
+
["output", "output"],
|
|
75
|
+
["cacheRead", "cache_read_input_tokens"],
|
|
76
|
+
["cacheWrite", "cache_creation_input_tokens"],
|
|
77
|
+
["totalTokens", "total"]
|
|
78
|
+
]) {
|
|
79
|
+
const value = message.usage[key];
|
|
80
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) usageDetails[field] = value;
|
|
81
|
+
}
|
|
82
|
+
for (const [key, field] of [
|
|
83
|
+
["input", "input"],
|
|
84
|
+
["output", "output"],
|
|
85
|
+
["cacheRead", "cache_read"],
|
|
86
|
+
["cacheWrite", "cache_write"],
|
|
87
|
+
["total", "total"]
|
|
88
|
+
]) {
|
|
89
|
+
const value = message.usage.cost[key];
|
|
90
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) costDetails[field] = value;
|
|
91
|
+
}
|
|
92
|
+
return { usageDetails, costDetails };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/durable-recorder.ts
|
|
96
|
+
var SAFE_UNANSWERED_REASONS = /* @__PURE__ */ new Set([
|
|
97
|
+
"aborted",
|
|
98
|
+
"model_error",
|
|
99
|
+
"no_model",
|
|
100
|
+
"reset",
|
|
101
|
+
"stale",
|
|
102
|
+
"faulted",
|
|
103
|
+
"missing_task",
|
|
104
|
+
"task_too_old",
|
|
105
|
+
"migration_failed"
|
|
106
|
+
]);
|
|
107
|
+
var DurableRecorder = class {
|
|
108
|
+
constructor(backend, options) {
|
|
109
|
+
this.backend = backend;
|
|
110
|
+
this.options = options;
|
|
111
|
+
this.root = this.start(options.traceName ?? "pi.durable.conversation", "agent", void 0, {
|
|
112
|
+
metadata: {
|
|
113
|
+
...this.customMetadata(),
|
|
114
|
+
"pi.runtime": "durable",
|
|
115
|
+
"pi.durable.schema_version": "1",
|
|
116
|
+
"pi.durable.conversation_id": options.conversationId,
|
|
117
|
+
...options.applicationId ? { "pi.application.id": options.applicationId } : {}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
this.safe(() => {
|
|
121
|
+
this.root.observation?.updateTrace?.(
|
|
122
|
+
durableAttributes({
|
|
123
|
+
name: options.traceName ?? "pi.durable.conversation",
|
|
124
|
+
sessionId: options.sessionId ?? String(options.conversationId),
|
|
125
|
+
...options.userId ? { userId: options.userId } : {},
|
|
126
|
+
metadata: this.customMetadata(),
|
|
127
|
+
version: "durable-1"
|
|
128
|
+
})
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
const traceId = this.root.observation?.traceId;
|
|
132
|
+
if (traceId) this.callback("trace-id callback", () => options.onTraceId?.(traceId));
|
|
133
|
+
}
|
|
134
|
+
backend;
|
|
135
|
+
options;
|
|
136
|
+
root;
|
|
137
|
+
run;
|
|
138
|
+
submissions = /* @__PURE__ */ new Map();
|
|
139
|
+
pendingSubmissions = /* @__PURE__ */ new Set();
|
|
140
|
+
entries = /* @__PURE__ */ new Set();
|
|
141
|
+
completedRuns = /* @__PURE__ */ new Set();
|
|
142
|
+
disposed = false;
|
|
143
|
+
gaps = 0;
|
|
144
|
+
get submissionIds() {
|
|
145
|
+
return [...this.pendingSubmissions];
|
|
146
|
+
}
|
|
147
|
+
report(stage) {
|
|
148
|
+
this.callback("error callback", () => this.options.onError?.(stage), false);
|
|
149
|
+
}
|
|
150
|
+
callback(stage, action, report = true) {
|
|
151
|
+
try {
|
|
152
|
+
Promise.resolve(action()).catch(() => {
|
|
153
|
+
if (report) this.report(stage);
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
if (report) this.report(stage);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
customMetadata() {
|
|
160
|
+
const value = sanitizeTraceValue(this.options.metadata, true);
|
|
161
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
162
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !key.startsWith("pi.")));
|
|
163
|
+
}
|
|
164
|
+
safe(action) {
|
|
165
|
+
try {
|
|
166
|
+
action();
|
|
167
|
+
} catch {
|
|
168
|
+
this.report("exporter observation");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
start(name, type, parent, attributes = {}) {
|
|
172
|
+
if (this.disposed || parent && (parent.ended || !parent.observation)) return { ended: true };
|
|
173
|
+
const span = { ended: false };
|
|
174
|
+
const inherited = parent === this.run?.span ? this.correlationAttributes(this.run?.correlation ?? {}) : {};
|
|
175
|
+
this.safe(() => {
|
|
176
|
+
span.observation = this.backend.start(
|
|
177
|
+
String(sanitizeTraceValue(name, true)),
|
|
178
|
+
durableAttributes({
|
|
179
|
+
version: "durable-1",
|
|
180
|
+
sessionId: this.options.sessionId ?? String(this.options.conversationId),
|
|
181
|
+
...this.options.userId ? { userId: this.options.userId } : {},
|
|
182
|
+
...inherited,
|
|
183
|
+
...attributes,
|
|
184
|
+
metadata: { ...inherited.metadata, ...attributes.metadata }
|
|
185
|
+
}),
|
|
186
|
+
{ asType: type, parent: parent?.observation }
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
if (!span.observation) span.ended = true;
|
|
190
|
+
if (this.disposed) this.interrupted(span, "Observer disposed during exporter start.");
|
|
191
|
+
return span;
|
|
192
|
+
}
|
|
193
|
+
update(span, attributes) {
|
|
194
|
+
if (!span.ended)
|
|
195
|
+
this.safe(() => {
|
|
196
|
+
span.observation?.update(durableAttributes(attributes));
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
end(span, attributes = {}) {
|
|
200
|
+
if (!span || span.ended) return;
|
|
201
|
+
span.ended = true;
|
|
202
|
+
const observation = span.observation;
|
|
203
|
+
span.observation = void 0;
|
|
204
|
+
this.safe(() => {
|
|
205
|
+
observation?.update(durableAttributes(attributes));
|
|
206
|
+
});
|
|
207
|
+
this.safe(() => {
|
|
208
|
+
observation?.end();
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
interrupted(span, reason) {
|
|
212
|
+
this.end(span, {
|
|
213
|
+
level: "WARNING",
|
|
214
|
+
statusMessage: reason,
|
|
215
|
+
metadata: { "pi.durable.outcome": "observation_interrupted" }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
correlationAttributes(correlation) {
|
|
219
|
+
return {
|
|
220
|
+
metadata: {
|
|
221
|
+
...correlation.userId ? { "pi.user.id": correlation.userId } : {},
|
|
222
|
+
...correlation.requestId ? { "pi.request.id": correlation.requestId } : {},
|
|
223
|
+
...correlation.applicationId ? { "pi.application.id": correlation.applicationId } : {}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async submission(record, readEntry, correlation, snapshot = false) {
|
|
228
|
+
if (this.disposed || record.conversationId !== this.options.conversationId) return;
|
|
229
|
+
let state = this.submissions.get(record.id);
|
|
230
|
+
if (!state) {
|
|
231
|
+
let host = {};
|
|
232
|
+
try {
|
|
233
|
+
host = this.options.submissionCorrelation?.(record) ?? {};
|
|
234
|
+
} catch {
|
|
235
|
+
this.report("submission correlation");
|
|
236
|
+
}
|
|
237
|
+
if (this.disposed) return;
|
|
238
|
+
const ids = sanitizeTraceValue(
|
|
239
|
+
{
|
|
240
|
+
requestId: record.requestId,
|
|
241
|
+
userId: this.options.userId,
|
|
242
|
+
applicationId: this.options.applicationId,
|
|
243
|
+
...host,
|
|
244
|
+
...correlation
|
|
245
|
+
},
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
state = {
|
|
249
|
+
span: this.start("pi.durable.submission", "span", this.root, {
|
|
250
|
+
...this.correlationAttributes(ids),
|
|
251
|
+
metadata: {
|
|
252
|
+
...this.correlationAttributes(ids).metadata,
|
|
253
|
+
"pi.durable.submission_id": record.id,
|
|
254
|
+
"pi.durable.submission_type": record.type,
|
|
255
|
+
"pi.durable.snapshot_observation": snapshot
|
|
256
|
+
}
|
|
257
|
+
}),
|
|
258
|
+
correlation: ids,
|
|
259
|
+
terminal: false
|
|
260
|
+
};
|
|
261
|
+
if (this.disposed) return;
|
|
262
|
+
this.submissions.set(record.id, state);
|
|
263
|
+
this.pendingSubmissions.add(record.id);
|
|
264
|
+
}
|
|
265
|
+
if (state.terminal) return;
|
|
266
|
+
if (!snapshot && record.status === "placed" && this.run && !this.run.inputs.includes(record.id)) {
|
|
267
|
+
this.run.inputs = [...this.run.inputs, record.id];
|
|
268
|
+
this.update(this.run.span, { metadata: { "pi.durable.inputs": this.run.inputs } });
|
|
269
|
+
}
|
|
270
|
+
if (correlation) {
|
|
271
|
+
state.correlation = sanitizeTraceValue({ ...state.correlation, ...correlation }, true);
|
|
272
|
+
this.update(state.span, this.correlationAttributes(state.correlation));
|
|
273
|
+
}
|
|
274
|
+
const run = this.run;
|
|
275
|
+
if (run?.primary === record.id) {
|
|
276
|
+
run.correlation = state.correlation;
|
|
277
|
+
const attributes = this.correlationAttributes(state.correlation);
|
|
278
|
+
this.update(run.span, attributes);
|
|
279
|
+
if (run.generation) this.update(run.generation, attributes);
|
|
280
|
+
for (const tool of run.tools.values()) this.update(tool, attributes);
|
|
281
|
+
}
|
|
282
|
+
this.update(state.span, { metadata: { "pi.durable.submission_status": record.status } });
|
|
283
|
+
if (record.entry !== void 0 && record.type === "input") {
|
|
284
|
+
const entry = await readEntry(record.entry);
|
|
285
|
+
if (this.disposed || state.terminal) return;
|
|
286
|
+
this.update(state.span, { input: durableContent(entry?.model, this.options.captureContent ?? true) });
|
|
287
|
+
}
|
|
288
|
+
if (record.status === "done" || record.status === "unanswered") {
|
|
289
|
+
let output;
|
|
290
|
+
if (record.status === "done" && record.type === "input") {
|
|
291
|
+
const entry = await readEntry(record.answer);
|
|
292
|
+
if (this.disposed || state.terminal) return;
|
|
293
|
+
output = durableContent(entry?.model, this.options.captureContent ?? true);
|
|
294
|
+
}
|
|
295
|
+
const reason = record.status === "unanswered" && SAFE_UNANSWERED_REASONS.has(record.reason) ? record.reason : void 0;
|
|
296
|
+
state.terminal = true;
|
|
297
|
+
this.pendingSubmissions.delete(record.id);
|
|
298
|
+
this.end(state.span, {
|
|
299
|
+
...output !== void 0 ? { output } : {},
|
|
300
|
+
level: record.status === "done" ? "DEFAULT" : reason === "model_error" || reason === "no_model" || reason === "faulted" ? "ERROR" : "WARNING",
|
|
301
|
+
// Only known structural reason codes are safe; arbitrary reason/detail can contain provider secrets.
|
|
302
|
+
metadata: {
|
|
303
|
+
"pi.durable.submission_status": record.status,
|
|
304
|
+
"pi.durable.answer_entry_id": record.answer,
|
|
305
|
+
...record.status === "unanswered" ? { "pi.durable.unanswered_reason": reason ?? "unknown" } : {}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
snapshot(snapshot, initial = false) {
|
|
311
|
+
if (this.disposed) return;
|
|
312
|
+
for (const entry of snapshot.entries) this.entries.add(entry.id);
|
|
313
|
+
if (!initial) {
|
|
314
|
+
this.gaps += 1;
|
|
315
|
+
this.update(this.root, {
|
|
316
|
+
metadata: { "pi.durable.resync_count": this.gaps, "pi.durable.history_complete": false }
|
|
317
|
+
});
|
|
318
|
+
} else
|
|
319
|
+
this.update(this.root, {
|
|
320
|
+
metadata: { "pi.durable.history_complete": false, "pi.durable.attachment": "snapshot" }
|
|
321
|
+
});
|
|
322
|
+
const primary = snapshot.run?.inputs[0];
|
|
323
|
+
if (this.run && this.run.primary !== primary)
|
|
324
|
+
this.closeRun("Run boundary not observed; snapshot replaced the observation.");
|
|
325
|
+
if (this.run) this.closeRound("Snapshot replaced generation/tool observation; activity may have been missed.");
|
|
326
|
+
if (snapshot.run) {
|
|
327
|
+
this.beginRun(snapshot.run.inputs, true);
|
|
328
|
+
if (this.run) this.update(this.run.span, { metadata: { "pi.durable.observation_incomplete": true } });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
beginRun(inputs, snapshot = false) {
|
|
332
|
+
const primary = inputs[0];
|
|
333
|
+
if (primary === void 0 || this.completedRuns.has(primary)) return;
|
|
334
|
+
if (this.run?.primary === primary) {
|
|
335
|
+
this.run.inputs = [...inputs];
|
|
336
|
+
this.update(this.run.span, { metadata: { "pi.durable.inputs": this.run.inputs } });
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.run) this.closeRun("Unexpected run replacement.");
|
|
340
|
+
const correlation = this.submissions.get(primary)?.correlation ?? {};
|
|
341
|
+
this.run = {
|
|
342
|
+
span: this.start("pi.durable.run", "agent", this.root, {
|
|
343
|
+
...this.correlationAttributes(correlation),
|
|
344
|
+
metadata: {
|
|
345
|
+
...this.correlationAttributes(correlation).metadata,
|
|
346
|
+
"pi.durable.primary_submission_id": primary,
|
|
347
|
+
"pi.durable.inputs": [...inputs],
|
|
348
|
+
"pi.durable.snapshot_observation": snapshot
|
|
349
|
+
}
|
|
350
|
+
}),
|
|
351
|
+
primary,
|
|
352
|
+
inputs: [...inputs],
|
|
353
|
+
correlation,
|
|
354
|
+
round: 0,
|
|
355
|
+
tools: /* @__PURE__ */ new Map(),
|
|
356
|
+
completedTools: /* @__PURE__ */ new Set()
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
closeRound(reason) {
|
|
360
|
+
const run = this.run;
|
|
361
|
+
if (!run) return;
|
|
362
|
+
this.interrupted(run.generation, reason);
|
|
363
|
+
run.generation = void 0;
|
|
364
|
+
for (const tool of run.tools.values()) this.interrupted(tool, reason);
|
|
365
|
+
run.tools.clear();
|
|
366
|
+
}
|
|
367
|
+
closeRun(reason) {
|
|
368
|
+
const run = this.run;
|
|
369
|
+
if (!run) return;
|
|
370
|
+
this.closeRound(reason ?? "Run ended without an observed descendant completion.");
|
|
371
|
+
this.end(
|
|
372
|
+
run.span,
|
|
373
|
+
reason ? { level: "WARNING", statusMessage: reason, metadata: { "pi.durable.observation_incomplete": true } } : {}
|
|
374
|
+
);
|
|
375
|
+
this.completedRuns.add(run.primary);
|
|
376
|
+
this.run = void 0;
|
|
377
|
+
}
|
|
378
|
+
generation() {
|
|
379
|
+
const run = this.run;
|
|
380
|
+
if (!run) return void 0;
|
|
381
|
+
if (!run.generation) {
|
|
382
|
+
run.round += 1;
|
|
383
|
+
run.generation = this.start("pi.durable.llm", "generation", run.span, {
|
|
384
|
+
metadata: { "pi.durable.observed_generation_index": run.round }
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return run.generation;
|
|
388
|
+
}
|
|
389
|
+
event(event) {
|
|
390
|
+
if (this.disposed) return;
|
|
391
|
+
switch (event.type) {
|
|
392
|
+
case "run_start":
|
|
393
|
+
this.beginRun(event.inputs);
|
|
394
|
+
break;
|
|
395
|
+
case "run_end":
|
|
396
|
+
if (this.run?.primary === event.inputs[0]) this.closeRun();
|
|
397
|
+
break;
|
|
398
|
+
case "message_start":
|
|
399
|
+
if (event.message.role === "assistant") this.generation();
|
|
400
|
+
break;
|
|
401
|
+
case "message_end": {
|
|
402
|
+
const entry = event.entry;
|
|
403
|
+
if (this.entries.has(entry.id)) break;
|
|
404
|
+
this.entries.add(entry.id);
|
|
405
|
+
const message = entry.model?.[0];
|
|
406
|
+
if (message?.role !== "assistant") break;
|
|
407
|
+
const generation = this.generation();
|
|
408
|
+
if (!generation) break;
|
|
409
|
+
this.end(generation, {
|
|
410
|
+
output: durableContent(entry.model, this.options.captureContent ?? true),
|
|
411
|
+
model: message.model,
|
|
412
|
+
...durableUsage(message),
|
|
413
|
+
level: message.stopReason === "error" ? "ERROR" : message.stopReason === "aborted" ? "WARNING" : "DEFAULT",
|
|
414
|
+
metadata: {
|
|
415
|
+
"pi.durable.entry_id": entry.id,
|
|
416
|
+
"pi.durable.task_id": entry.byTaskId,
|
|
417
|
+
"pi.durable.stop_reason": message.stopReason,
|
|
418
|
+
"pi.durable.provider": message.provider
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
if (this.run) this.run.generation = void 0;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
case "tool_execution_start": {
|
|
425
|
+
if (!this.run || this.run.tools.has(event.toolCallId) || this.run.completedTools.has(`${this.run.round}:${event.toolCallId}`))
|
|
426
|
+
break;
|
|
427
|
+
this.run.tools.set(
|
|
428
|
+
event.toolCallId,
|
|
429
|
+
this.start("pi.durable.tool", "tool", this.run.span, {
|
|
430
|
+
input: this.options.captureContent === false ? "[content capture disabled]" : sanitizeTraceValue(event.args, true),
|
|
431
|
+
metadata: {
|
|
432
|
+
"pi.durable.tool_call_id": event.toolCallId,
|
|
433
|
+
"pi.durable.tool_name": sanitizeTraceValue(event.toolName),
|
|
434
|
+
"pi.durable.observed_generation_index": this.run.round
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
);
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
case "tool_execution_end": {
|
|
441
|
+
const run = this.run;
|
|
442
|
+
if (!run) break;
|
|
443
|
+
const key = `${run.round}:${event.toolCallId}`;
|
|
444
|
+
if (run.completedTools.has(key) || event.entry && this.entries.has(event.entry.id)) break;
|
|
445
|
+
run.completedTools.add(key);
|
|
446
|
+
if (event.entry) this.entries.add(event.entry.id);
|
|
447
|
+
const tool = run.tools.get(event.toolCallId) ?? this.start("pi.durable.tool", "tool", run.span, {
|
|
448
|
+
metadata: { "pi.durable.start_not_observed": true, "pi.durable.tool_call_id": event.toolCallId }
|
|
449
|
+
});
|
|
450
|
+
const message = event.entry?.model?.[0];
|
|
451
|
+
this.end(tool, {
|
|
452
|
+
output: durableContent(
|
|
453
|
+
event.entry?.model,
|
|
454
|
+
this.options.captureContent ?? true,
|
|
455
|
+
durableDiagnosticCount(event.entry)
|
|
456
|
+
),
|
|
457
|
+
...message ? durableUsage(message) : {},
|
|
458
|
+
level: message?.role === "toolResult" && message.isError ? "ERROR" : event.entry ? "DEFAULT" : "WARNING",
|
|
459
|
+
metadata: { "pi.durable.result_entry_id": event.entry?.id, "pi.durable.result_missing": !event.entry }
|
|
460
|
+
});
|
|
461
|
+
run.tools.delete(event.toolCallId);
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "auto_retry_start":
|
|
465
|
+
this.end(this.run?.generation, {
|
|
466
|
+
level: "ERROR",
|
|
467
|
+
statusMessage: "Provider attempt will retry.",
|
|
468
|
+
metadata: { "pi.durable.retry_attempt": event.attempt }
|
|
469
|
+
});
|
|
470
|
+
if (this.run) this.run.generation = void 0;
|
|
471
|
+
break;
|
|
472
|
+
case "task_failed":
|
|
473
|
+
if (this.run)
|
|
474
|
+
this.update(this.run.span, { level: "ERROR", metadata: { "pi.durable.failed_task_id": event.taskId } });
|
|
475
|
+
break;
|
|
476
|
+
// turn_end is a round boundary, never a submission boundary. Deltas and arbitrary diagnostics are not captured.
|
|
477
|
+
default:
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
dispose(reason = "Tracing observer disposed; execution outcome is unknown.") {
|
|
482
|
+
if (this.disposed) return;
|
|
483
|
+
this.disposed = true;
|
|
484
|
+
this.closeRun(reason);
|
|
485
|
+
for (const state of this.submissions.values()) if (!state.terminal) this.interrupted(state.span, reason);
|
|
486
|
+
this.end(this.root, { metadata: { "pi.durable.resync_count": this.gaps } });
|
|
487
|
+
this.submissions.clear();
|
|
488
|
+
this.pendingSubmissions.clear();
|
|
489
|
+
this.entries.clear();
|
|
490
|
+
this.completedRuns.clear();
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/durable.ts
|
|
495
|
+
var observedConversations = /* @__PURE__ */ new WeakMap();
|
|
496
|
+
async function createPiLangfuseDurableConversation(runtime, options) {
|
|
497
|
+
options = { ...options };
|
|
498
|
+
const { context, cancel } = withCancel(options.context);
|
|
499
|
+
let stream;
|
|
500
|
+
let recorder;
|
|
501
|
+
let releaseRuntime;
|
|
502
|
+
let disposed = false;
|
|
503
|
+
let ownsConversation = false;
|
|
504
|
+
let initialization = Promise.resolve();
|
|
505
|
+
let disposePromise;
|
|
506
|
+
let resolveClosed;
|
|
507
|
+
const closed = new Promise((resolve) => {
|
|
508
|
+
resolveClosed = resolve;
|
|
509
|
+
});
|
|
510
|
+
function report(stage) {
|
|
511
|
+
if (recorder) recorder.report(stage);
|
|
512
|
+
else {
|
|
513
|
+
try {
|
|
514
|
+
Promise.resolve(options.onError?.(stage)).catch(() => void 0);
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
async function readEntry(id) {
|
|
520
|
+
const entry = await options.harness.commit((tx) => tx.entry(id), context);
|
|
521
|
+
return disposed ? void 0 : entry;
|
|
522
|
+
}
|
|
523
|
+
async function observeSubmission(id, correlation) {
|
|
524
|
+
if (disposed) return;
|
|
525
|
+
try {
|
|
526
|
+
const submission = await options.harness.submission(id, context);
|
|
527
|
+
if (disposed || !submission) return;
|
|
528
|
+
const record = await submission.status(context);
|
|
529
|
+
if (disposed) return;
|
|
530
|
+
await recorder?.submission(record, readEntry, correlation, true);
|
|
531
|
+
} catch {
|
|
532
|
+
if (!disposed) report("submission status read");
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function dispose() {
|
|
536
|
+
if (disposePromise) return disposePromise;
|
|
537
|
+
disposed = true;
|
|
538
|
+
cancel();
|
|
539
|
+
if (ownsConversation) {
|
|
540
|
+
observedConversations.get(options.harness)?.delete(options.conversationId);
|
|
541
|
+
ownsConversation = false;
|
|
542
|
+
}
|
|
543
|
+
releaseRuntime?.();
|
|
544
|
+
releaseRuntime = void 0;
|
|
545
|
+
recorder?.dispose();
|
|
546
|
+
disposePromise = (async () => {
|
|
547
|
+
await initialization;
|
|
548
|
+
try {
|
|
549
|
+
await stream?.stop();
|
|
550
|
+
} catch {
|
|
551
|
+
report("observer stop");
|
|
552
|
+
}
|
|
553
|
+
resolveClosed();
|
|
554
|
+
})();
|
|
555
|
+
return disposePromise;
|
|
556
|
+
}
|
|
557
|
+
const controller = {
|
|
558
|
+
get active() {
|
|
559
|
+
return !disposed && !runtime.closed && stream !== void 0;
|
|
560
|
+
},
|
|
561
|
+
closed,
|
|
562
|
+
observeSubmission,
|
|
563
|
+
dispose
|
|
564
|
+
};
|
|
565
|
+
async function initialize() {
|
|
566
|
+
try {
|
|
567
|
+
const conversations = observedConversations.get(options.harness) ?? /* @__PURE__ */ new Set();
|
|
568
|
+
if (conversations.has(options.conversationId)) throw new Error("Conversation already has a tracing observer.");
|
|
569
|
+
conversations.add(options.conversationId);
|
|
570
|
+
observedConversations.set(options.harness, conversations);
|
|
571
|
+
ownsConversation = true;
|
|
572
|
+
const internal = getLangfuseRuntimeInternal(runtime);
|
|
573
|
+
releaseRuntime = internal.registerSession(dispose);
|
|
574
|
+
const attached = await watchEvents(options.harness, options.conversationId, context);
|
|
575
|
+
stream = attached;
|
|
576
|
+
if (disposed || runtime.closed) {
|
|
577
|
+
void dispose();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
recorder = new DurableRecorder(internal.backend, { ...options, metadata: { ...options.metadata } });
|
|
581
|
+
if (disposed || runtime.closed) {
|
|
582
|
+
recorder.dispose();
|
|
583
|
+
void dispose();
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
recorder.snapshot(stream.snapshot, true);
|
|
587
|
+
stream.start(async (events) => {
|
|
588
|
+
for (const event of events) {
|
|
589
|
+
if (disposed) return;
|
|
590
|
+
try {
|
|
591
|
+
if (event.type === "snapshot") {
|
|
592
|
+
recorder?.snapshot(event);
|
|
593
|
+
const ids2 = /* @__PURE__ */ new Set([
|
|
594
|
+
...recorder?.submissionIds ?? [],
|
|
595
|
+
...event.run?.inputs ?? [],
|
|
596
|
+
...event.inbox.map((item) => item.id)
|
|
597
|
+
]);
|
|
598
|
+
for (const id of ids2) {
|
|
599
|
+
await observeSubmission(id);
|
|
600
|
+
if (disposed) return;
|
|
601
|
+
}
|
|
602
|
+
} else if (event.type === "submission") {
|
|
603
|
+
await recorder?.submission(event.record, readEntry);
|
|
604
|
+
if (disposed) return;
|
|
605
|
+
} else recorder?.event(event);
|
|
606
|
+
} catch {
|
|
607
|
+
if (!disposed) report("observer delivery");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
void stream.closed.then(
|
|
612
|
+
() => dispose(),
|
|
613
|
+
() => {
|
|
614
|
+
report("observer closure");
|
|
615
|
+
return dispose();
|
|
616
|
+
}
|
|
617
|
+
).catch(() => void 0);
|
|
618
|
+
const ids = /* @__PURE__ */ new Set([...stream.snapshot.run?.inputs ?? [], ...stream.snapshot.inbox.map((item) => item.id)]);
|
|
619
|
+
for (const id of ids) {
|
|
620
|
+
await observeSubmission(id);
|
|
621
|
+
if (disposed) return;
|
|
622
|
+
}
|
|
623
|
+
} catch {
|
|
624
|
+
report("observer initialization");
|
|
625
|
+
void dispose();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
initialization = initialize();
|
|
629
|
+
await initialization;
|
|
630
|
+
if (disposed) await dispose();
|
|
631
|
+
return controller;
|
|
632
|
+
}
|
|
633
|
+
export {
|
|
634
|
+
createLangfuseRuntime,
|
|
635
|
+
createLangfuseRuntimeFromBackend,
|
|
636
|
+
createPiLangfuseDurableConversation
|
|
637
|
+
};
|
|
638
|
+
//# sourceMappingURL=durable.js.map
|