coze_lab 0.1.54 → 0.1.55
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/codex/cozeloop_hook.py +296 -182
package/package.json
CHANGED
|
@@ -741,13 +741,45 @@ def _assistant_bounds(turn):
|
|
|
741
741
|
return min(values), max(values)
|
|
742
742
|
|
|
743
743
|
|
|
744
|
-
def
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
744
|
+
def extract_model_invocations(turns: List[Dict[str, Any]],
|
|
745
|
+
entries: List[Dict[str, Any]],
|
|
746
|
+
is_terminal: bool = False) -> List[Dict[str, Any]]:
|
|
747
|
+
"""Group visible model output, tool calls, and usage into model invocations.
|
|
748
|
+
|
|
749
|
+
Codex emits one non-empty ``token_count.last_token_usage`` after each model
|
|
750
|
+
invocation. It is the only reliable boundary shared by tool and no-tool
|
|
751
|
+
calls, so records since the previous boundary are reported together. The
|
|
752
|
+
cumulative ``total_token_usage`` is retained as metadata only.
|
|
749
753
|
"""
|
|
750
754
|
events: List[Dict[str, Any]] = []
|
|
755
|
+
tool_results: Dict[str, Dict[str, Any]] = {}
|
|
756
|
+
|
|
757
|
+
for turn in turns:
|
|
758
|
+
for result in turn.get("tool_results", []):
|
|
759
|
+
call_id = result.get("call_id")
|
|
760
|
+
if call_id:
|
|
761
|
+
tool_results[call_id] = result
|
|
762
|
+
for record in turn.get("model_records", []):
|
|
763
|
+
events.append({
|
|
764
|
+
"kind": "record",
|
|
765
|
+
"line": record.get("_line_number", -1),
|
|
766
|
+
"timestamp": record.get("_ts"),
|
|
767
|
+
"record": record,
|
|
768
|
+
"turn": turn,
|
|
769
|
+
})
|
|
770
|
+
for tool_call in turn.get("tool_calls", []):
|
|
771
|
+
events.append({
|
|
772
|
+
"kind": "tool",
|
|
773
|
+
"line": tool_call.get("_line_number", -1),
|
|
774
|
+
"timestamp": tool_call.get("_ts"),
|
|
775
|
+
"tool_call": tool_call,
|
|
776
|
+
"turn": turn,
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
turn_starts = sorted(
|
|
780
|
+
((turn.get("start_line", -1), turn) for turn in turns),
|
|
781
|
+
key=lambda item: item[0],
|
|
782
|
+
)
|
|
751
783
|
for entry in entries:
|
|
752
784
|
if entry.get("type") != "event_msg":
|
|
753
785
|
continue
|
|
@@ -758,13 +790,114 @@ def extract_token_events(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
|
758
790
|
last_usage = info.get("last_token_usage")
|
|
759
791
|
if not isinstance(last_usage, dict) or not last_usage:
|
|
760
792
|
continue
|
|
793
|
+
line = entry.get("_line_number", 0)
|
|
794
|
+
boundary_turn = None
|
|
795
|
+
for start_line, turn in turn_starts:
|
|
796
|
+
if start_line > line:
|
|
797
|
+
break
|
|
798
|
+
boundary_turn = turn
|
|
761
799
|
events.append({
|
|
762
|
-
"
|
|
800
|
+
"kind": "usage",
|
|
801
|
+
"line": line,
|
|
763
802
|
"timestamp": entry.get("timestamp"),
|
|
764
803
|
"usage": last_usage,
|
|
765
804
|
"total_usage": info.get("total_token_usage") or {},
|
|
805
|
+
"turn": boundary_turn,
|
|
766
806
|
})
|
|
767
|
-
|
|
807
|
+
|
|
808
|
+
events.sort(key=lambda item: (item.get("line", -1),
|
|
809
|
+
{"record": 0, "tool": 1, "usage": 2}.get(item.get("kind"), 3)))
|
|
810
|
+
invocations: List[Dict[str, Any]] = []
|
|
811
|
+
current: Dict[str, Any] = {
|
|
812
|
+
"content_parts": [],
|
|
813
|
+
"reasoning_parts": [],
|
|
814
|
+
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
815
|
+
"tool_calls": [],
|
|
816
|
+
"turn": None,
|
|
817
|
+
"start_timestamp": None,
|
|
818
|
+
"end_timestamp": None,
|
|
819
|
+
"line": -1,
|
|
820
|
+
"usage": {},
|
|
821
|
+
"total_usage": {},
|
|
822
|
+
}
|
|
823
|
+
seen = {"content": set(), "reasoning": set()}
|
|
824
|
+
|
|
825
|
+
def append_text(category: str, value: Any):
|
|
826
|
+
if not isinstance(value, str):
|
|
827
|
+
return
|
|
828
|
+
text = value.strip()
|
|
829
|
+
if not text or text in seen[category]:
|
|
830
|
+
return
|
|
831
|
+
seen[category].add(text)
|
|
832
|
+
current[f"{category}_parts"].append(text)
|
|
833
|
+
|
|
834
|
+
def finish_current(boundary_line: int, boundary_ts: Optional[str],
|
|
835
|
+
usage: Optional[Dict[str, Any]] = None,
|
|
836
|
+
total_usage: Optional[Dict[str, Any]] = None):
|
|
837
|
+
has_payload = (
|
|
838
|
+
current["content_parts"] or current["reasoning_parts"]
|
|
839
|
+
or current["tool_calls"] or current["protected"].get("count")
|
|
840
|
+
or usage
|
|
841
|
+
)
|
|
842
|
+
if not has_payload:
|
|
843
|
+
return
|
|
844
|
+
current["line"] = boundary_line
|
|
845
|
+
current["end_timestamp"] = boundary_ts or current["end_timestamp"]
|
|
846
|
+
current["usage"] = usage or {}
|
|
847
|
+
current["total_usage"] = total_usage or {}
|
|
848
|
+
current["content"] = "\n".join(current.pop("content_parts"))
|
|
849
|
+
current["reasoning"] = "\n".join(current.pop("reasoning_parts"))
|
|
850
|
+
for tool_call in current["tool_calls"]:
|
|
851
|
+
tool_call["result"] = tool_results.get(tool_call.get("call_id"))
|
|
852
|
+
invocations.append(dict(current))
|
|
853
|
+
|
|
854
|
+
for event in events:
|
|
855
|
+
kind = event["kind"]
|
|
856
|
+
if kind == "usage":
|
|
857
|
+
if current["turn"] is None:
|
|
858
|
+
current["turn"] = event.get("turn")
|
|
859
|
+
if current["start_timestamp"] is None:
|
|
860
|
+
current["start_timestamp"] = event.get("timestamp")
|
|
861
|
+
finish_current(event["line"], event.get("timestamp"),
|
|
862
|
+
event.get("usage"), event.get("total_usage"))
|
|
863
|
+
current = {
|
|
864
|
+
"content_parts": [],
|
|
865
|
+
"reasoning_parts": [],
|
|
866
|
+
"protected": {"source": "", "count": 0, "bytes": 0},
|
|
867
|
+
"tool_calls": [],
|
|
868
|
+
"turn": None,
|
|
869
|
+
"start_timestamp": None,
|
|
870
|
+
"end_timestamp": None,
|
|
871
|
+
"line": -1,
|
|
872
|
+
"usage": {},
|
|
873
|
+
"total_usage": {},
|
|
874
|
+
}
|
|
875
|
+
seen = {"content": set(), "reasoning": set()}
|
|
876
|
+
continue
|
|
877
|
+
|
|
878
|
+
current["turn"] = current["turn"] or event.get("turn")
|
|
879
|
+
current["start_timestamp"] = current["start_timestamp"] or event.get("timestamp")
|
|
880
|
+
current["end_timestamp"] = event.get("timestamp") or current["end_timestamp"]
|
|
881
|
+
current["line"] = max(current["line"], event.get("line", -1))
|
|
882
|
+
if kind == "record":
|
|
883
|
+
record = event["record"]
|
|
884
|
+
append_text("content", record.get("content"))
|
|
885
|
+
append_text("reasoning", record.get("reasoning"))
|
|
886
|
+
protected = record.get("protected") or {}
|
|
887
|
+
if protected.get("count"):
|
|
888
|
+
current["protected"]["source"] = protected.get("source", "")
|
|
889
|
+
current["protected"]["count"] += protected.get("count", 0)
|
|
890
|
+
current["protected"]["bytes"] += protected.get("bytes", 0)
|
|
891
|
+
elif kind == "tool":
|
|
892
|
+
current["tool_calls"].append(dict(event["tool_call"]))
|
|
893
|
+
|
|
894
|
+
if is_terminal and (
|
|
895
|
+
current["content_parts"] or current["reasoning_parts"]
|
|
896
|
+
or current["tool_calls"] or current["protected"].get("count")
|
|
897
|
+
):
|
|
898
|
+
finish_current(current["line"], current["end_timestamp"])
|
|
899
|
+
|
|
900
|
+
return invocations
|
|
768
901
|
|
|
769
902
|
|
|
770
903
|
def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
@@ -923,6 +1056,7 @@ def group_messages_into_turns(entries: List[Dict[str, Any]]) -> List[Dict[str, A
|
|
|
923
1056
|
"input": args,
|
|
924
1057
|
"custom": item_type == "custom_tool_call",
|
|
925
1058
|
"_ts": payload.get("_ts"),
|
|
1059
|
+
"_line_number": entry.get("_line_number", -1),
|
|
926
1060
|
})
|
|
927
1061
|
pending_calls[call_id] = {"kind": "tool", "_start_ts": payload.get("_ts")}
|
|
928
1062
|
|
|
@@ -1481,14 +1615,15 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
|
|
|
1481
1615
|
return ctx
|
|
1482
1616
|
|
|
1483
1617
|
|
|
1484
|
-
# --- 实时
|
|
1618
|
+
# --- 实时 model invocation 上报 -------------------------------------------
|
|
1485
1619
|
#
|
|
1486
1620
|
# 与 claude-code 同架构:一个会话 = 一条稳定 trace。首次建 root(codex_request)立即 finish 并存
|
|
1487
|
-
# to_header() 到 state["rt_root_header"]
|
|
1488
|
-
#
|
|
1489
|
-
#
|
|
1621
|
+
# to_header() 到 state["rt_root_header"];之后按 token_count 边界把模型可见内容、推理、工具调用和
|
|
1622
|
+
# last_token_usage 合成 model_call span,工具执行作为其子 span。全局行号水位线
|
|
1623
|
+
# state["rt_last_invocation_line"] 保证重复 hook 不重发。
|
|
1490
1624
|
|
|
1491
|
-
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None,
|
|
1625
|
+
def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_override=None,
|
|
1626
|
+
is_terminal=False, model_invocations=None):
|
|
1492
1627
|
import cozeloop
|
|
1493
1628
|
from cozeloop.spec.tracespec import Runtime
|
|
1494
1629
|
token = get_fresh_token()
|
|
@@ -1562,197 +1697,162 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1562
1697
|
return None
|
|
1563
1698
|
hook_log(f"[rt] root created trace_id={getattr(root_ctx,'trace_id','?')}")
|
|
1564
1699
|
|
|
1565
|
-
#
|
|
1566
|
-
#
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1700
|
+
# One atomic model span per Codex invocation. Upgrade existing sessions
|
|
1701
|
+
# without replaying events already emitted by the pre-0.1.55 watermarks.
|
|
1702
|
+
legacy_line = max(state.get("rt_last_model_line", -1),
|
|
1703
|
+
state.get("rt_last_token_line", -1))
|
|
1704
|
+
last_invocation_line = state.get("rt_last_invocation_line", legacy_line)
|
|
1705
|
+
pending_invocations = [
|
|
1706
|
+
invocation for invocation in (model_invocations or [])
|
|
1707
|
+
if invocation.get("line", -1) > last_invocation_line
|
|
1708
|
+
]
|
|
1709
|
+
if pending_invocations and not is_terminal:
|
|
1710
|
+
since = time.time() - state.get("rt_last_upload_ts", 0)
|
|
1711
|
+
if since < REALTIME_BATCH_INTERVAL:
|
|
1712
|
+
hook_log(
|
|
1713
|
+
f"[rt] batch throttle {since:.1f}s<{REALTIME_BATCH_INTERVAL}s "
|
|
1714
|
+
f"defer {len(pending_invocations)} invocation(s)"
|
|
1715
|
+
)
|
|
1716
|
+
pending_invocations = []
|
|
1717
|
+
|
|
1718
|
+
invocation_sent = 0
|
|
1719
|
+
tool_sent = 0
|
|
1720
|
+
model_call_base = state.get(
|
|
1721
|
+
"rt_model_call_count", state.get("rt_last_tool", 0)
|
|
1722
|
+
)
|
|
1723
|
+
for invocation in pending_invocations:
|
|
1724
|
+
line = invocation.get("line", -1)
|
|
1725
|
+
turn = invocation.get("turn") or {}
|
|
1726
|
+
tool_calls = invocation.get("tool_calls") or []
|
|
1727
|
+
model_tool_calls = []
|
|
1728
|
+
for tool_call in tool_calls:
|
|
1729
|
+
model_tool_calls.append(ModelToolCall(
|
|
1730
|
+
id=tool_call.get("call_id", ""),
|
|
1731
|
+
type="function",
|
|
1732
|
+
function=ModelToolCallFunction(
|
|
1733
|
+
name=tool_call.get("name", ""),
|
|
1734
|
+
arguments=_json_text(tool_call.get("input", {})),
|
|
1735
|
+
),
|
|
1736
|
+
))
|
|
1737
|
+
|
|
1738
|
+
start_dt = _parse_ts_value(invocation.get("start_timestamp"))
|
|
1739
|
+
end_dt = _parse_ts_value(invocation.get("end_timestamp")) or start_dt
|
|
1587
1740
|
mspan = client.start_span(
|
|
1588
|
-
name=f"
|
|
1589
|
-
start_time=start_dt, child_of=root_ctx,
|
|
1741
|
+
name=f"model_call_{invocation_sent + model_call_base}",
|
|
1742
|
+
span_type="model", start_time=start_dt, child_of=root_ctx,
|
|
1590
1743
|
)
|
|
1591
1744
|
mspan.set_runtime(Runtime(library="codex-cli"))
|
|
1592
1745
|
try:
|
|
1593
1746
|
mspan.set_model_name(model_name)
|
|
1594
1747
|
except Exception:
|
|
1595
1748
|
pass
|
|
1596
|
-
if
|
|
1597
|
-
mspan
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1749
|
+
if start_dt is not None and end_dt is not None:
|
|
1750
|
+
_set_finish_time_safe(mspan, _max_dt(start_dt, end_dt))
|
|
1751
|
+
|
|
1752
|
+
input_messages = []
|
|
1753
|
+
for message in turn.get("input_messages", []):
|
|
1754
|
+
input_messages.append(_make_model_message(
|
|
1755
|
+
message.get("role", "user"), message.get("content", "")
|
|
1601
1756
|
))
|
|
1602
|
-
|
|
1603
|
-
|
|
1757
|
+
if not input_messages and turn.get("user_message_text"):
|
|
1758
|
+
input_messages.append(_make_model_message("user", turn["user_message_text"]))
|
|
1759
|
+
mspan.set_input(ModelInput(
|
|
1760
|
+
messages=input_messages,
|
|
1761
|
+
tools=[],
|
|
1762
|
+
tool_choice=ModelToolChoice(type="", function=None),
|
|
1763
|
+
))
|
|
1764
|
+
mspan.set_output(ModelOutput(choices=[ModelChoice(
|
|
1765
|
+
finish_reason="tool_calls" if model_tool_calls else "stop",
|
|
1604
1766
|
index=0,
|
|
1605
1767
|
message=ModelMessage(
|
|
1606
1768
|
role="assistant",
|
|
1607
|
-
content=content,
|
|
1608
|
-
reasoning_content=reasoning,
|
|
1769
|
+
content=invocation.get("content", ""),
|
|
1770
|
+
reasoning_content=invocation.get("reasoning", ""),
|
|
1609
1771
|
parts=[],
|
|
1610
1772
|
name="",
|
|
1611
|
-
tool_calls=
|
|
1773
|
+
tool_calls=model_tool_calls,
|
|
1612
1774
|
tool_call_id="",
|
|
1613
1775
|
metadata={},
|
|
1614
1776
|
),
|
|
1615
|
-
)
|
|
1616
|
-
|
|
1777
|
+
)]))
|
|
1778
|
+
|
|
1779
|
+
usage = invocation.get("usage") or {}
|
|
1780
|
+
in_tk = _usage_int(usage, "input_tokens")
|
|
1781
|
+
out_tk = _usage_int(usage, "output_tokens")
|
|
1782
|
+
if in_tk > 0:
|
|
1783
|
+
try:
|
|
1784
|
+
mspan.set_input_tokens(in_tk)
|
|
1785
|
+
except Exception:
|
|
1786
|
+
pass
|
|
1787
|
+
if out_tk > 0:
|
|
1788
|
+
try:
|
|
1789
|
+
mspan.set_output_tokens(out_tk)
|
|
1790
|
+
except Exception:
|
|
1791
|
+
pass
|
|
1792
|
+
|
|
1617
1793
|
tags = {
|
|
1618
|
-
"
|
|
1619
|
-
"
|
|
1794
|
+
"invocation_boundary_line": line,
|
|
1795
|
+
"token_source": "codex_token_count" if usage else "unavailable",
|
|
1796
|
+
"cached_input_tokens": _usage_int(usage, "cached_input_tokens"),
|
|
1797
|
+
"reasoning_output_tokens": _usage_int(usage, "reasoning_output_tokens"),
|
|
1798
|
+
"total_tokens": _usage_int(usage, "total_tokens"),
|
|
1620
1799
|
}
|
|
1621
|
-
|
|
1800
|
+
total = invocation.get("total_usage") or {}
|
|
1801
|
+
if total:
|
|
1802
|
+
tags.update({
|
|
1803
|
+
"total_input_tokens": _usage_int(total, "input_tokens"),
|
|
1804
|
+
"total_output_tokens": _usage_int(total, "output_tokens"),
|
|
1805
|
+
"total_cached_input_tokens": _usage_int(total, "cached_input_tokens"),
|
|
1806
|
+
"total_reasoning_output_tokens": _usage_int(total, "reasoning_output_tokens"),
|
|
1807
|
+
"total_all_tokens": _usage_int(total, "total_tokens"),
|
|
1808
|
+
})
|
|
1809
|
+
protected = invocation.get("protected") or {}
|
|
1810
|
+
if protected.get("count"):
|
|
1622
1811
|
tags.update({
|
|
1623
1812
|
"protected_reasoning_source": protected.get("source", ""),
|
|
1624
1813
|
"protected_reasoning_count": protected.get("count", 0),
|
|
1625
1814
|
"protected_reasoning_bytes": protected.get("bytes", 0),
|
|
1626
1815
|
})
|
|
1627
1816
|
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
|
-
|
|
1642
|
-
# 线性展开所有 tool_call(带 result 的才算完成),按全局序增量发
|
|
1643
|
-
flat = []
|
|
1644
|
-
for ti, turn in enumerate(turns):
|
|
1645
|
-
results = {r.get("call_id"): r for r in turn.get("tool_results", [])}
|
|
1646
|
-
for tc in turn.get("tool_calls", []):
|
|
1647
|
-
cid = tc.get("call_id")
|
|
1648
|
-
flat.append({"tc": tc, "result": results.get(cid), "turn_index": ti})
|
|
1649
|
-
# 只发已完成(有 result)的,且全局最后一个若无 result 则留到下次
|
|
1650
|
-
completed = []
|
|
1651
|
-
for idx, item in enumerate(flat):
|
|
1652
|
-
is_last = (idx == len(flat) - 1)
|
|
1653
|
-
if item["result"] is not None or (not is_last):
|
|
1654
|
-
completed.append(item)
|
|
1655
|
-
last_tool = state.get("rt_last_tool", 0)
|
|
1656
|
-
new_items = completed[last_tool:]
|
|
1657
|
-
# 批量节流:非终态时距上次上报<5s 则本次不发,攒到下次一起发。终态不节流。
|
|
1658
|
-
if new_items and not is_terminal:
|
|
1659
|
-
since = time.time() - state.get("rt_last_upload_ts", 0)
|
|
1660
|
-
if since < REALTIME_BATCH_INTERVAL:
|
|
1661
|
-
hook_log(f"[rt] batch throttle {since:.1f}s<{REALTIME_BATCH_INTERVAL}s defer {len(new_items)} tool(s)")
|
|
1662
|
-
new_items = []
|
|
1663
|
-
sent = 0
|
|
1664
|
-
for item in new_items:
|
|
1665
|
-
tc = item["tc"]; result = item["result"]
|
|
1666
|
-
gidx = last_tool + sent
|
|
1667
|
-
tname = tc.get("name", "unknown")
|
|
1668
|
-
start_dt = _parse_ts_value(tc.get("_ts"))
|
|
1669
|
-
end_dt = _parse_ts_value(result.get("_ts")) if result else None
|
|
1670
|
-
# 每个 tool_call 用一个 model_call_+tool 子树(与 claude-code 视觉一致)
|
|
1671
|
-
mspan = client.start_span(name=f"model_call_{gidx}", span_type="model",
|
|
1672
|
-
start_time=start_dt, child_of=root_ctx)
|
|
1673
|
-
mspan.set_runtime(Runtime(library="codex-cli"))
|
|
1674
|
-
try: mspan.set_model_name(model_name)
|
|
1675
|
-
except Exception: pass
|
|
1676
|
-
if start_dt and end_dt:
|
|
1677
|
-
_set_finish_time_safe(mspan, end_dt)
|
|
1678
|
-
mspan.set_input(truncate_text(_json_text(tc.get("input", {}))))
|
|
1679
1817
|
mspan_ctx = client.get_span_from_header(mspan.to_header())
|
|
1680
1818
|
mspan.finish()
|
|
1681
1819
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1820
|
+
for tool_call in tool_calls:
|
|
1821
|
+
result = tool_call.get("result")
|
|
1822
|
+
if result is None:
|
|
1823
|
+
continue
|
|
1824
|
+
tool_start_dt = _parse_ts_value(tool_call.get("_ts")) or start_dt
|
|
1825
|
+
tool_end_dt = _parse_ts_value(result.get("_ts")) or tool_start_dt
|
|
1826
|
+
tname = tool_call.get("name", "unknown")
|
|
1827
|
+
tspan = client.start_span(
|
|
1828
|
+
name=f"tool_{tname}", span_type="tool",
|
|
1829
|
+
start_time=tool_start_dt, child_of=mspan_ctx,
|
|
1830
|
+
)
|
|
1831
|
+
tspan.set_runtime(Runtime(library="codex-cli"))
|
|
1832
|
+
if tool_end_dt is not None:
|
|
1833
|
+
_set_finish_time_safe(tspan, _max_dt(tool_start_dt, tool_end_dt))
|
|
1834
|
+
tspan.set_tags({
|
|
1835
|
+
"tool_name": tname,
|
|
1836
|
+
"tool_call_id": tool_call.get("call_id"),
|
|
1837
|
+
"model_invocation_line": line,
|
|
1838
|
+
})
|
|
1839
|
+
tspan.set_input(truncate_text(_json_text(tool_call.get("input", {}))))
|
|
1690
1840
|
tspan.set_output(truncate_text(_json_text(result.get("output", ""))))
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
client.flush() # 每个 tool_call 立即 flush → 结束即可见
|
|
1694
|
-
if upload_events:
|
|
1695
|
-
hook_log(f"[rt] upload failed after tool flush detail={upload_events[-1][:500]}")
|
|
1696
|
-
return None
|
|
1697
|
-
|
|
1698
|
-
state["rt_last_tool"] = last_tool + sent
|
|
1699
|
-
if sent > 0:
|
|
1700
|
-
state["rt_last_upload_ts"] = time.time()
|
|
1841
|
+
tspan.finish()
|
|
1842
|
+
tool_sent += 1
|
|
1701
1843
|
|
|
1702
|
-
# --- token_count 独立增量, 与 tool span 同一 root 下, 按行号水位线幂等 ---
|
|
1703
|
-
# 每条 token_count 挂一个 model_usage_<line> span:
|
|
1704
|
-
# last_token_usage → set_input_tokens/set_output_tokens (SDK 官方求和字段)
|
|
1705
|
-
# total_token_usage → tags (只观察, 不参与后端聚合, 否则会重复累加)
|
|
1706
|
-
# 只在 flush 成功后推进 rt_last_token_line, 保证上报失败下次能重发.
|
|
1707
|
-
token_sent = 0
|
|
1708
|
-
last_token_line = state.get("rt_last_token_line", -1)
|
|
1709
|
-
pending_tokens = [ev for ev in (token_events or []) if ev["line"] > last_token_line]
|
|
1710
|
-
max_flushed_line = last_token_line
|
|
1711
|
-
for ev in pending_tokens:
|
|
1712
|
-
line = ev["line"]
|
|
1713
|
-
last = ev.get("usage") or {}
|
|
1714
|
-
total = ev.get("total_usage") or {}
|
|
1715
|
-
in_tk = _usage_int(last, "input_tokens")
|
|
1716
|
-
out_tk = _usage_int(last, "output_tokens")
|
|
1717
|
-
start_dt = _parse_ts_value(ev.get("timestamp"))
|
|
1718
|
-
tk_span = client.start_span(name=f"model_usage_{line}", span_type="model",
|
|
1719
|
-
start_time=start_dt, child_of=root_ctx)
|
|
1720
|
-
tk_span.set_runtime(Runtime(library="codex-cli"))
|
|
1721
|
-
try: tk_span.set_model_name(model_name)
|
|
1722
|
-
except Exception: pass
|
|
1723
|
-
if start_dt is not None:
|
|
1724
|
-
_set_finish_time_safe(tk_span, start_dt)
|
|
1725
|
-
if in_tk > 0:
|
|
1726
|
-
try: tk_span.set_input_tokens(in_tk)
|
|
1727
|
-
except Exception: pass
|
|
1728
|
-
if out_tk > 0:
|
|
1729
|
-
try: tk_span.set_output_tokens(out_tk)
|
|
1730
|
-
except Exception: pass
|
|
1731
|
-
tk_tags = {
|
|
1732
|
-
"token_event_line": line,
|
|
1733
|
-
"token_source": "codex_token_count",
|
|
1734
|
-
"cached_input_tokens": _usage_int(last, "cached_input_tokens"),
|
|
1735
|
-
"reasoning_output_tokens": _usage_int(last, "reasoning_output_tokens"),
|
|
1736
|
-
"total_tokens": _usage_int(last, "total_tokens"),
|
|
1737
|
-
}
|
|
1738
|
-
if total:
|
|
1739
|
-
tk_tags["total_input_tokens"] = _usage_int(total, "input_tokens")
|
|
1740
|
-
tk_tags["total_output_tokens"] = _usage_int(total, "output_tokens")
|
|
1741
|
-
tk_tags["total_cached_input_tokens"] = _usage_int(total, "cached_input_tokens")
|
|
1742
|
-
tk_tags["total_reasoning_output_tokens"] = _usage_int(total, "reasoning_output_tokens")
|
|
1743
|
-
tk_tags["total_all_tokens"] = _usage_int(total, "total_tokens")
|
|
1744
|
-
tk_span.set_tags(tk_tags)
|
|
1745
|
-
tk_span.finish()
|
|
1746
1844
|
client.flush()
|
|
1747
1845
|
if upload_events:
|
|
1748
|
-
hook_log(
|
|
1749
|
-
|
|
1750
|
-
|
|
1846
|
+
hook_log(
|
|
1847
|
+
f"[rt] upload failed after invocation flush line={line} "
|
|
1848
|
+
f"detail={upload_events[-1][:500]}"
|
|
1849
|
+
)
|
|
1751
1850
|
return None
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1851
|
+
state["rt_last_invocation_line"] = line
|
|
1852
|
+
invocation_sent += 1
|
|
1853
|
+
|
|
1854
|
+
if invocation_sent > 0:
|
|
1855
|
+
state["rt_model_call_count"] = model_call_base + invocation_sent
|
|
1756
1856
|
state["rt_last_upload_ts"] = time.time()
|
|
1757
1857
|
|
|
1758
1858
|
if is_terminal:
|
|
@@ -1774,13 +1874,17 @@ def send_steps_realtime(turns, session_id, state, model_name="codex", coze_tags_
|
|
|
1774
1874
|
if upload_events:
|
|
1775
1875
|
hook_log(f"[rt] upload failed after final flush detail={upload_events[-1][:500]}")
|
|
1776
1876
|
return None
|
|
1777
|
-
hook_log(f"[rt] finalized
|
|
1877
|
+
hook_log(f"[rt] finalized total_model_calls={state.get('rt_model_call_count', 0)}")
|
|
1778
1878
|
|
|
1779
1879
|
if upload_events:
|
|
1780
1880
|
hook_log(f"[rt] upload failed state not advanced detail={upload_events[-1][:500]}")
|
|
1781
1881
|
return None
|
|
1782
1882
|
|
|
1783
|
-
hook_log(
|
|
1883
|
+
hook_log(
|
|
1884
|
+
f"[rt] sent {invocation_sent} model invocation(s), {tool_sent} tool span(s), "
|
|
1885
|
+
f"last_invocation_line={state.get('rt_last_invocation_line', -1)}, "
|
|
1886
|
+
f"terminal={is_terminal}"
|
|
1887
|
+
)
|
|
1784
1888
|
return True
|
|
1785
1889
|
except Exception as e:
|
|
1786
1890
|
hook_log(f"[rt] error: {e}")
|
|
@@ -1855,8 +1959,7 @@ def main():
|
|
|
1855
1959
|
state_file = get_state_file_path(transcript_path)
|
|
1856
1960
|
state = load_state(state_file)
|
|
1857
1961
|
|
|
1858
|
-
#
|
|
1859
|
-
# state["rt_last_tool"] 增量,每次触发把新完成的 tool_call 发出,结束即可见,不节流。
|
|
1962
|
+
# 实时路径读取全量 entries 重建 invocation 序列,再用 token 边界行号水位线增量上报。
|
|
1860
1963
|
entries = read_rollout_messages(transcript_path, 0)
|
|
1861
1964
|
|
|
1862
1965
|
if not entries:
|
|
@@ -1898,9 +2001,13 @@ def main():
|
|
|
1898
2001
|
|
|
1899
2002
|
# Group entries into turns
|
|
1900
2003
|
turns = group_messages_into_turns(entries)
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2004
|
+
model_invocations = extract_model_invocations(
|
|
2005
|
+
turns, entries, is_terminal=is_terminal_event
|
|
2006
|
+
)
|
|
2007
|
+
debug_log(
|
|
2008
|
+
f"Grouped into {len(turns)} turns, "
|
|
2009
|
+
f"{len(model_invocations)} model invocations"
|
|
2010
|
+
)
|
|
1904
2011
|
|
|
1905
2012
|
# If this is a subagent, save data for parent to include later
|
|
1906
2013
|
if is_subagent:
|
|
@@ -1937,18 +2044,25 @@ def main():
|
|
|
1937
2044
|
break
|
|
1938
2045
|
coze_tags_override = state.get("coze_tags") or None
|
|
1939
2046
|
|
|
1940
|
-
# 方案 B
|
|
2047
|
+
# 方案 B 实时:按 token_count 边界把内容、推理、工具调用和 usage 合成 model_call。
|
|
1941
2048
|
ok = send_steps_realtime(turns, session_id, state, model_name=model_name,
|
|
1942
2049
|
coze_tags_override=coze_tags_override,
|
|
1943
2050
|
is_terminal=is_terminal_event,
|
|
1944
|
-
|
|
2051
|
+
model_invocations=model_invocations)
|
|
1945
2052
|
if ok is None:
|
|
1946
2053
|
hook_log(f"realtime send failed session_id={session_id}")
|
|
1947
2054
|
debug_log("Realtime send failed, state not advanced")
|
|
1948
2055
|
return
|
|
1949
2056
|
save_state(state_file, state)
|
|
1950
|
-
hook_log(
|
|
1951
|
-
|
|
2057
|
+
hook_log(
|
|
2058
|
+
f"rt state saved event={hook_event} "
|
|
2059
|
+
f"last_invocation_line={state.get('rt_last_invocation_line')} "
|
|
2060
|
+
f"session_id={session_id}"
|
|
2061
|
+
)
|
|
2062
|
+
debug_log(
|
|
2063
|
+
f"State saved. rt_last_invocation_line="
|
|
2064
|
+
f"{state.get('rt_last_invocation_line')}"
|
|
2065
|
+
)
|
|
1952
2066
|
else:
|
|
1953
2067
|
hook_log(f"skip no turns session_id={session_id}")
|
|
1954
2068
|
debug_log("No turns to send")
|