pi-langfuse 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README_CN.md CHANGED
@@ -5,89 +5,59 @@
5
5
 
6
6
  [**English**](./README.md) | [**简体中文**](./README_CN.md)
7
7
 
8
- [Pi Coding Agent](https://github.com/earendil-works/pi-coding-agent) 的 Langfuse 可观测性扩展。将完整的 Pi 代理运行发送到 [Langfuse](https://langfuse.com),以便您可以在一个追踪(trace)中检查用户提示词、根代理工作流、每次 LLM 生成、每次工具调用、最终助手响应、使用情况、成本和健康分数。
8
+ [Pi Coding Agent](https://github.com/earendil-works/pi-coding-agent) 的 Langfuse 可观测性扩展。它会将完整的 Pi 运行发送到 [Langfuse](https://langfuse.com),在一个 trace 中展示提示词、代理工作流、LLM 生成、工具调用、最终回复、用量、成本和健康分数。
9
9
 
10
- ## 为什么选择 Langfuse?
10
+ ## 这个插件提供什么
11
11
 
12
- Langfuse LLM 应用程序提供开源的可观测性。此扩展允许您以生产级细节**追踪**、**监控**和**调试**您的 Pi 会话,帮助您准确了解代理的执行情况、成本消耗以及可能出现故障的环节。
13
-
14
- ## 功能
15
-
16
- - **完整的代理追踪**:为每个用户提示词创建一个追踪,包含一个根 `agent`(代理)观察节点,其中记录了提示词输入和最终助手输出。
17
- - **自建 Langfuse REST 兜底**:优先使用 Langfuse OpenTelemetry SDK 上报,然后验证追踪是否可见。如果自建 OTel 摄取链路接受了 span 但没有生成 trace,扩展会通过 Langfuse REST ingestion API 补写本次运行。
18
- - **每次请求生成记录**:为每次提供商请求记录单独的 `generation`(生成)观察节点,包含实际的提供商请求负载,而不仅仅是原始提示词。
19
- - **捕获最终消息**:在生成和根输出中使用已定型的助手消息,因此 Langfuse 会显示用户在 Pi 中实际看到的内容。
20
- - **工具可观测性**:为每次工具调用创建 Langfuse `tool`(工具)观察节点,包括参数、结果和错误状态。
21
- - **并行工具安全性**:通过 `toolCallId` 关联工具观察节点,避免在 Pi 并发运行工具时出现结果混淆。
22
- - **会话关联**:将同一 Pi 会话中的所有追踪分组到一个共享的 Langfuse 会话 ID 下。
23
- - **成本和 Token 追踪**:当 Pi/提供商负载公开时,记录每次生成的使用情况和成本详细信息。
24
- - **评估分数**:自动计算并发送工具成功率、错误计数和会话健康指标。
25
- - **防御性负载整形**:尽可能解析类似 JSON 的字符串,限制对象深度,并在上传前截断超大负载。
26
-
27
- ## 亮点
28
-
29
- `pi-langfuse` 旨在使 Pi 运行作为代理工作流具有可读性,而不仅仅是一堆日志:
30
-
31
- - 追踪(trace)的输入/输出与根 `agent` 观察节点镜像同步,使得从 Langfuse 追踪列表和详情视图中即可理解运行情况。
32
- - 使用工具的运行中的首次生成可以显示助手的工具调用消息,工具观察节点显示执行的输入/输出,而后续的生成显示最终的自然语言答案。
33
- - 工具故障会在工具观察节点上标记,并反映在追踪级别的分数中,而后续的生成仍会在其输入历史中保留工具错误结果。
34
- - 关机和中断的运行会刷新待处理的遥测数据,并将未完成的观察节点标记为已取消/警告,而不是默默丢失追踪记录。
12
+ - 每个用户提示词对应一个 Langfuse trace,并按 Pi 会话分组。
13
+ - 为根代理创建 `agent` 观察节点,为每次模型请求创建 `generation`,为每次工具调用创建 `tool`。
14
+ - 记录最终助手输出、工具错误状态和追踪级别分数。
15
+ - 提供输入、输出、工具 I/O、system prompt 和 cwd 的隐私采集开关。
16
+ - 上传前脱敏常见密钥,并对本地绝对路径做 hash。
17
+ - 针对自托管 Langfuse 提供 REST 兜底,覆盖 OTel span 已到达但 trace 未可见的场景。
35
18
 
36
19
  ## 前提条件
37
20
 
38
21
  - **Node.js** >= 22
39
- - **Pi Coding Agent** 已安装并配置
40
- - **Langfuse** 账户([云服务](https://cloud.langfuse.com)或自托管)
41
-
42
- ## 安装
43
-
44
- ### 方式 1:通过 npm 安装(推荐给用户)
22
+ - **Pi Coding Agent** 已安装并完成基础配置
23
+ - **Langfuse** 账户,支持 [云服务](https://cloud.langfuse.com) 和自托管
45
24
 
46
- ```bash
47
- pi install npm:pi-langfuse
48
- ```
25
+ ## 快速开始
49
26
 
50
- Pi 会自动下载包并将其注册为扩展。
27
+ 1. 安装扩展:
51
28
 
52
- ### 方式 2:从本地源码安装(推荐给开发者)
29
+ ```bash
30
+ pi install npm:pi-langfuse
31
+ ```
53
32
 
54
- ```bash
55
- git clone <你的仓库地址>
56
- cd pi-langfuse
57
- npm install
58
- ```
33
+ 2. 首次运行 Pi 时,如果尚未配置凭据,Pi 会提示输入:
34
+ - Langfuse 公钥,以 `pk-lf-...` 开头
35
+ - Langfuse 密钥,以 `sk-lf-...` 开头
36
+ - Langfuse 主机地址,默认 `https://cloud.langfuse.com`
59
37
 
60
- 然后告诉 Pi 使用它:
38
+ 3. 正常运行 Pi
61
39
 
62
- ```bash
63
- pi link /path/to/pi-langfuse
64
- ```
40
+ ```bash
41
+ pi "解释 Redis 的架构"
42
+ ```
65
43
 
66
- 或者直接在项目目录中运行 Pi——Pi 会自动发现当前目录中 `package.json` 的扩展。
44
+ 4. 打开 Langfuse,查看新生成的 trace。
67
45
 
68
46
  ## 配置
69
47
 
70
- 你需要 Langfuse API 密钥。从 **Langfuse Cloud** **设置** **API 密钥** 获取。
48
+ Langfuse API 密钥可在 **Langfuse Cloud** -> **Settings** -> **API Keys** 中获取。
71
49
 
72
- 有三种配置方式:
50
+ ### 方式 1:交互式设置
73
51
 
74
- ### 方式 1:交互式设置(最简单)
52
+ 加载扩展后运行任意 `pi` 命令。首次运行且未配置时,Pi 会在 CLI 或 TUI 中提示输入,并将结果保存到 `~/.pi/agent/pi-langfuse/config.json`。
75
53
 
76
- 加载扩展后运行任意 `pi` 命令。首次运行且未配置时,Pi 会在 CLI 或 TUI 中提示输入:
54
+ 如需重新执行设置:
77
55
 
78
- 1. **Langfuse 公钥** — 以 `pk-lf-...` 开头
79
- 2. **Langfuse 密钥** — 以 `sk-lf-...` 开头
80
- 3. **Langfuse 主机地址** — 默认为 `https://cloud.langfuse.com`
81
-
82
- 扩展会将这些保存到 `~/.pi/agent/pi-langfuse/config.json`,这样 Pi 更新、重装扩展时不会覆盖你的 Langfuse 凭据。
83
-
84
- 随时重新运行设置:
85
-
86
- ```
56
+ ```text
87
57
  /langfuse-setup
88
58
  ```
89
59
 
90
- ### 方式 2:环境变量(兜底)
60
+ ### 方式 2:环境变量
91
61
 
92
62
  在启动 Pi 前设置:
93
63
 
@@ -97,235 +67,125 @@ export LANGFUSE_SECRET_KEY="sk-lf-xxxx"
97
67
  export LANGFUSE_BASE_URL="https://cloud.langfuse.com" # 可选;也支持 LANGFUSE_HOST
98
68
  ```
99
69
 
100
- 保存的配置文件优先级更高。只有当 `~/.pi/agent/pi-langfuse/config.json` 不存在或不完整时,扩展才会使用环境变量,这样重新运行 `/langfuse-setup` 后不会出现配置漂移。
70
+ 保存的配置优先级更高。只有当 `~/.pi/agent/pi-langfuse/config.json` 缺失或不完整时,扩展才会使用环境变量。
101
71
 
102
- ### 方式 3:持久化 config.json
72
+ 隐私采集策略也可以通过环境变量设置:
103
73
 
104
- 如需使用持久化本地配置,创建或更新 `~/.pi/agent/pi-langfuse/config.json`:
105
-
106
- ```json
107
- {
108
- "publicKey": "pk-lf-xxxx",
109
- "secretKey": "sk-lf-xxxx",
110
- "host": "https://cloud.langfuse.com"
111
- }
74
+ ```bash
75
+ export LANGFUSE_PRIVACY_PRESET="full-debug"
112
76
  ```
113
77
 
114
- > **⚠️ 安全提醒**:请保护好 `~/.pi/agent/pi-langfuse/config.json`。切勿将 API 密钥提交到版本控制。
78
+ 可用预设:
115
79
 
116
- ## 使用
80
+ | 预设 | 采集内容 |
81
+ |------|----------|
82
+ | `metadata-only` | 仅采集元数据;不采集输入、输出、工具 I/O、system prompt 和 cwd |
83
+ | `prompts-only` | 采集提示词或提供商输入,以及元数据 |
84
+ | `conversations` | 采集输入和助手输出,但不采集工具 I/O、system prompt 和 cwd |
85
+ | `full-debug` | 完整追踪细节;默认值 |
117
86
 
118
- ### 基本使用
119
-
120
- 像往常一样运行 Pi——扩展会自动加载并追踪每次代理运行:
87
+ 细粒度开关会覆盖预设:
121
88
 
122
89
  ```bash
123
- pi "解释 Redis 的架构"
90
+ export LANGFUSE_CAPTURE_INPUTS=true
91
+ export LANGFUSE_CAPTURE_OUTPUTS=true
92
+ export LANGFUSE_CAPTURE_TOOL_IO=false
93
+ export LANGFUSE_CAPTURE_SYSTEM_PROMPT=false
94
+ export LANGFUSE_CAPTURE_CWD=false
124
95
  ```
125
96
 
126
- 会话结束后,在 [Langfuse 仪表板](https://cloud.langfuse.com) 中查看追踪信息。
97
+ 所有被采集的负载在上传前仍会脱敏。扩展会隐藏常见 API key、Bearer token、密码、Cookie、私钥、Langfuse key、GitHub/npm/AWS 风格 token,并对本地绝对路径做 hash。
127
98
 
128
- ### 验证扩展已加载
99
+ ### 方式 3:持久化 `config.json`
129
100
 
130
- ```bash
131
- pi list
101
+ 创建或更新 `~/.pi/agent/pi-langfuse/config.json`:
102
+
103
+ ```json
104
+ {
105
+ "publicKey": "pk-lf-xxxx",
106
+ "secretKey": "sk-lf-xxxx",
107
+ "host": "https://cloud.langfuse.com",
108
+ "privacyPreset": "conversations"
109
+ }
132
110
  ```
133
111
 
134
- 你应该能看到 `pi-langfuse` 在已安装包列表中。
112
+ 也可以持久化细粒度采集开关:
135
113
 
136
- ### 多个会话
114
+ ```json
115
+ {
116
+ "publicKey": "pk-lf-xxxx",
117
+ "secretKey": "sk-lf-xxxx",
118
+ "host": "https://cloud.langfuse.com",
119
+ "capture": {
120
+ "LANGFUSE_PRIVACY_PRESET": "metadata-only",
121
+ "LANGFUSE_CAPTURE_INPUTS": "true"
122
+ }
123
+ }
124
+ ```
137
125
 
138
- 每个 Pi 会话对应一个独立的 Langfuse 会话 ID。在该 Pi 会话中的每个用户提示词都会成为归入同一会话下的独立 Langfuse 追踪。
126
+ > **安全提醒**:`~/.pi/agent/pi-langfuse/config.json` 包含敏感信息,不应提交到版本控制。
139
127
 
140
- ## 开发设置
128
+ ## 验证扩展是否已加载
141
129
 
142
- 如果你为此扩展贡献代码:
130
+ 执行:
143
131
 
144
132
  ```bash
145
- # 克隆并安装依赖
146
- git clone <你的仓库地址>
147
- cd pi-langfuse
148
- npm install
149
-
150
- # 检查 TypeScript 类型
151
- npm run typecheck
152
-
153
- # 用 Pi 测试
154
- pi "test prompt"
155
- ```
156
-
157
- ### 项目结构
158
-
159
- ```
160
- pi-langfuse/
161
- ├── index.ts # 扩展入口和核心逻辑
162
- ├── package.json # 包元数据
163
- ├── tsconfig.json # TypeScript 配置
164
- ├── types/
165
- │ ├── pi-coding-agent.d.ts # Pi 扩展 API 类型
166
- │ └── node-shims.d.ts # Node.js 模块 shims
167
- ├── .agents/
168
- │ └── skills/
169
- │ └── langfuse/
170
- │ └── SKILL.md # 用于数据查询的 Langfuse CLI 技能
171
- ├── AGENTS.md # 开发者指南(扩展版)
172
- ├── README.md # 英文 README
173
- ├── README_CN.md # 本文件(中文)
174
- └── AGENTS_CN.md # 开发者指南(中文)
133
+ pi list
175
134
  ```
176
135
 
177
- ### 验证
136
+ 已安装包列表中应出现 `pi-langfuse`。
178
137
 
179
- 目前没有专门的测试套件。验证更改的方法:
138
+ ## 在 Langfuse 中会看到什么
180
139
 
181
- 1. 运行 `npm run typecheck` 检查 TypeScript 错误
182
- 2. 启用扩展启动 Pi
183
- 3. 运行几个提示词
184
- 4. 确认追踪、根代理观察节点、工具观察节点、生成和评估分数出现在您的 Langfuse 项目中
140
+ - 每个 Pi 会话对应一个独立的 Langfuse session ID。
141
+ - 该会话中的每个用户提示词都会生成一个独立 trace。
142
+ - trace 中会包含 Pi 实际显示的最终助手回复。
143
+ - 工具执行会以工具观察节点展示参数、结果和错误状态。
144
+ - 模型请求会以生成观察节点展示;如果提供商暴露相关信息,还会包含用量和成本。
145
+ - trace 级别会记录工具调用次数、工具成功率和是否出现错误。
185
146
 
186
- ## 追踪模型
147
+ 此包还包含一个内置 Langfuse 技能,可直接在 Pi 中查询 Langfuse 数据:
187
148
 
188
- ```
189
- Trace (name: "pi-agent")
190
- ├── Session ID: <pi-session-id>
191
- ├── input: 用户提示词,存在时包含图片/上下文摘要
192
- ├── output: 最终助手响应
193
- └── Agent observation (name: "pi-agent", type: agent)
194
- ├── input: 当前用户提示词
195
- ├── output: 最终助手响应
196
- ├── Generation observation (name: "llm-generation", type: generation)
197
- │ ├── input: 提供商请求负载 / 消息历史记录
198
- │ ├── output: 已定型的助手消息或工具调用消息
199
- │ ├── model, usageDetails, costDetails
200
- │ └── metadata: 提供商/请求详细信息
201
- └── Tool observation (name: "<tool-name>", type: tool)
202
- ├── input: 工具参数
203
- ├── output: 工具结果
204
- └── metadata: toolCallId, isError
149
+ ```text
150
+ /pi-langfuse-langfuse <查询内容>
205
151
  ```
206
152
 
207
- ## 追踪内容
208
-
209
- ### 追踪级别 (Trace Level)
210
- | 字段 | 说明 |
211
- |------|------|
212
- | `input` | 用户提示词,可用时包含图片/上下文摘要 |
213
- | `output` | Pi 中显示的最终助手响应 |
214
- | `sessionId` | Pi 会话标识符 |
215
- | `metadata.model` | 模型标识符(例如 "MiniMax-M2.7") |
216
- | `metadata.provider` | LLM 提供商名称 |
217
- | `metadata.cwd` | 工作目录 |
218
-
219
- ### 代理观察节点 (Agent Observation / 根工作流)
220
- | 字段 | 说明 |
221
- |------|------|
222
- | `type` | `agent` |
223
- | `name` | `pi-agent` |
224
- | `input` | 当前用户提示词负载 |
225
- | `output` | 最终助手响应 |
226
- | `metadata.sessionId` | Pi 会话标识符 |
227
- | `metadata.cwd` | 工作目录 |
228
- | `metadata.model` | 可用时的所选模型 |
229
- | `metadata.provider` | 可用时的提供商 |
230
-
231
- ### 评估分数 (追踪级别)
232
-
233
- | 分数名称 | 类型 | 说明 |
234
- |----------|------|------|
235
- | `tool_call_count` | number | 会话中的工具调用总数 |
236
- | `turn_count` | number | 助手交互轮数 |
237
- | `total_tool_errors` | number | 返回错误的工具数 |
238
- | `tool_success_rate` | float (0-1) | 工具调用成功率 |
239
- | `session_had_errors` | 0 或 1 | 是否有任何工具出错 |
240
-
241
- ### 生成观察节点 (Generation Observations / LLM 调用)
242
- | 字段 | 说明 |
243
- |------|------|
244
- | `type` | `generation` |
245
- | `name` | `llm-generation` |
246
- | `input` | 实际提供商请求负载 / 消息历史记录 |
247
- | `output` | 已定型的助手消息,包含工具调用轮次的工具调用负载 |
248
- | `model` | 模型标识符(例如 "MiniMax-M2.7") |
249
- | `usageDetails.input` | 输入 Token 数 |
250
- | `usageDetails.output` | 输出 Token 数 |
251
- | `usageDetails.total` | 总 Token 数 |
252
- | `costDetails.total` | 总成本(美元) |
253
- | `costDetails.input` | 输入成本(美元) |
254
- | `costDetails.output` | 输出成本(美元) |
255
- | `metadata.provider` | 提供商名称 |
256
- | `metadata.requestId` | 可用时的提供商/Pi 请求标识符 |
257
- | `metadata.status` | 可用时的 HTTP/提供商状态 |
258
-
259
- ### 工具观察节点 (Tool Observations)
260
- | 字段 | 说明 |
261
- |------|------|
262
- | `type` | `tool` |
263
- | `name` | 工具名称(例如 "bash", "read") |
264
- | `input` | 工具参数 |
265
- | `output` | 工具结果,为了可读性进行整形和截断 |
266
- | `metadata.toolCallId` | 稳定的 Pi 工具调用标识符 |
267
- | `metadata.isError` | 工具是否失败 |
268
- | `level` | 失败的工具调用为 `ERROR`,否则为 `DEFAULT` |
269
-
270
- ### 观察节点级别分数
271
- | 分数名称 | 说明 |
272
- |----------|------|
273
- | `tool_is_error` | 分配给出错个体工具观察节点的值 1 |
274
-
275
- ## Langfuse 仪表板
276
-
277
- 运行后,在您的 Langfuse 项目中检查:
278
-
279
- 1. **Traces(追踪)** — 所有带输入/输出的 pi 代理运行
280
- 2. **Sessions(会话)** — 按会话 ID 分组的追踪
281
- 3. **Observations(观察)** — 工具调用和 LLM 生成
282
- 4. **Scores(分数)** — 评估指标(工具错误、成功率等)
283
- 5. **Model Usage(模型使用)** — 按模型划分的使用情况细分
284
-
285
- 您也可以通过内置的 Langfuse 技能直接在终端中监控 Langfuse 数据:
153
+ ## 故障排除
286
154
 
287
- ```
288
- /pi-langfuse-langfuse <您的查询>
289
- ```
155
+ ### 没有看到 trace
290
156
 
291
- ## 故障排除
157
+ - 先检查 API 密钥是否正确,必要时重新执行 `/langfuse-setup`。
158
+ - 确认 Langfuse 项目处于可写状态。
159
+ - 确认密钥具备写权限。
160
+ - 在 Pi 输出中查找 `📊 Langfuse:` 日志。
292
161
 
293
- ### 没有追踪出现?
294
- - 验证 API 密钥是否正确 — 运行 `/langfuse-setup` 重新配置
295
- - 检查您的 Langfuse 项目是否活跃且有写入容量
296
- - 确保 API 密钥有写入权限(非只读)
297
- - 在 Pi 输出中查找 `📊 Langfuse:` 日志信息
162
+ ### 扩展未加载
298
163
 
299
- ### 扩展未加载?
300
164
  ```bash
301
- pi list # 确认 pi-langfuse 已安装
302
- pi install npm:pi-langfuse # 如果缺失则重新安装
165
+ pi list
166
+ pi install npm:pi-langfuse
303
167
  ```
304
168
 
305
- ### 启动时显示 "Missing config"?
306
- - 扩展需要凭据。使用交互式 `/langfuse-setup` 命令
307
- - 或设置 `LANGFUSE_PUBLIC_KEY` 和 `LANGFUSE_SECRET_KEY` 环境变量
169
+ ### 启动时显示 `Missing config`
170
+
171
+ - 执行 `/langfuse-setup`。
172
+ - 或在启动 Pi 前设置 `LANGFUSE_PUBLIC_KEY` 和 `LANGFUSE_SECRET_KEY`。
308
173
 
309
- ### 模型/成本未显示?
310
- - 并非所有提供商都公开成本信息
311
- - 检查 Langfuse traces API 获取原始观察数据
312
- - 生成中的 `model` 字段来自提供商事件、已定型的助手消息、`model_select` 或 `ctx.model`
174
+ ### 模型或成本未显示
313
175
 
314
- ### API 密钥错误?
315
- - Langfuse 公钥以 `pk-lf-` 开头,密钥以 `sk-lf-` 开头
316
- - 如果使用自托管,请验证您的主机 URL 是否正确
176
+ - 并非所有提供商都会返回成本信息。
177
+ - 可在 Langfuse trace 中查看原始观察数据。
178
+ - `model` 字段可能来自提供商事件、已定型的助手消息、`model_select` 或 `ctx.model`。
317
179
 
318
- ## 依赖项
180
+ ### API 密钥错误
319
181
 
320
- - [@langfuse/tracing](https://www.npmjs.com/package/@langfuse/tracing) — 用于 `agent`、`generation` 和 `tool` 追踪的 Langfuse 观察 API
321
- - [@langfuse/otel](https://www.npmjs.com/package/@langfuse/otel) 用于将追踪导出到 Langfuse 的 OpenTelemetry 跨度处理器
322
- - [@langfuse/client](https://www.npmjs.com/package/@langfuse/client) — 用于分数的 Langfuse API 客户端
323
- - [@opentelemetry/sdk-node](https://www.npmjs.com/package/@opentelemetry/sdk-node) — Node OpenTelemetry SDK
324
- - [@earendil-works/pi-coding-agent](https://www.npmjs.com/package/@earendil-works/pi-coding-agent) — Pi 扩展 API(对等依赖)
182
+ - 公钥以 `pk-lf-` 开头。
183
+ - 密钥以 `sk-lf-` 开头。
184
+ - 使用自托管时,还需要确认主机地址是否正确。
325
185
 
326
- ## 关于 Langfuse 技能
186
+ ## 开发文档
327
187
 
328
- 此包包含一个 Langfuse CLI 技能(位于 `.agents/skills/langfuse/`),使您可以直接从 Pi 查询 Langfuse 数据。无需离开终端即可查看追踪、提示词、数据集和分数。全局安装扩展时该技能会自动注册。
188
+ 源码安装、开发流程、运行时架构、追踪模型、字段明细和验证步骤已迁移到 [DEVELOPMENT.md](./DEVELOPMENT.md) [DEVELOPMENT_CN.md](./DEVELOPMENT_CN.md)。
329
189
 
330
190
  ## 许可证
331
191
 
package/image.png CHANGED
Binary file
package/index.ts CHANGED
@@ -13,7 +13,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { state, resetRunState, runWithSession, setCurrentSession } from "./src/state.js";
14
14
  import { ensureConfig, promptForConfig, loadConfig } from "./src/config.js";
15
15
  import { shutdownRuntime } from "./src/langfuse.js";
16
- import { getMessageFromEvent, extractAssistantOutput } from "./src/utils.js";
16
+ import { getMessageFromEvent, extractAssistantOutput, getCapturePolicy } from "./src/utils.js";
17
+ import { applyCapturePolicy } from "./src/capture-policy.js";
17
18
  import { startAgentRun, finishAgentRun } from "./src/handlers/agent.js";
18
19
  import { startTurnObservation, finishTurnObservation } from "./src/handlers/turn.js";
19
20
  import {
@@ -135,8 +136,9 @@ export default async function (pi: ExtensionAPI) {
135
136
 
136
137
  pi.on("agent_end", async (event, ctx) => withSession(ctx, async () => {
137
138
  await finishAgentRun(event);
139
+ const sessionId = state.currentSessionId;
138
140
  setTimeout(() => {
139
- shutdownRuntime().catch((error) => {
141
+ shutdownRuntime(sessionId).catch((error) => {
140
142
  console.warn("📊 Langfuse: Deferred shutdown failed", error);
141
143
  });
142
144
  }, 0);
@@ -173,7 +175,7 @@ export default async function (pi: ExtensionAPI) {
173
175
  {
174
176
  level: "DEFAULT",
175
177
  statusMessage: "Context was compacted",
176
- metadata: { ...event }
178
+ metadata: applyCapturePolicy({ metadata: { ...event } }, getCapturePolicy()).metadata
177
179
  },
178
180
  { asType: "span" }
179
181
  ) : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-langfuse",
3
- "version": "1.4.2",
3
+ "version": "1.4.4",
4
4
  "description": "Langfuse extension for Pi coding agent",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "homepage": "https://github.com/gooyoung/pi-langfuse#readme",
13
13
  "type": "module",
14
+ "packageManager": "npm@11.12.1",
14
15
  "main": "index.ts",
15
16
  "files": [
16
17
  "index.ts",
@@ -0,0 +1,141 @@
1
+ import { redactValue } from "./redaction.js";
2
+
3
+ export interface CapturePolicy {
4
+ readonly captureInputs: boolean;
5
+ readonly captureOutputs: boolean;
6
+ readonly captureToolIo: boolean;
7
+ readonly captureSystemPrompt: boolean;
8
+ readonly captureCwd: boolean;
9
+ }
10
+
11
+ export type PrivacyPreset = "metadata-only" | "prompts-only" | "conversations" | "full-debug";
12
+ export type EnvLike = Readonly<Record<string, string | undefined>>;
13
+
14
+ export interface RawTelemetryPayload {
15
+ input?: unknown;
16
+ output?: unknown;
17
+ toolInput?: unknown;
18
+ toolOutput?: unknown;
19
+ systemPrompt?: unknown;
20
+ metadata?: Record<string, unknown>;
21
+ }
22
+
23
+ export interface CapturedTelemetryPayload {
24
+ input?: unknown;
25
+ output?: unknown;
26
+ toolInput?: unknown;
27
+ toolOutput?: unknown;
28
+ systemPrompt?: unknown;
29
+ metadata?: Record<string, unknown>;
30
+ }
31
+
32
+ const PRESETS: Record<PrivacyPreset, CapturePolicy> = {
33
+ "metadata-only": {
34
+ captureInputs: false,
35
+ captureOutputs: false,
36
+ captureToolIo: false,
37
+ captureSystemPrompt: false,
38
+ captureCwd: false,
39
+ },
40
+ "prompts-only": {
41
+ captureInputs: true,
42
+ captureOutputs: false,
43
+ captureToolIo: false,
44
+ captureSystemPrompt: false,
45
+ captureCwd: false,
46
+ },
47
+ conversations: {
48
+ captureInputs: true,
49
+ captureOutputs: true,
50
+ captureToolIo: false,
51
+ captureSystemPrompt: false,
52
+ captureCwd: false,
53
+ },
54
+ "full-debug": {
55
+ captureInputs: true,
56
+ captureOutputs: true,
57
+ captureToolIo: true,
58
+ captureSystemPrompt: true,
59
+ captureCwd: true,
60
+ },
61
+ };
62
+
63
+ const FLAG_TO_FIELD = {
64
+ LANGFUSE_CAPTURE_INPUTS: "captureInputs",
65
+ LANGFUSE_CAPTURE_OUTPUTS: "captureOutputs",
66
+ LANGFUSE_CAPTURE_TOOL_IO: "captureToolIo",
67
+ LANGFUSE_CAPTURE_SYSTEM_PROMPT: "captureSystemPrompt",
68
+ LANGFUSE_CAPTURE_CWD: "captureCwd",
69
+ } as const;
70
+
71
+ function parseFlag(value: string | undefined): boolean | undefined {
72
+ if (value === undefined) {
73
+ return undefined;
74
+ }
75
+ if (/^(1|true|yes|on)$/i.test(value)) {
76
+ return true;
77
+ }
78
+ if (/^(0|false|no|off)$/i.test(value)) {
79
+ return false;
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ function normalizePreset(value: string | undefined): PrivacyPreset {
85
+ return value && value in PRESETS ? (value as PrivacyPreset) : "full-debug";
86
+ }
87
+
88
+ export function createCapturePolicy(env: EnvLike = process.env as EnvLike): CapturePolicy {
89
+ const policy: CapturePolicy = { ...PRESETS[normalizePreset(env.LANGFUSE_PRIVACY_PRESET)] };
90
+ for (const [envName, field] of Object.entries(FLAG_TO_FIELD) as Array<
91
+ [keyof typeof FLAG_TO_FIELD, (typeof FLAG_TO_FIELD)[keyof typeof FLAG_TO_FIELD]]
92
+ >) {
93
+ const override = parseFlag(env[envName]);
94
+ if (override !== undefined) {
95
+ (policy as Record<typeof field, boolean>)[field] = override;
96
+ }
97
+ }
98
+ return policy;
99
+ }
100
+
101
+ function redactMetadata(metadata: Record<string, unknown> | undefined, policy: CapturePolicy) {
102
+ if (!metadata) {
103
+ return undefined;
104
+ }
105
+
106
+ const output: Record<string, unknown> = {};
107
+ for (const [key, value] of Object.entries(metadata)) {
108
+ if (key === "cwd" && !policy.captureCwd) {
109
+ continue;
110
+ }
111
+ output[key] = redactValue(value);
112
+ }
113
+ return Object.keys(output).length > 0 ? output : undefined;
114
+ }
115
+
116
+ export function applyCapturePolicy(
117
+ payload: RawTelemetryPayload,
118
+ policy: CapturePolicy = createCapturePolicy(),
119
+ ): CapturedTelemetryPayload {
120
+ const captured: CapturedTelemetryPayload = {
121
+ metadata: redactMetadata(payload.metadata, policy),
122
+ };
123
+
124
+ if (policy.captureInputs && "input" in payload) {
125
+ captured.input = redactValue(payload.input);
126
+ }
127
+ if (policy.captureOutputs && "output" in payload) {
128
+ captured.output = redactValue(payload.output);
129
+ }
130
+ if (policy.captureToolIo && "toolInput" in payload) {
131
+ captured.toolInput = redactValue(payload.toolInput);
132
+ }
133
+ if (policy.captureToolIo && "toolOutput" in payload) {
134
+ captured.toolOutput = redactValue(payload.toolOutput);
135
+ }
136
+ if (policy.captureSystemPrompt && "systemPrompt" in payload) {
137
+ captured.systemPrompt = redactValue(payload.systemPrompt);
138
+ }
139
+
140
+ return captured;
141
+ }