coze_lab 0.1.25 → 0.1.27
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/index.js +35 -12
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +54 -12
- package/scripts/codex/cozeloop_hook.py +196 -50
- package/scripts/openclaw/package.json +1 -1
package/index.js
CHANGED
|
@@ -103,6 +103,18 @@ function successBox(lines) {
|
|
|
103
103
|
// ─── 2. Script loading (scripts live as real files under ./scripts) ───────────
|
|
104
104
|
const SCRIPTS_DIR = require('path').join(__dirname, 'scripts');
|
|
105
105
|
|
|
106
|
+
// OpenClaw 插件版本:来自 scripts/openclaw/package.json。写进 openclaw.json 的 pcfg,
|
|
107
|
+
// 参与 isOpenClawAlreadyInjected 幂等比对——插件代码升级(bump 该 version)后即使
|
|
108
|
+
// token/endpoint/workspace 不变也会被判定为“需更新”,强制重写插件文件 + npm install +
|
|
109
|
+
// gateway restart,避免旧插件 dist 滞留在云端 pluginDir 里不生效。
|
|
110
|
+
const OPENCLAW_PLUGIN_VERSION = (() => {
|
|
111
|
+
try {
|
|
112
|
+
return require('./scripts/openclaw/package.json').version || '';
|
|
113
|
+
} catch {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
})();
|
|
117
|
+
|
|
106
118
|
// Read a single script file (Python hook / refresh) as a UTF-8 string.
|
|
107
119
|
function readScript(relPath) {
|
|
108
120
|
return require('fs').readFileSync(require('path').join(SCRIPTS_DIR, relPath), 'utf8');
|
|
@@ -4378,28 +4390,34 @@ function checkPython() {
|
|
|
4378
4390
|
return pythonCmd;
|
|
4379
4391
|
}
|
|
4380
4392
|
|
|
4381
|
-
// Verify the `cozeloop` Python SDK is importable;
|
|
4382
|
-
// Claude Code / Codex hooks `import cozeloop` at runtime
|
|
4383
|
-
//
|
|
4393
|
+
// Verify the `cozeloop` Python SDK is importable AND new enough; install/upgrade otherwise.
|
|
4394
|
+
// Claude Code / Codex hooks `import cozeloop` at runtime and call set_finish_time(0.1.25+)。
|
|
4395
|
+
// 旧版(<=0.1.24)能 import 但缺该方法,会让 hook 上报整条 trace 失败,所以这里用能力探测
|
|
4396
|
+
// (hasattr set_finish_time)而非单纯 import,并在不达标时升级到带下限的版本。
|
|
4397
|
+
// 注意:这是安装阶段的 python,可能不是跑 hook 的那个 python(已知坑),所以它只是双保险——
|
|
4398
|
+
// 真正的运行时拦截在 hook 脚本的 _ensure_cozeloop_sdk 里。
|
|
4399
|
+
const COZELOOP_MIN_SPEC = 'cozeloop>=0.1.28';
|
|
4400
|
+
// 探测脚本:import 成功且具备 set_finish_time 能力 → exit 0;否则非 0。
|
|
4401
|
+
const COZELOOP_CAPABLE_PROBE = `import cozeloop,sys; sys.exit(0 if hasattr(cozeloop.Span,'set_finish_time') else 3)`;
|
|
4384
4402
|
function checkCozeloopSdk(pythonCmd) {
|
|
4385
4403
|
try {
|
|
4386
|
-
execSync(`${pythonCmd} -c "
|
|
4404
|
+
execSync(`${pythonCmd} -c "${COZELOOP_CAPABLE_PROBE}"`, { stdio: 'pipe' });
|
|
4387
4405
|
ok('cozeloop SDK — OK');
|
|
4388
4406
|
return;
|
|
4389
|
-
} catch { /*
|
|
4407
|
+
} catch { /* 未装或版本过旧 — 下面安装/升级 */ }
|
|
4390
4408
|
|
|
4391
|
-
info(
|
|
4409
|
+
info(`cozeloop SDK 不可用或版本过旧,正在安装/升级 (pip install -U '${COZELOOP_MIN_SPEC}')...`);
|
|
4392
4410
|
try {
|
|
4393
|
-
execSync(`${pythonCmd} -m pip install --quiet --upgrade
|
|
4394
|
-
// Confirm it imports now
|
|
4395
|
-
execSync(`${pythonCmd} -c "
|
|
4396
|
-
ok('cozeloop SDK
|
|
4411
|
+
execSync(`${pythonCmd} -m pip install --quiet --upgrade '${COZELOOP_MIN_SPEC}'`, { stdio: 'pipe' });
|
|
4412
|
+
// Confirm it imports and is capable now
|
|
4413
|
+
execSync(`${pythonCmd} -c "${COZELOOP_CAPABLE_PROBE}"`, { stdio: 'pipe' });
|
|
4414
|
+
ok('cozeloop SDK 安装/升级成功');
|
|
4397
4415
|
} catch (e) {
|
|
4398
4416
|
warnBox([
|
|
4399
|
-
'⚠ WARNING:
|
|
4417
|
+
'⚠ WARNING: 无法自动安装/升级 cozeloop SDK 到所需版本',
|
|
4400
4418
|
'',
|
|
4401
4419
|
'请手动安装后再使用,否则 hook 触发时 trace 上报会失败:',
|
|
4402
|
-
` ${pythonCmd} -m pip install
|
|
4420
|
+
` ${pythonCmd} -m pip install -U '${COZELOOP_MIN_SPEC}'`,
|
|
4403
4421
|
'',
|
|
4404
4422
|
(e.stderr ? e.stderr.toString().trim() : e.message),
|
|
4405
4423
|
]);
|
|
@@ -4772,6 +4790,11 @@ function applyOpenClawPluginConfig(existing, token, workspaceId, agentId, cloud)
|
|
|
4772
4790
|
pcfg.endpoint = getOtelEndpointBase(cloud);
|
|
4773
4791
|
pcfg.workspaceId = workspaceId;
|
|
4774
4792
|
pcfg.debug = true;
|
|
4793
|
+
// 插件代码版本:参与幂等比对,升级插件(bump scripts/openclaw/package.json version)后
|
|
4794
|
+
// 强制触发重写+重装+重启,避免云端 pluginDir 滞留旧插件 dist。
|
|
4795
|
+
if (OPENCLAW_PLUGIN_VERSION) {
|
|
4796
|
+
pcfg.pluginVersion = OPENCLAW_PLUGIN_VERSION;
|
|
4797
|
+
}
|
|
4775
4798
|
// per-agent trace 放行:把当前 agentId 并入 traceAgentIds(去重、归一为小写,
|
|
4776
4799
|
// 与插件侧 resolveAgentIdFromHookCtx 的归一一致)。无 agentId(全局模式)则不动
|
|
4777
4800
|
// allowlist —— 空 allowlist 表示全部放行。
|
package/package.json
CHANGED
|
@@ -29,19 +29,43 @@ from pathlib import Path
|
|
|
29
29
|
from typing import Optional, List, Dict, Any
|
|
30
30
|
|
|
31
31
|
# --- SDK Import ---
|
|
32
|
-
|
|
32
|
+
# 最低版本:set_finish_time 是 cozeloop SDK 0.1.25 才引入的方法,云端预装旧版(<=0.1.24)
|
|
33
|
+
# 调用会抛 AttributeError 并使整条 trace 上报失败。统一用能力探测(hasattr)判定,与
|
|
34
|
+
# _set_finish_time_safe 兜底口径一致,避免版本号字符串比较的边界问题。
|
|
35
|
+
_MIN_COZELOOP_SPEC = "cozeloop>=0.1.28"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _cozeloop_capable():
|
|
39
|
+
"""已装 cozeloop 是否具备 set_finish_time 能力。未装/异常返回 None(无法判定)。"""
|
|
33
40
|
try:
|
|
34
41
|
import cozeloop # noqa: F401
|
|
35
|
-
return True
|
|
36
42
|
except ImportError:
|
|
37
|
-
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
return hasattr(cozeloop.Span, "set_finish_time")
|
|
46
|
+
except Exception:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _ensure_cozeloop_sdk():
|
|
51
|
+
"""确保 cozeloop 可 import 且尽量满足 set_finish_time 能力。
|
|
52
|
+
|
|
53
|
+
返回 True 表示 cozeloop 可 import(不保证版本达标——能力不足时由
|
|
54
|
+
_set_finish_time_safe 兜底,不阻断上报);返回 False 表示完全无法 import。
|
|
55
|
+
"""
|
|
56
|
+
capable = _cozeloop_capable()
|
|
57
|
+
if capable is True:
|
|
58
|
+
return True
|
|
59
|
+
# 已装但能力不足(capable is False)→ 需升级;未装(None)→ 需安装。
|
|
38
60
|
import subprocess
|
|
39
61
|
import importlib
|
|
40
62
|
import site
|
|
63
|
+
pkg = _MIN_COZELOOP_SPEC
|
|
64
|
+
base_flags = ["--quiet", "--disable-pip-version-check", "--upgrade"]
|
|
41
65
|
attempts = (
|
|
42
|
-
[
|
|
43
|
-
[
|
|
44
|
-
[
|
|
66
|
+
[*base_flags, pkg],
|
|
67
|
+
[*base_flags, "--break-system-packages", pkg],
|
|
68
|
+
[*base_flags, "--break-system-packages", "--user", pkg],
|
|
45
69
|
)
|
|
46
70
|
for extra in attempts:
|
|
47
71
|
try:
|
|
@@ -60,11 +84,12 @@ def _ensure_cozeloop_sdk():
|
|
|
60
84
|
sys.path.insert(0, p)
|
|
61
85
|
importlib.invalidate_caches()
|
|
62
86
|
import cozeloop # noqa: F401
|
|
63
|
-
print("[CozeLoop] cozeloop SDK
|
|
87
|
+
print("[CozeLoop] cozeloop SDK installed/upgraded at runtime.", file=sys.stderr)
|
|
64
88
|
return True
|
|
65
89
|
except ImportError:
|
|
66
90
|
continue
|
|
67
|
-
|
|
91
|
+
# 升级没成功,但只要原本能 import(capable is False)就继续——兜底会处理能力缺失。
|
|
92
|
+
return capable is False
|
|
68
93
|
|
|
69
94
|
|
|
70
95
|
if _ensure_cozeloop_sdk():
|
|
@@ -539,6 +564,23 @@ def _ts_ms(dt):
|
|
|
539
564
|
return int(dt.timestamp() * 1000) if dt is not None else None
|
|
540
565
|
|
|
541
566
|
|
|
567
|
+
def _set_finish_time_safe(span, dt):
|
|
568
|
+
"""安全设置 span 结束时间。
|
|
569
|
+
|
|
570
|
+
set_finish_time 是 cozeloop SDK 0.1.25+ 的方法,云端旧版没有;缺失或异常都不能
|
|
571
|
+
让整条 trace 上报失败(real_*_ms tag 仍保留耗时信息)。
|
|
572
|
+
"""
|
|
573
|
+
if dt is None:
|
|
574
|
+
return
|
|
575
|
+
fn = getattr(span, "set_finish_time", None)
|
|
576
|
+
if fn is None:
|
|
577
|
+
return
|
|
578
|
+
try:
|
|
579
|
+
fn(dt)
|
|
580
|
+
except Exception:
|
|
581
|
+
pass
|
|
582
|
+
|
|
583
|
+
|
|
542
584
|
def group_messages_into_turns(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
543
585
|
"""Group messages into conversation turns (user -> assistant -> tool_results).
|
|
544
586
|
|
|
@@ -926,7 +968,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
926
968
|
}
|
|
927
969
|
# 真实耗时写 tag(SDK finish 不支持显式结束时间,duration 用 tag 兜底)
|
|
928
970
|
if root_start_dt is not None and root_end_dt is not None:
|
|
929
|
-
root_span
|
|
971
|
+
_set_finish_time_safe(root_span, root_end_dt)
|
|
930
972
|
root_tags["real_start_ms"] = _ts_ms(root_start_dt)
|
|
931
973
|
root_tags["real_end_ms"] = _ts_ms(root_end_dt)
|
|
932
974
|
root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
|
|
@@ -985,7 +1027,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
985
1027
|
"source": "claude_code",
|
|
986
1028
|
}
|
|
987
1029
|
if turn_start_dt is not None and turn_end_dt is not None:
|
|
988
|
-
turn_span
|
|
1030
|
+
_set_finish_time_safe(turn_span, turn_end_dt)
|
|
989
1031
|
_turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
|
|
990
1032
|
_turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
|
|
991
1033
|
_turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
|
|
@@ -1019,7 +1061,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1019
1061
|
model_span.set_runtime(Runtime(library="claude-code"))
|
|
1020
1062
|
model_span.set_model_name(model_name)
|
|
1021
1063
|
if step_start_dt is not None and step_end_dt is not None:
|
|
1022
|
-
model_span
|
|
1064
|
+
_set_finish_time_safe(model_span, step_end_dt)
|
|
1023
1065
|
model_span.set_tags({
|
|
1024
1066
|
"real_start_ms": _ts_ms(step_start_dt),
|
|
1025
1067
|
"real_end_ms": _ts_ms(step_end_dt),
|
|
@@ -1132,7 +1174,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1132
1174
|
tool_span.set_runtime(Runtime(library="claude-code"))
|
|
1133
1175
|
# 设真实完成时间,让每个 tool 的 duration 反映实际耗时。
|
|
1134
1176
|
if tool_end_dt is not None:
|
|
1135
|
-
tool_span
|
|
1177
|
+
_set_finish_time_safe(tool_span, tool_end_dt)
|
|
1136
1178
|
tags = {
|
|
1137
1179
|
"tool_name": tool_name,
|
|
1138
1180
|
"tool_call_id": tool_call.get("id"),
|
|
@@ -249,19 +249,44 @@ def get_fresh_token():
|
|
|
249
249
|
# -------------------------------------------------------------------------
|
|
250
250
|
|
|
251
251
|
# --- SDK Import ---
|
|
252
|
-
|
|
252
|
+
# 最低版本:set_finish_time 是 cozeloop SDK 0.1.25 才引入的方法,云端预装旧版(<=0.1.24)
|
|
253
|
+
# 调用会抛 AttributeError 并使整条 trace 上报失败。统一用能力探测(hasattr)判定,与
|
|
254
|
+
# _set_finish_time_safe 兜底口径一致,避免版本号字符串比较的边界问题。
|
|
255
|
+
_MIN_COZELOOP_SPEC = "cozeloop>=0.1.28"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _cozeloop_capable():
|
|
259
|
+
"""已装 cozeloop 是否具备 set_finish_time 能力。未装/异常返回 None(无法判定)。"""
|
|
253
260
|
try:
|
|
254
261
|
import cozeloop # noqa: F401
|
|
255
|
-
return True
|
|
256
262
|
except ImportError:
|
|
257
|
-
|
|
263
|
+
return None
|
|
264
|
+
try:
|
|
265
|
+
return hasattr(cozeloop.Span, "set_finish_time")
|
|
266
|
+
except Exception:
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _ensure_cozeloop_sdk():
|
|
271
|
+
"""确保 cozeloop 可 import 且尽量满足 set_finish_time 能力。
|
|
272
|
+
|
|
273
|
+
返回 True 表示 cozeloop 可 import(不保证版本达标——能力不足时由
|
|
274
|
+
_set_finish_time_safe 兜底,不阻断上报);返回 False 表示完全无法 import。
|
|
275
|
+
"""
|
|
276
|
+
capable = _cozeloop_capable()
|
|
277
|
+
if capable is True:
|
|
278
|
+
return True
|
|
279
|
+
# 已装但能力不足(capable is False)→ 需升级;未装(None)→ 需安装。
|
|
258
280
|
import subprocess
|
|
259
281
|
import importlib
|
|
260
282
|
import site
|
|
283
|
+
# 能力不足时强制升级到带下限的版本;未装时直接装下限版本。
|
|
284
|
+
pkg = _MIN_COZELOOP_SPEC
|
|
285
|
+
base_flags = ["--quiet", "--disable-pip-version-check", "--upgrade"]
|
|
261
286
|
attempts = (
|
|
262
|
-
[
|
|
263
|
-
[
|
|
264
|
-
[
|
|
287
|
+
[*base_flags, pkg],
|
|
288
|
+
[*base_flags, "--break-system-packages", pkg],
|
|
289
|
+
[*base_flags, "--break-system-packages", "--user", pkg],
|
|
265
290
|
)
|
|
266
291
|
for extra in attempts:
|
|
267
292
|
try:
|
|
@@ -280,11 +305,12 @@ def _ensure_cozeloop_sdk():
|
|
|
280
305
|
sys.path.insert(0, p)
|
|
281
306
|
importlib.invalidate_caches()
|
|
282
307
|
import cozeloop # noqa: F401
|
|
283
|
-
print("[CozeLoop] cozeloop SDK
|
|
308
|
+
print("[CozeLoop] cozeloop SDK installed/upgraded at runtime.", file=sys.stderr)
|
|
284
309
|
return True
|
|
285
310
|
except ImportError:
|
|
286
311
|
continue
|
|
287
|
-
|
|
312
|
+
# 升级没成功,但只要原本能 import(capable is False)就继续——兜底会处理能力缺失。
|
|
313
|
+
return capable is False
|
|
288
314
|
|
|
289
315
|
|
|
290
316
|
if _ensure_cozeloop_sdk():
|
|
@@ -640,6 +666,71 @@ def _ts_ms(dt):
|
|
|
640
666
|
return int(dt.timestamp() * 1000) if dt is not None else None
|
|
641
667
|
|
|
642
668
|
|
|
669
|
+
def _set_finish_time_safe(span, dt):
|
|
670
|
+
"""安全设置 span 结束时间。
|
|
671
|
+
|
|
672
|
+
set_finish_time 是 cozeloop SDK 0.1.25+ 的方法,云端旧版没有;缺失或异常都不能
|
|
673
|
+
让整条 trace 上报失败(real_*_ms tag 仍保留耗时信息)。
|
|
674
|
+
"""
|
|
675
|
+
if dt is None:
|
|
676
|
+
return
|
|
677
|
+
fn = getattr(span, "set_finish_time", None)
|
|
678
|
+
if fn is None:
|
|
679
|
+
return
|
|
680
|
+
try:
|
|
681
|
+
fn(dt)
|
|
682
|
+
except Exception:
|
|
683
|
+
pass
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def _max_dt(*values):
|
|
687
|
+
result = None
|
|
688
|
+
for value in values:
|
|
689
|
+
if value is not None and (result is None or value > result):
|
|
690
|
+
result = value
|
|
691
|
+
return result
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _parse_ts_value(value):
|
|
695
|
+
return _parse_ts({"_ts": value}) if value else None
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def _turn_timestamps(turn):
|
|
699
|
+
values = []
|
|
700
|
+
for item in [turn.get("user_message"), *turn.get("assistant_messages", [])]:
|
|
701
|
+
dt = _parse_ts(item)
|
|
702
|
+
if dt:
|
|
703
|
+
values.append(dt)
|
|
704
|
+
for item in [*turn.get("tool_calls", []), *turn.get("tool_results", [])]:
|
|
705
|
+
dt = _parse_ts(item)
|
|
706
|
+
if dt:
|
|
707
|
+
values.append(dt)
|
|
708
|
+
for sc in turn.get("subagent_calls", []):
|
|
709
|
+
for key in ("_start_ts", "_end_ts"):
|
|
710
|
+
dt = _parse_ts_value(sc.get(key))
|
|
711
|
+
if dt:
|
|
712
|
+
values.append(dt)
|
|
713
|
+
return values
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _turn_bounds(turn):
|
|
717
|
+
values = _turn_timestamps(turn)
|
|
718
|
+
if not values:
|
|
719
|
+
return None, None
|
|
720
|
+
return min(values), max(values)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def _assistant_bounds(turn):
|
|
724
|
+
values = []
|
|
725
|
+
for item in turn.get("assistant_messages", []):
|
|
726
|
+
dt = _parse_ts(item)
|
|
727
|
+
if dt:
|
|
728
|
+
values.append(dt)
|
|
729
|
+
if not values:
|
|
730
|
+
return None, None
|
|
731
|
+
return min(values), max(values)
|
|
732
|
+
|
|
733
|
+
|
|
643
734
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
644
735
|
"""Group raw JSONL entries into conversation turns.
|
|
645
736
|
|
|
@@ -749,6 +840,8 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
749
840
|
"message": args.get("message", ""),
|
|
750
841
|
"model": args.get("model"),
|
|
751
842
|
"result": None,
|
|
843
|
+
"_start_ts": payload.get("_ts"),
|
|
844
|
+
"_end_ts": None,
|
|
752
845
|
}
|
|
753
846
|
current_turn["subagent_calls"].append(subagent_call)
|
|
754
847
|
pending_calls[call_id] = {"kind": "spawn", "subagent_call": subagent_call}
|
|
@@ -756,14 +849,16 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
756
849
|
pending_calls[call_id] = {
|
|
757
850
|
"kind": "wait",
|
|
758
851
|
"ids": args.get("ids", []),
|
|
852
|
+
"_start_ts": payload.get("_ts"),
|
|
759
853
|
}
|
|
760
854
|
else:
|
|
761
855
|
current_turn["tool_calls"].append({
|
|
762
856
|
"call_id": call_id,
|
|
763
857
|
"name": name,
|
|
764
858
|
"input": args,
|
|
859
|
+
"_ts": payload.get("_ts"),
|
|
765
860
|
})
|
|
766
|
-
pending_calls[call_id] = {"kind": "tool"}
|
|
861
|
+
pending_calls[call_id] = {"kind": "tool", "_start_ts": payload.get("_ts")}
|
|
767
862
|
|
|
768
863
|
elif item_type == "function_call_output":
|
|
769
864
|
if current_turn is None:
|
|
@@ -783,6 +878,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
783
878
|
subagent_call["nickname"] = out.get("nickname")
|
|
784
879
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
785
880
|
pass
|
|
881
|
+
subagent_call["_end_ts"] = payload.get("_ts")
|
|
786
882
|
|
|
787
883
|
elif kind == "wait":
|
|
788
884
|
try:
|
|
@@ -795,6 +891,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
795
891
|
for sc in current_turn["subagent_calls"]:
|
|
796
892
|
if sc.get("agent_id") == agent_id and sc.get("result") is None:
|
|
797
893
|
sc["result"] = result_text
|
|
894
|
+
sc["_end_ts"] = payload.get("_ts")
|
|
798
895
|
break
|
|
799
896
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
800
897
|
pass
|
|
@@ -803,6 +900,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
803
900
|
current_turn["tool_results"].append({
|
|
804
901
|
"call_id": call_id,
|
|
805
902
|
"output": raw_output,
|
|
903
|
+
"_ts": payload.get("_ts"),
|
|
806
904
|
})
|
|
807
905
|
|
|
808
906
|
if current_turn is not None:
|
|
@@ -888,13 +986,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
888
986
|
# 整体时间范围:所有 user/assistant payload 的真实 timestamp 极值
|
|
889
987
|
_all_ts = []
|
|
890
988
|
for _t in turns:
|
|
891
|
-
|
|
892
|
-
if _u:
|
|
893
|
-
_all_ts.append(_u)
|
|
894
|
-
for _a in _t.get("assistant_messages", []):
|
|
895
|
-
_ad = _parse_ts(_a)
|
|
896
|
-
if _ad:
|
|
897
|
-
_all_ts.append(_ad)
|
|
989
|
+
_all_ts.extend(_turn_timestamps(_t))
|
|
898
990
|
root_start_dt = min(_all_ts) if _all_ts else None
|
|
899
991
|
root_end_dt = max(_all_ts) if _all_ts else None
|
|
900
992
|
|
|
@@ -906,6 +998,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
906
998
|
"source": "codex_cli",
|
|
907
999
|
}
|
|
908
1000
|
if root_start_dt is not None and root_end_dt is not None:
|
|
1001
|
+
_set_finish_time_safe(root_span, root_end_dt)
|
|
909
1002
|
root_tags["real_start_ms"] = _ts_ms(root_start_dt)
|
|
910
1003
|
root_tags["real_end_ms"] = _ts_ms(root_end_dt)
|
|
911
1004
|
root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
|
|
@@ -947,14 +1040,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
947
1040
|
for i, turn in enumerate(turns):
|
|
948
1041
|
try:
|
|
949
1042
|
# turn 真实时间:start=user payload 时间;end=最后一条 assistant payload 时间
|
|
950
|
-
turn_start_dt =
|
|
951
|
-
turn_end_dt = None
|
|
952
|
-
for _a in turn.get("assistant_messages", []):
|
|
953
|
-
_ad = _parse_ts(_a)
|
|
954
|
-
if _ad:
|
|
955
|
-
turn_end_dt = _ad
|
|
956
|
-
if turn_start_dt is None:
|
|
957
|
-
turn_start_dt = turn_end_dt
|
|
1043
|
+
turn_start_dt, turn_end_dt = _turn_bounds(turn)
|
|
958
1044
|
|
|
959
1045
|
with client.start_span(name=f"turn_{i}", span_type="main", start_time=turn_start_dt) as turn_span:
|
|
960
1046
|
turn_span.set_runtime(Runtime(library="codex-cli"))
|
|
@@ -965,6 +1051,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
965
1051
|
"source": "codex_cli",
|
|
966
1052
|
}
|
|
967
1053
|
if turn_start_dt is not None and turn_end_dt is not None:
|
|
1054
|
+
_set_finish_time_safe(turn_span, turn_end_dt)
|
|
968
1055
|
_turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
|
|
969
1056
|
_turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
|
|
970
1057
|
_turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
|
|
@@ -973,21 +1060,20 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
973
1060
|
# --- Model span for assistant response ---
|
|
974
1061
|
if turn.get("assistant_messages"):
|
|
975
1062
|
# model span start:第一条 assistant payload 时间,回退到 turn 起点
|
|
976
|
-
_model_start_dt =
|
|
977
|
-
for _a in turn.get("assistant_messages", []):
|
|
978
|
-
_model_start_dt = _parse_ts(_a)
|
|
979
|
-
if _model_start_dt:
|
|
980
|
-
break
|
|
1063
|
+
_model_start_dt, _model_end_dt = _assistant_bounds(turn)
|
|
981
1064
|
if _model_start_dt is None:
|
|
982
1065
|
_model_start_dt = turn_start_dt
|
|
1066
|
+
if _model_end_dt is None:
|
|
1067
|
+
_model_end_dt = turn_end_dt
|
|
983
1068
|
with client.start_span(name="assistant_response", span_type="model", start_time=_model_start_dt) as model_span:
|
|
984
1069
|
model_span.set_runtime(Runtime(library="codex-cli"))
|
|
985
1070
|
model_span.set_model_name(model_name)
|
|
986
|
-
if _model_start_dt is not None and
|
|
1071
|
+
if _model_start_dt is not None and _model_end_dt is not None:
|
|
1072
|
+
_set_finish_time_safe(model_span, _model_end_dt)
|
|
987
1073
|
model_span.set_tags({
|
|
988
1074
|
"real_start_ms": _ts_ms(_model_start_dt),
|
|
989
|
-
"real_end_ms": _ts_ms(
|
|
990
|
-
"latency_ms": _ts_ms(
|
|
1075
|
+
"real_end_ms": _ts_ms(_model_end_dt),
|
|
1076
|
+
"latency_ms": _ts_ms(_model_end_dt) - _ts_ms(_model_start_dt),
|
|
991
1077
|
})
|
|
992
1078
|
|
|
993
1079
|
# Build input messages: history + current turn input
|
|
@@ -1056,17 +1142,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1056
1142
|
# --- Tool call spans ---
|
|
1057
1143
|
for tool_call in turn.get("tool_calls", []):
|
|
1058
1144
|
tool_name = tool_call.get("name", "unknown")
|
|
1059
|
-
|
|
1145
|
+
tool_start_dt = _parse_ts(tool_call) or turn_start_dt
|
|
1146
|
+
tool_end_dt = None
|
|
1147
|
+
call_id = tool_call.get("call_id")
|
|
1148
|
+
for result in turn.get("tool_results", []):
|
|
1149
|
+
if result.get("call_id") == call_id:
|
|
1150
|
+
tool_end_dt = _parse_ts(result)
|
|
1151
|
+
break
|
|
1152
|
+
tool_finish_dt = _max_dt(tool_end_dt, tool_start_dt)
|
|
1153
|
+
with client.start_span(name=f"tool_{tool_name}", span_type="tool", start_time=tool_start_dt) as tool_span:
|
|
1060
1154
|
tool_span.set_runtime(Runtime(library="codex-cli"))
|
|
1061
|
-
|
|
1155
|
+
tool_tags = {
|
|
1062
1156
|
"tool_name": tool_name,
|
|
1063
|
-
"call_id":
|
|
1064
|
-
}
|
|
1157
|
+
"call_id": call_id,
|
|
1158
|
+
}
|
|
1159
|
+
if tool_start_dt is not None and tool_finish_dt is not None:
|
|
1160
|
+
_set_finish_time_safe(tool_span, tool_finish_dt)
|
|
1161
|
+
tool_tags["real_start_ms"] = _ts_ms(tool_start_dt)
|
|
1162
|
+
tool_tags["real_end_ms"] = _ts_ms(tool_finish_dt)
|
|
1163
|
+
tool_tags["latency_ms"] = _ts_ms(tool_finish_dt) - _ts_ms(tool_start_dt)
|
|
1164
|
+
tool_span.set_tags(tool_tags)
|
|
1065
1165
|
tool_span.set_input(
|
|
1066
1166
|
json.dumps(tool_call.get("input", {}), ensure_ascii=False)[:2000]
|
|
1067
1167
|
)
|
|
1068
1168
|
# Find matching tool result
|
|
1069
|
-
call_id = tool_call.get("call_id")
|
|
1070
1169
|
for result in turn.get("tool_results", []):
|
|
1071
1170
|
if result.get("call_id") == call_id:
|
|
1072
1171
|
output = result.get("output", "")
|
|
@@ -1079,15 +1178,24 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1079
1178
|
for sc in turn.get("subagent_calls", []):
|
|
1080
1179
|
agent_id = sc.get("agent_id") or "unknown"
|
|
1081
1180
|
nickname = sc.get("nickname") or agent_id
|
|
1181
|
+
subagent_start_dt = _parse_ts_value(sc.get("_start_ts")) or turn_start_dt
|
|
1182
|
+
subagent_end_dt = _parse_ts_value(sc.get("_end_ts")) or turn_end_dt
|
|
1183
|
+
subagent_finish_dt = _max_dt(subagent_end_dt, subagent_start_dt)
|
|
1082
1184
|
|
|
1083
|
-
with client.start_span(name=f"subagent_{nickname}", span_type="agent", start_time=
|
|
1185
|
+
with client.start_span(name=f"subagent_{nickname}", span_type="agent", start_time=subagent_start_dt) as subagent_span:
|
|
1084
1186
|
subagent_span.set_runtime(Runtime(library="codex-cli"))
|
|
1085
|
-
|
|
1187
|
+
subagent_tags = {
|
|
1086
1188
|
"agent_id": agent_id,
|
|
1087
1189
|
"agent_nickname": nickname,
|
|
1088
1190
|
"agent_role": sc.get("role") or "",
|
|
1089
1191
|
"agent_model": sc.get("model") or "",
|
|
1090
|
-
}
|
|
1192
|
+
}
|
|
1193
|
+
if subagent_start_dt is not None and subagent_finish_dt is not None:
|
|
1194
|
+
_set_finish_time_safe(subagent_span, subagent_finish_dt)
|
|
1195
|
+
subagent_tags["real_start_ms"] = _ts_ms(subagent_start_dt)
|
|
1196
|
+
subagent_tags["real_end_ms"] = _ts_ms(subagent_finish_dt)
|
|
1197
|
+
subagent_tags["latency_ms"] = _ts_ms(subagent_finish_dt) - _ts_ms(subagent_start_dt)
|
|
1198
|
+
subagent_span.set_tags(subagent_tags)
|
|
1091
1199
|
subagent_span.set_input(sc.get("message", "")[:2000])
|
|
1092
1200
|
|
|
1093
1201
|
# Load and include saved subagent turn data
|
|
@@ -1097,20 +1205,45 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1097
1205
|
sa_model = sa_data.get("model_name", "codex")
|
|
1098
1206
|
|
|
1099
1207
|
for si, sa_turn in enumerate(sa_turns):
|
|
1100
|
-
|
|
1208
|
+
sa_turn_start_dt, sa_turn_end_dt = _turn_bounds(sa_turn)
|
|
1209
|
+
if sa_turn_start_dt is None:
|
|
1210
|
+
sa_turn_start_dt = subagent_start_dt
|
|
1211
|
+
if sa_turn_end_dt is None:
|
|
1212
|
+
sa_turn_end_dt = sa_turn_start_dt
|
|
1213
|
+
sa_turn_finish_dt = _max_dt(sa_turn_end_dt, sa_turn_start_dt)
|
|
1214
|
+
|
|
1215
|
+
with client.start_span(name=f"turn_{si}", span_type="main", start_time=sa_turn_start_dt) as sa_turn_span:
|
|
1101
1216
|
sa_turn_span.set_runtime(Runtime(library="codex-cli"))
|
|
1102
|
-
|
|
1217
|
+
sa_turn_tags = {
|
|
1103
1218
|
"turn_index": si,
|
|
1104
1219
|
"turn_id": sa_turn.get("turn_id", ""),
|
|
1105
1220
|
"agent_name": nickname,
|
|
1106
|
-
}
|
|
1221
|
+
}
|
|
1222
|
+
if sa_turn_start_dt is not None and sa_turn_finish_dt is not None:
|
|
1223
|
+
_set_finish_time_safe(sa_turn_span, sa_turn_finish_dt)
|
|
1224
|
+
sa_turn_tags["real_start_ms"] = _ts_ms(sa_turn_start_dt)
|
|
1225
|
+
sa_turn_tags["real_end_ms"] = _ts_ms(sa_turn_finish_dt)
|
|
1226
|
+
sa_turn_tags["latency_ms"] = _ts_ms(sa_turn_finish_dt) - _ts_ms(sa_turn_start_dt)
|
|
1227
|
+
sa_turn_span.set_tags(sa_turn_tags)
|
|
1107
1228
|
|
|
1108
1229
|
# Subagent model span
|
|
1109
1230
|
if sa_turn.get("assistant_messages"):
|
|
1110
|
-
|
|
1231
|
+
sa_model_start_dt, sa_model_end_dt = _assistant_bounds(sa_turn)
|
|
1232
|
+
if sa_model_start_dt is None:
|
|
1233
|
+
sa_model_start_dt = sa_turn_start_dt
|
|
1234
|
+
if sa_model_end_dt is None:
|
|
1235
|
+
sa_model_end_dt = sa_turn_end_dt
|
|
1236
|
+
sa_model_finish_dt = _max_dt(sa_model_end_dt, sa_model_start_dt)
|
|
1237
|
+
with client.start_span(name="assistant_response", span_type="model", start_time=sa_model_start_dt) as sa_model_span:
|
|
1111
1238
|
sa_model_span.set_runtime(Runtime(library="codex-cli"))
|
|
1112
1239
|
sa_model_span.set_model_name(sa_model)
|
|
1113
|
-
|
|
1240
|
+
sa_model_tags = {"agent_name": nickname}
|
|
1241
|
+
if sa_model_start_dt is not None and sa_model_finish_dt is not None:
|
|
1242
|
+
_set_finish_time_safe(sa_model_span, sa_model_finish_dt)
|
|
1243
|
+
sa_model_tags["real_start_ms"] = _ts_ms(sa_model_start_dt)
|
|
1244
|
+
sa_model_tags["real_end_ms"] = _ts_ms(sa_model_finish_dt)
|
|
1245
|
+
sa_model_tags["latency_ms"] = _ts_ms(sa_model_finish_dt) - _ts_ms(sa_model_start_dt)
|
|
1246
|
+
sa_model_span.set_tags(sa_model_tags)
|
|
1114
1247
|
|
|
1115
1248
|
sa_input = sa_turn.get("input_messages", [])
|
|
1116
1249
|
if not sa_input:
|
|
@@ -1154,17 +1287,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1154
1287
|
# Subagent tool spans
|
|
1155
1288
|
for sa_tc in sa_turn.get("tool_calls", []):
|
|
1156
1289
|
sa_tool_name = sa_tc.get("name", "unknown")
|
|
1157
|
-
|
|
1290
|
+
sa_tool_start_dt = _parse_ts(sa_tc) or sa_turn_start_dt
|
|
1291
|
+
sa_tool_end_dt = None
|
|
1292
|
+
sa_cid = sa_tc.get("call_id")
|
|
1293
|
+
for sa_r in sa_turn.get("tool_results", []):
|
|
1294
|
+
if sa_r.get("call_id") == sa_cid:
|
|
1295
|
+
sa_tool_end_dt = _parse_ts(sa_r)
|
|
1296
|
+
break
|
|
1297
|
+
sa_tool_finish_dt = _max_dt(sa_tool_end_dt, sa_tool_start_dt)
|
|
1298
|
+
with client.start_span(name=f"tool_{sa_tool_name}", span_type="tool", start_time=sa_tool_start_dt) as sa_tool_span:
|
|
1158
1299
|
sa_tool_span.set_runtime(Runtime(library="codex-cli"))
|
|
1159
|
-
|
|
1300
|
+
sa_tool_tags = {
|
|
1160
1301
|
"tool_name": sa_tool_name,
|
|
1161
|
-
"call_id":
|
|
1302
|
+
"call_id": sa_cid,
|
|
1162
1303
|
"agent_name": nickname,
|
|
1163
|
-
}
|
|
1304
|
+
}
|
|
1305
|
+
if sa_tool_start_dt is not None and sa_tool_finish_dt is not None:
|
|
1306
|
+
_set_finish_time_safe(sa_tool_span, sa_tool_finish_dt)
|
|
1307
|
+
sa_tool_tags["real_start_ms"] = _ts_ms(sa_tool_start_dt)
|
|
1308
|
+
sa_tool_tags["real_end_ms"] = _ts_ms(sa_tool_finish_dt)
|
|
1309
|
+
sa_tool_tags["latency_ms"] = _ts_ms(sa_tool_finish_dt) - _ts_ms(sa_tool_start_dt)
|
|
1310
|
+
sa_tool_span.set_tags(sa_tool_tags)
|
|
1164
1311
|
sa_tool_span.set_input(
|
|
1165
1312
|
json.dumps(sa_tc.get("input", {}), ensure_ascii=False)[:2000]
|
|
1166
1313
|
)
|
|
1167
|
-
sa_cid = sa_tc.get("call_id")
|
|
1168
1314
|
for sa_r in sa_turn.get("tool_results", []):
|
|
1169
1315
|
if sa_r.get("call_id") == sa_cid:
|
|
1170
1316
|
sa_out = sa_r.get("output", "")
|