pi-langfuse 1.5.18 → 1.6.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 CHANGED
@@ -269,6 +269,12 @@ This command makes a timeout-bounded authenticated request to Langfuse and, if i
269
269
  - Tool runs appear as tool observations with arguments, results, and error state.
270
270
  - LLM requests appear as generation observations, including usage and cost when the provider exposes them.
271
271
  Reasoning tokens are reported as their own usage bucket when `PI_LANGFUSE_SPLIT_REASONING_TOKENS` is enabled.
272
+ - Pi 0.86 transcript-aware prompt/tool state appears as `system-state` events. Generations reference the
273
+ effective prompt/tool state hashes, active tool count, update transport, and cache hit ratio.
274
+ - Automatic retries are grouped under `agent-attempt` spans and the trace remains open until `agent_settled`.
275
+ - Cache-warming decisions and persisted `cache_warm` usage are reported, including standalone session-grouped
276
+ traces when warming happens while no user-request trace is active.
277
+ - Compaction is recorded as `session-compaction`, with a nested `compaction-summary` generation when usage is available.
272
278
  - Trace-level scores include tool counts, tool success rate, and whether the run had errors.
273
279
 
274
280
  The package also includes a Langfuse CLI skill, so Langfuse data can be queried directly from Pi:
package/README_CN.md CHANGED
@@ -250,6 +250,12 @@ pi list
250
250
  - 工具执行会以工具观察节点展示参数、结果和错误状态。
251
251
  - 模型请求会以生成观察节点展示;如果提供商暴露相关信息,还会包含用量和成本。
252
252
  开启 `PI_LANGFUSE_SPLIT_REASONING_TOKENS` 后,推理 token 会作为独立用量桶上报。
253
+ - Pi 0.86 的 transcript-aware system/tool 状态会作为 `system-state` 事件上报;generation 会引用有效的
254
+ prompt/tool 状态指纹、活动工具数、更新传输方式和缓存命中率。
255
+ - 自动重试会归入 `agent-attempt` span,trace 会持续到 `agent_settled` 才结束。
256
+ - cache warming 决策及持久化的 `cache_warm` 用量会被记录;若 warming 发生时没有活动的用户请求 trace,
257
+ 会创建按相同 session 分组的独立 trace。
258
+ - compaction 会记录为 `session-compaction`;若存在摘要用量,则包含 `compaction-summary` generation。
253
259
  - trace 级别会记录工具调用次数、工具成功率和是否出现错误。
254
260
 
255
261
  此包还包含一个内置 Langfuse 技能,可直接在 Pi 中查询 Langfuse 数据:
package/index.ts CHANGED
@@ -14,10 +14,23 @@ import { state, resetRunState, runWithSession, setCurrentSession } from "./src/s
14
14
  import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
15
15
  import { shutdownRuntime } from "./src/langfuse.js";
16
16
  import { handleLangfusePrivacyCommand, handleLangfuseStatusCommand, handleLangfuseTestCommand } from "./src/commands.js";
17
- import { getMessageFromEvent, extractAssistantOutput, getCapturePolicy } from "./src/utils.js";
18
- import { applyCapturePolicy } from "./src/capture-policy.js";
19
- import { startAgentRun, finishAgentRun, recordSystemPrompt } from "./src/handlers/agent.js";
17
+ import { getMessageFromEvent, extractAssistantOutput } from "./src/utils.js";
18
+ import {
19
+ startAgentRun,
20
+ finishAgentRun,
21
+ finishAgentAttempt,
22
+ cancelAgentRun,
23
+ recordSystemPrompt,
24
+ startAgentAttempt,
25
+ } from "./src/handlers/agent.js";
20
26
  import { startTurnObservation, finishTurnObservation } from "./src/handlers/turn.js";
27
+ import { initializeSystemStateTracking, recordSystemState } from "./src/handlers/system-state.js";
28
+ import {
29
+ initializeUsageTracking,
30
+ recordCacheWarmingDecision,
31
+ recordNewUsageEntries,
32
+ } from "./src/handlers/cache.js";
33
+ import { recordSessionCompaction } from "./src/handlers/session.js";
21
34
  import {
22
35
  startGeneration,
23
36
  updateGenerationMetadata,
@@ -28,7 +41,6 @@ import {
28
41
  import {
29
42
  startToolObservation,
30
43
  finishToolObservation,
31
- closeDanglingObservations,
32
44
  } from "./src/handlers/tool.js";
33
45
 
34
46
  // ============================================
@@ -36,6 +48,7 @@ import {
36
48
  // ============================================
37
49
 
38
50
  export default async function (pi: ExtensionAPI) {
51
+ const asRecord = (value: object): Record<string, unknown> => value as unknown as Record<string, unknown>;
39
52
  if (!state.config) {
40
53
  state.config = loadConfig();
41
54
  }
@@ -97,6 +110,8 @@ export default async function (pi: ExtensionAPI) {
97
110
  state.setupAttemptedThisSession = false;
98
111
  await ensureConfig(ctx);
99
112
  resetRunState();
113
+ initializeSystemStateTracking(ctx);
114
+ initializeUsageTracking(ctx);
100
115
  }));
101
116
 
102
117
  pi.on("model_select", async (event, ctx) => withSession(ctx, async () => {
@@ -105,84 +120,87 @@ export default async function (pi: ExtensionAPI) {
105
120
  }));
106
121
 
107
122
  pi.on("before_agent_start", async (event, ctx) => withSession(ctx, async () => {
108
- await startAgentRun(event, ctx);
123
+ await recordNewUsageEntries(ctx);
124
+ await startAgentRun(asRecord(event), ctx);
109
125
  }));
110
126
 
111
127
  pi.on("agent_start", async (event, ctx) => withSession(ctx, async () => {
112
128
  if (!state.agentState?.root) {
113
- await startAgentRun(event, ctx);
129
+ await startAgentRun(asRecord(event), ctx);
114
130
  }
131
+ await startAgentAttempt();
115
132
  // The system prompt is only final here: before_agent_start handlers that
116
133
  // run after this extension may still rewrite it.
117
134
  await recordSystemPrompt(ctx);
135
+ await recordSystemState(ctx, pi.getActiveTools());
136
+ }));
137
+
138
+ pi.on("cache_warming_decision", async (event, ctx) => withSession(ctx, async () => {
139
+ await recordNewUsageEntries(ctx);
140
+ await recordCacheWarmingDecision(asRecord(event), ctx);
118
141
  }));
119
142
 
120
143
  pi.on("turn_start", async (event, ctx) => withSession(ctx, async () => {
121
- await startTurnObservation(event);
144
+ await startTurnObservation(asRecord(event));
122
145
  }));
123
146
 
124
147
  pi.on("before_provider_request", async (event, ctx) => withSession(ctx, async () => {
125
- await startGeneration(event);
148
+ await startGeneration(asRecord(event));
126
149
  }));
127
150
 
128
151
  pi.on("after_provider_response", async (event, ctx) => withSession(ctx, async () => {
129
- updateGenerationMetadata(event);
152
+ updateGenerationMetadata(asRecord(event));
130
153
  }));
131
154
 
132
155
  pi.on("message_update", async (event, ctx) => withSession(ctx, async () => {
133
- recordTTFT(event);
134
- const message = getMessageFromEvent(event);
156
+ recordTTFT(asRecord(event));
157
+ const message = getMessageFromEvent(asRecord(event));
135
158
  if (message?.role === "assistant" && state.agentState) {
136
159
  state.agentState.latestAssistantOutput = extractAssistantOutput(message);
137
160
  }
138
161
  }));
139
162
 
140
163
  pi.on("message_end", async (event, ctx) => withSession(ctx, async () => {
141
- await finishGenerationFromMessage(event);
164
+ await finishGenerationFromMessage(asRecord(event));
142
165
  }));
143
166
 
144
167
  pi.on("tool_execution_start", async (event, ctx) => withSession(ctx, async () => {
145
- await startToolObservation(event);
168
+ await startToolObservation(asRecord(event));
146
169
  }));
147
170
 
148
171
  pi.on("tool_call", async (event, ctx) => withSession(ctx, async () => {
149
- await startToolObservation(event);
172
+ await startToolObservation(asRecord(event));
150
173
  }));
151
174
 
152
175
  pi.on("tool_result", async (event, ctx) => withSession(ctx, async () => {
153
- await finishToolObservation(event);
176
+ await finishToolObservation(asRecord(event));
154
177
  }));
155
178
 
156
179
  pi.on("tool_execution_end", async (event, ctx) => withSession(ctx, async () => {
157
- await finishToolObservation(event);
180
+ await finishToolObservation(asRecord(event));
158
181
  }));
159
182
 
160
183
  pi.on("turn_end", async (event, ctx) => withSession(ctx, async () => {
161
184
  state.turnCount++;
162
- const message = getMessageFromEvent(event);
185
+ const record = asRecord(event);
186
+ const message = getMessageFromEvent(record);
163
187
  if (message?.role === "assistant") {
164
- await createFallbackGenerationFromTurn(event, message);
165
- await finishGenerationFromMessage(event);
188
+ await createFallbackGenerationFromTurn(record, message);
189
+ await finishGenerationFromMessage(record);
166
190
  }
167
- finishTurnObservation(event);
191
+ finishTurnObservation(record);
168
192
  }));
169
193
 
170
194
  pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
171
- await finishAgentRun(event);
172
- const sessionId = state.currentSessionId;
173
- try {
174
- await shutdownRuntime(sessionId);
175
- } catch (error) {
176
- console.warn("📊 Langfuse: Shutdown failed", error);
177
- }
195
+ finishAgentAttempt(asRecord(event));
196
+ }));
197
+
198
+ pi.on("agent_settled", async (_event, ctx) => withSession(ctx, async () => {
199
+ await finishAgentRun();
178
200
  }));
179
201
 
180
202
  const handleSessionInterruption = (reason: string) => {
181
- if (state.agentState?.root) {
182
- closeDanglingObservations(reason);
183
- state.agentState.root.update({ metadata: { completed: false, cancelled: true } }).end();
184
- }
185
- resetRunState();
203
+ cancelAgentRun(reason);
186
204
  };
187
205
 
188
206
  pi.on("session_before_switch", async (_event, ctx) => {
@@ -200,26 +218,11 @@ export default async function (pi: ExtensionAPI) {
200
218
  });
201
219
 
202
220
  pi.on("session_compact", async (event, ctx) => withSession(ctx, async () => {
203
- if (state.agentState?.root) {
204
- const parent = state.agentState.activeTurn ?? state.agentState.root;
205
- try {
206
- const observation = parent.startObservation ? parent.startObservation(
207
- "session_compact",
208
- {
209
- level: "DEFAULT",
210
- statusMessage: "Context was compacted",
211
- metadata: applyCapturePolicy({ metadata: { ...event } }, getCapturePolicy()).metadata
212
- },
213
- { asType: "span" }
214
- ) : undefined;
215
- observation?.end();
216
- } catch (e) {
217
- // ignore
218
- }
219
- }
221
+ await recordSessionCompaction(asRecord(event));
220
222
  }));
221
223
 
222
224
  pi.on("session_shutdown", async (_event, ctx) => withSession(ctx, async () => {
225
+ await recordNewUsageEntries(ctx);
223
226
  handleSessionInterruption("Session shutdown before agent completed");
224
227
  await shutdownRuntime();
225
228
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.5.18",
3
+ "version": "1.6.0",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,6 @@
16
16
  "files": [
17
17
  "index.ts",
18
18
  "src/",
19
- "types/",
20
19
  "README.md",
21
20
  "README_CN.md",
22
21
  "image.png",
@@ -52,7 +51,7 @@
52
51
  "@opentelemetry/sdk-trace-base": "^2.0.1"
53
52
  },
54
53
  "peerDependencies": {
55
- "@earendil-works/pi-coding-agent": "*"
54
+ "@earendil-works/pi-coding-agent": ">=0.86.0"
56
55
  },
57
56
  "publishConfig": {
58
57
  "access": "public",
@@ -63,6 +62,7 @@
63
62
  "node": ">=22"
64
63
  },
65
64
  "devDependencies": {
65
+ "@earendil-works/pi-coding-agent": "^0.86.0",
66
66
  "tsx": "^4.19.0",
67
67
  "typescript": "^6.0.3"
68
68
  }
@@ -5,6 +5,7 @@ import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput,
5
5
  import { closeDanglingObservations } from "./tool.js";
6
6
  import { applyCapturePolicy } from "../capture-policy.js";
7
7
  import { collectSourceMetadata } from "../source-metadata.js";
8
+ import { startChildObservation } from "../observation.js";
8
9
 
9
10
  function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
10
11
  if (!metadata) {
@@ -70,6 +71,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
70
71
  ...(state.currentModel ? { model: state.currentModel } : {}),
71
72
  ...(state.currentProvider ? { provider: state.currentProvider } : {}),
72
73
  sessionId: state.currentSessionId || undefined,
74
+ sessionLeafId: ctx?.sessionManager?.getLeafId?.() || undefined,
73
75
  },
74
76
  },
75
77
  capturePolicy,
@@ -84,6 +86,11 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
84
86
  activeTools: new Map(),
85
87
  sourceMetadata,
86
88
  providerMetadataByRequest: new Map(),
89
+ attemptCount: 0,
90
+ systemStateChangeCount: 0,
91
+ cacheReadTokens: 0,
92
+ cacheWriteTokens: 0,
93
+ uncachedInputTokens: 0,
87
94
  };
88
95
 
89
96
  const root = rt.propagateAttributes(
@@ -112,6 +119,82 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
112
119
  }
113
120
  }
114
121
 
122
+ export async function startAgentAttempt() {
123
+ const agent = state.agentState;
124
+ if (state.isTracingDisabled || !agent?.root) return;
125
+
126
+ if (agent.activeAttempt) {
127
+ agent.activeAttempt
128
+ .update({ level: "WARNING", statusMessage: "A new agent attempt started before the previous attempt ended" })
129
+ .end();
130
+ }
131
+
132
+ try {
133
+ agent.attemptCount++;
134
+ agent.activeAttempt = await startChildObservation({
135
+ parent: agent.root,
136
+ runtime: getRuntime,
137
+ name: "agent-attempt",
138
+ body: { metadata: { attemptIndex: agent.attemptCount } },
139
+ asType: "span",
140
+ });
141
+ } catch (error) {
142
+ console.warn("📊 Langfuse: Failed to start agent attempt", error);
143
+ }
144
+ }
145
+
146
+ export function finishAgentAttempt(event: Record<string, unknown> = {}) {
147
+ const agent = state.agentState;
148
+ if (!agent) return;
149
+ agent.lastAgentEndEvent = event;
150
+
151
+ closeDanglingObservations("Agent attempt ended before observation finalized");
152
+ if (agent.activeTurn) {
153
+ agent.activeTurn
154
+ .update({ level: "WARNING", statusMessage: "Agent attempt ended before turn finalized" })
155
+ .end();
156
+ agent.activeTurn = undefined;
157
+ }
158
+
159
+ if (!agent.activeAttempt) return;
160
+ try {
161
+ const lastAssistant = extractFinalAssistant(event.messages);
162
+ const captured = applyCapturePolicy(
163
+ { output: lastAssistant ? extractAssistantOutput(lastAssistant) : undefined },
164
+ getCapturePolicy(),
165
+ );
166
+ agent.activeAttempt.update({ output: captured.output }).end();
167
+ } catch (error) {
168
+ console.warn("📊 Langfuse: Failed to finish agent attempt", error);
169
+ } finally {
170
+ agent.activeAttempt = undefined;
171
+ }
172
+ }
173
+
174
+ export function cancelAgentRun(reason: string) {
175
+ const agent = state.agentState;
176
+ if (!agent?.root) {
177
+ resetRunState();
178
+ return;
179
+ }
180
+
181
+ closeDanglingObservations(reason);
182
+ if (agent.activeTurn) {
183
+ agent.activeTurn.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
184
+ agent.activeTurn = undefined;
185
+ }
186
+ if (agent.activeAttempt) {
187
+ agent.activeAttempt.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
188
+ agent.activeAttempt = undefined;
189
+ }
190
+ agent.root.update({
191
+ level: "WARNING",
192
+ statusMessage: reason,
193
+ metadata: { completed: false, cancelled: true },
194
+ }).end();
195
+ resetRunState();
196
+ }
197
+
115
198
  /**
116
199
  * Records the effective system prompt on the root agent observation.
117
200
  *
@@ -161,7 +244,8 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
161
244
  return;
162
245
  }
163
246
 
164
- const lastAssistant = extractFinalAssistant(event.messages);
247
+ const finalEvent = Object.keys(event).length > 0 ? event : state.agentState.lastAgentEndEvent ?? {};
248
+ const lastAssistant = extractFinalAssistant(finalEvent.messages);
165
249
  const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
166
250
  const captured = applyCapturePolicy(
167
251
  {
@@ -173,6 +257,19 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
173
257
  model: state.currentModel || undefined,
174
258
  provider: state.currentProvider || undefined,
175
259
  totalTools: state.toolCallCount,
260
+ agentAttemptCount: state.agentState.attemptCount,
261
+ systemStateChangeCount: state.agentState.systemStateChangeCount,
262
+ promptStateHash: state.agentState.promptStateHash,
263
+ toolStateHash: state.agentState.toolStateHash,
264
+ activeToolCount: state.agentState.activeToolCount,
265
+ cacheReadTokens: state.agentState.cacheReadTokens,
266
+ cacheWriteTokens: state.agentState.cacheWriteTokens,
267
+ uncachedInputTokens: state.agentState.uncachedInputTokens,
268
+ cacheHitRatio:
269
+ state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens > 0
270
+ ? state.agentState.cacheReadTokens /
271
+ (state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens)
272
+ : undefined,
176
273
  ...computeEvaluationScores(),
177
274
  },
178
275
  },
@@ -181,6 +278,10 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
181
278
  const scores = computeEvaluationScores();
182
279
 
183
280
  closeDanglingObservations("Agent run ended before observation finalized");
281
+ if (state.agentState.activeAttempt) {
282
+ state.agentState.activeAttempt.end();
283
+ state.agentState.activeAttempt = undefined;
284
+ }
184
285
 
185
286
  try {
186
287
  state.agentState.root
@@ -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
  }