oh-langfuse 0.1.56 → 0.1.57
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 +10 -4
- package/codex_langfuse_notify.py +7 -6
- package/langfuse_hook.py +7 -6
- package/package.json +1 -1
- package/scripts/opencode-langfuse-setup.mjs +1 -1
- package/scripts/real-self-verify.mjs +25 -12
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`oh-langfuse` 是用于给 Claude Code、OpenCode、Codex 配置 Langfuse 追踪的命令行工具。它既提供交互式安装向导,也支持 `setup`、`check`、`update`、`auto-update` 等直接命令。
|
|
4
4
|
|
|
5
|
-
当前 npm 版本:`0.1.
|
|
5
|
+
当前 npm 版本:`0.1.57`
|
|
6
6
|
|
|
7
7
|
## 能做什么
|
|
8
8
|
|
|
9
9
|
- 为 Claude Code 安装 `Stop` hook,把会话事件写入 Langfuse。
|
|
10
|
-
- 为 OpenCode 安装并 patch `opencode-plugin-langfuse`,开启 OpenTelemetry,并输出 `Agent Turn` 指标。
|
|
10
|
+
- 为 OpenCode 安装并 patch `opencode-plugin-langfuse`,开启 OpenTelemetry,并输出 `OpenCode Agent Turn` 指标。
|
|
11
11
|
- 为 Codex 安装 `notify` hook,增量读取 session JSONL 并写入 Langfuse。
|
|
12
12
|
- 记录本地 runtime 版本,支持启动前更新检查和手动更新。
|
|
13
13
|
- 校验员工号、环境变量、插件、hook、launcher、命令 shim 是否安装完整。
|
|
@@ -98,7 +98,13 @@ npx oh-langfuse@latest auto-update codex
|
|
|
98
98
|
|
|
99
99
|
## Skill 使用统计
|
|
100
100
|
|
|
101
|
-
当前版本把 skill 使用汇总写入每次交互的 `Agent Turn` observation,不再额外生成独立 `Skill Use` observation。
|
|
101
|
+
当前版本把 skill 使用汇总写入每次交互的 agent-specific `Agent Turn` observation,不再额外生成独立 `Skill Use` observation。
|
|
102
|
+
|
|
103
|
+
Agent Turn 的 observation name 会带上来源,方便在 Langfuse 后台直接区分:
|
|
104
|
+
|
|
105
|
+
- `Claude Agent Turn`
|
|
106
|
+
- `OpenCode Agent Turn`
|
|
107
|
+
- `Codex Agent Turn`
|
|
102
108
|
|
|
103
109
|
常见字段:
|
|
104
110
|
|
|
@@ -116,7 +122,7 @@ Dashboard 统计建议:
|
|
|
116
122
|
|
|
117
123
|
```text
|
|
118
124
|
View: Observations
|
|
119
|
-
Filter: Observation Name
|
|
125
|
+
Filter: Observation Name contains Agent Turn
|
|
120
126
|
Metric: Sum skill_use_count
|
|
121
127
|
```
|
|
122
128
|
|
package/codex_langfuse_notify.py
CHANGED
|
@@ -62,7 +62,8 @@ LOG_FILE = STATE_DIR / "codex_langfuse_notify.log"
|
|
|
62
62
|
DEBUG = os.environ.get("CODEX_LANGFUSE_DEBUG", "").lower() == "true"
|
|
63
63
|
MAX_CHARS = int(os.environ.get("CODEX_LANGFUSE_MAX_CHARS", "20000"))
|
|
64
64
|
METRICS_SCHEMA_VERSION = "1.1"
|
|
65
|
-
AGENT_NAME = "codex"
|
|
65
|
+
AGENT_NAME = "codex"
|
|
66
|
+
AGENT_TURN_NAME = "Codex Agent Turn"
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
def log(level: str, message: str) -> None:
|
|
@@ -839,11 +840,11 @@ def emit_codex_turn(
|
|
|
839
840
|
with propagate_attributes(
|
|
840
841
|
user_id=user_id,
|
|
841
842
|
session_id=session_id,
|
|
842
|
-
trace_name=
|
|
843
|
-
tags=[AGENT_NAME],
|
|
844
|
-
):
|
|
845
|
-
with langfuse.start_as_current_observation(
|
|
846
|
-
name=
|
|
843
|
+
trace_name=AGENT_TURN_NAME,
|
|
844
|
+
tags=[AGENT_NAME],
|
|
845
|
+
):
|
|
846
|
+
with langfuse.start_as_current_observation(
|
|
847
|
+
name=AGENT_TURN_NAME,
|
|
847
848
|
input={"role": "user", "content": user_text},
|
|
848
849
|
output={"role": "assistant", "content": assistant_text},
|
|
849
850
|
metadata={
|
package/langfuse_hook.py
CHANGED
|
@@ -66,7 +66,8 @@ LOCK_FILE = STATE_DIR / "langfuse_state.lock"
|
|
|
66
66
|
DEBUG = os.environ.get("CC_LANGFUSE_DEBUG", "").lower() == "true"
|
|
67
67
|
MAX_CHARS = int(os.environ.get("CC_LANGFUSE_MAX_CHARS", "20000"))
|
|
68
68
|
METRICS_SCHEMA_VERSION = "1.1"
|
|
69
|
-
AGENT_NAME = "claude"
|
|
69
|
+
AGENT_NAME = "claude"
|
|
70
|
+
AGENT_TURN_NAME = "Claude Agent Turn"
|
|
70
71
|
|
|
71
72
|
# ----------------- Logging -----------------
|
|
72
73
|
def _log(level: str, message: str) -> None:
|
|
@@ -882,11 +883,11 @@ def emit_turn(
|
|
|
882
883
|
with propagate_attributes(
|
|
883
884
|
user_id=user_id,
|
|
884
885
|
session_id=session_id,
|
|
885
|
-
trace_name=
|
|
886
|
-
tags=[AGENT_NAME],
|
|
887
|
-
):
|
|
888
|
-
with langfuse.start_as_current_observation(
|
|
889
|
-
name=
|
|
886
|
+
trace_name=AGENT_TURN_NAME,
|
|
887
|
+
tags=[AGENT_NAME],
|
|
888
|
+
):
|
|
889
|
+
with langfuse.start_as_current_observation(
|
|
890
|
+
name=AGENT_TURN_NAME,
|
|
890
891
|
input={"role": "user", "content": user_text},
|
|
891
892
|
output={"role": "assistant", "content": assistant_text},
|
|
892
893
|
metadata={
|
package/package.json
CHANGED
|
@@ -618,7 +618,7 @@ function getPatchedLangfuseDistIndexJs() {
|
|
|
618
618
|
" const total = tokenMetrics.total ?? (tokenMetrics.input !== undefined && tokenMetrics.output !== undefined ? tokenMetrics.input + tokenMetrics.output : undefined);",
|
|
619
619
|
" const tokenAvailable = [tokenMetrics.input, tokenMetrics.output, total, tokenMetrics.cacheRead, tokenMetrics.reasoning].some((value) => value !== undefined);",
|
|
620
620
|
" await sdkStartPromise;",
|
|
621
|
-
" const span = getMetricsTracer().startSpan('Agent Turn');",
|
|
621
|
+
" const span = getMetricsTracer().startSpan('OpenCode Agent Turn');",
|
|
622
622
|
" const text = messageTextById.get(messageId) || '';",
|
|
623
623
|
" const skillUsages = dedupeSkillUsages([...(skillUsagesByMessageId.get(messageId) ?? []), ...(skillUsagesBySessionId.get(sessionId) ?? [])]);",
|
|
624
624
|
" const interactionId = `opencode:${userId || \"unknown\"}:${sessionId || \"unknown\"}:${messageId}`;",
|
|
@@ -365,18 +365,25 @@ function metricInteractionId(item, target) {
|
|
|
365
365
|
: metadataValue(item, "interaction_id");
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
function isAgentTurnObservation(item) {
|
|
369
|
-
if (item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
368
|
+
function isAgentTurnObservation(item) {
|
|
369
|
+
if (/^(Claude|OpenCode|Codex) Agent Turn$/.test(String(item?.name || "")) || item?.name === "Agent Turn" || metadataValue(item, "interaction_count") === 1 || metadataValue(item, "interaction_count") === "1") {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
372
|
const metadata = item?.metadata || {};
|
|
373
373
|
const attrs = metadata.attributes || {};
|
|
374
374
|
const isOpencode = metadata.source === "opencode" || attrs["oh.langfuse.source"] === "opencode";
|
|
375
375
|
const hasModel = attrs["ai.model.id"] !== undefined || attrs["ai.model.provider"] !== undefined;
|
|
376
376
|
const isTool = attrs["ai.toolCall.name"] !== undefined || attrs["ai.toolCall.id"] !== undefined;
|
|
377
|
-
return isOpencode && hasModel && !isTool && item?.type === "GENERATION";
|
|
378
|
-
}
|
|
379
|
-
|
|
377
|
+
return isOpencode && hasModel && !isTool && item?.type === "GENERATION";
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function expectedAgentTurnName(target) {
|
|
381
|
+
if (target === "claude") return "Claude Agent Turn";
|
|
382
|
+
if (target === "opencode") return "OpenCode Agent Turn";
|
|
383
|
+
if (target === "codex") return "Codex Agent Turn";
|
|
384
|
+
return "Agent Turn";
|
|
385
|
+
}
|
|
386
|
+
|
|
380
387
|
async function observationsForTrace(config, traceId, since) {
|
|
381
388
|
if (!traceId) return [];
|
|
382
389
|
const params = { limit: 100, traceId };
|
|
@@ -445,11 +452,17 @@ async function verifyMetricObservations(config, found, { since, target, marker =
|
|
|
445
452
|
throw new Error(`Metric verification failed for ${target}: Skill Use observations should not be emitted as standalone metrics.`);
|
|
446
453
|
}
|
|
447
454
|
|
|
448
|
-
if (!interactions.length) {
|
|
449
|
-
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const
|
|
455
|
+
if (!interactions.length) {
|
|
456
|
+
throw new Error(`Metric verification failed for ${target}: Agent Turn observation was not found for trace ${traceId || found.id}.`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const expectedName = expectedAgentTurnName(target);
|
|
460
|
+
if (!interactions.some((item) => item?.name === expectedName)) {
|
|
461
|
+
const names = interactions.map((item) => item?.name || "<unnamed>").join(", ");
|
|
462
|
+
throw new Error(`Metric verification failed for ${target}: expected observation name ${expectedName}, found ${names}.`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const byInteractionId = new Map();
|
|
453
466
|
const seenInteractionIds = new Set();
|
|
454
467
|
for (const item of interactions) {
|
|
455
468
|
const interactionId = metricInteractionId(item, target);
|