@narumitw/pi-langfuse 0.50.6 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -4
- package/dist/chunks/chunk-IASLJKKB.js +316 -0
- package/dist/chunks/chunk-IASLJKKB.js.map +7 -0
- package/dist/chunks/runtime-6MIKLWNJ.js +352 -0
- package/dist/chunks/runtime-6MIKLWNJ.js.map +7 -0
- package/dist/config.d.ts +33 -0
- package/dist/git.d.ts +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1551 -0
- package/dist/index.js.map +7 -0
- package/dist/index.ts +588 -479
- package/dist/index.ts.map +4 -4
- package/dist/langfuse.d.ts +13 -0
- package/dist/pi-session.d.ts +43 -0
- package/dist/public-api.d.ts +4 -0
- package/dist/runtime-core.d.ts +26 -0
- package/dist/runtime.d.ts +22 -0
- package/dist/sanitizer.d.ts +2 -0
- package/dist/tracing.d.ts +202 -0
- package/package.json +11 -3
- package/src/git.ts +60 -0
- package/src/index.ts +9 -0
- package/src/langfuse.ts +88 -292
- package/src/pi-session.ts +570 -0
- package/src/public-api.ts +17 -0
- package/src/runtime-core.ts +113 -0
- package/src/runtime.ts +234 -51
- package/src/sanitizer.ts +10 -7
- package/src/tracing.ts +52 -14
- package/dist/chunks/runtime-X75FRK2E.js +0 -215
- package/dist/chunks/runtime-X75FRK2E.js.map +0 -7
package/src/runtime.ts
CHANGED
|
@@ -9,32 +9,62 @@ import {
|
|
|
9
9
|
} from "@langfuse/tracing";
|
|
10
10
|
import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
11
11
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
12
|
-
import type
|
|
12
|
+
import { DEFAULT_BASE_URL, type LangfuseConfig } from "./config.js";
|
|
13
|
+
import {
|
|
14
|
+
type CreateLangfuseRuntimeOptions,
|
|
15
|
+
createLangfuseRuntimeFromBackend,
|
|
16
|
+
getLangfuseRuntimeInternal,
|
|
17
|
+
type LangfuseRuntime,
|
|
18
|
+
type LangfuseRuntimeInternal,
|
|
19
|
+
} from "./runtime-core.js";
|
|
13
20
|
import type { Observation, ObservationAttributes, ObservationType, TraceBackend } from "./tracing.js";
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
export type {
|
|
23
|
+
CreateLangfuseRuntimeOptions,
|
|
24
|
+
LangfuseRuntime,
|
|
25
|
+
LangfuseRuntimeConfig,
|
|
26
|
+
} from "./runtime-core.js";
|
|
27
|
+
|
|
28
|
+
const RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v2");
|
|
29
|
+
const LEGACY_RUNTIME_KEY = Symbol.for("@narumitw/pi-langfuse/runtime/v1");
|
|
30
|
+
|
|
31
|
+
interface ResolvedRuntimeConfig {
|
|
32
|
+
publicKey: string;
|
|
33
|
+
secretKey: string;
|
|
34
|
+
baseUrl: string;
|
|
35
|
+
environment?: string;
|
|
36
|
+
release?: string;
|
|
37
|
+
}
|
|
16
38
|
|
|
17
39
|
interface SharedRuntime {
|
|
18
40
|
fingerprint: string;
|
|
19
|
-
|
|
20
|
-
shutdown:
|
|
41
|
+
runtime: LangfuseRuntimeInternal;
|
|
42
|
+
shutdown: true;
|
|
21
43
|
}
|
|
22
44
|
|
|
23
45
|
type GlobalWithRuntime = typeof globalThis & {
|
|
24
46
|
[RUNTIME_KEY]?: Promise<SharedRuntime>;
|
|
47
|
+
[LEGACY_RUNTIME_KEY]?: unknown;
|
|
25
48
|
};
|
|
26
49
|
|
|
27
50
|
export interface RuntimeFactories {
|
|
28
|
-
createProcessor(config:
|
|
51
|
+
createProcessor(config: ResolvedRuntimeConfig): SpanProcessor;
|
|
29
52
|
createProvider(processor: SpanProcessor): NodeTracerProvider;
|
|
30
53
|
selectProvider(provider: NodeTracerProvider): void;
|
|
31
54
|
}
|
|
32
55
|
|
|
33
56
|
class ProductionObservation implements Observation {
|
|
34
|
-
|
|
57
|
+
readonly traceId: string;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
readonly native: LangfuseObservation,
|
|
61
|
+
private readonly secrets: readonly string[],
|
|
62
|
+
) {
|
|
63
|
+
this.traceId = native.traceId;
|
|
64
|
+
}
|
|
35
65
|
|
|
36
66
|
update(attributes: ObservationAttributes): Observation {
|
|
37
|
-
const { sessionId, userId, ...observationAttributes } = attributes;
|
|
67
|
+
const { sessionId, userId, ...observationAttributes } = maskObservationAttributes(attributes, this.secrets);
|
|
38
68
|
this.native.updateOtelSpanAttributes(observationAttributes as LangfuseObservationAttributes);
|
|
39
69
|
applySessionId(this.native, sessionId);
|
|
40
70
|
applyUserId(this.native, userId);
|
|
@@ -42,7 +72,10 @@ class ProductionObservation implements Observation {
|
|
|
42
72
|
}
|
|
43
73
|
|
|
44
74
|
updateTrace(attributes: ObservationAttributes): Observation {
|
|
45
|
-
const { input, output, metadata, name, sessionId, userId, tags, version } =
|
|
75
|
+
const { input, output, metadata, name, sessionId, userId, tags, version } = maskObservationAttributes(
|
|
76
|
+
attributes,
|
|
77
|
+
this.secrets,
|
|
78
|
+
);
|
|
46
79
|
if (input !== undefined || output !== undefined) {
|
|
47
80
|
this.native.setTraceIO({ input, output });
|
|
48
81
|
}
|
|
@@ -58,7 +91,10 @@ class ProductionObservation implements Observation {
|
|
|
58
91
|
this.native.otelSpan.setAttribute(LangfuseOtelSpanAttributes.VERSION, version);
|
|
59
92
|
}
|
|
60
93
|
for (const [key, value] of Object.entries(metadata ?? {})) {
|
|
61
|
-
|
|
94
|
+
const serialized = serializeMetadataValue(value);
|
|
95
|
+
if (serialized !== undefined) {
|
|
96
|
+
this.native.otelSpan.setAttribute(`${LangfuseOtelSpanAttributes.TRACE_METADATA}.${key}`, serialized);
|
|
97
|
+
}
|
|
62
98
|
}
|
|
63
99
|
return this;
|
|
64
100
|
}
|
|
@@ -73,22 +109,24 @@ class ProductionTraceBackend implements TraceBackend {
|
|
|
73
109
|
constructor(
|
|
74
110
|
private readonly provider: NodeTracerProvider,
|
|
75
111
|
private readonly processor: SpanProcessor,
|
|
112
|
+
private readonly secrets: readonly string[],
|
|
76
113
|
) {}
|
|
77
114
|
|
|
78
115
|
start(
|
|
79
116
|
name: string,
|
|
80
117
|
attributes: ObservationAttributes,
|
|
81
|
-
options: { asType: ObservationType; parent?: Observation },
|
|
118
|
+
options: { asType: ObservationType; parent?: Observation; startTime?: Date },
|
|
82
119
|
): Observation {
|
|
83
|
-
const
|
|
120
|
+
const maskedName = maskSecretString(name, this.secrets);
|
|
121
|
+
const { sessionId, userId, ...observationAttributes } = maskObservationAttributes(attributes, this.secrets);
|
|
84
122
|
const parent = options.parent;
|
|
85
123
|
const native =
|
|
86
124
|
parent instanceof ProductionObservation
|
|
87
|
-
? startChild(parent.native,
|
|
88
|
-
: startRoot(
|
|
125
|
+
? startChild(parent.native, maskedName, observationAttributes, options.asType, options.startTime)
|
|
126
|
+
: startRoot(maskedName, observationAttributes, options.asType, options.startTime);
|
|
89
127
|
applySessionId(native, sessionId);
|
|
90
128
|
applyUserId(native, userId);
|
|
91
|
-
return new ProductionObservation(native);
|
|
129
|
+
return new ProductionObservation(native, this.secrets);
|
|
92
130
|
}
|
|
93
131
|
|
|
94
132
|
async forceFlush(): Promise<void> {
|
|
@@ -100,37 +138,58 @@ class ProductionTraceBackend implements TraceBackend {
|
|
|
100
138
|
}
|
|
101
139
|
}
|
|
102
140
|
|
|
141
|
+
export async function createLangfuseRuntime(options: CreateLangfuseRuntimeOptions = {}): Promise<LangfuseRuntime> {
|
|
142
|
+
return createSharedRuntime(resolveLangfuseRuntimeConfig(options), defaultFactories);
|
|
143
|
+
}
|
|
144
|
+
|
|
103
145
|
export async function createProductionBackend(
|
|
104
146
|
config: LangfuseConfig,
|
|
105
147
|
factoryOverrides: Partial<RuntimeFactories> = {},
|
|
106
148
|
): Promise<TraceBackend> {
|
|
149
|
+
const runtime = await createSharedRuntime(resolveLangfuseRuntimeConfig({ config, env: false }), {
|
|
150
|
+
...defaultFactories,
|
|
151
|
+
...factoryOverrides,
|
|
152
|
+
});
|
|
153
|
+
return getLangfuseRuntimeInternal(runtime).backend;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function createSharedRuntime(
|
|
157
|
+
config: ResolvedRuntimeConfig,
|
|
158
|
+
factories: RuntimeFactories,
|
|
159
|
+
): Promise<LangfuseRuntime> {
|
|
107
160
|
const globalRuntime = globalThis as GlobalWithRuntime;
|
|
108
161
|
const fingerprint = configFingerprint(config);
|
|
109
162
|
const existing = globalRuntime[RUNTIME_KEY];
|
|
163
|
+
const legacyRuntime = globalRuntime[LEGACY_RUNTIME_KEY];
|
|
164
|
+
if (legacyRuntime !== undefined && legacyRuntime !== existing) {
|
|
165
|
+
throw new Error("An older Langfuse runtime is already loaded; restart the process before enabling this version.");
|
|
166
|
+
}
|
|
110
167
|
if (existing) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
168
|
+
if (legacyRuntime === undefined) globalRuntime[LEGACY_RUNTIME_KEY] = existing;
|
|
169
|
+
const shared = await existing;
|
|
170
|
+
if (shared.runtime.closed) {
|
|
171
|
+
throw new Error("Langfuse tracing was already shut down; restart the process to enable it again.");
|
|
114
172
|
}
|
|
115
|
-
if (
|
|
116
|
-
throw new Error("Langfuse configuration changed; restart
|
|
173
|
+
if (shared.fingerprint !== fingerprint) {
|
|
174
|
+
throw new Error("Langfuse configuration changed; restart the process to apply the new credentials.");
|
|
117
175
|
}
|
|
118
|
-
return runtime
|
|
176
|
+
return shared.runtime;
|
|
119
177
|
}
|
|
120
178
|
|
|
121
|
-
const factories = { ...defaultFactories, ...factoryOverrides };
|
|
122
179
|
const initializing = initializeRuntime(config, fingerprint, factories);
|
|
123
180
|
globalRuntime[RUNTIME_KEY] = initializing;
|
|
181
|
+
globalRuntime[LEGACY_RUNTIME_KEY] = initializing;
|
|
124
182
|
try {
|
|
125
|
-
return (await initializing).
|
|
183
|
+
return (await initializing).runtime;
|
|
126
184
|
} catch (error) {
|
|
127
|
-
delete globalRuntime[RUNTIME_KEY];
|
|
185
|
+
if (globalRuntime[RUNTIME_KEY] === initializing) delete globalRuntime[RUNTIME_KEY];
|
|
186
|
+
if (globalRuntime[LEGACY_RUNTIME_KEY] === initializing) delete globalRuntime[LEGACY_RUNTIME_KEY];
|
|
128
187
|
throw error;
|
|
129
188
|
}
|
|
130
189
|
}
|
|
131
190
|
|
|
132
191
|
async function initializeRuntime(
|
|
133
|
-
config:
|
|
192
|
+
config: ResolvedRuntimeConfig,
|
|
134
193
|
fingerprint: string,
|
|
135
194
|
factories: RuntimeFactories,
|
|
136
195
|
): Promise<SharedRuntime> {
|
|
@@ -143,15 +202,12 @@ async function initializeRuntime(
|
|
|
143
202
|
await (provider?.shutdown() ?? processor.shutdown()).catch(() => undefined);
|
|
144
203
|
throw error;
|
|
145
204
|
}
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await originalShutdown();
|
|
153
|
-
};
|
|
154
|
-
return runtime;
|
|
205
|
+
const runtime = getLangfuseRuntimeInternal(
|
|
206
|
+
createLangfuseRuntimeFromBackend(
|
|
207
|
+
new ProductionTraceBackend(provider, processor, [config.publicKey, config.secretKey]),
|
|
208
|
+
),
|
|
209
|
+
);
|
|
210
|
+
return { fingerprint, runtime, shutdown: true };
|
|
155
211
|
}
|
|
156
212
|
|
|
157
213
|
const defaultFactories: RuntimeFactories = {
|
|
@@ -174,6 +230,102 @@ const defaultFactories: RuntimeFactories = {
|
|
|
174
230
|
selectProvider: setLangfuseTracerProvider,
|
|
175
231
|
};
|
|
176
232
|
|
|
233
|
+
export function resolveLangfuseRuntimeConfig(options: CreateLangfuseRuntimeOptions): ResolvedRuntimeConfig {
|
|
234
|
+
const env = options.env === false ? undefined : (options.env ?? process.env);
|
|
235
|
+
const publicKey = requiredSetting("publicKey", options.config?.publicKey, env?.LANGFUSE_PUBLIC_KEY);
|
|
236
|
+
const secretKey = requiredSetting("secretKey", options.config?.secretKey, env?.LANGFUSE_SECRET_KEY);
|
|
237
|
+
const explicitBaseUrl = options.config?.baseUrl;
|
|
238
|
+
if (explicitBaseUrl !== undefined && !normalizeString(explicitBaseUrl)) {
|
|
239
|
+
throw new Error("Langfuse baseUrl must be a non-empty string.");
|
|
240
|
+
}
|
|
241
|
+
const rawBaseUrl = selectedSetting(explicitBaseUrl, env?.LANGFUSE_BASE_URL) ?? DEFAULT_BASE_URL;
|
|
242
|
+
const baseUrl = normalizeBaseUrl(rawBaseUrl);
|
|
243
|
+
if (!baseUrl) {
|
|
244
|
+
throw new Error("Langfuse baseUrl must use HTTP or HTTPS without credentials, a query, or a fragment.");
|
|
245
|
+
}
|
|
246
|
+
const environment = optionalSetting("environment", options.config?.environment, env?.LANGFUSE_TRACING_ENVIRONMENT);
|
|
247
|
+
if (environment && (environment.length > 40 || !/^(?!langfuse)[a-z0-9_-]+$/u.test(environment))) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"Langfuse environment must be at most 40 lowercase letters, numbers, hyphens, or underscores and must not start with langfuse.",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const release = optionalSetting("release", options.config?.release, env?.LANGFUSE_RELEASE);
|
|
253
|
+
return { publicKey, secretKey, baseUrl, ...(environment ? { environment } : {}), ...(release ? { release } : {}) };
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function requiredSetting(name: string, explicit: string | undefined, environment: string | undefined): string {
|
|
257
|
+
const value = selectedSetting(explicit, environment);
|
|
258
|
+
if (!value) throw new Error(`Langfuse ${name} is required.`);
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function optionalSetting(
|
|
263
|
+
name: string,
|
|
264
|
+
explicit: string | undefined,
|
|
265
|
+
environment: string | undefined,
|
|
266
|
+
): string | undefined {
|
|
267
|
+
const selected = explicit !== undefined ? explicit : environment;
|
|
268
|
+
if (selected === undefined) return undefined;
|
|
269
|
+
const value = normalizeString(selected);
|
|
270
|
+
if (!value) throw new Error(`Langfuse ${name} must be a non-empty string.`);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function selectedSetting(explicit: string | undefined, environment: string | undefined): string | undefined {
|
|
275
|
+
if (explicit !== undefined) return normalizeString(explicit);
|
|
276
|
+
return normalizeString(environment);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeBaseUrl(value: string): string | undefined {
|
|
280
|
+
try {
|
|
281
|
+
const url = new URL(value);
|
|
282
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
|
|
283
|
+
if (url.username || url.password || url.search || url.hash) return undefined;
|
|
284
|
+
return url.toString().replace(/\/+$/, "");
|
|
285
|
+
} catch {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizeString(value: unknown): string | undefined {
|
|
291
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function serializeMetadataValue(value: unknown): string | undefined {
|
|
295
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function maskObservationAttributes(
|
|
299
|
+
attributes: ObservationAttributes,
|
|
300
|
+
secrets: readonly string[],
|
|
301
|
+
): ObservationAttributes {
|
|
302
|
+
return {
|
|
303
|
+
...attributes,
|
|
304
|
+
...(attributes.metadata ? { metadata: maskSecrets(attributes.metadata, secrets) as Record<string, unknown> } : {}),
|
|
305
|
+
...(attributes.name ? { name: maskSecretString(attributes.name, secrets) } : {}),
|
|
306
|
+
...(attributes.sessionId ? { sessionId: maskSecretString(attributes.sessionId, secrets) } : {}),
|
|
307
|
+
...(attributes.userId ? { userId: maskSecretString(attributes.userId, secrets) } : {}),
|
|
308
|
+
...(attributes.tags ? { tags: attributes.tags.map((tag) => maskSecretString(tag, secrets)) } : {}),
|
|
309
|
+
...(attributes.statusMessage ? { statusMessage: maskSecretString(attributes.statusMessage, secrets) } : {}),
|
|
310
|
+
...(attributes.model ? { model: maskSecretString(attributes.model, secrets) } : {}),
|
|
311
|
+
...(attributes.version ? { version: maskSecretString(attributes.version, secrets) } : {}),
|
|
312
|
+
...(attributes.modelParameters
|
|
313
|
+
? {
|
|
314
|
+
modelParameters: Object.fromEntries(
|
|
315
|
+
Object.entries(attributes.modelParameters).map(([key, value]) => [
|
|
316
|
+
maskSecretString(key, secrets),
|
|
317
|
+
typeof value === "string" ? maskSecretString(value, secrets) : value,
|
|
318
|
+
]),
|
|
319
|
+
),
|
|
320
|
+
}
|
|
321
|
+
: {}),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function maskSecretString(value: string, secrets: readonly string[]): string {
|
|
326
|
+
return maskSecrets(value, secrets) as string;
|
|
327
|
+
}
|
|
328
|
+
|
|
177
329
|
function applySessionId(observation: LangfuseObservation, sessionId: string | undefined): void {
|
|
178
330
|
if (sessionId !== undefined) {
|
|
179
331
|
observation.otelSpan.setAttribute(LangfuseOtelSpanAttributes.TRACE_SESSION_ID, sessionId);
|
|
@@ -186,19 +338,25 @@ function applyUserId(observation: LangfuseObservation, userId: string | undefine
|
|
|
186
338
|
}
|
|
187
339
|
}
|
|
188
340
|
|
|
189
|
-
function startRoot(
|
|
341
|
+
function startRoot(
|
|
342
|
+
name: string,
|
|
343
|
+
attributes: ObservationAttributes,
|
|
344
|
+
type: ObservationType,
|
|
345
|
+
startTime?: Date,
|
|
346
|
+
): LangfuseObservation {
|
|
190
347
|
if (type === "agent") {
|
|
191
|
-
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
|
|
348
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent", startTime });
|
|
192
349
|
}
|
|
193
350
|
if (type === "generation") {
|
|
194
351
|
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
195
352
|
asType: "generation",
|
|
353
|
+
startTime,
|
|
196
354
|
});
|
|
197
355
|
}
|
|
198
356
|
if (type === "tool") {
|
|
199
|
-
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
|
|
357
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool", startTime });
|
|
200
358
|
}
|
|
201
|
-
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
|
|
359
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span", startTime });
|
|
202
360
|
}
|
|
203
361
|
|
|
204
362
|
function startChild(
|
|
@@ -206,28 +364,50 @@ function startChild(
|
|
|
206
364
|
name: string,
|
|
207
365
|
attributes: ObservationAttributes,
|
|
208
366
|
type: ObservationType,
|
|
367
|
+
startTime?: Date,
|
|
209
368
|
): LangfuseObservation {
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
369
|
+
if (startTime) {
|
|
370
|
+
const parentSpanContext = parent.otelSpan.spanContext();
|
|
371
|
+
if (type === "agent") {
|
|
372
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
373
|
+
asType: "agent",
|
|
374
|
+
parentSpanContext,
|
|
375
|
+
startTime,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (type === "generation") {
|
|
379
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
380
|
+
asType: "generation",
|
|
381
|
+
parentSpanContext,
|
|
382
|
+
startTime,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
if (type === "tool") {
|
|
386
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
387
|
+
asType: "tool",
|
|
388
|
+
parentSpanContext,
|
|
389
|
+
startTime,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
393
|
+
asType: "span",
|
|
394
|
+
parentSpanContext,
|
|
395
|
+
startTime,
|
|
213
396
|
});
|
|
214
397
|
}
|
|
398
|
+
if (type === "agent") {
|
|
399
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "agent" });
|
|
400
|
+
}
|
|
215
401
|
if (type === "generation") {
|
|
216
|
-
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
217
|
-
asType: "generation",
|
|
218
|
-
});
|
|
402
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "generation" });
|
|
219
403
|
}
|
|
220
404
|
if (type === "tool") {
|
|
221
|
-
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
222
|
-
asType: "tool",
|
|
223
|
-
});
|
|
405
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "tool" });
|
|
224
406
|
}
|
|
225
|
-
return parent.startObservation(name, attributes as LangfuseObservationAttributes, {
|
|
226
|
-
asType: "span",
|
|
227
|
-
});
|
|
407
|
+
return parent.startObservation(name, attributes as LangfuseObservationAttributes, { asType: "span" });
|
|
228
408
|
}
|
|
229
409
|
|
|
230
|
-
function configFingerprint(config:
|
|
410
|
+
function configFingerprint(config: ResolvedRuntimeConfig): string {
|
|
231
411
|
return createHash("sha256")
|
|
232
412
|
.update(
|
|
233
413
|
`${config.publicKey}\0${config.secretKey}\0${config.baseUrl}\0${config.environment ?? ""}\0${config.release ?? ""}`,
|
|
@@ -256,7 +436,10 @@ function maskSecretValue(data: unknown, secrets: readonly string[], active: Weak
|
|
|
256
436
|
return data.map((item) => maskSecretValue(item, secrets, active));
|
|
257
437
|
}
|
|
258
438
|
return Object.fromEntries(
|
|
259
|
-
Object.entries(data).map(([key, value]) => [
|
|
439
|
+
Object.entries(data).map(([key, value]) => [
|
|
440
|
+
maskSecretString(key, secrets),
|
|
441
|
+
maskSecretValue(value, secrets, active),
|
|
442
|
+
]),
|
|
260
443
|
);
|
|
261
444
|
} finally {
|
|
262
445
|
active.delete(data);
|
package/src/sanitizer.ts
CHANGED
|
@@ -19,9 +19,7 @@ function sanitize(value: unknown, active: WeakSet<object>, depth: number, budget
|
|
|
19
19
|
return consume(value, budget);
|
|
20
20
|
}
|
|
21
21
|
if (typeof value === "string") {
|
|
22
|
-
|
|
23
|
-
const redacted = bounded.replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
|
|
24
|
-
return consume(redacted, budget);
|
|
22
|
+
return consume(sanitizeString(value, Math.min(MAX_STRING_LENGTH, budget.remaining)), budget);
|
|
25
23
|
}
|
|
26
24
|
if (typeof value === "bigint") return consume(value.toString(), budget);
|
|
27
25
|
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
|
|
@@ -72,12 +70,13 @@ function sanitize(value: unknown, active: WeakSet<object>, depth: number, budget
|
|
|
72
70
|
omitted = true;
|
|
73
71
|
break;
|
|
74
72
|
}
|
|
75
|
-
|
|
73
|
+
const sanitizedKey = sanitizeString(key, keyBudget);
|
|
74
|
+
budget.remaining -= byteLength(sanitizedKey) + 4;
|
|
76
75
|
const property = readProperty(record, key);
|
|
77
76
|
if (redactData && key === "data") {
|
|
78
|
-
output[
|
|
79
|
-
} else if (!property.ok) output[
|
|
80
|
-
else output[
|
|
77
|
+
output[sanitizedKey] = consume("[base64 omitted]", budget);
|
|
78
|
+
} else if (!property.ok) output[sanitizedKey] = consume("[unreadable property]", budget);
|
|
79
|
+
else output[sanitizedKey] = sanitize(property.value, active, depth + 1, budget);
|
|
81
80
|
processed += 1;
|
|
82
81
|
}
|
|
83
82
|
} catch {
|
|
@@ -115,6 +114,10 @@ function consume<T>(value: T, budget: { remaining: number }): T | string {
|
|
|
115
114
|
return value;
|
|
116
115
|
}
|
|
117
116
|
|
|
117
|
+
function sanitizeString(value: string, maxBytes: number): string {
|
|
118
|
+
return truncateString(value, maxBytes).replace(BASE64_DATA_URI, BASE64_DATA_URI_OMITTED);
|
|
119
|
+
}
|
|
120
|
+
|
|
118
121
|
function truncateString(value: string, maxBytes: number): string {
|
|
119
122
|
const boundedPrefix = value.slice(0, maxBytes);
|
|
120
123
|
if (boundedPrefix.length === value.length && byteLength(boundedPrefix) <= maxBytes) {
|
package/src/tracing.ts
CHANGED
|
@@ -45,6 +45,7 @@ export interface ObservationAttributes {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface Observation {
|
|
48
|
+
readonly traceId?: string;
|
|
48
49
|
update(attributes: ObservationAttributes): Observation;
|
|
49
50
|
updateTrace?(attributes: ObservationAttributes): Observation;
|
|
50
51
|
end(endTime?: number): Observation;
|
|
@@ -56,18 +57,22 @@ export interface TraceBackend {
|
|
|
56
57
|
start(
|
|
57
58
|
name: string,
|
|
58
59
|
attributes: ObservationAttributes,
|
|
59
|
-
options: { asType: ObservationType; parent?: Observation },
|
|
60
|
+
options: { asType: ObservationType; parent?: Observation; startTime?: Date },
|
|
60
61
|
): Observation;
|
|
61
62
|
forceFlush(): Promise<void>;
|
|
62
63
|
shutdown(): Promise<void>;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
interface
|
|
66
|
+
export interface TraceRecorderOptions {
|
|
66
67
|
sessionId: string;
|
|
67
68
|
userId?: string;
|
|
68
69
|
cwd: string;
|
|
69
70
|
mode: string;
|
|
70
71
|
captureContent: boolean;
|
|
72
|
+
traceName?: string;
|
|
73
|
+
tags?: readonly string[];
|
|
74
|
+
metadata?: Readonly<Record<string, unknown>>;
|
|
75
|
+
onTraceId?: (traceId: string) => void;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
interface ModelDescriptor {
|
|
@@ -97,6 +102,7 @@ interface BeginAgentInput {
|
|
|
97
102
|
model?: ModelDescriptor;
|
|
98
103
|
git?: GitMetadata;
|
|
99
104
|
snapshot?: ContextSnapshot;
|
|
105
|
+
requestId?: string;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
interface AttemptInput {
|
|
@@ -104,6 +110,7 @@ interface AttemptInput {
|
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
interface GenerationInput {
|
|
113
|
+
startedAt?: number;
|
|
107
114
|
payload?: unknown;
|
|
108
115
|
payloadStage?: "before_provider_request";
|
|
109
116
|
model?: ModelDescriptor;
|
|
@@ -233,13 +240,13 @@ export class TraceRecorder {
|
|
|
233
240
|
|
|
234
241
|
constructor(
|
|
235
242
|
private readonly backend: TraceBackend,
|
|
236
|
-
private readonly context:
|
|
243
|
+
private readonly context: TraceRecorderOptions,
|
|
237
244
|
) {}
|
|
238
245
|
|
|
239
246
|
private startObservation(
|
|
240
247
|
name: string,
|
|
241
248
|
attributes: ObservationAttributes,
|
|
242
|
-
options: { asType: ObservationType; parent?: Observation },
|
|
249
|
+
options: { asType: ObservationType; parent?: Observation; startTime?: Date },
|
|
243
250
|
): Observation {
|
|
244
251
|
const { sessionId, userId } = this.context;
|
|
245
252
|
return this.backend.start(name, { ...attributes, sessionId, ...(userId ? { userId } : {}) }, options);
|
|
@@ -268,6 +275,7 @@ export class TraceRecorder {
|
|
|
268
275
|
...(hasItems(input.images) ? { images: input.images } : {}),
|
|
269
276
|
});
|
|
270
277
|
const metadata: Record<string, unknown> = {
|
|
278
|
+
...customTraceMetadata(this.context.metadata),
|
|
271
279
|
"pi.cwd": this.context.cwd,
|
|
272
280
|
...(input.git?.branch ? { "pi.git.branch": input.git.branch } : {}),
|
|
273
281
|
...(input.git?.commit ? { "pi.git.commit": input.git.commit } : {}),
|
|
@@ -275,6 +283,7 @@ export class TraceRecorder {
|
|
|
275
283
|
"pi.mode": this.context.mode,
|
|
276
284
|
...(input.model?.id ? { "pi.model": input.model.id } : {}),
|
|
277
285
|
...(input.model?.provider ? { "pi.provider": input.model.provider } : {}),
|
|
286
|
+
...(input.requestId ? { "pi.request.id": input.requestId } : {}),
|
|
278
287
|
"pi.session.id": this.context.sessionId,
|
|
279
288
|
"pi.trace.schema_version": Number(TRACE_SCHEMA_VERSION),
|
|
280
289
|
...snapshotMetadata("start", input.snapshot),
|
|
@@ -284,14 +293,22 @@ export class TraceRecorder {
|
|
|
284
293
|
|
|
285
294
|
this.root = this.startObservation("pi.agent", attributes, { asType: "agent" });
|
|
286
295
|
this.root.updateTrace?.({
|
|
287
|
-
name:
|
|
296
|
+
name: normalizeTraceName(this.context.traceName),
|
|
288
297
|
sessionId: this.context.sessionId,
|
|
289
298
|
...(this.context.userId ? { userId: this.context.userId } : {}),
|
|
290
299
|
version: TRACE_SCHEMA_VERSION,
|
|
291
300
|
input: traceInput,
|
|
292
301
|
metadata,
|
|
293
|
-
tags:
|
|
302
|
+
tags: traceTags(this.context.tags, gitTag),
|
|
294
303
|
});
|
|
304
|
+
const traceId = this.root.traceId;
|
|
305
|
+
if (traceId && this.context.onTraceId) {
|
|
306
|
+
try {
|
|
307
|
+
void Promise.resolve(this.context.onTraceId(traceId)).catch(() => undefined);
|
|
308
|
+
} catch {
|
|
309
|
+
// Host callbacks must not corrupt observation lifecycle state.
|
|
310
|
+
}
|
|
311
|
+
}
|
|
295
312
|
}
|
|
296
313
|
|
|
297
314
|
beginAttempt(input: AttemptInput = {}): void {
|
|
@@ -389,7 +406,11 @@ export class TraceRecorder {
|
|
|
389
406
|
...(Object.keys(requestMetadata).length > 0 ? { metadata: requestMetadata } : {}),
|
|
390
407
|
version: TRACE_SCHEMA_VERSION,
|
|
391
408
|
},
|
|
392
|
-
{
|
|
409
|
+
{
|
|
410
|
+
asType: "generation",
|
|
411
|
+
parent: this.turn ?? this.attempt ?? this.root,
|
|
412
|
+
...(input.startedAt === undefined ? {} : { startTime: new Date(input.startedAt) }),
|
|
413
|
+
},
|
|
393
414
|
);
|
|
394
415
|
this.generation = {
|
|
395
416
|
observation,
|
|
@@ -613,13 +634,8 @@ export class TraceRecorder {
|
|
|
613
634
|
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
614
635
|
}
|
|
615
636
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
async shutdown(snapshot?: ContextSnapshot): Promise<void> {
|
|
621
|
-
this.closeActiveTrace("Pi shut down before the active trace settled.", snapshot, "interrupted");
|
|
622
|
-
await this.backend.shutdown();
|
|
637
|
+
dispose(statusMessage = "Pi session ended before the active trace settled.", snapshot?: ContextSnapshot): void {
|
|
638
|
+
this.closeActiveTrace(statusMessage, snapshot, "interrupted");
|
|
623
639
|
}
|
|
624
640
|
|
|
625
641
|
private closeActiveTrace(statusMessage?: string, snapshot?: ContextSnapshot, forcedOutcome?: Outcome): void {
|
|
@@ -755,6 +771,28 @@ export class TraceRecorder {
|
|
|
755
771
|
}
|
|
756
772
|
}
|
|
757
773
|
|
|
774
|
+
function normalizeTraceName(value: string | undefined): string {
|
|
775
|
+
return typeof value === "string" && value.trim() ? value.trim() : "pi.trace";
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function traceTags(customTags: readonly string[] | undefined, gitTag: string | undefined): string[] {
|
|
779
|
+
const tags = ["pi", ...(gitTag ? [gitTag] : []), ...(customTags ?? [])];
|
|
780
|
+
return [...new Set(tags.map(sanitizeTraceTag).filter((tag): tag is string => tag !== undefined))];
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function sanitizeTraceTag(value: unknown): string | undefined {
|
|
784
|
+
if (typeof value !== "string" || !value.trim()) return undefined;
|
|
785
|
+
const sanitized = sanitizeTraceValue(value.trim());
|
|
786
|
+
return typeof sanitized === "string" && sanitized.trim() ? sanitized.trim() : undefined;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function customTraceMetadata(metadata: Readonly<Record<string, unknown>> | undefined): Record<string, unknown> {
|
|
790
|
+
if (!metadata) return {};
|
|
791
|
+
const sanitized = sanitizeTraceValue(metadata);
|
|
792
|
+
if (!sanitized || typeof sanitized !== "object" || Array.isArray(sanitized)) return {};
|
|
793
|
+
return Object.fromEntries(Object.entries(sanitized).filter(([key]) => !key.startsWith("pi.")));
|
|
794
|
+
}
|
|
795
|
+
|
|
758
796
|
function generationHttpMetadata(state: GenerationState): Record<string, unknown> {
|
|
759
797
|
const finalStatus = state.statuses.at(-1);
|
|
760
798
|
return {
|