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
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Langfuse observability extension for [Pi Coding Agent](https://github.com/earend
|
|
|
34
34
|
- Langfuse public key, starting with `pk-lf-...`
|
|
35
35
|
- Langfuse secret key, starting with `sk-lf-...`
|
|
36
36
|
- Langfuse host, defaulting to `https://cloud.langfuse.com`
|
|
37
|
+
- Optional Langfuse user ID for per-user usage and cost aggregation
|
|
37
38
|
|
|
38
39
|
3. Run Pi normally:
|
|
39
40
|
|
|
@@ -63,7 +64,7 @@ To inspect the active configuration without exposing secrets:
|
|
|
63
64
|
/langfuse-status
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
The status command reports the config source, host, masked public key, capture policy, active-run state, config path, and last runtime error.
|
|
67
|
+
The status command reports the config source, host, masked public key, whether a user ID is configured, capture policy, active-run state, config path, and last runtime error. It never prints the user ID itself.
|
|
67
68
|
|
|
68
69
|
### Method 2: Environment variables
|
|
69
70
|
|
|
@@ -73,9 +74,11 @@ Set these before starting Pi:
|
|
|
73
74
|
export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
|
|
74
75
|
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
|
|
75
76
|
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # optional; LANGFUSE_HOST is also supported
|
|
77
|
+
export LANGFUSE_USER_ID="user-123" # optional; max 200 characters
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
Saved config takes precedence. Environment variables are only used when `~/.pi/agent/pi-langfuse/config.json` is missing or incomplete.
|
|
81
|
+
If saved credentials exist but `userId` is absent, `LANGFUSE_USER_ID` is still used. An explicitly saved `userId` takes precedence.
|
|
79
82
|
|
|
80
83
|
For short-lived SDK hosts, set the bounded final score-delivery attempt during shutdown:
|
|
81
84
|
|
|
@@ -222,10 +225,13 @@ Create or update `~/.pi/agent/pi-langfuse/config.json`:
|
|
|
222
225
|
"publicKey": "pk-lf-xxxx",
|
|
223
226
|
"secretKey": "sk-lf-xxxx",
|
|
224
227
|
"host": "https://cloud.langfuse.com",
|
|
228
|
+
"userId": "user-123",
|
|
225
229
|
"privacyPreset": "conversations"
|
|
226
230
|
}
|
|
227
231
|
```
|
|
228
232
|
|
|
233
|
+
`userId` is optional, explicit-only, and limited to 200 characters. The extension does not infer it from the local operating-system account. When configured, it is propagated to every observation in the trace so Langfuse can provide per-user filtering and cost aggregation.
|
|
234
|
+
|
|
229
235
|
Fine-grained capture flags can also be persisted:
|
|
230
236
|
|
|
231
237
|
```json
|
|
@@ -269,6 +275,12 @@ This command makes a timeout-bounded authenticated request to Langfuse and, if i
|
|
|
269
275
|
- Tool runs appear as tool observations with arguments, results, and error state.
|
|
270
276
|
- LLM requests appear as generation observations, including usage and cost when the provider exposes them.
|
|
271
277
|
Reasoning tokens are reported as their own usage bucket when `PI_LANGFUSE_SPLIT_REASONING_TOKENS` is enabled.
|
|
278
|
+
- Pi 0.86 transcript-aware prompt/tool state appears as `system-state` events. Generations reference the
|
|
279
|
+
effective prompt/tool state hashes, active tool count, update transport, and cache hit ratio.
|
|
280
|
+
- Automatic retries are grouped under `agent-attempt` spans and the trace remains open until `agent_settled`.
|
|
281
|
+
- Cache-warming decisions and persisted `cache_warm` usage are reported, including standalone session-grouped
|
|
282
|
+
traces when warming happens while no user-request trace is active.
|
|
283
|
+
- Compaction is recorded as `session-compaction`, with a nested `compaction-summary` generation when usage is available.
|
|
272
284
|
- Trace-level scores include tool counts, tool success rate, and whether the run had errors.
|
|
273
285
|
|
|
274
286
|
The package also includes a Langfuse CLI skill, so Langfuse data can be queried directly from Pi:
|
package/README_CN.md
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- Langfuse 公钥,以 `pk-lf-...` 开头
|
|
35
35
|
- Langfuse 密钥,以 `sk-lf-...` 开头
|
|
36
36
|
- Langfuse 主机地址,默认 `https://cloud.langfuse.com`
|
|
37
|
+
- 可选的 Langfuse 用户 ID,用于按用户汇总用量和成本
|
|
37
38
|
|
|
38
39
|
3. 正常运行 Pi:
|
|
39
40
|
|
|
@@ -63,7 +64,7 @@ Langfuse API 密钥可在 **Langfuse Cloud** -> **Settings** -> **API Keys** 中
|
|
|
63
64
|
/langfuse-status
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
状态命令会显示配置来源、主机地址、脱敏后的公钥、是否已配置用户 ID、采集策略、是否有活跃运行、配置文件路径,以及最近一次运行时错误,但不会输出用户 ID 本身。
|
|
67
68
|
|
|
68
69
|
### 方式 2:环境变量
|
|
69
70
|
|
|
@@ -73,9 +74,11 @@ Langfuse API 密钥可在 **Langfuse Cloud** -> **Settings** -> **API Keys** 中
|
|
|
73
74
|
export LANGFUSE_PUBLIC_KEY="pk-lf-xxxx"
|
|
74
75
|
export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
|
|
75
76
|
export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 可选;也支持 LANGFUSE_HOST
|
|
77
|
+
export LANGFUSE_USER_ID="user-123" # 可选;最长 200 个字符
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
保存的配置优先级更高。只有当 `~/.pi/agent/pi-langfuse/config.json` 缺失或不完整时,扩展才会使用环境变量。
|
|
81
|
+
如果已保存凭据但没有 `userId`,仍会读取 `LANGFUSE_USER_ID`;显式保存的 `userId` 优先级更高。
|
|
79
82
|
|
|
80
83
|
对于短生命周期的 SDK 宿主,可设置关闭时最终分数发送尝试的上限:
|
|
81
84
|
|
|
@@ -203,10 +206,13 @@ export PI_LANGFUSE_SPLIT_REASONING_TOKENS=true
|
|
|
203
206
|
"publicKey": "pk-lf-xxxx",
|
|
204
207
|
"secretKey": "sk-lf-xxxx",
|
|
205
208
|
"host": "https://cloud.langfuse.com",
|
|
209
|
+
"userId": "user-123",
|
|
206
210
|
"privacyPreset": "conversations"
|
|
207
211
|
}
|
|
208
212
|
```
|
|
209
213
|
|
|
214
|
+
`userId` 是可选的显式配置,最长 200 个字符。扩展不会从本机操作系统账户推断该值。配置后,它会传播到 trace 中的每个 observation,以支持 Langfuse 按用户筛选并汇总成本。
|
|
215
|
+
|
|
210
216
|
也可以持久化细粒度采集开关:
|
|
211
217
|
|
|
212
218
|
```json
|
|
@@ -250,6 +256,12 @@ pi list
|
|
|
250
256
|
- 工具执行会以工具观察节点展示参数、结果和错误状态。
|
|
251
257
|
- 模型请求会以生成观察节点展示;如果提供商暴露相关信息,还会包含用量和成本。
|
|
252
258
|
开启 `PI_LANGFUSE_SPLIT_REASONING_TOKENS` 后,推理 token 会作为独立用量桶上报。
|
|
259
|
+
- Pi 0.86 的 transcript-aware system/tool 状态会作为 `system-state` 事件上报;generation 会引用有效的
|
|
260
|
+
prompt/tool 状态指纹、活动工具数、更新传输方式和缓存命中率。
|
|
261
|
+
- 自动重试会归入 `agent-attempt` span,trace 会持续到 `agent_settled` 才结束。
|
|
262
|
+
- cache warming 决策及持久化的 `cache_warm` 用量会被记录;若 warming 发生时没有活动的用户请求 trace,
|
|
263
|
+
会创建按相同 session 分组的独立 trace。
|
|
264
|
+
- compaction 会记录为 `session-compaction`;若存在摘要用量,则包含 `compaction-summary` generation。
|
|
253
265
|
- trace 级别会记录工具调用次数、工具成功率和是否出现错误。
|
|
254
266
|
|
|
255
267
|
此包还包含一个内置 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
|
|
18
|
-
import {
|
|
19
|
-
|
|
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
|
|
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
|
|
185
|
+
const record = asRecord(event);
|
|
186
|
+
const message = getMessageFromEvent(record);
|
|
163
187
|
if (message?.role === "assistant") {
|
|
164
|
-
await createFallbackGenerationFromTurn(
|
|
165
|
-
await finishGenerationFromMessage(
|
|
188
|
+
await createFallbackGenerationFromTurn(record, message);
|
|
189
|
+
await finishGenerationFromMessage(record);
|
|
166
190
|
}
|
|
167
|
-
finishTurnObservation(
|
|
191
|
+
finishTurnObservation(record);
|
|
168
192
|
}));
|
|
169
193
|
|
|
170
194
|
pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "1.6.1",
|
|
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
|
}
|
package/src/commands.ts
CHANGED
|
@@ -211,6 +211,7 @@ function formatStatus(configPath: string, env: Record<string, string | undefined
|
|
|
211
211
|
`Source: ${configSource(env, configPath)}`,
|
|
212
212
|
`Host: ${safeConfig?.host ?? config.host}`,
|
|
213
213
|
`Public key: ${safeConfig?.publicKey ?? "[REDACTED_SECRET]"}`,
|
|
214
|
+
`User ID: ${config.userId ? "configured" : "not set"}`,
|
|
214
215
|
`Config file: ${configPath}`,
|
|
215
216
|
`Privacy preset: ${inferPreset(policy)}`,
|
|
216
217
|
"Capture:",
|
package/src/config.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { createCapturePolicy, type EnvLike } from "./capture-policy.js";
|
|
|
8
8
|
import { createPayloadLimits } from "./limits.js";
|
|
9
9
|
import { createUsageOptions } from "./usage-options.js";
|
|
10
10
|
|
|
11
|
+
const MAX_LANGFUSE_USER_ID_LENGTH = 200;
|
|
12
|
+
|
|
13
|
+
function normalizeUserId(value: unknown): string | undefined {
|
|
14
|
+
if (typeof value !== "string") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
return trimmed ? trimmed.slice(0, MAX_LANGFUSE_USER_ID_LENGTH) : undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
11
21
|
export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.env as EnvLike): Config | null {
|
|
12
22
|
if (existsSync(path)) {
|
|
13
23
|
try {
|
|
@@ -23,6 +33,7 @@ export function loadConfigFromFile(path = CONFIG_PATH, env: EnvLike = process.en
|
|
|
23
33
|
publicKey: config.publicKey,
|
|
24
34
|
secretKey: config.secretKey,
|
|
25
35
|
host: config.host || DEFAULT_LANGFUSE_HOST,
|
|
36
|
+
userId: normalizeUserId(config.userId) ?? normalizeUserId(env.LANGFUSE_USER_ID),
|
|
26
37
|
capturePolicy: createCapturePolicy(captureSource),
|
|
27
38
|
limits: createPayloadLimits(env),
|
|
28
39
|
usage: createUsageOptions(captureSource),
|
|
@@ -47,6 +58,7 @@ export function loadConfigFromEnv(env: EnvLike = process.env as EnvLike): Config
|
|
|
47
58
|
publicKey,
|
|
48
59
|
secretKey,
|
|
49
60
|
host: env.LANGFUSE_BASE_URL || env.LANGFUSE_HOST || DEFAULT_LANGFUSE_HOST,
|
|
61
|
+
userId: normalizeUserId(env.LANGFUSE_USER_ID),
|
|
50
62
|
capturePolicy: createCapturePolicy(env),
|
|
51
63
|
limits: createPayloadLimits(env),
|
|
52
64
|
usage: createUsageOptions(env),
|
|
@@ -108,10 +120,12 @@ async function collectConfigFromUI(ctx: any, reason: string): Promise<Config | n
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
const hostInput = (await ctx.ui.input("Langfuse host:", DEFAULT_LANGFUSE_HOST))?.trim();
|
|
123
|
+
const userId = normalizeUserId(await ctx.ui.input("Langfuse user ID (optional):", ""));
|
|
111
124
|
return {
|
|
112
125
|
publicKey,
|
|
113
126
|
secretKey,
|
|
114
127
|
host: hostInput || DEFAULT_LANGFUSE_HOST,
|
|
128
|
+
...(userId ? { userId } : {}),
|
|
115
129
|
capturePolicy: createCapturePolicy(),
|
|
116
130
|
};
|
|
117
131
|
}
|
package/src/handlers/agent.ts
CHANGED
|
@@ -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,11 +86,17 @@ 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(
|
|
90
97
|
{
|
|
91
98
|
sessionId: state.currentSessionId ? truncate(state.currentSessionId, 200) : undefined,
|
|
99
|
+
userId: state.config?.userId,
|
|
92
100
|
traceName: "pi-agent",
|
|
93
101
|
metadata: stringMetadata(captured.metadata),
|
|
94
102
|
},
|
|
@@ -112,6 +120,82 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
export async function startAgentAttempt() {
|
|
124
|
+
const agent = state.agentState;
|
|
125
|
+
if (state.isTracingDisabled || !agent?.root) return;
|
|
126
|
+
|
|
127
|
+
if (agent.activeAttempt) {
|
|
128
|
+
agent.activeAttempt
|
|
129
|
+
.update({ level: "WARNING", statusMessage: "A new agent attempt started before the previous attempt ended" })
|
|
130
|
+
.end();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
agent.attemptCount++;
|
|
135
|
+
agent.activeAttempt = await startChildObservation({
|
|
136
|
+
parent: agent.root,
|
|
137
|
+
runtime: getRuntime,
|
|
138
|
+
name: "agent-attempt",
|
|
139
|
+
body: { metadata: { attemptIndex: agent.attemptCount } },
|
|
140
|
+
asType: "span",
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.warn("📊 Langfuse: Failed to start agent attempt", error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function finishAgentAttempt(event: Record<string, unknown> = {}) {
|
|
148
|
+
const agent = state.agentState;
|
|
149
|
+
if (!agent) return;
|
|
150
|
+
agent.lastAgentEndEvent = event;
|
|
151
|
+
|
|
152
|
+
closeDanglingObservations("Agent attempt ended before observation finalized");
|
|
153
|
+
if (agent.activeTurn) {
|
|
154
|
+
agent.activeTurn
|
|
155
|
+
.update({ level: "WARNING", statusMessage: "Agent attempt ended before turn finalized" })
|
|
156
|
+
.end();
|
|
157
|
+
agent.activeTurn = undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!agent.activeAttempt) return;
|
|
161
|
+
try {
|
|
162
|
+
const lastAssistant = extractFinalAssistant(event.messages);
|
|
163
|
+
const captured = applyCapturePolicy(
|
|
164
|
+
{ output: lastAssistant ? extractAssistantOutput(lastAssistant) : undefined },
|
|
165
|
+
getCapturePolicy(),
|
|
166
|
+
);
|
|
167
|
+
agent.activeAttempt.update({ output: captured.output }).end();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn("📊 Langfuse: Failed to finish agent attempt", error);
|
|
170
|
+
} finally {
|
|
171
|
+
agent.activeAttempt = undefined;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function cancelAgentRun(reason: string) {
|
|
176
|
+
const agent = state.agentState;
|
|
177
|
+
if (!agent?.root) {
|
|
178
|
+
resetRunState();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
closeDanglingObservations(reason);
|
|
183
|
+
if (agent.activeTurn) {
|
|
184
|
+
agent.activeTurn.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
|
|
185
|
+
agent.activeTurn = undefined;
|
|
186
|
+
}
|
|
187
|
+
if (agent.activeAttempt) {
|
|
188
|
+
agent.activeAttempt.update({ level: "WARNING", statusMessage: reason, metadata: { cancelled: true } }).end();
|
|
189
|
+
agent.activeAttempt = undefined;
|
|
190
|
+
}
|
|
191
|
+
agent.root.update({
|
|
192
|
+
level: "WARNING",
|
|
193
|
+
statusMessage: reason,
|
|
194
|
+
metadata: { completed: false, cancelled: true },
|
|
195
|
+
}).end();
|
|
196
|
+
resetRunState();
|
|
197
|
+
}
|
|
198
|
+
|
|
115
199
|
/**
|
|
116
200
|
* Records the effective system prompt on the root agent observation.
|
|
117
201
|
*
|
|
@@ -161,7 +245,8 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
161
245
|
return;
|
|
162
246
|
}
|
|
163
247
|
|
|
164
|
-
const
|
|
248
|
+
const finalEvent = Object.keys(event).length > 0 ? event : state.agentState.lastAgentEndEvent ?? {};
|
|
249
|
+
const lastAssistant = extractFinalAssistant(finalEvent.messages);
|
|
165
250
|
const rawOutput = lastAssistant ? extractAssistantOutput(lastAssistant) : state.agentState.latestAssistantOutput;
|
|
166
251
|
const captured = applyCapturePolicy(
|
|
167
252
|
{
|
|
@@ -173,6 +258,19 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
173
258
|
model: state.currentModel || undefined,
|
|
174
259
|
provider: state.currentProvider || undefined,
|
|
175
260
|
totalTools: state.toolCallCount,
|
|
261
|
+
agentAttemptCount: state.agentState.attemptCount,
|
|
262
|
+
systemStateChangeCount: state.agentState.systemStateChangeCount,
|
|
263
|
+
promptStateHash: state.agentState.promptStateHash,
|
|
264
|
+
toolStateHash: state.agentState.toolStateHash,
|
|
265
|
+
activeToolCount: state.agentState.activeToolCount,
|
|
266
|
+
cacheReadTokens: state.agentState.cacheReadTokens,
|
|
267
|
+
cacheWriteTokens: state.agentState.cacheWriteTokens,
|
|
268
|
+
uncachedInputTokens: state.agentState.uncachedInputTokens,
|
|
269
|
+
cacheHitRatio:
|
|
270
|
+
state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens > 0
|
|
271
|
+
? state.agentState.cacheReadTokens /
|
|
272
|
+
(state.agentState.cacheReadTokens + state.agentState.uncachedInputTokens)
|
|
273
|
+
: undefined,
|
|
176
274
|
...computeEvaluationScores(),
|
|
177
275
|
},
|
|
178
276
|
},
|
|
@@ -181,6 +279,10 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
181
279
|
const scores = computeEvaluationScores();
|
|
182
280
|
|
|
183
281
|
closeDanglingObservations("Agent run ended before observation finalized");
|
|
282
|
+
if (state.agentState.activeAttempt) {
|
|
283
|
+
state.agentState.activeAttempt.end();
|
|
284
|
+
state.agentState.activeAttempt = undefined;
|
|
285
|
+
}
|
|
184
286
|
|
|
185
287
|
try {
|
|
186
288
|
state.agentState.root
|