coze_lab 0.1.53 → 0.1.54
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 +1 -1
- package/scripts/claude-code/cozeloop_hook.py +110 -87
- package/scripts/codex/cozeloop_hook.py +192 -13
package/package.json
CHANGED
|
@@ -230,6 +230,7 @@ def _make_finish_event_processor(upload_events: Optional[List[str]] = None):
|
|
|
230
230
|
def _processor(info):
|
|
231
231
|
try:
|
|
232
232
|
if not getattr(info, "is_event_fail", False):
|
|
233
|
+
hook_log("upload success")
|
|
233
234
|
return
|
|
234
235
|
detail = getattr(info, "detail_msg", "") or ""
|
|
235
236
|
if upload_events is not None:
|
|
@@ -393,6 +394,73 @@ def format_content(content: Any, truncate: int = 4096) -> str:
|
|
|
393
394
|
return str(content)[:truncate]
|
|
394
395
|
|
|
395
396
|
|
|
397
|
+
def _normalize_assistant_content(content: Any) -> Dict[str, Any]:
|
|
398
|
+
"""Split supported Claude content without retaining protected payloads."""
|
|
399
|
+
result = {
|
|
400
|
+
"text": [],
|
|
401
|
+
"reasoning": [],
|
|
402
|
+
"tool_calls": [],
|
|
403
|
+
"parts": [],
|
|
404
|
+
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
405
|
+
"unknown_types": {},
|
|
406
|
+
}
|
|
407
|
+
if isinstance(content, str):
|
|
408
|
+
if content:
|
|
409
|
+
result["text"].append(content)
|
|
410
|
+
return result
|
|
411
|
+
if not isinstance(content, list):
|
|
412
|
+
return result
|
|
413
|
+
for item in content:
|
|
414
|
+
if not isinstance(item, dict):
|
|
415
|
+
continue
|
|
416
|
+
item_type = item.get("type", "")
|
|
417
|
+
if item_type == "text":
|
|
418
|
+
text = item.get("text", "")
|
|
419
|
+
if text:
|
|
420
|
+
result["text"].append(text)
|
|
421
|
+
result["parts"].append(
|
|
422
|
+
ModelMessagePart(type=ModelMessagePartType.TEXT, text=text)
|
|
423
|
+
)
|
|
424
|
+
elif item_type == "thinking":
|
|
425
|
+
thinking = item.get("thinking", "")
|
|
426
|
+
if thinking:
|
|
427
|
+
result["reasoning"].append(thinking)
|
|
428
|
+
elif item_type == "redacted_thinking":
|
|
429
|
+
data = item.get("data", "")
|
|
430
|
+
if isinstance(data, str) and data:
|
|
431
|
+
result["protected"]["source"] = "redacted_thinking"
|
|
432
|
+
result["protected"]["count"] += 1
|
|
433
|
+
result["protected"]["bytes"] += len(data.encode("utf-8"))
|
|
434
|
+
elif item_type == "tool_use":
|
|
435
|
+
result["tool_calls"].append(ModelToolCall(
|
|
436
|
+
id=item.get("id", ""),
|
|
437
|
+
type="function",
|
|
438
|
+
function=ModelToolCallFunction(
|
|
439
|
+
name=item.get("name", ""),
|
|
440
|
+
arguments=(
|
|
441
|
+
json.dumps(item.get("input", {}), ensure_ascii=False)
|
|
442
|
+
if isinstance(item.get("input"), dict)
|
|
443
|
+
else str(item.get("input", ""))
|
|
444
|
+
),
|
|
445
|
+
),
|
|
446
|
+
))
|
|
447
|
+
else:
|
|
448
|
+
key = item_type or "unknown"
|
|
449
|
+
result["unknown_types"][key] = result["unknown_types"].get(key, 0) + 1
|
|
450
|
+
return result
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _set_protected_reasoning_tags(span, normalized: Dict[str, Any]):
|
|
454
|
+
protected = normalized.get("protected") or {}
|
|
455
|
+
if not protected.get("count"):
|
|
456
|
+
return
|
|
457
|
+
span.set_tags({
|
|
458
|
+
"protected_reasoning_source": protected.get("source", ""),
|
|
459
|
+
"protected_reasoning_count": protected.get("count", 0),
|
|
460
|
+
"protected_reasoning_bytes": protected.get("bytes", 0),
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
|
|
396
464
|
# --- Message Parsing and Grouping ---
|
|
397
465
|
|
|
398
466
|
def is_tool_result_message(msg: Dict[str, Any]) -> bool:
|
|
@@ -1080,44 +1148,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1080
1148
|
tool_choice=ModelToolChoice(type="", function=None)
|
|
1081
1149
|
))
|
|
1082
1150
|
|
|
1083
|
-
|
|
1084
|
-
text_parts = []
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if isinstance(raw_content, list):
|
|
1089
|
-
for item in raw_content:
|
|
1090
|
-
if not isinstance(item, dict):
|
|
1091
|
-
continue
|
|
1092
|
-
item_type = item.get("type", "")
|
|
1093
|
-
if item_type == "text":
|
|
1094
|
-
text = item.get("text", "")
|
|
1095
|
-
if text:
|
|
1096
|
-
text_parts.append(text)
|
|
1097
|
-
parts_list.append(ModelMessagePart(type=ModelMessagePartType.TEXT, text=text))
|
|
1098
|
-
elif item_type == "thinking":
|
|
1099
|
-
thinking = item.get("thinking", "")
|
|
1100
|
-
if thinking:
|
|
1101
|
-
thinking_parts.append(thinking)
|
|
1102
|
-
elif item_type == "redacted_thinking":
|
|
1103
|
-
pass # encrypted, cannot extract
|
|
1104
|
-
elif item_type == "tool_use":
|
|
1105
|
-
tool_call_list.append(ModelToolCall(
|
|
1106
|
-
id=item.get("id", ""),
|
|
1107
|
-
type="function",
|
|
1108
|
-
function=ModelToolCallFunction(
|
|
1109
|
-
name=item.get("name", ""),
|
|
1110
|
-
arguments=json.dumps(item.get("input", {}), ensure_ascii=False) if isinstance(item.get("input"), dict) else str(item.get("input", ""))
|
|
1111
|
-
)
|
|
1112
|
-
))
|
|
1113
|
-
else:
|
|
1114
|
-
parts_list.append(ModelMessagePart(
|
|
1115
|
-
type=ModelMessagePartType.TEXT,
|
|
1116
|
-
text=json.dumps(item, ensure_ascii=False)[:4096]
|
|
1117
|
-
))
|
|
1118
|
-
elif isinstance(raw_content, str) and raw_content:
|
|
1119
|
-
text_parts.append(raw_content)
|
|
1120
|
-
|
|
1151
|
+
normalized = _normalize_assistant_content(raw_content)
|
|
1152
|
+
text_parts = normalized["text"]
|
|
1153
|
+
thinking_parts = normalized["reasoning"]
|
|
1154
|
+
tool_call_list = normalized["tool_calls"]
|
|
1155
|
+
parts_list = normalized["parts"]
|
|
1121
1156
|
content_text = "\n".join(text_parts) if text_parts else ""
|
|
1122
1157
|
reasoning_text = "\n".join(thinking_parts) if thinking_parts else ""
|
|
1123
1158
|
finish_reason = "tool_calls" if tool_call_list else "stop"
|
|
@@ -1138,6 +1173,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1138
1173
|
)
|
|
1139
1174
|
|
|
1140
1175
|
model_span.set_output(ModelOutput(choices=[output_choice]))
|
|
1176
|
+
_set_protected_reasoning_tags(model_span, normalized)
|
|
1141
1177
|
|
|
1142
1178
|
# Set token usage for this specific model call
|
|
1143
1179
|
usage = assistant_message_obj.get("usage", {})
|
|
@@ -1243,42 +1279,11 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1243
1279
|
tool_choice=ModelToolChoice(type="", function=None)
|
|
1244
1280
|
))
|
|
1245
1281
|
|
|
1246
|
-
|
|
1247
|
-
sub_text_parts = []
|
|
1248
|
-
sub_tc_list = []
|
|
1249
|
-
sub_parts_list = []
|
|
1250
|
-
sub_thinking_parts = []
|
|
1251
|
-
if isinstance(sub_content, list):
|
|
1252
|
-
for item in sub_content:
|
|
1253
|
-
if not isinstance(item, dict):
|
|
1254
|
-
continue
|
|
1255
|
-
item_type = item.get("type", "")
|
|
1256
|
-
if item_type == "text":
|
|
1257
|
-
t = item.get("text", "")
|
|
1258
|
-
if t:
|
|
1259
|
-
sub_text_parts.append(t)
|
|
1260
|
-
sub_parts_list.append(ModelMessagePart(type=ModelMessagePartType.TEXT, text=t))
|
|
1261
|
-
elif item_type == "thinking":
|
|
1262
|
-
t = item.get("thinking", "")
|
|
1263
|
-
if t:
|
|
1264
|
-
sub_thinking_parts.append(t)
|
|
1265
|
-
elif item_type == "redacted_thinking":
|
|
1266
|
-
pass
|
|
1267
|
-
elif item_type == "tool_use":
|
|
1268
|
-
sub_tc_list.append(ModelToolCall(
|
|
1269
|
-
id=item.get("id", ""),
|
|
1270
|
-
type="function",
|
|
1271
|
-
function=ModelToolCallFunction(
|
|
1272
|
-
name=item.get("name", ""),
|
|
1273
|
-
arguments=json.dumps(item.get("input", {}), ensure_ascii=False) if isinstance(item.get("input"), dict) else str(item.get("input", ""))
|
|
1274
|
-
)
|
|
1275
|
-
))
|
|
1276
|
-
else:
|
|
1277
|
-
sub_parts_list.append(ModelMessagePart(
|
|
1278
|
-
type=ModelMessagePartType.TEXT,
|
|
1279
|
-
text=json.dumps(item, ensure_ascii=False)[:4096]
|
|
1280
|
-
))
|
|
1281
|
-
|
|
1282
|
+
sub_normalized = _normalize_assistant_content(sub_content)
|
|
1283
|
+
sub_text_parts = sub_normalized["text"]
|
|
1284
|
+
sub_tc_list = sub_normalized["tool_calls"]
|
|
1285
|
+
sub_parts_list = sub_normalized["parts"]
|
|
1286
|
+
sub_thinking_parts = sub_normalized["reasoning"]
|
|
1282
1287
|
sub_content_text = "\n".join(sub_text_parts) if sub_text_parts else ""
|
|
1283
1288
|
sub_reasoning_text = "\n".join(sub_thinking_parts) if sub_thinking_parts else ""
|
|
1284
1289
|
sub_finish = "tool_calls" if sub_tc_list else "stop"
|
|
@@ -1296,6 +1301,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1296
1301
|
metadata={}
|
|
1297
1302
|
)
|
|
1298
1303
|
)]))
|
|
1304
|
+
_set_protected_reasoning_tags(sub_model_span, sub_normalized)
|
|
1299
1305
|
|
|
1300
1306
|
# Distribute tokens evenly; remainder goes to last step
|
|
1301
1307
|
step_in = per_step_in + (remainder_in if sk == n_model_steps - 1 else 0)
|
|
@@ -1435,7 +1441,7 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, history
|
|
|
1435
1441
|
def _rt_state_key_header():
|
|
1436
1442
|
return "rt_root_header"
|
|
1437
1443
|
|
|
1438
|
-
def _flatten_completed_steps(turns: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1444
|
+
def _flatten_completed_steps(turns: List[Dict[str, Any]], is_terminal: bool = False) -> List[Dict[str, Any]]:
|
|
1439
1445
|
"""把 turns 线性展开成 step 列表;只取【已完成】的 step。
|
|
1440
1446
|
|
|
1441
1447
|
一个 step(assistant 一次模型调用 + 其触发的 tool_results)视为已完成的判定:
|
|
@@ -1454,7 +1460,7 @@ def _flatten_completed_steps(turns: List[Dict[str, Any]]) -> List[Dict[str, Any]
|
|
|
1454
1460
|
for idx, item in enumerate(flat):
|
|
1455
1461
|
is_global_last = (idx == len(flat) - 1)
|
|
1456
1462
|
has_results = bool(item["step"].get("tool_results"))
|
|
1457
|
-
if (not is_global_last) or has_results:
|
|
1463
|
+
if is_terminal or (not is_global_last) or has_results:
|
|
1458
1464
|
completed.append(item)
|
|
1459
1465
|
return completed, len(flat)
|
|
1460
1466
|
|
|
@@ -1546,7 +1552,7 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
|
|
|
1546
1552
|
debug_log(f"[rt] root created+finished trace_id={getattr(root_ctx,'trace_id','?')}")
|
|
1547
1553
|
|
|
1548
1554
|
# ---- 发新完成的 step ----
|
|
1549
|
-
completed, total_steps = _flatten_completed_steps(turns)
|
|
1555
|
+
completed, total_steps = _flatten_completed_steps(turns, is_terminal=is_terminal)
|
|
1550
1556
|
last_global = state.get("rt_last_global_step", 0)
|
|
1551
1557
|
new_items = completed[last_global:]
|
|
1552
1558
|
# 批量节流:非终态时,距上次上报不足 REALTIME_BATCH_INTERVAL 秒则本次不发,
|
|
@@ -1581,16 +1587,33 @@ def send_steps_realtime(turns, session_id, history_turns, state, coze_tags_overr
|
|
|
1581
1587
|
if step_start_dt is not None and step_end_dt is not None:
|
|
1582
1588
|
_set_finish_time_safe(mspan, step_end_dt)
|
|
1583
1589
|
raw_content = amo.get("content", [])
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1590
|
+
normalized = _normalize_assistant_content(raw_content)
|
|
1591
|
+
turn_user = item["turn"].get("user_message", {}).get("message", {})
|
|
1592
|
+
turn_user_content = turn_user.get("content") if turn_user else None
|
|
1593
|
+
input_messages = list(_build_history_messages(history_turns or []))
|
|
1594
|
+
if not is_empty_content(turn_user_content):
|
|
1595
|
+
input_messages.append(_make_message("user", format_content(turn_user_content)))
|
|
1596
|
+
mspan.set_input(ModelInput(
|
|
1597
|
+
messages=input_messages,
|
|
1598
|
+
tools=[],
|
|
1599
|
+
tool_choice=ModelToolChoice(type="", function=None),
|
|
1600
|
+
))
|
|
1601
|
+
output_choice = ModelChoice(
|
|
1602
|
+
finish_reason="tool_calls" if normalized["tool_calls"] else "stop",
|
|
1603
|
+
index=0,
|
|
1604
|
+
message=ModelMessage(
|
|
1605
|
+
role="assistant",
|
|
1606
|
+
content="\n".join(normalized["text"]),
|
|
1607
|
+
reasoning_content="\n".join(normalized["reasoning"]),
|
|
1608
|
+
parts=normalized["parts"],
|
|
1609
|
+
name="",
|
|
1610
|
+
tool_calls=normalized["tool_calls"],
|
|
1611
|
+
tool_call_id="",
|
|
1612
|
+
metadata={},
|
|
1613
|
+
),
|
|
1614
|
+
)
|
|
1615
|
+
mspan.set_output(ModelOutput(choices=[output_choice]))
|
|
1616
|
+
_set_protected_reasoning_tags(mspan, normalized)
|
|
1594
1617
|
usage = amo.get("usage", {})
|
|
1595
1618
|
it_tok = _usage_int(usage, "input_tokens") + _usage_int(usage, "cache_creation_input_tokens") + _usage_int(usage, "cache_read_input_tokens")
|
|
1596
1619
|
if it_tok > 0:
|
|
@@ -575,6 +575,48 @@ def extract_assistant_text(payload: Dict[str, Any]) -> str:
|
|
|
575
575
|
return "\n".join(parts)
|
|
576
576
|
|
|
577
577
|
|
|
578
|
+
def _protected_reasoning_meta(source: str, payload: Any) -> Dict[str, Any]:
|
|
579
|
+
"""Return safe metadata for protected reasoning without retaining payload."""
|
|
580
|
+
if not isinstance(payload, str) or not payload:
|
|
581
|
+
return {}
|
|
582
|
+
return {
|
|
583
|
+
"source": source,
|
|
584
|
+
"count": 1,
|
|
585
|
+
"bytes": len(payload.encode("utf-8")),
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def extract_reasoning_record(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
590
|
+
"""Normalize a Codex reasoning item, excluding encrypted content itself."""
|
|
591
|
+
parts = [
|
|
592
|
+
item.get("text", "")
|
|
593
|
+
for item in payload.get("summary", [])
|
|
594
|
+
if isinstance(item, dict)
|
|
595
|
+
and item.get("type") == "summary_text"
|
|
596
|
+
and item.get("text")
|
|
597
|
+
]
|
|
598
|
+
return {
|
|
599
|
+
"content": "",
|
|
600
|
+
"reasoning": "\n".join(parts),
|
|
601
|
+
"protected": _protected_reasoning_meta(
|
|
602
|
+
"encrypted_content", payload.get("encrypted_content")
|
|
603
|
+
),
|
|
604
|
+
"_ts": payload.get("_ts"),
|
|
605
|
+
"_line_number": payload.get("_line_number", -1),
|
|
606
|
+
"source": "response_item.reasoning",
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _json_text(value: Any) -> str:
|
|
611
|
+
"""Render structured tool values deterministically for trace display."""
|
|
612
|
+
if isinstance(value, str):
|
|
613
|
+
return value
|
|
614
|
+
try:
|
|
615
|
+
return json.dumps(value, ensure_ascii=False)
|
|
616
|
+
except (TypeError, ValueError):
|
|
617
|
+
return str(value)
|
|
618
|
+
|
|
619
|
+
|
|
578
620
|
def extract_message_content_text(payload: Dict[str, Any]) -> str:
|
|
579
621
|
"""Extract all text content from a message payload regardless of role."""
|
|
580
622
|
parts = []
|
|
@@ -669,6 +711,10 @@ def _turn_timestamps(turn):
|
|
|
669
711
|
dt = _parse_ts(item)
|
|
670
712
|
if dt:
|
|
671
713
|
values.append(dt)
|
|
714
|
+
for item in turn.get("model_records", []):
|
|
715
|
+
dt = _parse_ts(item)
|
|
716
|
+
if dt:
|
|
717
|
+
values.append(dt)
|
|
672
718
|
for sc in turn.get("subagent_calls", []):
|
|
673
719
|
for key in ("_start_ts", "_end_ts"):
|
|
674
720
|
dt = _parse_ts_value(sc.get(key))
|
|
@@ -758,6 +804,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
758
804
|
"user_message": None,
|
|
759
805
|
"user_message_text": "",
|
|
760
806
|
"assistant_messages": [],
|
|
807
|
+
"model_records": [],
|
|
761
808
|
"tool_calls": [],
|
|
762
809
|
"tool_results": [],
|
|
763
810
|
"input_messages": [],
|
|
@@ -775,6 +822,17 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
775
822
|
if current_turn is not None:
|
|
776
823
|
info = payload.get("info") or {}
|
|
777
824
|
current_turn["token_usage"] = info.get("last_token_usage", {})
|
|
825
|
+
elif msg_type in ("agent_reasoning", "agent_message") and current_turn is not None:
|
|
826
|
+
text = payload.get("text") if msg_type == "agent_reasoning" else payload.get("message")
|
|
827
|
+
if isinstance(text, str) and text:
|
|
828
|
+
current_turn["model_records"].append({
|
|
829
|
+
"content": text if msg_type == "agent_message" else "",
|
|
830
|
+
"reasoning": text if msg_type == "agent_reasoning" else "",
|
|
831
|
+
"protected": {},
|
|
832
|
+
"_ts": payload.get("_ts"),
|
|
833
|
+
"_line_number": entry.get("_line_number", -1),
|
|
834
|
+
"source": f"event_msg.{msg_type}",
|
|
835
|
+
})
|
|
778
836
|
continue
|
|
779
837
|
|
|
780
838
|
# --- Content items ---
|
|
@@ -795,6 +853,14 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
795
853
|
elif role == "assistant":
|
|
796
854
|
if current_turn is not None:
|
|
797
855
|
current_turn["assistant_messages"].append(payload)
|
|
856
|
+
current_turn["model_records"].append({
|
|
857
|
+
"content": extract_assistant_text(payload),
|
|
858
|
+
"reasoning": "",
|
|
859
|
+
"protected": {},
|
|
860
|
+
"_ts": payload.get("_ts"),
|
|
861
|
+
"_line_number": entry.get("_line_number", -1),
|
|
862
|
+
"source": "response_item.message",
|
|
863
|
+
})
|
|
798
864
|
elif role in ("developer", "system"):
|
|
799
865
|
if current_turn is not None:
|
|
800
866
|
current_turn["input_messages"].append({
|
|
@@ -810,16 +876,25 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
810
876
|
"content": text,
|
|
811
877
|
})
|
|
812
878
|
|
|
813
|
-
elif item_type == "
|
|
879
|
+
elif item_type == "reasoning":
|
|
880
|
+
if current_turn is not None:
|
|
881
|
+
record = extract_reasoning_record(payload)
|
|
882
|
+
record["_line_number"] = entry.get("_line_number", -1)
|
|
883
|
+
current_turn["model_records"].append(record)
|
|
884
|
+
|
|
885
|
+
elif item_type in ("function_call", "custom_tool_call"):
|
|
814
886
|
if current_turn is None:
|
|
815
887
|
continue
|
|
816
888
|
call_id = payload.get("call_id")
|
|
817
889
|
name = payload.get("name", "")
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
890
|
+
if item_type == "custom_tool_call":
|
|
891
|
+
args = payload.get("input", "")
|
|
892
|
+
else:
|
|
893
|
+
args_raw = payload.get("arguments", "{}")
|
|
894
|
+
try:
|
|
895
|
+
args = json.loads(args_raw)
|
|
896
|
+
except (json.JSONDecodeError, TypeError):
|
|
897
|
+
args = {"_raw": args_raw}
|
|
823
898
|
|
|
824
899
|
if name == "spawn_agent":
|
|
825
900
|
subagent_call = {
|
|
@@ -846,11 +921,12 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
846
921
|
"call_id": call_id,
|
|
847
922
|
"name": name,
|
|
848
923
|
"input": args,
|
|
924
|
+
"custom": item_type == "custom_tool_call",
|
|
849
925
|
"_ts": payload.get("_ts"),
|
|
850
926
|
})
|
|
851
927
|
pending_calls[call_id] = {"kind": "tool", "_start_ts": payload.get("_ts")}
|
|
852
928
|
|
|
853
|
-
elif item_type
|
|
929
|
+
elif item_type in ("function_call_output", "custom_tool_call_output"):
|
|
854
930
|
if current_turn is None:
|
|
855
931
|
continue
|
|
856
932
|
call_id = payload.get("call_id")
|
|
@@ -896,10 +972,35 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
896
972
|
if current_turn is not None:
|
|
897
973
|
turns.append(current_turn)
|
|
898
974
|
|
|
975
|
+
# Codex may duplicate visible commentary/reasoning in response_item and
|
|
976
|
+
# event_msg. Prefer response_item, then retain event-only fallbacks.
|
|
977
|
+
for turn in turns:
|
|
978
|
+
seen = set()
|
|
979
|
+
deduped = []
|
|
980
|
+
records = sorted(
|
|
981
|
+
turn.get("model_records", []),
|
|
982
|
+
key=lambda record: (
|
|
983
|
+
1 if str(record.get("source", "")).startswith("event_msg.") else 0,
|
|
984
|
+
record.get("_line_number", -1),
|
|
985
|
+
),
|
|
986
|
+
)
|
|
987
|
+
for record in records:
|
|
988
|
+
category = "reasoning" if record.get("reasoning") else "content"
|
|
989
|
+
text = record.get(category, "").strip()
|
|
990
|
+
key = (category, text)
|
|
991
|
+
if text and key in seen:
|
|
992
|
+
continue
|
|
993
|
+
if text:
|
|
994
|
+
seen.add(key)
|
|
995
|
+
deduped.append(record)
|
|
996
|
+
turn["model_records"] = sorted(
|
|
997
|
+
deduped, key=lambda record: record.get("_line_number", -1)
|
|
998
|
+
)
|
|
999
|
+
|
|
899
1000
|
# Drop turns with no user input and no assistant response
|
|
900
1001
|
turns = [
|
|
901
1002
|
t for t in turns
|
|
902
|
-
if t["user_message"] is not None or t["assistant_messages"]
|
|
1003
|
+
if t["user_message"] is not None or t["assistant_messages"] or t.get("model_records")
|
|
903
1004
|
]
|
|
904
1005
|
|
|
905
1006
|
return turns
|
|
@@ -1439,7 +1540,8 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1439
1540
|
if turn.get("user_message_text"):
|
|
1440
1541
|
first_user_text = turn["user_message_text"]
|
|
1441
1542
|
break
|
|
1442
|
-
|
|
1543
|
+
all_timestamps = [dt for turn in turns for dt in _turn_timestamps(turn)]
|
|
1544
|
+
root_start_dt = min(all_timestamps) if all_timestamps else None
|
|
1443
1545
|
root_span = client.start_span(name="codex_request", span_type="main",
|
|
1444
1546
|
start_time=root_start_dt, start_new_trace=True)
|
|
1445
1547
|
root_span.set_runtime(Runtime(library="codex-cli"))
|
|
@@ -1460,6 +1562,83 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1460
1562
|
return None
|
|
1461
1563
|
hook_log(f"[rt] root created trace_id={getattr(root_ctx,'trace_id','?')}")
|
|
1462
1564
|
|
|
1565
|
+
# Model-visible records use their own line watermark. They are not
|
|
1566
|
+
# coupled to tool completion because reasoning/commentary can occur in
|
|
1567
|
+
# turns with no tools at all.
|
|
1568
|
+
model_records = []
|
|
1569
|
+
for turn in turns:
|
|
1570
|
+
for record in turn.get("model_records", []):
|
|
1571
|
+
model_records.append((turn, record))
|
|
1572
|
+
model_records.sort(key=lambda item: item[1].get("_line_number", -1))
|
|
1573
|
+
last_model_line = state.get("rt_last_model_line", -1)
|
|
1574
|
+
model_sent = 0
|
|
1575
|
+
max_model_line = last_model_line
|
|
1576
|
+
for turn, record in model_records:
|
|
1577
|
+
line = record.get("_line_number", -1)
|
|
1578
|
+
if line <= last_model_line:
|
|
1579
|
+
continue
|
|
1580
|
+
content = record.get("content", "")
|
|
1581
|
+
reasoning = record.get("reasoning", "")
|
|
1582
|
+
protected = record.get("protected") or {}
|
|
1583
|
+
if not content and not reasoning and not protected:
|
|
1584
|
+
max_model_line = max(max_model_line, line)
|
|
1585
|
+
continue
|
|
1586
|
+
start_dt = _parse_ts_value(record.get("_ts"))
|
|
1587
|
+
mspan = client.start_span(
|
|
1588
|
+
name=f"model_content_{line}", span_type="model",
|
|
1589
|
+
start_time=start_dt, child_of=root_ctx,
|
|
1590
|
+
)
|
|
1591
|
+
mspan.set_runtime(Runtime(library="codex-cli"))
|
|
1592
|
+
try:
|
|
1593
|
+
mspan.set_model_name(model_name)
|
|
1594
|
+
except Exception:
|
|
1595
|
+
pass
|
|
1596
|
+
if turn.get("user_message_text"):
|
|
1597
|
+
mspan.set_input(ModelInput(
|
|
1598
|
+
messages=[_make_model_message("user", turn["user_message_text"])],
|
|
1599
|
+
tools=[],
|
|
1600
|
+
tool_choice=ModelToolChoice(type="", function=None),
|
|
1601
|
+
))
|
|
1602
|
+
choice = ModelChoice(
|
|
1603
|
+
finish_reason="stop",
|
|
1604
|
+
index=0,
|
|
1605
|
+
message=ModelMessage(
|
|
1606
|
+
role="assistant",
|
|
1607
|
+
content=content,
|
|
1608
|
+
reasoning_content=reasoning,
|
|
1609
|
+
parts=[],
|
|
1610
|
+
name="",
|
|
1611
|
+
tool_calls=[],
|
|
1612
|
+
tool_call_id="",
|
|
1613
|
+
metadata={},
|
|
1614
|
+
),
|
|
1615
|
+
)
|
|
1616
|
+
mspan.set_output(ModelOutput(choices=[choice]))
|
|
1617
|
+
tags = {
|
|
1618
|
+
"model_record_line": line,
|
|
1619
|
+
"model_record_source": record.get("source", "unknown"),
|
|
1620
|
+
}
|
|
1621
|
+
if protected:
|
|
1622
|
+
tags.update({
|
|
1623
|
+
"protected_reasoning_source": protected.get("source", ""),
|
|
1624
|
+
"protected_reasoning_count": protected.get("count", 0),
|
|
1625
|
+
"protected_reasoning_bytes": protected.get("bytes", 0),
|
|
1626
|
+
})
|
|
1627
|
+
mspan.set_tags(tags)
|
|
1628
|
+
if start_dt is not None:
|
|
1629
|
+
_set_finish_time_safe(mspan, start_dt)
|
|
1630
|
+
mspan.finish()
|
|
1631
|
+
client.flush()
|
|
1632
|
+
if upload_events:
|
|
1633
|
+
hook_log(f"[rt] upload failed after model flush line={line} detail={upload_events[-1][:500]}")
|
|
1634
|
+
if max_model_line > last_model_line:
|
|
1635
|
+
state["rt_last_model_line"] = max_model_line
|
|
1636
|
+
return None
|
|
1637
|
+
max_model_line = line
|
|
1638
|
+
model_sent += 1
|
|
1639
|
+
if max_model_line > last_model_line:
|
|
1640
|
+
state["rt_last_model_line"] = max_model_line
|
|
1641
|
+
|
|
1463
1642
|
# 线性展开所有 tool_call(带 result 的才算完成),按全局序增量发
|
|
1464
1643
|
flat = []
|
|
1465
1644
|
for ti, turn in enumerate(turns):
|
|
@@ -1496,7 +1675,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1496
1675
|
except Exception: pass
|
|
1497
1676
|
if start_dt and end_dt:
|
|
1498
1677
|
_set_finish_time_safe(mspan, end_dt)
|
|
1499
|
-
mspan.set_input(truncate_text(
|
|
1678
|
+
mspan.set_input(truncate_text(_json_text(tc.get("input", {}))))
|
|
1500
1679
|
mspan_ctx = client.get_span_from_header(mspan.to_header())
|
|
1501
1680
|
mspan.finish()
|
|
1502
1681
|
|
|
@@ -1506,9 +1685,9 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1506
1685
|
if end_dt:
|
|
1507
1686
|
_set_finish_time_safe(tspan, end_dt)
|
|
1508
1687
|
tspan.set_tags({"tool_name": tname, "tool_call_id": tc.get("call_id"), "step_index": gidx})
|
|
1509
|
-
tspan.set_input(truncate_text(
|
|
1688
|
+
tspan.set_input(truncate_text(_json_text(tc.get("input", {}))))
|
|
1510
1689
|
if result is not None:
|
|
1511
|
-
tspan.set_output(truncate_text(
|
|
1690
|
+
tspan.set_output(truncate_text(_json_text(result.get("output", ""))))
|
|
1512
1691
|
tspan.finish()
|
|
1513
1692
|
sent += 1
|
|
1514
1693
|
client.flush() # 每个 tool_call 立即 flush → 结束即可见
|
|
@@ -1601,7 +1780,7 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1601
1780
|
hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
|
|
1602
1781
|
return None
|
|
1603
1782
|
|
|
1604
|
-
hook_log(f"[rt] sent {sent} tool span(s), {token_sent} token span(s), last_tool={state['rt_last_tool']}, last_token_line={state.get('rt_last_token_line', -1)}, terminal={is_terminal}")
|
|
1783
|
+
hook_log(f"[rt] sent {model_sent} model span(s), {sent} tool span(s), {token_sent} token span(s), last_model_line={state.get('rt_last_model_line', -1)}, last_tool={state['rt_last_tool']}, last_token_line={state.get('rt_last_token_line', -1)}, terminal={is_terminal}")
|
|
1605
1784
|
return True
|
|
1606
1785
|
except Exception as e:
|
|
1607
1786
|
hook_log(f"[rt] error: {e}")
|