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.
@@ -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
+ }
@@ -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,
@@ -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: typeof metadata?.sessionId === "string" ? metadata.sessionId : state.currentSessionId || undefined,
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
  },
@@ -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 setupAttemptedThisSession =
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
  }