pi-langfuse 1.5.19 → 1.6.1
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 +13 -1
- package/README_CN.md +13 -1
- package/index.ts +52 -49
- package/package.json +3 -3
- package/src/commands.ts +1 -0
- package/src/config.ts +14 -0
- package/src/handlers/agent.ts +103 -1
- package/src/handlers/cache.ts +147 -0
- package/src/handlers/generation.ts +39 -2
- package/src/handlers/session.ts +90 -0
- package/src/handlers/system-state.ts +186 -0
- package/src/handlers/tool.ts +1 -1
- package/src/handlers/turn.ts +1 -1
- package/src/langfuse.ts +6 -1
- package/src/observation.ts +1 -1
- package/src/state.ts +18 -3
- package/src/types.ts +14 -2
- package/src/utils.ts +75 -0
- package/types/langfuse-runtime-shims.d.ts +0 -49
- package/types/node-shims.d.ts +0 -29
- package/types/pi-coding-agent.d.ts +0 -16
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { getSessionRunState, state } from "../state.js";
|
|
5
|
+
import { extractCostDetails, extractUsage, getCapturePolicy, truncate } from "../utils.js";
|
|
6
|
+
|
|
7
|
+
type RecordLike = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function sessionIdForTrace(): string | undefined {
|
|
10
|
+
return state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function initializeUsageTracking(ctx: any): void {
|
|
14
|
+
const session = getSessionRunState();
|
|
15
|
+
try {
|
|
16
|
+
const entries = ctx?.sessionManager?.getEntries?.();
|
|
17
|
+
if (!Array.isArray(entries)) return;
|
|
18
|
+
for (const entry of entries) {
|
|
19
|
+
if (entry && typeof entry === "object" && (entry as RecordLike).type === "usage") {
|
|
20
|
+
const id = (entry as RecordLike).id;
|
|
21
|
+
if (typeof id === "string") session.seenUsageEntryIds.add(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Optional for SDK hosts without persisted sessions.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function recordCacheWarmingDecision(event: RecordLike, ctx: any): Promise<void> {
|
|
30
|
+
if (state.isTracingDisabled || !state.config) return;
|
|
31
|
+
const contextUsage = ctx?.getContextUsage?.();
|
|
32
|
+
const captured = applyCapturePolicy(
|
|
33
|
+
{
|
|
34
|
+
metadata: {
|
|
35
|
+
action: event.action,
|
|
36
|
+
warmCostEstimate: event.warmCost,
|
|
37
|
+
missCostEstimate: event.missCost,
|
|
38
|
+
continuationProbability: event.continuationProbability,
|
|
39
|
+
contextTokens: contextUsage?.tokens,
|
|
40
|
+
contextWindow: contextUsage?.contextWindow,
|
|
41
|
+
contextPercent: contextUsage?.percent,
|
|
42
|
+
idle: ctx?.isIdle?.(),
|
|
43
|
+
model: ctx?.model?.id,
|
|
44
|
+
provider: ctx?.model?.provider,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
getCapturePolicy(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const parent = state.agentState?.activeTurn ?? state.agentState?.activeAttempt ?? state.agentState?.root;
|
|
52
|
+
if (parent) {
|
|
53
|
+
const observation = await startChildObservation({
|
|
54
|
+
parent,
|
|
55
|
+
runtime: getRuntime,
|
|
56
|
+
name: "cache-warming-decision",
|
|
57
|
+
body: { metadata: captured.metadata },
|
|
58
|
+
asType: "event",
|
|
59
|
+
});
|
|
60
|
+
observation.end();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const runtime = await getRuntime();
|
|
65
|
+
const create = () => runtime.propagateAttributes(
|
|
66
|
+
{
|
|
67
|
+
sessionId: sessionIdForTrace(),
|
|
68
|
+
traceName: "pi-cache-warming",
|
|
69
|
+
metadata: {
|
|
70
|
+
...(ctx?.model?.id ? { model: String(ctx.model.id) } : {}),
|
|
71
|
+
...(ctx?.model?.provider ? { provider: String(ctx.model.provider) } : {}),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
() => runtime.startObservation(
|
|
75
|
+
"cache-warming-decision",
|
|
76
|
+
{ metadata: captured.metadata },
|
|
77
|
+
{ asType: "event" },
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
const observation = runtime.withRootContext ? runtime.withRootContext(create) : create();
|
|
81
|
+
observation.end();
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn("📊 Langfuse: Failed to record cache warming decision", error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function recordNewUsageEntries(ctx: any): Promise<void> {
|
|
88
|
+
if (state.isTracingDisabled || !state.config) return;
|
|
89
|
+
const session = getSessionRunState();
|
|
90
|
+
let entries: unknown;
|
|
91
|
+
try {
|
|
92
|
+
entries = ctx?.sessionManager?.getEntries?.();
|
|
93
|
+
} catch {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(entries)) return;
|
|
97
|
+
|
|
98
|
+
for (const rawEntry of entries) {
|
|
99
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
100
|
+
const entry = rawEntry as RecordLike;
|
|
101
|
+
if (entry.type !== "usage" || typeof entry.id !== "string" || session.seenUsageEntryIds.has(entry.id)) continue;
|
|
102
|
+
if (entry.kind !== "cache_warm") {
|
|
103
|
+
session.seenUsageEntryIds.add(entry.id);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const usageDetails = extractUsage(entry);
|
|
108
|
+
const costDetails = extractCostDetails(entry);
|
|
109
|
+
const captured = applyCapturePolicy(
|
|
110
|
+
{
|
|
111
|
+
metadata: {
|
|
112
|
+
usageEntryId: entry.id,
|
|
113
|
+
kind: entry.kind,
|
|
114
|
+
note: entry.note,
|
|
115
|
+
provider: entry.provider,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
getCapturePolicy(),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const runtime = await getRuntime();
|
|
123
|
+
const parent = state.agentState?.activeTurn ?? state.agentState?.activeAttempt ?? state.agentState?.root;
|
|
124
|
+
const body = {
|
|
125
|
+
model: typeof entry.model === "string" ? entry.model : undefined,
|
|
126
|
+
usageDetails,
|
|
127
|
+
...(costDetails ? { costDetails } : {}),
|
|
128
|
+
metadata: captured.metadata,
|
|
129
|
+
};
|
|
130
|
+
const observation = parent
|
|
131
|
+
? await startChildObservation({ parent, runtime: getRuntime, name: "cache-warm", body, asType: "generation" })
|
|
132
|
+
: (runtime.withRootContext
|
|
133
|
+
? runtime.withRootContext(() => runtime.propagateAttributes(
|
|
134
|
+
{ sessionId: sessionIdForTrace(), traceName: "pi-cache-warm" },
|
|
135
|
+
() => runtime.startObservation("cache-warm", body, { asType: "generation" }),
|
|
136
|
+
))
|
|
137
|
+
: runtime.propagateAttributes(
|
|
138
|
+
{ sessionId: sessionIdForTrace(), traceName: "pi-cache-warm" },
|
|
139
|
+
() => runtime.startObservation("cache-warm", body, { asType: "generation" }),
|
|
140
|
+
));
|
|
141
|
+
observation.end();
|
|
142
|
+
session.seenUsageEntryIds.add(entry.id);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.warn("📊 Langfuse: Failed to record cache warming usage", error);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
extractCostDetails,
|
|
13
13
|
getCapturePolicy,
|
|
14
14
|
extractModelParameters,
|
|
15
|
+
extractCacheMetrics,
|
|
16
|
+
inferToolUpdateTransport,
|
|
15
17
|
} from "../utils.js";
|
|
16
18
|
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
17
19
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
@@ -43,11 +45,26 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
43
45
|
const modelParameters = extractModelParameters(payload);
|
|
44
46
|
const model = String(event.model ?? event.modelId ?? state.currentModel ?? "");
|
|
45
47
|
const provider = String(event.provider ?? state.currentProvider ?? "");
|
|
48
|
+
const toolUpdateTransport = inferToolUpdateTransport(payload, (state.agentState.systemStateSequence ?? 0) > 1);
|
|
46
49
|
const metadata = shapePayload({
|
|
47
50
|
provider,
|
|
48
51
|
requestId: key,
|
|
49
52
|
url: event.url,
|
|
50
53
|
method: event.method,
|
|
54
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
55
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
56
|
+
systemStateSequence: state.agentState.systemStateSequence,
|
|
57
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
58
|
+
toolUpdateTransport,
|
|
59
|
+
cachePreservationExpected:
|
|
60
|
+
toolUpdateTransport === "anthropic-native" ||
|
|
61
|
+
toolUpdateTransport === "openai-additional-tools" ||
|
|
62
|
+
toolUpdateTransport === "openai-tool-search" ||
|
|
63
|
+
toolUpdateTransport === "mid-conversation-system"
|
|
64
|
+
? true
|
|
65
|
+
: toolUpdateTransport === "collapsed-leading-system"
|
|
66
|
+
? false
|
|
67
|
+
: undefined,
|
|
51
68
|
}) as Record<string, unknown>;
|
|
52
69
|
const captured = applyCapturePolicy(
|
|
53
70
|
{
|
|
@@ -57,7 +74,7 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
57
74
|
getCapturePolicy(),
|
|
58
75
|
);
|
|
59
76
|
|
|
60
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
77
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
61
78
|
const generation = await startChildObservation({
|
|
62
79
|
parent,
|
|
63
80
|
runtime: getRuntime,
|
|
@@ -177,6 +194,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
177
194
|
|
|
178
195
|
const usageDetails = extractUsage({ ...event, message });
|
|
179
196
|
const costDetails = extractCostDetails({ ...event, message });
|
|
197
|
+
const cacheMetrics = extractCacheMetrics({ ...event, message });
|
|
180
198
|
const modelParameters = extractModelParameters(getProviderPayload(event)) ?? generation.modelParameters;
|
|
181
199
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
182
200
|
const update: ObservationUpdate = {
|
|
@@ -188,6 +206,7 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
188
206
|
metadata: {
|
|
189
207
|
...generation.metadata,
|
|
190
208
|
finishReason: message.finishReason ?? message.stopReason ?? event.finishReason,
|
|
209
|
+
...cacheMetrics,
|
|
191
210
|
},
|
|
192
211
|
};
|
|
193
212
|
update.metadata = applyCapturePolicy({ metadata: update.metadata }, getCapturePolicy()).metadata;
|
|
@@ -195,6 +214,12 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
195
214
|
try {
|
|
196
215
|
generation.observation.update(update).end();
|
|
197
216
|
generation.ended = true;
|
|
217
|
+
if (cacheMetrics) {
|
|
218
|
+
state.agentState.cacheReadTokens = (state.agentState.cacheReadTokens ?? 0) + cacheMetrics.cacheReadTokens;
|
|
219
|
+
state.agentState.cacheWriteTokens = (state.agentState.cacheWriteTokens ?? 0) + cacheMetrics.cacheWriteTokens;
|
|
220
|
+
state.agentState.uncachedInputTokens =
|
|
221
|
+
(state.agentState.uncachedInputTokens ?? 0) + cacheMetrics.uncachedInputTokens;
|
|
222
|
+
}
|
|
198
223
|
} catch (e) {
|
|
199
224
|
console.warn("📊 Langfuse: Failed to finish generation", e);
|
|
200
225
|
}
|
|
@@ -208,6 +233,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
208
233
|
try {
|
|
209
234
|
const usageDetails = extractUsage({ ...event, message });
|
|
210
235
|
const costDetails = extractCostDetails({ ...event, message });
|
|
236
|
+
const cacheMetrics = extractCacheMetrics({ ...event, message });
|
|
211
237
|
const modelParameters = extractModelParameters(getProviderPayload(event));
|
|
212
238
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
213
239
|
const captured = applyCapturePolicy(
|
|
@@ -217,11 +243,16 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
217
243
|
metadata: {
|
|
218
244
|
provider: state.currentProvider || undefined,
|
|
219
245
|
sourceEvent: "turn_end",
|
|
246
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
247
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
248
|
+
systemStateSequence: state.agentState.systemStateSequence,
|
|
249
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
250
|
+
...cacheMetrics,
|
|
220
251
|
},
|
|
221
252
|
},
|
|
222
253
|
getCapturePolicy(),
|
|
223
254
|
);
|
|
224
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
255
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
225
256
|
const generation = await startChildObservation({
|
|
226
257
|
parent,
|
|
227
258
|
runtime: getRuntime,
|
|
@@ -240,6 +271,12 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
240
271
|
|
|
241
272
|
generation.end();
|
|
242
273
|
state.agentState.generationOrder.push("turn-end-fallback");
|
|
274
|
+
if (cacheMetrics) {
|
|
275
|
+
state.agentState.cacheReadTokens = (state.agentState.cacheReadTokens ?? 0) + cacheMetrics.cacheReadTokens;
|
|
276
|
+
state.agentState.cacheWriteTokens = (state.agentState.cacheWriteTokens ?? 0) + cacheMetrics.cacheWriteTokens;
|
|
277
|
+
state.agentState.uncachedInputTokens =
|
|
278
|
+
(state.agentState.uncachedInputTokens ?? 0) + cacheMetrics.uncachedInputTokens;
|
|
279
|
+
}
|
|
243
280
|
} catch (e) {
|
|
244
281
|
console.warn("📊 Langfuse: Failed to create fallback generation", e);
|
|
245
282
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
2
|
+
import { getRuntime } from "../langfuse.js";
|
|
3
|
+
import { startChildObservation } from "../observation.js";
|
|
4
|
+
import { state } from "../state.js";
|
|
5
|
+
import {
|
|
6
|
+
extractCostDetails,
|
|
7
|
+
extractUsage,
|
|
8
|
+
getCapturePolicy,
|
|
9
|
+
shapePayload,
|
|
10
|
+
} from "../utils.js";
|
|
11
|
+
|
|
12
|
+
type RecordLike = Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
function namesFromTools(value: unknown): string[] {
|
|
15
|
+
if (!Array.isArray(value)) return [];
|
|
16
|
+
return value.flatMap((tool) => {
|
|
17
|
+
if (typeof tool === "string") return [tool];
|
|
18
|
+
if (!tool || typeof tool !== "object") return [];
|
|
19
|
+
const name = (tool as RecordLike).name;
|
|
20
|
+
return typeof name === "string" ? [name] : [];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function recordSessionCompaction(event: RecordLike): Promise<void> {
|
|
25
|
+
const agent = state.agentState;
|
|
26
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
27
|
+
|
|
28
|
+
const entry = event.compactionEntry && typeof event.compactionEntry === "object"
|
|
29
|
+
? event.compactionEntry as RecordLike
|
|
30
|
+
: {};
|
|
31
|
+
const checkpoint = entry.systemMessage && typeof entry.systemMessage === "object"
|
|
32
|
+
? entry.systemMessage as RecordLike
|
|
33
|
+
: undefined;
|
|
34
|
+
const checkpointTools = namesFromTools(checkpoint?.toolsAdded);
|
|
35
|
+
const checkpointSections = checkpoint?.sections && typeof checkpoint.sections === "object"
|
|
36
|
+
? Object.keys(checkpoint.sections as RecordLike)
|
|
37
|
+
: [];
|
|
38
|
+
const captured = applyCapturePolicy(
|
|
39
|
+
{
|
|
40
|
+
input: shapePayload({ summary: entry.summary }),
|
|
41
|
+
metadata: {
|
|
42
|
+
reason: event.reason,
|
|
43
|
+
willRetry: event.willRetry,
|
|
44
|
+
fromExtension: event.fromExtension,
|
|
45
|
+
compactionEntryId: entry.id,
|
|
46
|
+
firstKeptEntryId: entry.firstKeptEntryId,
|
|
47
|
+
tokensBefore: entry.tokensBefore,
|
|
48
|
+
checkpointSectionNames: checkpointSections,
|
|
49
|
+
checkpointToolNames: checkpointTools,
|
|
50
|
+
checkpointToolCount: checkpointTools.length,
|
|
51
|
+
promptStateHash: agent.promptStateHash,
|
|
52
|
+
toolStateHash: agent.toolStateHash,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
getCapturePolicy(),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const parent = agent.activeTurn ?? agent.activeAttempt ?? agent.root;
|
|
60
|
+
const compaction = await startChildObservation({
|
|
61
|
+
parent,
|
|
62
|
+
runtime: getRuntime,
|
|
63
|
+
name: "session-compaction",
|
|
64
|
+
body: { input: captured.input, metadata: captured.metadata },
|
|
65
|
+
asType: "span",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (entry.usage && typeof entry.usage === "object") {
|
|
69
|
+
const summary = applyCapturePolicy({ output: entry.summary }, getCapturePolicy());
|
|
70
|
+
const costDetails = extractCostDetails({ usage: entry.usage });
|
|
71
|
+
const generation = await startChildObservation({
|
|
72
|
+
parent: compaction,
|
|
73
|
+
runtime: getRuntime,
|
|
74
|
+
name: "compaction-summary",
|
|
75
|
+
body: {
|
|
76
|
+
output: summary.output,
|
|
77
|
+
model: state.currentModel || undefined,
|
|
78
|
+
usageDetails: extractUsage({ usage: entry.usage }),
|
|
79
|
+
...(costDetails ? { costDetails } : {}),
|
|
80
|
+
metadata: { provider: state.currentProvider || undefined },
|
|
81
|
+
},
|
|
82
|
+
asType: "generation",
|
|
83
|
+
});
|
|
84
|
+
generation.end();
|
|
85
|
+
}
|
|
86
|
+
compaction.end();
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.warn("📊 Langfuse: Failed to record session compaction", error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { applyCapturePolicy } from "../capture-policy.js";
|
|
4
|
+
import { getRuntime } from "../langfuse.js";
|
|
5
|
+
import { startChildObservation } from "../observation.js";
|
|
6
|
+
import { getSessionRunState, state } from "../state.js";
|
|
7
|
+
import { getCapturePolicy, shapePayload } from "../utils.js";
|
|
8
|
+
|
|
9
|
+
type RecordLike = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function fingerprint(value: string): string {
|
|
12
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toolName(tool: unknown): string | undefined {
|
|
16
|
+
if (typeof tool === "string") return tool;
|
|
17
|
+
if (!tool || typeof tool !== "object") return undefined;
|
|
18
|
+
const name = (tool as RecordLike).name;
|
|
19
|
+
return typeof name === "string" && name ? name : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sortedUnique(values: Iterable<string>): string[] {
|
|
23
|
+
return [...new Set(values)].sort();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readNewSystemMessages(ctx: any): Array<{ entryId: string; message: RecordLike }> {
|
|
27
|
+
const session = getSessionRunState();
|
|
28
|
+
let branch: unknown;
|
|
29
|
+
try {
|
|
30
|
+
branch = ctx?.sessionManager?.getBranch?.();
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
if (!Array.isArray(branch)) return [];
|
|
35
|
+
|
|
36
|
+
const messages: Array<{ entryId: string; message: RecordLike }> = [];
|
|
37
|
+
for (const rawEntry of branch) {
|
|
38
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
39
|
+
const entry = rawEntry as RecordLike;
|
|
40
|
+
const message = entry.message;
|
|
41
|
+
if (entry.type !== "message" || !message || typeof message !== "object") continue;
|
|
42
|
+
if ((message as RecordLike).role !== "system") continue;
|
|
43
|
+
const entryId = typeof entry.id === "string" ? entry.id : "";
|
|
44
|
+
if (!entryId || session.seenSystemEntryIds.has(entryId)) continue;
|
|
45
|
+
session.seenSystemEntryIds.add(entryId);
|
|
46
|
+
messages.push({ entryId, message: message as RecordLike });
|
|
47
|
+
}
|
|
48
|
+
return messages;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Mark restored transcript state as already seen so the first trace emits one snapshot, not the full history. */
|
|
52
|
+
export function initializeSystemStateTracking(ctx: any): void {
|
|
53
|
+
const session = getSessionRunState();
|
|
54
|
+
try {
|
|
55
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
56
|
+
if (!Array.isArray(branch)) return;
|
|
57
|
+
for (const rawEntry of branch) {
|
|
58
|
+
if (!rawEntry || typeof rawEntry !== "object") continue;
|
|
59
|
+
const entry = rawEntry as RecordLike;
|
|
60
|
+
const message = entry.message;
|
|
61
|
+
if (
|
|
62
|
+
entry.type === "message" &&
|
|
63
|
+
typeof entry.id === "string" &&
|
|
64
|
+
message &&
|
|
65
|
+
typeof message === "object" &&
|
|
66
|
+
(message as RecordLike).role === "system"
|
|
67
|
+
) {
|
|
68
|
+
session.seenSystemEntryIds.add(entry.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Session history is optional in SDK/headless hosts.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function recordSystemState(ctx: any, activeTools: string[]): Promise<void> {
|
|
77
|
+
const agent = state.agentState;
|
|
78
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
79
|
+
|
|
80
|
+
let prompt = "";
|
|
81
|
+
try {
|
|
82
|
+
prompt = String((await ctx?.getSystemPrompt?.()) ?? "");
|
|
83
|
+
} catch {
|
|
84
|
+
// The tool state is still useful when a host cannot expose the rendered prompt.
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tools = sortedUnique(activeTools);
|
|
88
|
+
const promptStateHash = prompt ? fingerprint(prompt) : undefined;
|
|
89
|
+
const toolStateHash = fingerprint(JSON.stringify(tools));
|
|
90
|
+
const session = getSessionRunState();
|
|
91
|
+
const systemMessages = readNewSystemMessages(ctx);
|
|
92
|
+
|
|
93
|
+
const transcriptToolsAdded: string[] = [];
|
|
94
|
+
const transcriptToolsRemoved: string[] = [];
|
|
95
|
+
const sectionsChanged = new Set<string>();
|
|
96
|
+
const sectionsRemoved = new Set<string>();
|
|
97
|
+
const entryIds: string[] = [];
|
|
98
|
+
for (const { entryId, message } of systemMessages) {
|
|
99
|
+
entryIds.push(entryId);
|
|
100
|
+
const sections = message.sections;
|
|
101
|
+
if (sections && typeof sections === "object" && !Array.isArray(sections)) {
|
|
102
|
+
for (const [name, value] of Object.entries(sections as RecordLike)) {
|
|
103
|
+
(value === null ? sectionsRemoved : sectionsChanged).add(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(message.toolsAdded)) {
|
|
107
|
+
for (const tool of message.toolsAdded) {
|
|
108
|
+
const name = toolName(tool);
|
|
109
|
+
if (name) transcriptToolsAdded.push(name);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(message.toolsRemoved)) {
|
|
113
|
+
for (const tool of message.toolsRemoved) {
|
|
114
|
+
const name = toolName(tool);
|
|
115
|
+
if (name) transcriptToolsRemoved.push(name);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const previousTools = new Set(session.lastActiveTools);
|
|
121
|
+
const currentTools = new Set(tools);
|
|
122
|
+
const toolsAdded = sortedUnique([
|
|
123
|
+
...tools.filter((name) => !previousTools.has(name)),
|
|
124
|
+
...transcriptToolsAdded,
|
|
125
|
+
]);
|
|
126
|
+
const toolsRemoved = sortedUnique([
|
|
127
|
+
...session.lastActiveTools.filter((name) => !currentTools.has(name)),
|
|
128
|
+
...transcriptToolsRemoved,
|
|
129
|
+
]);
|
|
130
|
+
const promptChanged = session.lastPromptStateHash !== promptStateHash;
|
|
131
|
+
const toolsChanged = session.lastToolStateHash !== toolStateHash;
|
|
132
|
+
const isInitial = session.lastPromptStateHash === undefined && session.lastToolStateHash === undefined;
|
|
133
|
+
const changed = isInitial || promptChanged || toolsChanged || systemMessages.length > 0;
|
|
134
|
+
if (changed) session.systemStateSequence++;
|
|
135
|
+
|
|
136
|
+
const capturePolicy = getCapturePolicy();
|
|
137
|
+
const rawSystemUpdate = systemMessages.length > 0
|
|
138
|
+
? systemMessages.map(({ entryId, message }) => ({ entryId, ...message }))
|
|
139
|
+
: { role: "system", content: prompt };
|
|
140
|
+
const captured = applyCapturePolicy(
|
|
141
|
+
{
|
|
142
|
+
systemPrompt: shapePayload(rawSystemUpdate),
|
|
143
|
+
metadata: {
|
|
144
|
+
stateSequence: session.systemStateSequence,
|
|
145
|
+
changeKind: isInitial ? "initial" : changed ? "updated" : "unchanged",
|
|
146
|
+
source: systemMessages.length > 0 ? "transcript" : "effective-snapshot",
|
|
147
|
+
transcriptEntryIds: entryIds,
|
|
148
|
+
promptStateHash,
|
|
149
|
+
toolStateHash,
|
|
150
|
+
promptChars: prompt.length,
|
|
151
|
+
activeToolCount: tools.length,
|
|
152
|
+
activeTools: tools,
|
|
153
|
+
toolsAdded,
|
|
154
|
+
toolsRemoved,
|
|
155
|
+
sectionsChanged: sortedUnique(sectionsChanged),
|
|
156
|
+
sectionsRemoved: sortedUnique(sectionsRemoved),
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
capturePolicy,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const observation = await startChildObservation({
|
|
164
|
+
parent: agent.activeAttempt ?? agent.root,
|
|
165
|
+
runtime: getRuntime,
|
|
166
|
+
name: "system-state",
|
|
167
|
+
body: {
|
|
168
|
+
input: captured.systemPrompt,
|
|
169
|
+
metadata: captured.metadata,
|
|
170
|
+
},
|
|
171
|
+
asType: "event",
|
|
172
|
+
});
|
|
173
|
+
observation.end();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
console.warn("📊 Langfuse: Failed to record system state", error);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (changed) agent.systemStateChangeCount = (agent.systemStateChangeCount ?? 0) + 1;
|
|
179
|
+
agent.promptStateHash = promptStateHash;
|
|
180
|
+
agent.toolStateHash = toolStateHash;
|
|
181
|
+
agent.systemStateSequence = session.systemStateSequence;
|
|
182
|
+
agent.activeToolCount = tools.length;
|
|
183
|
+
session.lastPromptStateHash = promptStateHash;
|
|
184
|
+
session.lastToolStateHash = toolStateHash;
|
|
185
|
+
session.lastActiveTools = tools;
|
|
186
|
+
}
|
package/src/handlers/tool.ts
CHANGED
|
@@ -37,7 +37,7 @@ export async function startToolObservation(event: Record<string, unknown>) {
|
|
|
37
37
|
getCapturePolicy(),
|
|
38
38
|
);
|
|
39
39
|
const inputBytes = estimatePayloadBytes(captured.toolInput, getLimits().maxToolPayload);
|
|
40
|
-
const parent = state.agentState.activeTurn ?? state.agentState.root;
|
|
40
|
+
const parent = state.agentState.activeTurn ?? state.agentState.activeAttempt ?? state.agentState.root;
|
|
41
41
|
const tool = await startChildObservation({
|
|
42
42
|
parent,
|
|
43
43
|
runtime: getRuntime,
|
package/src/handlers/turn.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function startTurnObservation(event: Record<string, unknown>) {
|
|
|
25
25
|
getCapturePolicy(),
|
|
26
26
|
);
|
|
27
27
|
const observation = await startChildObservation({
|
|
28
|
-
parent: state.agentState.root,
|
|
28
|
+
parent: state.agentState.activeAttempt ?? state.agentState.root,
|
|
29
29
|
runtime: getRuntime,
|
|
30
30
|
name: "turn",
|
|
31
31
|
body: {
|
package/src/langfuse.ts
CHANGED
|
@@ -29,6 +29,7 @@ interface RestFallbackTrace {
|
|
|
29
29
|
input?: unknown;
|
|
30
30
|
output?: unknown;
|
|
31
31
|
sessionId?: string;
|
|
32
|
+
userId?: string;
|
|
32
33
|
metadata?: Record<string, unknown>;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -558,12 +559,15 @@ function wrapObservation(
|
|
|
558
559
|
store.observationById.set(id, record);
|
|
559
560
|
|
|
560
561
|
if (!parentObservationId && !store.trace) {
|
|
562
|
+
const propagated = readPropagatedAttributes(observation);
|
|
561
563
|
store.trace = {
|
|
562
564
|
id: traceId,
|
|
563
565
|
timestamp: record.startTime,
|
|
564
566
|
name,
|
|
565
567
|
input: body?.input,
|
|
566
|
-
sessionId:
|
|
568
|
+
sessionId: propagated.sessionId
|
|
569
|
+
?? (typeof metadata?.sessionId === "string" ? metadata.sessionId : state.currentSessionId || undefined),
|
|
570
|
+
userId: propagated.userId ?? state.config?.userId,
|
|
567
571
|
metadata,
|
|
568
572
|
};
|
|
569
573
|
}
|
|
@@ -743,6 +747,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime, signal: AbortSignal)
|
|
|
743
747
|
input: trace.input,
|
|
744
748
|
output: trace.output,
|
|
745
749
|
sessionId: trace.sessionId,
|
|
750
|
+
userId: trace.userId,
|
|
746
751
|
metadata: trace.metadata,
|
|
747
752
|
},
|
|
748
753
|
},
|
package/src/observation.ts
CHANGED
|
@@ -11,7 +11,7 @@ export async function startChildObservation({
|
|
|
11
11
|
runtime: () => Promise<LangfuseRuntime>;
|
|
12
12
|
name: string;
|
|
13
13
|
body?: ObservationUpdate;
|
|
14
|
-
asType: "generation" | "tool" | "span";
|
|
14
|
+
asType: "event" | "generation" | "tool" | "span";
|
|
15
15
|
}): Promise<LangfuseObservation> {
|
|
16
16
|
if (parent.startObservation) {
|
|
17
17
|
return parent.startObservation(name, body, { asType });
|
package/src/state.ts
CHANGED
|
@@ -10,6 +10,12 @@ export interface SessionRunState {
|
|
|
10
10
|
turnCount: number;
|
|
11
11
|
tracingDisabled: boolean;
|
|
12
12
|
setupAttemptedThisSession: boolean;
|
|
13
|
+
lastPromptStateHash?: string;
|
|
14
|
+
lastToolStateHash?: string;
|
|
15
|
+
lastActiveTools: string[];
|
|
16
|
+
systemStateSequence: number;
|
|
17
|
+
seenSystemEntryIds: Set<string>;
|
|
18
|
+
seenUsageEntryIds: Set<string>;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
const DEFAULT_SESSION_ID = "__pi_langfuse_default_session__";
|
|
@@ -27,6 +33,10 @@ function createSessionRunState(): SessionRunState {
|
|
|
27
33
|
turnCount: 0,
|
|
28
34
|
tracingDisabled: false,
|
|
29
35
|
setupAttemptedThisSession: false,
|
|
36
|
+
lastActiveTools: [],
|
|
37
|
+
systemStateSequence: 0,
|
|
38
|
+
seenSystemEntryIds: new Set(),
|
|
39
|
+
seenUsageEntryIds: new Set(),
|
|
30
40
|
};
|
|
31
41
|
}
|
|
32
42
|
|
|
@@ -130,11 +140,16 @@ export const state = {
|
|
|
130
140
|
|
|
131
141
|
export function resetRunState(sessionId = getActiveSessionId()) {
|
|
132
142
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
133
|
-
const
|
|
134
|
-
state.sessionStates.get(normalizedSessionId)?.setupAttemptedThisSession ?? false;
|
|
143
|
+
const previous = state.sessionStates.get(normalizedSessionId);
|
|
135
144
|
state.sessionStates.set(normalizedSessionId, {
|
|
136
145
|
...createSessionRunState(),
|
|
137
|
-
setupAttemptedThisSession,
|
|
146
|
+
setupAttemptedThisSession: previous?.setupAttemptedThisSession ?? false,
|
|
147
|
+
lastPromptStateHash: previous?.lastPromptStateHash,
|
|
148
|
+
lastToolStateHash: previous?.lastToolStateHash,
|
|
149
|
+
lastActiveTools: previous?.lastActiveTools ?? [],
|
|
150
|
+
systemStateSequence: previous?.systemStateSequence ?? 0,
|
|
151
|
+
seenSystemEntryIds: previous?.seenSystemEntryIds ?? new Set(),
|
|
152
|
+
seenUsageEntryIds: previous?.seenUsageEntryIds ?? new Set(),
|
|
138
153
|
});
|
|
139
154
|
}
|
|
140
155
|
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface Config {
|
|
|
6
6
|
publicKey: string;
|
|
7
7
|
secretKey: string;
|
|
8
8
|
host: string;
|
|
9
|
+
userId?: string;
|
|
9
10
|
capturePolicy?: CapturePolicy;
|
|
10
11
|
limits?: PayloadLimits;
|
|
11
12
|
usage?: UsageOptions;
|
|
@@ -19,7 +20,7 @@ export interface LangfuseObservation {
|
|
|
19
20
|
startObservation?(
|
|
20
21
|
name: string,
|
|
21
22
|
body?: ObservationUpdate,
|
|
22
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
23
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
23
24
|
): LangfuseObservation;
|
|
24
25
|
setTraceIO?(body?: { input?: unknown; output?: unknown }): void;
|
|
25
26
|
}
|
|
@@ -82,7 +83,7 @@ export interface LangfuseRuntime {
|
|
|
82
83
|
startObservation: (
|
|
83
84
|
name: string,
|
|
84
85
|
body?: ObservationUpdate,
|
|
85
|
-
options?: { asType?: "agent" | "generation" | "tool" | "span" },
|
|
86
|
+
options?: { asType?: "agent" | "event" | "generation" | "tool" | "span" },
|
|
86
87
|
) => LangfuseObservation;
|
|
87
88
|
propagateAttributes: (
|
|
88
89
|
params: {
|
|
@@ -136,6 +137,7 @@ export interface ToolState {
|
|
|
136
137
|
|
|
137
138
|
export interface AgentState {
|
|
138
139
|
root?: LangfuseObservation;
|
|
140
|
+
activeAttempt?: LangfuseObservation;
|
|
139
141
|
activeTurn?: LangfuseObservation;
|
|
140
142
|
traceId?: string;
|
|
141
143
|
promptInput?: unknown;
|
|
@@ -147,4 +149,14 @@ export interface AgentState {
|
|
|
147
149
|
latestAssistantOutput?: unknown;
|
|
148
150
|
sourceMetadata?: Record<string, unknown>;
|
|
149
151
|
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
152
|
+
attemptCount: number;
|
|
153
|
+
lastAgentEndEvent?: Record<string, unknown>;
|
|
154
|
+
promptStateHash?: string;
|
|
155
|
+
toolStateHash?: string;
|
|
156
|
+
systemStateSequence?: number;
|
|
157
|
+
activeToolCount?: number;
|
|
158
|
+
systemStateChangeCount: number;
|
|
159
|
+
cacheReadTokens: number;
|
|
160
|
+
cacheWriteTokens: number;
|
|
161
|
+
uncachedInputTokens: number;
|
|
150
162
|
}
|