coze_lab 0.1.24 → 0.1.26
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 +2 -0
- package/index.js +69 -6
- package/package.json +1 -1
- package/scripts/claude-code/cozeloop_hook.py +1 -1
- package/scripts/codex/cozeloop_hook.py +146 -43
package/README.md
CHANGED
|
@@ -74,6 +74,8 @@ In cloud mode, trace verification and hook uploads prefer
|
|
|
74
74
|
`COZELOOP_API_TOKEN` and fall back to `COZE_API_TOKEN`. The selfcheck result is
|
|
75
75
|
authoritative: if the token does not have trace ingest permission, onboard still
|
|
76
76
|
writes the hook configuration but reports `verify=fail` with `token_source`.
|
|
77
|
+
For Python SDK uploads, `OTEL_ENDPOINT` is not used as the SDK base URL; set
|
|
78
|
+
`COZELOOP_API_BASE_URL` only when the SDK ingest endpoint should be overridden.
|
|
77
79
|
|
|
78
80
|
**At hook execution time** (Claude Code / Codex), the Python hook script automatically:
|
|
79
81
|
1. Reads `~/.cozeloop/credentials.json`
|
package/index.js
CHANGED
|
@@ -4706,6 +4706,10 @@ function normalizeTraceAgentIds(ids) {
|
|
|
4706
4706
|
|
|
4707
4707
|
function getCloudCozeloopApiBaseUrl() {
|
|
4708
4708
|
const raw = process.env.COZELOOP_API_BASE_URL || process.env.OTEL_ENDPOINT || '';
|
|
4709
|
+
return normalizeCozeloopApiBaseUrl(raw);
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
function normalizeCozeloopApiBaseUrl(raw) {
|
|
4709
4713
|
const base = raw.trim().replace(/\/+$/, '');
|
|
4710
4714
|
if (!base) return '';
|
|
4711
4715
|
if (base.endsWith('/v1/loop/opentelemetry/v1/traces')) {
|
|
@@ -4730,6 +4734,14 @@ function getCozeloopApiBaseUrl(cloud) {
|
|
|
4730
4734
|
return cloud ? (getCloudCozeloopApiBaseUrl() || COZE_API) : COZE_API;
|
|
4731
4735
|
}
|
|
4732
4736
|
|
|
4737
|
+
function getCloudCozeloopSdkApiBaseUrl() {
|
|
4738
|
+
return normalizeCozeloopApiBaseUrl(process.env.COZELOOP_API_BASE_URL || '');
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4741
|
+
function getCozeloopSdkApiBaseUrl(cloud) {
|
|
4742
|
+
return cloud ? (getCloudCozeloopSdkApiBaseUrl() || COZE_API) : COZE_API;
|
|
4743
|
+
}
|
|
4744
|
+
|
|
4733
4745
|
function getOtelEndpointBase(cloud) {
|
|
4734
4746
|
return `${getCozeloopApiBaseUrl(cloud).replace(/\/+$/, '')}/v1/loop/opentelemetry`;
|
|
4735
4747
|
}
|
|
@@ -4935,11 +4947,13 @@ function runCommand(input) {
|
|
|
4935
4947
|
|
|
4936
4948
|
async function verifyTraceReportViaSdk(token, workspaceId, pairCode, pythonCmd, tokenSource) {
|
|
4937
4949
|
const pair = pairCode || crypto.randomBytes(6).toString('hex');
|
|
4938
|
-
const apiBase =
|
|
4950
|
+
const apiBase = getCozeloopSdkApiBaseUrl(true);
|
|
4939
4951
|
const script = `
|
|
4940
4952
|
import json
|
|
4941
4953
|
import os
|
|
4942
4954
|
import sys
|
|
4955
|
+
import urllib.error
|
|
4956
|
+
import urllib.request
|
|
4943
4957
|
|
|
4944
4958
|
events = []
|
|
4945
4959
|
|
|
@@ -4947,6 +4961,51 @@ def finish_event(info):
|
|
|
4947
4961
|
if getattr(info, "is_event_fail", False):
|
|
4948
4962
|
events.append(getattr(info, "detail_msg", "") or "trace export failed")
|
|
4949
4963
|
|
|
4964
|
+
def extract_logid(text):
|
|
4965
|
+
if not text:
|
|
4966
|
+
return ""
|
|
4967
|
+
marker = "logid="
|
|
4968
|
+
idx = text.find(marker)
|
|
4969
|
+
if idx >= 0:
|
|
4970
|
+
out = []
|
|
4971
|
+
for ch in text[idx + len(marker):]:
|
|
4972
|
+
if ch.isalnum():
|
|
4973
|
+
out.append(ch)
|
|
4974
|
+
else:
|
|
4975
|
+
break
|
|
4976
|
+
return "".join(out)
|
|
4977
|
+
for marker in ('"logid":"', '"log_id":"', '"Logid":"'):
|
|
4978
|
+
idx = text.find(marker)
|
|
4979
|
+
if idx >= 0:
|
|
4980
|
+
rest = text[idx + len(marker):]
|
|
4981
|
+
return rest.split('"', 1)[0]
|
|
4982
|
+
return ""
|
|
4983
|
+
|
|
4984
|
+
def http_diag():
|
|
4985
|
+
base = os.environ.get("COZELOOP_API_BASE_URL", "").strip().rstrip("/") or "https://api.coze.cn"
|
|
4986
|
+
url = base + "/v1/loop/traces/ingest"
|
|
4987
|
+
body = json.dumps({"spans": []}).encode()
|
|
4988
|
+
req = urllib.request.Request(
|
|
4989
|
+
url,
|
|
4990
|
+
data=body,
|
|
4991
|
+
headers={
|
|
4992
|
+
"Content-Type": "application/json",
|
|
4993
|
+
"Authorization": "Bearer " + os.environ.get("COZELOOP_API_TOKEN", ""),
|
|
4994
|
+
"x-tt-env": os.environ.get("x_tt_env", ""),
|
|
4995
|
+
"x-use-ppe": os.environ.get("x_use_ppe", ""),
|
|
4996
|
+
},
|
|
4997
|
+
method="POST",
|
|
4998
|
+
)
|
|
4999
|
+
try:
|
|
5000
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
5001
|
+
text = resp.read().decode("utf-8", "replace")
|
|
5002
|
+
return {"url": url, "status": resp.status, "logid": resp.headers.get("x-tt-logid", "") or extract_logid(text), "body": text[:300]}
|
|
5003
|
+
except urllib.error.HTTPError as e:
|
|
5004
|
+
text = e.read().decode("utf-8", "replace")
|
|
5005
|
+
return {"url": url, "status": e.code, "logid": e.headers.get("x-tt-logid", "") or extract_logid(text), "body": text[:300]}
|
|
5006
|
+
except Exception as e:
|
|
5007
|
+
return {"url": url, "status": 0, "logid": "", "body": type(e).__name__ + ": " + str(e)}
|
|
5008
|
+
|
|
4950
5009
|
try:
|
|
4951
5010
|
import cozeloop
|
|
4952
5011
|
kwargs = {
|
|
@@ -4966,11 +5025,15 @@ try:
|
|
|
4966
5025
|
client.flush()
|
|
4967
5026
|
client.close()
|
|
4968
5027
|
if events:
|
|
4969
|
-
|
|
5028
|
+
diag = http_diag()
|
|
5029
|
+
body = "\\n".join(events) + "\\nhttp_diag=" + json.dumps(diag, ensure_ascii=False)
|
|
5030
|
+
print(json.dumps({"success": False, "body": body, "logid": diag.get("logid", ""), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4970
5031
|
sys.exit(1)
|
|
4971
|
-
print(json.dumps({"success": True, "body": "", "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
5032
|
+
print(json.dumps({"success": True, "body": "", "logid": "", "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4972
5033
|
except Exception as e:
|
|
4973
|
-
|
|
5034
|
+
diag = http_diag()
|
|
5035
|
+
body = str(e) + "\\nhttp_diag=" + json.dumps(diag, ensure_ascii=False)
|
|
5036
|
+
print(json.dumps({"success": False, "body": body, "logid": diag.get("logid", ""), "api_base_url": os.environ.get("COZELOOP_API_BASE_URL", ""), "token_source": os.environ.get("COZELOOP_TOKEN_SOURCE", "")}, ensure_ascii=False))
|
|
4974
5037
|
sys.exit(1)
|
|
4975
5038
|
`;
|
|
4976
5039
|
const env = {
|
|
@@ -5005,7 +5068,7 @@ except Exception as e:
|
|
|
5005
5068
|
const snippet = String(body || '').slice(0, 300);
|
|
5006
5069
|
if (snippet) console.log(snippet);
|
|
5007
5070
|
}
|
|
5008
|
-
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource };
|
|
5071
|
+
return { success, status: result.code || 0, body, traceId: '', pairCode: pair, apiBaseUrl: apiBase, tokenSource, logid: parsed?.logid || '' };
|
|
5009
5072
|
}
|
|
5010
5073
|
|
|
5011
5074
|
// 真实发一条最小 OTLP trace 到 CozeLoop,验证上报链路是否打通。
|
|
@@ -5538,7 +5601,7 @@ async function main() {
|
|
|
5538
5601
|
} else if (CLOUD_MODE) {
|
|
5539
5602
|
// 云端:注入已成功,验证失败不阻断(放行),记录结果供后台弹 warning。
|
|
5540
5603
|
cloudResult.verify = 'fail';
|
|
5541
|
-
cloudResult.logid = extractLogid(verifyResult.body) || cloudResult.logid;
|
|
5604
|
+
cloudResult.logid = verifyResult.logid || extractLogid(verifyResult.body) || cloudResult.logid;
|
|
5542
5605
|
cloudResult.message = `trace 上报自检失败 HTTP ${verifyResult.status}: ${(verifyResult.body || '').slice(0, 200)}`;
|
|
5543
5606
|
warn('trace 上报自检失败,但 hook 配置已写入(云端放行)。');
|
|
5544
5607
|
console.log('');
|
package/package.json
CHANGED
|
@@ -264,7 +264,7 @@ def _normalize_api_base_url(url: str) -> str:
|
|
|
264
264
|
|
|
265
265
|
def get_api_base_url() -> str:
|
|
266
266
|
return _normalize_api_base_url(
|
|
267
|
-
os.environ.get("COZELOOP_API_BASE_URL", "")
|
|
267
|
+
os.environ.get("COZELOOP_API_BASE_URL", "")
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
def get_fresh_token() -> Optional[str]:
|
|
@@ -225,7 +225,7 @@ def _normalize_api_base_url(url: str) -> str:
|
|
|
225
225
|
|
|
226
226
|
def get_api_base_url() -> str:
|
|
227
227
|
return _normalize_api_base_url(
|
|
228
|
-
os.environ.get("COZELOOP_API_BASE_URL", "")
|
|
228
|
+
os.environ.get("COZELOOP_API_BASE_URL", "")
|
|
229
229
|
)
|
|
230
230
|
|
|
231
231
|
|
|
@@ -640,6 +640,54 @@ def _ts_ms(dt):
|
|
|
640
640
|
return int(dt.timestamp() * 1000) if dt is not None else None
|
|
641
641
|
|
|
642
642
|
|
|
643
|
+
def _max_dt(*values):
|
|
644
|
+
result = None
|
|
645
|
+
for value in values:
|
|
646
|
+
if value is not None and (result is None or value > result):
|
|
647
|
+
result = value
|
|
648
|
+
return result
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _parse_ts_value(value):
|
|
652
|
+
return _parse_ts({"_ts": value}) if value else None
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _turn_timestamps(turn):
|
|
656
|
+
values = []
|
|
657
|
+
for item in [turn.get("user_message"), *turn.get("assistant_messages", [])]:
|
|
658
|
+
dt = _parse_ts(item)
|
|
659
|
+
if dt:
|
|
660
|
+
values.append(dt)
|
|
661
|
+
for item in [*turn.get("tool_calls", []), *turn.get("tool_results", [])]:
|
|
662
|
+
dt = _parse_ts(item)
|
|
663
|
+
if dt:
|
|
664
|
+
values.append(dt)
|
|
665
|
+
for sc in turn.get("subagent_calls", []):
|
|
666
|
+
for key in ("_start_ts", "_end_ts"):
|
|
667
|
+
dt = _parse_ts_value(sc.get(key))
|
|
668
|
+
if dt:
|
|
669
|
+
values.append(dt)
|
|
670
|
+
return values
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def _turn_bounds(turn):
|
|
674
|
+
values = _turn_timestamps(turn)
|
|
675
|
+
if not values:
|
|
676
|
+
return None, None
|
|
677
|
+
return min(values), max(values)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def _assistant_bounds(turn):
|
|
681
|
+
values = []
|
|
682
|
+
for item in turn.get("assistant_messages", []):
|
|
683
|
+
dt = _parse_ts(item)
|
|
684
|
+
if dt:
|
|
685
|
+
values.append(dt)
|
|
686
|
+
if not values:
|
|
687
|
+
return None, None
|
|
688
|
+
return min(values), max(values)
|
|
689
|
+
|
|
690
|
+
|
|
643
691
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
644
692
|
"""Group raw JSONL entries into conversation turns.
|
|
645
693
|
|
|
@@ -749,6 +797,8 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
749
797
|
"message": args.get("message", ""),
|
|
750
798
|
"model": args.get("model"),
|
|
751
799
|
"result": None,
|
|
800
|
+
"_start_ts": payload.get("_ts"),
|
|
801
|
+
"_end_ts": None,
|
|
752
802
|
}
|
|
753
803
|
current_turn["subagent_calls"].append(subagent_call)
|
|
754
804
|
pending_calls[call_id] = {"kind": "spawn", "subagent_call": subagent_call}
|
|
@@ -756,14 +806,16 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
756
806
|
pending_calls[call_id] = {
|
|
757
807
|
"kind": "wait",
|
|
758
808
|
"ids": args.get("ids", []),
|
|
809
|
+
"_start_ts": payload.get("_ts"),
|
|
759
810
|
}
|
|
760
811
|
else:
|
|
761
812
|
current_turn["tool_calls"].append({
|
|
762
813
|
"call_id": call_id,
|
|
763
814
|
"name": name,
|
|
764
815
|
"input": args,
|
|
816
|
+
"_ts": payload.get("_ts"),
|
|
765
817
|
})
|
|
766
|
-
pending_calls[call_id] = {"kind": "tool"}
|
|
818
|
+
pending_calls[call_id] = {"kind": "tool", "_start_ts": payload.get("_ts")}
|
|
767
819
|
|
|
768
820
|
elif item_type == "function_call_output":
|
|
769
821
|
if current_turn is None:
|
|
@@ -783,6 +835,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
783
835
|
subagent_call["nickname"] = out.get("nickname")
|
|
784
836
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
785
837
|
pass
|
|
838
|
+
subagent_call["_end_ts"] = payload.get("_ts")
|
|
786
839
|
|
|
787
840
|
elif kind == "wait":
|
|
788
841
|
try:
|
|
@@ -795,6 +848,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
795
848
|
for sc in current_turn["subagent_calls"]:
|
|
796
849
|
if sc.get("agent_id") == agent_id and sc.get("result") is None:
|
|
797
850
|
sc["result"] = result_text
|
|
851
|
+
sc["_end_ts"] = payload.get("_ts")
|
|
798
852
|
break
|
|
799
853
|
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
800
854
|
pass
|
|
@@ -803,6 +857,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
803
857
|
current_turn["tool_results"].append({
|
|
804
858
|
"call_id": call_id,
|
|
805
859
|
"output": raw_output,
|
|
860
|
+
"_ts": payload.get("_ts"),
|
|
806
861
|
})
|
|
807
862
|
|
|
808
863
|
if current_turn is not None:
|
|
@@ -888,13 +943,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
888
943
|
# 整体时间范围:所有 user/assistant payload 的真实 timestamp 极值
|
|
889
944
|
_all_ts = []
|
|
890
945
|
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)
|
|
946
|
+
_all_ts.extend(_turn_timestamps(_t))
|
|
898
947
|
root_start_dt = min(_all_ts) if _all_ts else None
|
|
899
948
|
root_end_dt = max(_all_ts) if _all_ts else None
|
|
900
949
|
|
|
@@ -906,6 +955,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
906
955
|
"source": "codex_cli",
|
|
907
956
|
}
|
|
908
957
|
if root_start_dt is not None and root_end_dt is not None:
|
|
958
|
+
root_span.set_finish_time(root_end_dt)
|
|
909
959
|
root_tags["real_start_ms"] = _ts_ms(root_start_dt)
|
|
910
960
|
root_tags["real_end_ms"] = _ts_ms(root_end_dt)
|
|
911
961
|
root_tags["latency_ms"] = _ts_ms(root_end_dt) - _ts_ms(root_start_dt)
|
|
@@ -947,14 +997,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
947
997
|
for i, turn in enumerate(turns):
|
|
948
998
|
try:
|
|
949
999
|
# 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
|
|
1000
|
+
turn_start_dt, turn_end_dt = _turn_bounds(turn)
|
|
958
1001
|
|
|
959
1002
|
with client.start_span(name=f"turn_{i}", span_type="main", start_time=turn_start_dt) as turn_span:
|
|
960
1003
|
turn_span.set_runtime(Runtime(library="codex-cli"))
|
|
@@ -965,6 +1008,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
965
1008
|
"source": "codex_cli",
|
|
966
1009
|
}
|
|
967
1010
|
if turn_start_dt is not None and turn_end_dt is not None:
|
|
1011
|
+
turn_span.set_finish_time(turn_end_dt)
|
|
968
1012
|
_turn_tags["real_start_ms"] = _ts_ms(turn_start_dt)
|
|
969
1013
|
_turn_tags["real_end_ms"] = _ts_ms(turn_end_dt)
|
|
970
1014
|
_turn_tags["latency_ms"] = _ts_ms(turn_end_dt) - _ts_ms(turn_start_dt)
|
|
@@ -973,21 +1017,20 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
973
1017
|
# --- Model span for assistant response ---
|
|
974
1018
|
if turn.get("assistant_messages"):
|
|
975
1019
|
# 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
|
|
1020
|
+
_model_start_dt, _model_end_dt = _assistant_bounds(turn)
|
|
981
1021
|
if _model_start_dt is None:
|
|
982
1022
|
_model_start_dt = turn_start_dt
|
|
1023
|
+
if _model_end_dt is None:
|
|
1024
|
+
_model_end_dt = turn_end_dt
|
|
983
1025
|
with client.start_span(name="assistant_response", span_type="model", start_time=_model_start_dt) as model_span:
|
|
984
1026
|
model_span.set_runtime(Runtime(library="codex-cli"))
|
|
985
1027
|
model_span.set_model_name(model_name)
|
|
986
|
-
if _model_start_dt is not None and
|
|
1028
|
+
if _model_start_dt is not None and _model_end_dt is not None:
|
|
1029
|
+
model_span.set_finish_time(_model_end_dt)
|
|
987
1030
|
model_span.set_tags({
|
|
988
1031
|
"real_start_ms": _ts_ms(_model_start_dt),
|
|
989
|
-
"real_end_ms": _ts_ms(
|
|
990
|
-
"latency_ms": _ts_ms(
|
|
1032
|
+
"real_end_ms": _ts_ms(_model_end_dt),
|
|
1033
|
+
"latency_ms": _ts_ms(_model_end_dt) - _ts_ms(_model_start_dt),
|
|
991
1034
|
})
|
|
992
1035
|
|
|
993
1036
|
# Build input messages: history + current turn input
|
|
@@ -1056,17 +1099,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1056
1099
|
# --- Tool call spans ---
|
|
1057
1100
|
for tool_call in turn.get("tool_calls", []):
|
|
1058
1101
|
tool_name = tool_call.get("name", "unknown")
|
|
1059
|
-
|
|
1102
|
+
tool_start_dt = _parse_ts(tool_call) or turn_start_dt
|
|
1103
|
+
tool_end_dt = None
|
|
1104
|
+
call_id = tool_call.get("call_id")
|
|
1105
|
+
for result in turn.get("tool_results", []):
|
|
1106
|
+
if result.get("call_id") == call_id:
|
|
1107
|
+
tool_end_dt = _parse_ts(result)
|
|
1108
|
+
break
|
|
1109
|
+
tool_finish_dt = _max_dt(tool_end_dt, tool_start_dt)
|
|
1110
|
+
with client.start_span(name=f"tool_{tool_name}", span_type="tool", start_time=tool_start_dt) as tool_span:
|
|
1060
1111
|
tool_span.set_runtime(Runtime(library="codex-cli"))
|
|
1061
|
-
|
|
1112
|
+
tool_tags = {
|
|
1062
1113
|
"tool_name": tool_name,
|
|
1063
|
-
"call_id":
|
|
1064
|
-
}
|
|
1114
|
+
"call_id": call_id,
|
|
1115
|
+
}
|
|
1116
|
+
if tool_start_dt is not None and tool_finish_dt is not None:
|
|
1117
|
+
tool_span.set_finish_time(tool_finish_dt)
|
|
1118
|
+
tool_tags["real_start_ms"] = _ts_ms(tool_start_dt)
|
|
1119
|
+
tool_tags["real_end_ms"] = _ts_ms(tool_finish_dt)
|
|
1120
|
+
tool_tags["latency_ms"] = _ts_ms(tool_finish_dt) - _ts_ms(tool_start_dt)
|
|
1121
|
+
tool_span.set_tags(tool_tags)
|
|
1065
1122
|
tool_span.set_input(
|
|
1066
1123
|
json.dumps(tool_call.get("input", {}), ensure_ascii=False)[:2000]
|
|
1067
1124
|
)
|
|
1068
1125
|
# Find matching tool result
|
|
1069
|
-
call_id = tool_call.get("call_id")
|
|
1070
1126
|
for result in turn.get("tool_results", []):
|
|
1071
1127
|
if result.get("call_id") == call_id:
|
|
1072
1128
|
output = result.get("output", "")
|
|
@@ -1079,15 +1135,24 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1079
1135
|
for sc in turn.get("subagent_calls", []):
|
|
1080
1136
|
agent_id = sc.get("agent_id") or "unknown"
|
|
1081
1137
|
nickname = sc.get("nickname") or agent_id
|
|
1138
|
+
subagent_start_dt = _parse_ts_value(sc.get("_start_ts")) or turn_start_dt
|
|
1139
|
+
subagent_end_dt = _parse_ts_value(sc.get("_end_ts")) or turn_end_dt
|
|
1140
|
+
subagent_finish_dt = _max_dt(subagent_end_dt, subagent_start_dt)
|
|
1082
1141
|
|
|
1083
|
-
with client.start_span(name=f"subagent_{nickname}", span_type="agent", start_time=
|
|
1142
|
+
with client.start_span(name=f"subagent_{nickname}", span_type="agent", start_time=subagent_start_dt) as subagent_span:
|
|
1084
1143
|
subagent_span.set_runtime(Runtime(library="codex-cli"))
|
|
1085
|
-
|
|
1144
|
+
subagent_tags = {
|
|
1086
1145
|
"agent_id": agent_id,
|
|
1087
1146
|
"agent_nickname": nickname,
|
|
1088
1147
|
"agent_role": sc.get("role") or "",
|
|
1089
1148
|
"agent_model": sc.get("model") or "",
|
|
1090
|
-
}
|
|
1149
|
+
}
|
|
1150
|
+
if subagent_start_dt is not None and subagent_finish_dt is not None:
|
|
1151
|
+
subagent_span.set_finish_time(subagent_finish_dt)
|
|
1152
|
+
subagent_tags["real_start_ms"] = _ts_ms(subagent_start_dt)
|
|
1153
|
+
subagent_tags["real_end_ms"] = _ts_ms(subagent_finish_dt)
|
|
1154
|
+
subagent_tags["latency_ms"] = _ts_ms(subagent_finish_dt) - _ts_ms(subagent_start_dt)
|
|
1155
|
+
subagent_span.set_tags(subagent_tags)
|
|
1091
1156
|
subagent_span.set_input(sc.get("message", "")[:2000])
|
|
1092
1157
|
|
|
1093
1158
|
# Load and include saved subagent turn data
|
|
@@ -1097,20 +1162,45 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1097
1162
|
sa_model = sa_data.get("model_name", "codex")
|
|
1098
1163
|
|
|
1099
1164
|
for si, sa_turn in enumerate(sa_turns):
|
|
1100
|
-
|
|
1165
|
+
sa_turn_start_dt, sa_turn_end_dt = _turn_bounds(sa_turn)
|
|
1166
|
+
if sa_turn_start_dt is None:
|
|
1167
|
+
sa_turn_start_dt = subagent_start_dt
|
|
1168
|
+
if sa_turn_end_dt is None:
|
|
1169
|
+
sa_turn_end_dt = sa_turn_start_dt
|
|
1170
|
+
sa_turn_finish_dt = _max_dt(sa_turn_end_dt, sa_turn_start_dt)
|
|
1171
|
+
|
|
1172
|
+
with client.start_span(name=f"turn_{si}", span_type="main", start_time=sa_turn_start_dt) as sa_turn_span:
|
|
1101
1173
|
sa_turn_span.set_runtime(Runtime(library="codex-cli"))
|
|
1102
|
-
|
|
1174
|
+
sa_turn_tags = {
|
|
1103
1175
|
"turn_index": si,
|
|
1104
1176
|
"turn_id": sa_turn.get("turn_id", ""),
|
|
1105
1177
|
"agent_name": nickname,
|
|
1106
|
-
}
|
|
1178
|
+
}
|
|
1179
|
+
if sa_turn_start_dt is not None and sa_turn_finish_dt is not None:
|
|
1180
|
+
sa_turn_span.set_finish_time(sa_turn_finish_dt)
|
|
1181
|
+
sa_turn_tags["real_start_ms"] = _ts_ms(sa_turn_start_dt)
|
|
1182
|
+
sa_turn_tags["real_end_ms"] = _ts_ms(sa_turn_finish_dt)
|
|
1183
|
+
sa_turn_tags["latency_ms"] = _ts_ms(sa_turn_finish_dt) - _ts_ms(sa_turn_start_dt)
|
|
1184
|
+
sa_turn_span.set_tags(sa_turn_tags)
|
|
1107
1185
|
|
|
1108
1186
|
# Subagent model span
|
|
1109
1187
|
if sa_turn.get("assistant_messages"):
|
|
1110
|
-
|
|
1188
|
+
sa_model_start_dt, sa_model_end_dt = _assistant_bounds(sa_turn)
|
|
1189
|
+
if sa_model_start_dt is None:
|
|
1190
|
+
sa_model_start_dt = sa_turn_start_dt
|
|
1191
|
+
if sa_model_end_dt is None:
|
|
1192
|
+
sa_model_end_dt = sa_turn_end_dt
|
|
1193
|
+
sa_model_finish_dt = _max_dt(sa_model_end_dt, sa_model_start_dt)
|
|
1194
|
+
with client.start_span(name="assistant_response", span_type="model", start_time=sa_model_start_dt) as sa_model_span:
|
|
1111
1195
|
sa_model_span.set_runtime(Runtime(library="codex-cli"))
|
|
1112
1196
|
sa_model_span.set_model_name(sa_model)
|
|
1113
|
-
|
|
1197
|
+
sa_model_tags = {"agent_name": nickname}
|
|
1198
|
+
if sa_model_start_dt is not None and sa_model_finish_dt is not None:
|
|
1199
|
+
sa_model_span.set_finish_time(sa_model_finish_dt)
|
|
1200
|
+
sa_model_tags["real_start_ms"] = _ts_ms(sa_model_start_dt)
|
|
1201
|
+
sa_model_tags["real_end_ms"] = _ts_ms(sa_model_finish_dt)
|
|
1202
|
+
sa_model_tags["latency_ms"] = _ts_ms(sa_model_finish_dt) - _ts_ms(sa_model_start_dt)
|
|
1203
|
+
sa_model_span.set_tags(sa_model_tags)
|
|
1114
1204
|
|
|
1115
1205
|
sa_input = sa_turn.get("input_messages", [])
|
|
1116
1206
|
if not sa_input:
|
|
@@ -1154,17 +1244,30 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1154
1244
|
# Subagent tool spans
|
|
1155
1245
|
for sa_tc in sa_turn.get("tool_calls", []):
|
|
1156
1246
|
sa_tool_name = sa_tc.get("name", "unknown")
|
|
1157
|
-
|
|
1247
|
+
sa_tool_start_dt = _parse_ts(sa_tc) or sa_turn_start_dt
|
|
1248
|
+
sa_tool_end_dt = None
|
|
1249
|
+
sa_cid = sa_tc.get("call_id")
|
|
1250
|
+
for sa_r in sa_turn.get("tool_results", []):
|
|
1251
|
+
if sa_r.get("call_id") == sa_cid:
|
|
1252
|
+
sa_tool_end_dt = _parse_ts(sa_r)
|
|
1253
|
+
break
|
|
1254
|
+
sa_tool_finish_dt = _max_dt(sa_tool_end_dt, sa_tool_start_dt)
|
|
1255
|
+
with client.start_span(name=f"tool_{sa_tool_name}", span_type="tool", start_time=sa_tool_start_dt) as sa_tool_span:
|
|
1158
1256
|
sa_tool_span.set_runtime(Runtime(library="codex-cli"))
|
|
1159
|
-
|
|
1257
|
+
sa_tool_tags = {
|
|
1160
1258
|
"tool_name": sa_tool_name,
|
|
1161
|
-
"call_id":
|
|
1259
|
+
"call_id": sa_cid,
|
|
1162
1260
|
"agent_name": nickname,
|
|
1163
|
-
}
|
|
1261
|
+
}
|
|
1262
|
+
if sa_tool_start_dt is not None and sa_tool_finish_dt is not None:
|
|
1263
|
+
sa_tool_span.set_finish_time(sa_tool_finish_dt)
|
|
1264
|
+
sa_tool_tags["real_start_ms"] = _ts_ms(sa_tool_start_dt)
|
|
1265
|
+
sa_tool_tags["real_end_ms"] = _ts_ms(sa_tool_finish_dt)
|
|
1266
|
+
sa_tool_tags["latency_ms"] = _ts_ms(sa_tool_finish_dt) - _ts_ms(sa_tool_start_dt)
|
|
1267
|
+
sa_tool_span.set_tags(sa_tool_tags)
|
|
1164
1268
|
sa_tool_span.set_input(
|
|
1165
1269
|
json.dumps(sa_tc.get("input", {}), ensure_ascii=False)[:2000]
|
|
1166
1270
|
)
|
|
1167
|
-
sa_cid = sa_tc.get("call_id")
|
|
1168
1271
|
for sa_r in sa_turn.get("tool_results", []):
|
|
1169
1272
|
if sa_r.get("call_id") == sa_cid:
|
|
1170
1273
|
sa_out = sa_r.get("output", "")
|