pi-langfuse 1.4.3 → 1.4.4
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 +104 -248
- package/README_CN.md +108 -248
- package/image.png +0 -0
- package/index.ts +5 -3
- package/package.json +2 -1
- package/src/capture-policy.ts +141 -0
- package/src/config.ts +21 -12
- package/src/handlers/agent.ts +59 -29
- package/src/handlers/generation.ts +52 -53
- package/src/handlers/tool.ts +37 -24
- package/src/handlers/turn.ts +21 -19
- package/src/langfuse.ts +58 -14
- package/src/observation.ts +21 -0
- package/src/redaction.ts +115 -0
- package/src/state.ts +16 -2
- package/src/types.ts +3 -0
- package/src/utils.ts +20 -2
package/src/config.ts
CHANGED
|
@@ -2,18 +2,25 @@ import { mkdirSync, readFileSync, existsSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import type { Config } from "./types.js";
|
|
3
3
|
import { CONFIG_DIR, CONFIG_PATH, DEFAULT_LANGFUSE_HOST } from "./constants.js";
|
|
4
4
|
import { state } from "./state.js";
|
|
5
|
-
import {
|
|
5
|
+
import { forceShutdownRuntime } from "./langfuse.js";
|
|
6
|
+
import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
6
7
|
|
|
7
|
-
export function loadConfigFromFile(): Config | null {
|
|
8
|
-
if (existsSync(
|
|
8
|
+
export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
|
|
9
|
+
if (existsSync(path)) {
|
|
9
10
|
try {
|
|
10
|
-
const content = readFileSync(
|
|
11
|
-
const config = JSON.parse(content) as Config;
|
|
11
|
+
const content = readFileSync(path, "utf-8");
|
|
12
|
+
const config = JSON.parse(content) as Config & { capture?: EnvLike; privacyPreset?: string };
|
|
12
13
|
if (config.publicKey && config.secretKey) {
|
|
14
|
+
const captureSource: EnvLike = {
|
|
15
|
+
...(config.capture ?? {}),
|
|
16
|
+
...(config.privacyPreset ? { LANGFUSE_PRIVACY_PRESET: config.privacyPreset } : {}),
|
|
17
|
+
...env,
|
|
18
|
+
};
|
|
13
19
|
return {
|
|
14
20
|
publicKey: config.publicKey,
|
|
15
21
|
secretKey: config.secretKey,
|
|
16
22
|
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
23
|
+
capturePolicy: createCapturePolicy(captureSource),
|
|
17
24
|
};
|
|
18
25
|
}
|
|
19
26
|
} catch (e) {
|
|
@@ -24,9 +31,9 @@ export function loadConfigFromFile(): Config | null {
|
|
|
24
31
|
return null;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
export function loadConfigFromEnv(): Config | null {
|
|
28
|
-
const publicKey =
|
|
29
|
-
const secretKey =
|
|
34
|
+
export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config | null {
|
|
35
|
+
const publicKey = env.LANGFUSE_PUBLIC_KEY || "";
|
|
36
|
+
const secretKey = env.LANGFUSE_SECRET_KEY || "";
|
|
30
37
|
if (!publicKey || !secretKey) {
|
|
31
38
|
return null;
|
|
32
39
|
}
|
|
@@ -34,12 +41,13 @@ export function loadConfigFromEnv(): Config | null {
|
|
|
34
41
|
return {
|
|
35
42
|
publicKey,
|
|
36
43
|
secretKey,
|
|
37
|
-
host:
|
|
44
|
+
host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
45
|
+
capturePolicy: createCapturePolicy(env),
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
|
|
41
|
-
export function loadConfig(): Config | null {
|
|
42
|
-
return loadConfigFromFile() || loadConfigFromEnv();
|
|
49
|
+
export function loadConfig(env: EnvLike = process.env as EnvLike, path = CONFIG_PATH): Config | null {
|
|
50
|
+
return loadConfigFromFile(path, env) || loadConfigFromEnv(env);
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
export function saveConfig(config: Config) {
|
|
@@ -72,6 +80,7 @@ async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | n
|
|
|
72
80
|
publicKey,
|
|
73
81
|
secretKey,
|
|
74
82
|
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
83
|
+
capturePolicy: createCapturePolicy(),
|
|
75
84
|
};
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -115,7 +124,7 @@ export async function ensureConfig(ctx: any): Promise<boolean> {
|
|
|
115
124
|
export async function promptForConfig(ctx: any): Promise<boolean> {
|
|
116
125
|
state.setupAttemptedThisSession = false;
|
|
117
126
|
state.config = null;
|
|
118
|
-
await
|
|
127
|
+
await forceShutdownRuntime();
|
|
119
128
|
|
|
120
129
|
const config = await collectConfigFromUI(ctx, "Manual setup requested");
|
|
121
130
|
if (!config) {
|
package/src/handlers/agent.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import { state, resetRunState, computeEvaluationScores } from "../state.js";
|
|
2
2
|
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
3
|
import { ensureConfig } from "../config.js";
|
|
4
|
-
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput } from "../utils.js";
|
|
4
|
+
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
|
+
|
|
8
|
+
function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
9
|
+
if (!metadata) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const output: Record<string, string> = {};
|
|
14
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
output[key] = value;
|
|
17
|
+
} else if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
18
|
+
output[key] = String(value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(output).length > 0 ? output : undefined;
|
|
22
|
+
}
|
|
6
23
|
|
|
7
24
|
export function updateTraceIO(input?: unknown, output?: unknown) {
|
|
8
25
|
const root = state.agentState?.root;
|
|
@@ -45,15 +62,28 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
45
62
|
// Ignore if getSystemPrompt is not available or fails
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
const
|
|
65
|
+
const rawPromptInput = shapePayload({
|
|
49
66
|
prompt: event.prompt,
|
|
50
67
|
images: event.images,
|
|
51
68
|
context: event.context ?? event.attachments,
|
|
52
69
|
});
|
|
70
|
+
const captured = applyCapturePolicy(
|
|
71
|
+
{
|
|
72
|
+
input: rawPromptInput,
|
|
73
|
+
metadata: {
|
|
74
|
+
cwd,
|
|
75
|
+
...(state.currentModel ? { model: state.currentModel } : {}),
|
|
76
|
+
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
77
|
+
sessionId: state.currentSessionId || undefined,
|
|
78
|
+
},
|
|
79
|
+
systemPrompt: systemPrompt ? truncate(String(systemPrompt), 20000) : undefined,
|
|
80
|
+
},
|
|
81
|
+
getCapturePolicy(),
|
|
82
|
+
);
|
|
53
83
|
|
|
54
84
|
state.agentState = {
|
|
55
85
|
cwd,
|
|
56
|
-
promptInput,
|
|
86
|
+
promptInput: captured.input,
|
|
57
87
|
generationSeq: 0,
|
|
58
88
|
activeGenerations: new Map(),
|
|
59
89
|
generationOrder: [],
|
|
@@ -65,32 +95,25 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
65
95
|
{
|
|
66
96
|
sessionId: state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined,
|
|
67
97
|
traceName: "pi-agent",
|
|
68
|
-
metadata:
|
|
69
|
-
cwd: truncate(cwd, 200),
|
|
70
|
-
...(state.currentModel ? { model: truncate(state.currentModel, 200) } : {}),
|
|
71
|
-
...(state.currentProvider ? { provider: truncate(state.currentProvider, 200) } : {}),
|
|
72
|
-
},
|
|
98
|
+
metadata: stringMetadata(captured.metadata),
|
|
73
99
|
},
|
|
74
100
|
() =>
|
|
75
101
|
rt.startObservation(
|
|
76
102
|
"pi-agent",
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
sessionId: state.currentSessionId || undefined,
|
|
84
|
-
...(systemPrompt ? { systemPrompt: truncate(String(systemPrompt), 20000) } : {}),
|
|
103
|
+
{
|
|
104
|
+
input: captured.input,
|
|
105
|
+
metadata: {
|
|
106
|
+
...(captured.metadata ?? {}),
|
|
107
|
+
...(captured.systemPrompt ? { systemPrompt: captured.systemPrompt } : {}),
|
|
108
|
+
},
|
|
85
109
|
},
|
|
86
|
-
},
|
|
87
110
|
{ asType: "agent" },
|
|
88
111
|
),
|
|
89
112
|
);
|
|
90
113
|
|
|
91
114
|
state.agentState.root = root;
|
|
92
115
|
state.agentState.traceId = root.traceId;
|
|
93
|
-
updateTraceIO(
|
|
116
|
+
updateTraceIO(captured.input, undefined);
|
|
94
117
|
} catch (e) {
|
|
95
118
|
console.warn("📊 Langfuse: Failed to create agent observation", e);
|
|
96
119
|
state.isTracingDisabled = true;
|
|
@@ -104,7 +127,21 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
const lastAssistant = extractFinalAssistant(event.messages);
|
|
107
|
-
const
|
|
130
|
+
const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
|
|
131
|
+
const captured = applyCapturePolicy(
|
|
132
|
+
{
|
|
133
|
+
output: rawOutput,
|
|
134
|
+
metadata: {
|
|
135
|
+
cwd: state.agentState.cwd,
|
|
136
|
+
completed: true,
|
|
137
|
+
model: state.currentModel || undefined,
|
|
138
|
+
provider: state.currentProvider || undefined,
|
|
139
|
+
totalTools: state.toolCallCount,
|
|
140
|
+
...computeEvaluationScores(),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
getCapturePolicy(),
|
|
144
|
+
);
|
|
108
145
|
const scores = computeEvaluationScores();
|
|
109
146
|
|
|
110
147
|
closeDanglingObservations("Agent run ended before observation finalized");
|
|
@@ -112,18 +149,11 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
112
149
|
try {
|
|
113
150
|
state.agentState.root
|
|
114
151
|
.update({
|
|
115
|
-
output,
|
|
116
|
-
metadata:
|
|
117
|
-
cwd: state.agentState.cwd,
|
|
118
|
-
completed: true,
|
|
119
|
-
model: state.currentModel || undefined,
|
|
120
|
-
provider: state.currentProvider || undefined,
|
|
121
|
-
totalTools: state.toolCallCount,
|
|
122
|
-
...scores,
|
|
123
|
-
},
|
|
152
|
+
output: captured.output,
|
|
153
|
+
metadata: captured.metadata,
|
|
124
154
|
})
|
|
125
155
|
.end();
|
|
126
|
-
updateTraceIO(state.agentState.promptInput, output);
|
|
156
|
+
updateTraceIO(state.agentState.promptInput, captured.output);
|
|
127
157
|
|
|
128
158
|
await sendScore("tool_call_count", scores.tool_call_count, { traceId: state.agentState.traceId });
|
|
129
159
|
await sendScore("turn_count", scores.turn_count, { traceId: state.agentState.traceId });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { state } from "../state.js";
|
|
2
2
|
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
3
4
|
import {
|
|
4
5
|
getRequestKey,
|
|
5
6
|
getProviderPayload,
|
|
@@ -9,8 +10,10 @@ import {
|
|
|
9
10
|
extractAssistantOutput,
|
|
10
11
|
extractUsage,
|
|
11
12
|
extractCostDetails,
|
|
13
|
+
getCapturePolicy,
|
|
12
14
|
} from "../utils.js";
|
|
13
15
|
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
16
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
14
17
|
|
|
15
18
|
export function getOpenGeneration(): GenerationState | undefined {
|
|
16
19
|
if (state.isTracingDisabled || !state.agentState) {
|
|
@@ -44,33 +47,32 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
44
47
|
url: event.url,
|
|
45
48
|
method: event.method,
|
|
46
49
|
}) as Record<string, unknown>;
|
|
50
|
+
const captured = applyCapturePolicy(
|
|
51
|
+
{
|
|
52
|
+
input: shapePayload(payload),
|
|
53
|
+
metadata,
|
|
54
|
+
},
|
|
55
|
+
getCapturePolicy(),
|
|
56
|
+
);
|
|
47
57
|
|
|
48
58
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
49
|
-
const generation =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"llm-generation",
|
|
61
|
-
{
|
|
62
|
-
input: shapePayload(payload),
|
|
63
|
-
model: model || undefined,
|
|
64
|
-
metadata,
|
|
65
|
-
},
|
|
66
|
-
{ asType: "generation" },
|
|
67
|
-
);
|
|
59
|
+
const generation = await startChildObservation({
|
|
60
|
+
parent,
|
|
61
|
+
runtime: getRuntime,
|
|
62
|
+
name: "llm-generation",
|
|
63
|
+
body: {
|
|
64
|
+
input: captured.input,
|
|
65
|
+
model: model || undefined,
|
|
66
|
+
metadata: captured.metadata,
|
|
67
|
+
},
|
|
68
|
+
asType: "generation",
|
|
69
|
+
});
|
|
68
70
|
|
|
69
71
|
state.agentState.activeGenerations.set(key, {
|
|
70
72
|
observation: generation,
|
|
71
73
|
requestKey: key,
|
|
72
74
|
ended: false,
|
|
73
|
-
metadata,
|
|
75
|
+
metadata: captured.metadata ?? {},
|
|
74
76
|
});
|
|
75
77
|
state.agentState.generationOrder.push(key);
|
|
76
78
|
} catch (e) {
|
|
@@ -84,7 +86,7 @@ export function updateGenerationMetadata(event: Record<string, unknown>) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
const key = getRequestKey(event, "");
|
|
87
|
-
const metadata = extractResponseMetadata(event);
|
|
89
|
+
const metadata = applyCapturePolicy({ metadata: extractResponseMetadata(event) }, getCapturePolicy()).metadata ?? {};
|
|
88
90
|
if (!key) {
|
|
89
91
|
const generation = getOpenGeneration();
|
|
90
92
|
if (generation) {
|
|
@@ -160,7 +162,9 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
160
162
|
}
|
|
161
163
|
|
|
162
164
|
const generation = getOpenGeneration();
|
|
163
|
-
const
|
|
165
|
+
const rawOutput = extractAssistantOutput(message);
|
|
166
|
+
const captured = applyCapturePolicy({ output: rawOutput }, getCapturePolicy());
|
|
167
|
+
const output = captured.output;
|
|
164
168
|
state.agentState.latestAssistantOutput = output;
|
|
165
169
|
|
|
166
170
|
if (!generation) {
|
|
@@ -180,6 +184,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
180
184
|
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
181
185
|
},
|
|
182
186
|
};
|
|
187
|
+
update.metadata = applyCapturePolicy({ metadata: update.metadata }, getCapturePolicy()).metadata;
|
|
183
188
|
|
|
184
189
|
try {
|
|
185
190
|
generation.observation.update(update).end();
|
|
@@ -198,38 +203,32 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
198
203
|
const usageDetails = extractUsage({ ...event, message });
|
|
199
204
|
const costDetails = extractCostDetails({ ...event, message });
|
|
200
205
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
206
|
+
const captured = applyCapturePolicy(
|
|
207
|
+
{
|
|
208
|
+
input: state.agentState.promptInput,
|
|
209
|
+
output: extractAssistantOutput(message),
|
|
210
|
+
metadata: {
|
|
211
|
+
provider: state.currentProvider || undefined,
|
|
212
|
+
sourceEvent: "turn_end",
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
getCapturePolicy(),
|
|
216
|
+
);
|
|
201
217
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
202
|
-
const generation =
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{ asType: "generation" },
|
|
217
|
-
)
|
|
218
|
-
: (await getRuntime()).startObservation(
|
|
219
|
-
"llm-generation",
|
|
220
|
-
{
|
|
221
|
-
input: state.agentState.promptInput,
|
|
222
|
-
output: extractAssistantOutput(message),
|
|
223
|
-
model: model || undefined,
|
|
224
|
-
usageDetails,
|
|
225
|
-
costDetails,
|
|
226
|
-
metadata: {
|
|
227
|
-
provider: state.currentProvider || undefined,
|
|
228
|
-
sourceEvent: "turn_end",
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
{ asType: "generation" },
|
|
232
|
-
);
|
|
218
|
+
const generation = await startChildObservation({
|
|
219
|
+
parent,
|
|
220
|
+
runtime: getRuntime,
|
|
221
|
+
name: "llm-generation",
|
|
222
|
+
body: {
|
|
223
|
+
input: captured.input,
|
|
224
|
+
output: captured.output,
|
|
225
|
+
model: model || undefined,
|
|
226
|
+
usageDetails,
|
|
227
|
+
costDetails,
|
|
228
|
+
metadata: captured.metadata,
|
|
229
|
+
},
|
|
230
|
+
asType: "generation",
|
|
231
|
+
});
|
|
233
232
|
|
|
234
233
|
generation.end();
|
|
235
234
|
state.agentState.generationOrder.push("turn-end-fallback");
|
package/src/handlers/tool.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { state } from "../state.js";
|
|
2
2
|
import { getRuntime, sendScore } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
3
4
|
import {
|
|
4
5
|
getToolCallId,
|
|
5
6
|
getToolName,
|
|
@@ -8,8 +9,11 @@ import {
|
|
|
8
9
|
extractTextContent,
|
|
9
10
|
truncate,
|
|
10
11
|
estimatePayloadBytes,
|
|
12
|
+
getCapturePolicy,
|
|
11
13
|
} from "../utils.js";
|
|
12
14
|
import { MAX_TOOL_PAYLOAD_LENGTH } from "../constants.js";
|
|
15
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
16
|
+
import { redactString } from "../redaction.js";
|
|
13
17
|
|
|
14
18
|
export async function startToolObservation(event: Record<string, unknown>) {
|
|
15
19
|
if (state.isTracingDisabled || !state.agentState?.root) {
|
|
@@ -25,25 +29,25 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
25
29
|
const toolName = getToolName(event);
|
|
26
30
|
const toolInput = getToolInput(event);
|
|
27
31
|
const shapedInput = shapePayload(toolInput, { maxString: MAX_TOOL_PAYLOAD_LENGTH });
|
|
28
|
-
const
|
|
32
|
+
const captured = applyCapturePolicy(
|
|
33
|
+
{
|
|
34
|
+
toolInput: shapedInput,
|
|
35
|
+
metadata: { toolName, toolCallId },
|
|
36
|
+
},
|
|
37
|
+
getCapturePolicy(),
|
|
38
|
+
);
|
|
39
|
+
const inputBytes = estimatePayloadBytes(captured.toolInput, MAX_TOOL_PAYLOAD_LENGTH);
|
|
29
40
|
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
30
|
-
const tool =
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
toolName,
|
|
41
|
-
{
|
|
42
|
-
input: shapedInput,
|
|
43
|
-
metadata: { toolName, toolCallId, inputBytes },
|
|
44
|
-
},
|
|
45
|
-
{ asType: "tool" },
|
|
46
|
-
);
|
|
41
|
+
const tool = await startChildObservation({
|
|
42
|
+
parent,
|
|
43
|
+
runtime: getRuntime,
|
|
44
|
+
name: toolName,
|
|
45
|
+
body: {
|
|
46
|
+
input: captured.toolInput,
|
|
47
|
+
metadata: { ...(captured.metadata ?? {}), inputBytes },
|
|
48
|
+
},
|
|
49
|
+
asType: "tool",
|
|
50
|
+
});
|
|
47
51
|
|
|
48
52
|
state.toolCallCount++;
|
|
49
53
|
state.agentState.activeTools.set(toolCallId, {
|
|
@@ -84,18 +88,27 @@ export async function finishToolObservation(event: Record<string, unknown>) {
|
|
|
84
88
|
|
|
85
89
|
try {
|
|
86
90
|
const shapedOutput = shapePayload(output, { maxString: MAX_TOOL_PAYLOAD_LENGTH });
|
|
87
|
-
const
|
|
91
|
+
const captured = applyCapturePolicy(
|
|
92
|
+
{
|
|
93
|
+
toolOutput: shapedOutput,
|
|
94
|
+
metadata: {
|
|
95
|
+
toolName: activeTool.toolName,
|
|
96
|
+
toolCallId,
|
|
97
|
+
isError,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
getCapturePolicy(),
|
|
101
|
+
);
|
|
102
|
+
const outputBytes = estimatePayloadBytes(captured.toolOutput, MAX_TOOL_PAYLOAD_LENGTH);
|
|
88
103
|
const durationMs = Math.max(0, Date.now() - activeTool.startedAt);
|
|
89
104
|
|
|
90
105
|
activeTool.observation
|
|
91
106
|
.update({
|
|
92
|
-
output:
|
|
107
|
+
output: captured.toolOutput,
|
|
93
108
|
level: isError ? "ERROR" : "DEFAULT",
|
|
94
|
-
statusMessage: isError ? truncate(String(event.error ?? output), 1_000) : undefined,
|
|
109
|
+
statusMessage: isError ? redactString(truncate(String(event.error ?? output), 1_000)) : undefined,
|
|
95
110
|
metadata: {
|
|
96
|
-
|
|
97
|
-
toolCallId,
|
|
98
|
-
isError,
|
|
111
|
+
...(captured.metadata ?? {}),
|
|
99
112
|
durationMs,
|
|
100
113
|
inputBytes: activeTool.inputBytes,
|
|
101
114
|
outputBytes,
|
package/src/handlers/turn.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { state } from "../state.js";
|
|
2
2
|
import { getRuntime } from "../langfuse.js";
|
|
3
|
-
import {
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { shapePayload, getCapturePolicy } from "../utils.js";
|
|
5
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
4
6
|
|
|
5
7
|
export async function startTurnObservation(event: Record<string, unknown>) {
|
|
6
8
|
if (state.isTracingDisabled || !state.agentState?.root) {
|
|
@@ -15,23 +17,23 @@ export async function startTurnObservation(event: Record<string, unknown>) {
|
|
|
15
17
|
|
|
16
18
|
try {
|
|
17
19
|
const turnIndex = event.turnIndex ?? state.turnCount;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
20
|
+
const captured = applyCapturePolicy(
|
|
21
|
+
{
|
|
22
|
+
input: shapePayload(event.context ?? event),
|
|
23
|
+
metadata: { turnIndex },
|
|
24
|
+
},
|
|
25
|
+
getCapturePolicy(),
|
|
26
|
+
);
|
|
27
|
+
const observation = await startChildObservation({
|
|
28
|
+
parent: state.agentState.root,
|
|
29
|
+
runtime: getRuntime,
|
|
30
|
+
name: "turn",
|
|
31
|
+
body: {
|
|
32
|
+
input: captured.input,
|
|
33
|
+
metadata: captured.metadata,
|
|
34
|
+
},
|
|
35
|
+
asType: "span",
|
|
36
|
+
});
|
|
35
37
|
|
|
36
38
|
state.agentState.activeTurn = observation;
|
|
37
39
|
} catch (e) {
|
|
@@ -39,7 +41,7 @@ export async function startTurnObservation(event: Record<string, unknown>) {
|
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
export function finishTurnObservation(
|
|
44
|
+
export function finishTurnObservation(_event?: Record<string, unknown>) {
|
|
43
45
|
if (state.isTracingDisabled || !state.agentState?.activeTurn) {
|
|
44
46
|
return;
|
|
45
47
|
}
|
package/src/langfuse.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { state } from "./state.js";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
|
|
5
5
|
let runtime: LangfuseRuntime | null = null;
|
|
6
|
+
const activeSessions = new Set<string>();
|
|
6
7
|
|
|
7
8
|
type FallbackObservationType = "SPAN" | "GENERATION";
|
|
8
9
|
|
|
@@ -294,6 +295,14 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
294
295
|
throw new Error("Langfuse config is not set");
|
|
295
296
|
}
|
|
296
297
|
|
|
298
|
+
// Track the current session as a runtime consumer.
|
|
299
|
+
// Multiple sessions can share the same runtime; shutdown is deferred
|
|
300
|
+
// until the last session releases it.
|
|
301
|
+
const sessionId = state.currentSessionId;
|
|
302
|
+
if (sessionId) {
|
|
303
|
+
activeSessions.add(sessionId);
|
|
304
|
+
}
|
|
305
|
+
|
|
297
306
|
if (!runtime) {
|
|
298
307
|
const [{ BasicTracerProvider }, { LangfuseSpanProcessor }, tracing, { LangfuseClient }] = await Promise.all([
|
|
299
308
|
import("@opentelemetry/sdk-trace-base"),
|
|
@@ -337,23 +346,58 @@ export async function getRuntime(): Promise<LangfuseRuntime> {
|
|
|
337
346
|
return runtime as LangfuseRuntime;
|
|
338
347
|
}
|
|
339
348
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
function doShutdownRuntime(): Promise<void> {
|
|
350
|
+
return (async () => {
|
|
351
|
+
if (!runtime) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
344
354
|
|
|
345
|
-
|
|
346
|
-
await runtime.tracerProvider?.forceFlush?.();
|
|
347
|
-
await fallbackToRestIngestion(runtime);
|
|
348
|
-
await runtime.scoreClient.flush?.();
|
|
349
|
-
await runtime.scoreClient.shutdown?.();
|
|
350
|
-
await runtime.tracerProvider?.shutdown?.();
|
|
351
|
-
} catch (e) {
|
|
352
|
-
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
353
|
-
} finally {
|
|
354
|
-
runtime.clearTracerProvider?.();
|
|
355
|
+
const rt = runtime;
|
|
355
356
|
runtime = null;
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await rt.tracerProvider?.forceFlush?.();
|
|
360
|
+
await fallbackToRestIngestion(rt);
|
|
361
|
+
await rt.scoreClient.flush?.();
|
|
362
|
+
await rt.scoreClient.shutdown?.();
|
|
363
|
+
await rt.tracerProvider?.shutdown?.();
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.warn("📊 Langfuse: Failed to flush/shutdown cleanly", e);
|
|
366
|
+
} finally {
|
|
367
|
+
if (!runtime) {
|
|
368
|
+
rt.clearTracerProvider?.();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Release the current session's reference to the Langfuse runtime.
|
|
376
|
+
* Only actually shuts down the runtime when the last session releases it.
|
|
377
|
+
* Accepts an optional sessionId for use outside of withSession (e.g. deferred callbacks).
|
|
378
|
+
*/
|
|
379
|
+
export async function shutdownRuntime(sessionId?: string): Promise<void> {
|
|
380
|
+
const sid = sessionId ?? state.currentSessionId;
|
|
381
|
+
if (sid) {
|
|
382
|
+
activeSessions.delete(sid);
|
|
356
383
|
}
|
|
384
|
+
|
|
385
|
+
// Still have active sessions — keep the runtime alive.
|
|
386
|
+
if (activeSessions.size > 0) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
await doShutdownRuntime();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Force-shutdown the Langfuse runtime regardless of active session references.
|
|
395
|
+
* Used when the user manually reconfigures (e.g. /langfuse-setup) and needs
|
|
396
|
+
* a fresh runtime with new credentials.
|
|
397
|
+
*/
|
|
398
|
+
export async function forceShutdownRuntime(): Promise<void> {
|
|
399
|
+
activeSessions.clear();
|
|
400
|
+
await doShutdownRuntime();
|
|
357
401
|
}
|
|
358
402
|
|
|
359
403
|
export async function sendScore(name: string, value: number, options: { traceId?: string; observationId?: string } = {}) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LangfuseObservation, LangfuseRuntime, ObservationUpdate } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export async function startChildObservation({
|
|
4
|
+
parent,
|
|
5
|
+
runtime,
|
|
6
|
+
name,
|
|
7
|
+
body,
|
|
8
|
+
asType,
|
|
9
|
+
}: {
|
|
10
|
+
parent: LangfuseObservation;
|
|
11
|
+
runtime: () => Promise<LangfuseRuntime>;
|
|
12
|
+
name: string;
|
|
13
|
+
body?: ObservationUpdate;
|
|
14
|
+
asType: "generation" | "tool" | "span";
|
|
15
|
+
}): Promise<LangfuseObservation> {
|
|
16
|
+
if (parent.startObservation) {
|
|
17
|
+
return parent.startObservation(name, body, { asType });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (await runtime()).startObservation(name, body, { asType });
|
|
21
|
+
}
|