coze_lab 0.1.25 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -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
- _u = _parse_ts(_t.get("user_message"))
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 = _parse_ts(turn.get("user_message"))
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 = None
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 turn_end_dt is not None:
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(turn_end_dt),
990
- "latency_ms": _ts_ms(turn_end_dt) - _ts_ms(_model_start_dt),
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
- with client.start_span(name=f"tool_{tool_name}", span_type="tool", start_time=turn_start_dt) as tool_span:
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
- tool_span.set_tags({
1112
+ tool_tags = {
1062
1113
  "tool_name": tool_name,
1063
- "call_id": tool_call.get("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=turn_start_dt) as subagent_span:
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
- subagent_span.set_tags({
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
- with client.start_span(name=f"turn_{si}", span_type="main") as sa_turn_span:
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
- sa_turn_span.set_tags({
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
- with client.start_span(name="assistant_response", span_type="model") as sa_model_span:
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
- sa_model_span.set_tags({"agent_name": nickname})
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
- with client.start_span(name=f"tool_{sa_tool_name}", span_type="tool") as sa_tool_span:
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
- sa_tool_span.set_tags({
1257
+ sa_tool_tags = {
1160
1258
  "tool_name": sa_tool_name,
1161
- "call_id": sa_tc.get("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", "")