blob: d11e465df87484ba71ada676ecc3361599f123d5 [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import argparse
import base64
from datetime import datetime, timedelta, timezone
import json
import os
import secrets
import time
import urllib.error
import urllib.parse
import urllib.request
def read_text(path, max_chars, tail=False, optional=False):
try:
with open(path, "r", encoding="utf-8", errors="replace") as handle:
data = handle.read()
except FileNotFoundError:
if optional:
return ""
raise
return truncate_text(data, max_chars, tail)
def truncate_text(data, max_chars, tail=False):
if max_chars <= 0 or len(data) <= max_chars:
return data
if tail:
return f"[truncated to last {max_chars} chars]\n" + data[-max_chars:]
return data[:max_chars] + f"\n[truncated to first {max_chars} chars]"
def truncate_json(value, max_chars):
text = json.dumps(value, ensure_ascii=False, sort_keys=True)
if max_chars <= 0 or len(text) <= max_chars:
return value
return {"truncated_json": truncate_text(text, max_chars)}
def json_attr(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True)
def json_payload_bytes(value):
return len(
json.dumps(value, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
)
def load_jsonl(path):
events = []
with open(path, "r", encoding="utf-8", errors="replace") as handle:
for line_number, line in enumerate(handle, start=1):
stripped = line.strip()
if not stripped:
continue
try:
event = json.loads(stripped)
except json.JSONDecodeError as exc:
events.append(
{
"type": "error",
"message": f"invalid jsonl line {line_number}: {exc}",
}
)
continue
event["_line_number"] = line_number
events.append(event)
return events
def find_final_message(events, output_text):
if output_text.strip():
return output_text
for event in reversed(events):
item = event.get("item") or {}
if event.get("type") == "item.completed" and item.get("type") == "agent_message":
text = item.get("text") or ""
if text.strip():
return text
return ""
def latest_turn_result(events):
for event in reversed(events):
if event.get("type") == "turn.completed":
return "completed", event.get("usage") or {}
if event.get("type") == "turn.failed":
return "failed", event.get("error") or {}
return "unknown", {}
def event_context_payload(event, max_json_chars):
item = event.get("item") if isinstance(event.get("item"), dict) else None
payload = {
"event_type": event.get("type"),
"line_number": event.get("_line_number"),
}
if not item:
event_payload = {
key: value for key, value in event.items() if key != "_line_number"
}
payload["payload"] = truncate_json(event_payload, max_json_chars)
return payload
item_type = item.get("type", "unknown")
payload.update(
{
"item_id": item.get("id", ""),
"item_type": item_type,
"status": item.get("status"),
}
)
if item_type == "command_execution":
payload.update(
{
"command": item.get("command") or "",
"exit_code": item.get("exit_code"),
"aggregated_output": truncate_text(
item.get("aggregated_output") or "", max_json_chars, tail=True
),
}
)
elif item_type == "mcp_tool_call":
payload.update(
{
"server": item.get("server"),
"tool": item.get("tool"),
"arguments": truncate_json(item.get("arguments"), max_json_chars),
"result": truncate_json(item.get("result"), max_json_chars),
"error": truncate_json(item.get("error"), max_json_chars),
}
)
elif item_type == "collab_tool_call":
payload.update(
{
"tool": item.get("tool"),
"prompt": truncate_text(item.get("prompt") or "", max_json_chars),
"sender_thread_id": item.get("sender_thread_id"),
"receiver_thread_ids": item.get("receiver_thread_ids") or [],
"agents_states": truncate_json(item.get("agents_states"), max_json_chars),
}
)
elif item_type == "agent_message":
payload["text"] = truncate_text(item.get("text") or "", max_json_chars)
else:
payload["item"] = truncate_json(item, max_json_chars)
return {key: value for key, value in payload.items() if value not in (None, "")}
def build_agent_message_inputs(
events, turn_input, max_json_chars, max_context_json_chars
):
inputs = {}
previous_agent_message = None
context_events = []
for event in events:
item = event.get("item") if isinstance(event.get("item"), dict) else {}
is_agent_message = (
event.get("type") == "item.completed"
and item.get("type") == "agent_message"
)
if is_agent_message:
current_id = item.get("id") or f"line:{event.get('_line_number', 0)}"
input_payload = {
"item_type": "agent_message",
"context_window": {
"from": (
previous_agent_message.get("id")
if previous_agent_message
else "turn_start"
),
"to": current_id,
"event_count": len(context_events),
},
"events_since_previous_agent_message": [
event_context_payload(context_event, max_context_json_chars)
for context_event in context_events
],
}
if previous_agent_message:
input_payload["previous_agent_message"] = {
"id": previous_agent_message.get("id", ""),
"text": truncate_text(
previous_agent_message.get("text") or "", max_json_chars
),
}
else:
input_payload["turn_input"] = truncate_text(turn_input, max_json_chars)
inputs[event.get("_line_number")] = input_payload
previous_agent_message = item
context_events = []
else:
context_events.append(event)
return inputs
def observation_shape(item, max_json_chars, agent_message_input=None):
item_type = item.get("type", "unknown")
status = item.get("status")
if item_type == "command_execution":
command = item.get("command") or ""
return {
"name": "codex.command",
"type": "span",
"input": {"command": command},
"output": {
"status": status,
"exit_code": item.get("exit_code"),
"aggregated_output": truncate_text(
item.get("aggregated_output") or "", max_json_chars, tail=True
),
},
"metadata": {"command": command[:240], "status": status, "step_kind": "tool"},
}
if item_type == "mcp_tool_call":
return {
"name": f"codex.mcp.{item.get('server', 'unknown')}.{item.get('tool', 'unknown')}",
"type": "span",
"input": {
"server": item.get("server"),
"tool": item.get("tool"),
"arguments": truncate_json(item.get("arguments"), max_json_chars),
},
"output": {
"status": status,
"result": truncate_json(item.get("result"), max_json_chars),
"error": truncate_json(item.get("error"), max_json_chars),
},
"metadata": {
"server": item.get("server"),
"tool": item.get("tool"),
"status": status,
"step_kind": "tool",
},
}
if item_type == "collab_tool_call":
return {
"name": f"codex.collab.{item.get('tool', 'unknown')}",
"type": "span",
"input": {
"tool": item.get("tool"),
"prompt": truncate_text(item.get("prompt") or "", max_json_chars),
"sender_thread_id": item.get("sender_thread_id"),
},
"output": {
"status": status,
"receiver_thread_ids": item.get("receiver_thread_ids") or [],
"agents_states": truncate_json(item.get("agents_states"), max_json_chars),
},
"metadata": {"tool": item.get("tool"), "status": status, "step_kind": "tool"},
}
if item_type == "web_search":
return {
"name": "codex.web_search",
"type": "span",
"input": {"query": item.get("query"), "action": item.get("action")},
"output": {"status": "completed", "query": item.get("query")},
"metadata": {"query": (item.get("query") or "")[:240], "step_kind": "tool"},
}
if item_type == "file_change":
return {
"name": "codex.file_change",
"type": "span",
"input": {"changes": item.get("changes") or []},
"output": {"status": status, "changes": item.get("changes") or []},
"metadata": {"status": status},
}
if item_type == "todo_list":
return {
"name": "codex.todo_list",
"type": "span",
"input": {"item_type": item_type},
"output": {"items": item.get("items") or []},
"metadata": {"item_count": len(item.get("items") or [])},
}
if item_type == "reasoning":
return {
"name": "codex.reasoning",
"type": "span",
"input": {"item_type": item_type},
"output": {"text": truncate_text(item.get("text") or "", max_json_chars)},
"metadata": {},
}
if item_type == "agent_message":
return {
"name": "codex.agent_message",
"type": "generation",
"input": agent_message_input or {"item_type": item_type},
"output": {"text": truncate_text(item.get("text") or "", max_json_chars)},
"metadata": {},
}
if item_type == "error":
return {
"name": "codex.error",
"type": "span",
"input": {"item_type": item_type},
"output": {"message": item.get("message") or ""},
"metadata": {},
}
return {
"name": f"codex.{item_type}",
"type": "span",
"input": {"item_type": item_type},
"output": truncate_json(item, max_json_chars),
"metadata": {"item_type": item_type},
}
def iso_from_ns(ns):
return (
datetime.fromtimestamp(ns / 1_000_000_000, timezone.utc)
.isoformat(timespec="milliseconds")
.replace("+00:00", "Z")
)
def ingestion_event(event_type, timestamp, body):
return {
"id": secrets.token_hex(16),
"timestamp": timestamp,
"type": event_type,
"body": body,
}
def build_ingestion_payload(args, input_text, output_text, events):
now = time.time_ns()
trace_id = secrets.token_hex(16)
root_observation_id = secrets.token_hex(16)
turn_status, turn_payload = latest_turn_result(events)
final_message = find_final_message(events, output_text)
trace_output = final_message or json_attr({"turn_status": turn_status})
trace_metadata = {
"repository": args.repository,
"workflow": args.workflow,
"run_id": args.run_id,
"pr_number": args.pr_number,
"head_sha": args.head_sha,
"base_sha": args.base_sha,
"model_reasoning_effort": args.reasoning_effort,
"codex_jsonl": True,
}
trace_metadata = {
key: value for key, value in trace_metadata.items() if value not in (None, "")
}
completed_items = [
event
for event in events
if event.get("type") == "item.completed" and isinstance(event.get("item"), dict)
]
agent_message_inputs = build_agent_message_inputs(
events, input_text, args.max_json_chars, args.max_context_json_chars
)
child_count = len(completed_items) + 1
root_end = now + (child_count + 2) * 1_000_000
timestamp = iso_from_ns(now)
batch = [
ingestion_event(
"trace-create",
timestamp,
{
"id": trace_id,
"timestamp": timestamp,
"name": args.trace_name,
"input": input_text,
"output": trace_output,
"sessionId": args.session_id,
"environment": args.environment,
"metadata": trace_metadata,
"tags": ["doris-ai-review", "codex-jsonl"],
},
),
ingestion_event(
"span-create",
timestamp,
{
"id": root_observation_id,
"traceId": trace_id,
"name": "codex.review",
"startTime": iso_from_ns(now),
"endTime": iso_from_ns(root_end),
"input": {"prompt": input_text},
"output": {"final_message": trace_output, "turn_status": turn_status},
"environment": args.environment,
"metadata": {
**trace_metadata,
"codex_event_count": len(events),
"codex_completed_item_count": len(completed_items),
},
},
),
]
turn_body = {
"id": secrets.token_hex(16),
"traceId": trace_id,
"parentObservationId": root_observation_id,
"name": "codex.turn",
"startTime": iso_from_ns(now + 1_000_000),
"endTime": iso_from_ns(now + 2_000_000),
"model": args.model,
"input": {"prompt": input_text},
"output": {"status": turn_status, "final_message": trace_output},
"environment": args.environment,
"metadata": {**trace_metadata, "item_type": "turn"},
"level": "ERROR" if turn_status == "failed" else "DEFAULT",
}
if turn_status == "completed":
turn_body["usageDetails"] = {
"input": int(turn_payload.get("input_tokens") or 0),
"output": int(turn_payload.get("output_tokens") or 0),
"cache_read_input_tokens": int(turn_payload.get("cached_input_tokens") or 0),
"reasoning_output_tokens": int(
turn_payload.get("reasoning_output_tokens") or 0
),
}
else:
turn_body["statusMessage"] = json_attr(turn_payload)
batch.append(ingestion_event("generation-create", iso_from_ns(now + 1_000_000), turn_body))
for offset, event in enumerate(completed_items, start=2):
item = event["item"]
shape = observation_shape(
item, args.max_json_chars, agent_message_inputs.get(event.get("_line_number"))
)
item_type = item.get("type", "unknown")
item_status = item.get("status", "completed")
body = {
"id": secrets.token_hex(16),
"traceId": trace_id,
"parentObservationId": root_observation_id,
"name": shape["name"],
"startTime": iso_from_ns(now + offset * 1_000_000),
"endTime": iso_from_ns(now + offset * 1_000_000 + 750_000),
"input": shape["input"],
"output": shape["output"],
"environment": args.environment,
"level": "ERROR" if item_status in ("failed", "declined") else "DEFAULT",
"metadata": {
**trace_metadata,
"item_id": item.get("id", ""),
"item_type": item_type,
"event_line": event.get("_line_number", 0),
},
}
for key, value in shape.get("metadata", {}).items():
if value not in (None, ""):
body["metadata"][key] = value
event_type = "generation-create" if shape["type"] == "generation" else "span-create"
if shape["type"] == "generation":
body["model"] = args.model
batch.append(ingestion_event(event_type, body["startTime"], body))
return trace_id, {"batch": batch}, len(batch) - 1
def main_trace_metadata(args):
trace_metadata = {
"repository": args.repository,
"workflow": args.workflow,
"run_id": args.run_id,
"pr_number": args.pr_number,
"head_sha": args.head_sha,
"base_sha": args.base_sha,
"model_reasoning_effort": args.reasoning_effort,
}
return {
key: value for key, value in trace_metadata.items() if value not in (None, "")
}
def receiver_thread_ids(events):
thread_ids = set()
for event in events:
item = event.get("item") if isinstance(event.get("item"), dict) else {}
if item.get("type") != "collab_tool_call":
continue
for thread_id in item.get("receiver_thread_ids") or []:
if thread_id:
thread_ids.add(str(thread_id))
return thread_ids
def session_jsonl_files(root):
if not root or not os.path.isdir(root):
return []
paths = []
for dirpath, _dirnames, filenames in os.walk(root):
for filename in filenames:
if filename.endswith(".jsonl"):
paths.append(os.path.join(dirpath, filename))
return sorted(paths)
def session_meta(events):
for event in events:
if event.get("type") != "session_meta":
continue
payload = event.get("payload")
if isinstance(payload, dict):
return payload
return {}
def subagent_spawn(meta):
source = meta.get("source") if isinstance(meta, dict) else {}
source = source if isinstance(source, dict) else {}
subagent = source.get("subagent")
subagent = subagent if isinstance(subagent, dict) else {}
spawn = subagent.get("thread_spawn")
return spawn if isinstance(spawn, dict) else {}
def is_subagent_meta(meta, expected_thread_ids):
if not isinstance(meta, dict):
return False
thread_id = str(meta.get("id") or "")
if expected_thread_ids:
return thread_id in expected_thread_ids
if meta.get("thread_source") == "subagent":
return True
source = meta.get("source")
return isinstance(source, dict) and bool(source.get("subagent"))
def compact_session_meta(meta, max_json_chars):
compact = {}
for key in (
"id",
"parent_thread_id",
"timestamp",
"cwd",
"originator",
"cli_version",
"thread_source",
"agent_nickname",
"agent_role",
"model_provider",
"model",
):
if meta.get(key) not in (None, ""):
compact[key] = meta.get(key)
for key in ("base_instructions", "user_instructions"):
if key in meta:
compact[f"{key}_present"] = True
source = meta.get("source")
if isinstance(source, dict):
compact["source"] = truncate_json(source, max_json_chars)
return compact
def session_content_text(content, max_chars):
if isinstance(content, str):
return truncate_text(content, max_chars)
if not isinstance(content, list):
return truncate_json(content, max_chars)
parts = []
for item in content:
if isinstance(item, str):
parts.append(item)
continue
if not isinstance(item, dict):
parts.append(json_attr(item))
continue
for key in ("text", "input_text", "output_text"):
if isinstance(item.get(key), str):
parts.append(item[key])
break
else:
parts.append(json_attr(item))
return truncate_text("\n".join(parts), max_chars)
def session_first_user_message(events, max_chars):
for event in events:
if event.get("type") != "response_item":
continue
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
if payload.get("type") == "message" and payload.get("role") == "user":
text = session_content_text(payload.get("content"), max_chars)
if isinstance(text, str) and text.strip():
return text
return ""
def session_final_assistant_message(events, max_chars):
for event in reversed(events):
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
if event.get("type") == "response_item":
if payload.get("type") == "message" and payload.get("role") == "assistant":
text = session_content_text(payload.get("content"), max_chars)
if isinstance(text, str) and text.strip():
return text
elif event.get("type") == "event_msg" and payload.get("type") == "agent_message":
text = payload.get("message") or ""
if text.strip():
return truncate_text(text, max_chars)
return ""
def safe_name_component(value):
text = str(value or "unknown")
safe = "".join(
char if char.isalnum() or char in "._-" else "_" for char in text
).strip("._-")
return safe[:120] or "unknown"
def jsonish(value):
if not isinstance(value, str):
return value
try:
return json.loads(value)
except json.JSONDecodeError:
return value
def session_call_outputs(events):
outputs = {}
for event in events:
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
if payload.get("type") != "function_call_output":
continue
call_id = payload.get("call_id")
if call_id and call_id not in outputs:
outputs[call_id] = (payload, event)
return outputs
def session_event_timestamp(event, fallback_ns):
timestamp = event.get("timestamp")
if isinstance(timestamp, str) and timestamp:
return timestamp
return iso_from_ns(fallback_ns)
def ns_from_iso(timestamp):
if not isinstance(timestamp, str) or not timestamp:
return None
try:
normalized = timestamp.replace("Z", "+00:00")
return int(datetime.fromisoformat(normalized).timestamp() * 1_000_000_000)
except ValueError:
return None
def session_observation_shape(event, call_outputs, max_json_chars):
event_type = event.get("type") or "unknown"
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
if event_type == "session_meta":
compact_meta = compact_session_meta(payload, max_json_chars)
return {
"name": "codex.subagent.session_meta",
"type": "span",
"input": compact_meta,
"output": {
"thread_source": compact_meta.get("thread_source"),
"status": "recorded",
},
"metadata": {"session_event_type": event_type},
}
if event_type == "turn_context":
return {
"name": "codex.subagent.turn_context",
"type": "span",
"input": truncate_json(payload, max_json_chars),
"output": {"status": "recorded"},
"metadata": {"session_event_type": event_type},
}
if event_type == "event_msg":
message_type = payload.get("type") or "unknown"
message = payload.get("message") or ""
body = {
key: value
for key, value in payload.items()
if key not in ("message", "text_elements", "images", "local_images")
}
if payload.get("text_elements"):
body["text_elements"] = truncate_json(
payload.get("text_elements"), max_json_chars
)
if payload.get("images"):
body["images"] = truncate_json(payload.get("images"), max_json_chars)
return {
"name": f"codex.subagent.event.{safe_name_component(message_type)}",
"type": "generation" if message_type == "agent_message" else "span",
"input": {
"session_event_type": event_type,
"message_type": message_type,
"payload": truncate_json(body, max_json_chars),
},
"output": {
"message": truncate_text(message, max_json_chars),
"status": "recorded",
},
"metadata": {
"session_event_type": event_type,
"message_type": message_type,
},
}
if event_type != "response_item":
return {
"name": f"codex.subagent.{safe_name_component(event_type)}",
"type": "span",
"input": {"session_event_type": event_type},
"output": truncate_json(payload, max_json_chars),
"metadata": {"session_event_type": event_type},
}
item_type = payload.get("type") or "unknown"
if item_type == "message":
role = payload.get("role") or "unknown"
text = session_content_text(payload.get("content"), max_json_chars)
is_assistant = role == "assistant"
return {
"name": f"codex.subagent.message.{safe_name_component(role)}",
"type": "generation" if is_assistant else "span",
"input": {
"session_event_type": event_type,
"item_type": item_type,
"role": role,
"phase": payload.get("phase"),
**({} if is_assistant else {"content": text}),
},
"output": (
{"text": text}
if is_assistant
else {"status": "recorded", "role": role}
),
"metadata": {
"session_event_type": event_type,
"item_type": item_type,
"role": role,
"phase": payload.get("phase"),
},
}
if item_type == "function_call":
call_id = payload.get("call_id")
output_payload, output_event = call_outputs.get(call_id, ({}, {}))
return {
"name": f"codex.subagent.tool.{safe_name_component(payload.get('name'))}",
"type": "span",
"input": {
"name": payload.get("name"),
"call_id": call_id,
"arguments": truncate_json(
jsonish(payload.get("arguments")), max_json_chars
),
},
"output": {
"call_id": call_id,
"output": truncate_json(
jsonish(output_payload.get("output")), max_json_chars
),
"status": "completed" if output_payload else "unknown",
},
"metadata": {
"session_event_type": event_type,
"item_type": item_type,
"tool_name": payload.get("name"),
"call_id": call_id,
"output_line": output_event.get("_line_number"),
},
}
if item_type == "function_call_output":
return {
"name": "codex.subagent.tool_output",
"type": "span",
"input": {"call_id": payload.get("call_id")},
"output": truncate_json(jsonish(payload.get("output")), max_json_chars),
"metadata": {
"session_event_type": event_type,
"item_type": item_type,
"call_id": payload.get("call_id"),
},
}
if item_type == "reasoning":
encrypted_content_present = bool(payload.get("encrypted_content"))
return {
"name": "codex.subagent.reasoning",
"type": "span",
"input": {
"session_event_type": event_type,
"item_type": item_type,
"encrypted_content_present": encrypted_content_present,
},
"output": {
"summary": truncate_json(payload.get("summary"), max_json_chars),
"encrypted_content_present": encrypted_content_present,
"status": "recorded",
},
"metadata": {"session_event_type": event_type, "item_type": item_type},
}
return {
"name": f"codex.subagent.response_item.{safe_name_component(item_type)}",
"type": "span",
"input": {"session_event_type": event_type, "item_type": item_type},
"output": truncate_json(payload, max_json_chars),
"metadata": {"session_event_type": event_type, "item_type": item_type},
}
def build_subagent_session_payload(args, session_path, session_events):
now = time.time_ns()
meta = session_meta(session_events)
spawn = subagent_spawn(meta)
thread_id = str(meta.get("id") or os.path.basename(session_path))
parent_thread_id = (
spawn.get("parent_thread_id") or meta.get("parent_thread_id") or ""
)
agent_nickname = meta.get("agent_nickname") or spawn.get("agent_nickname") or ""
agent_role = meta.get("agent_role") or spawn.get("agent_role") or ""
trace_id = secrets.token_hex(16)
root_observation_id = secrets.token_hex(16)
trace_input = session_first_user_message(session_events, args.max_input_chars)
if not trace_input:
trace_input = json_attr(compact_session_meta(meta, args.max_json_chars))
trace_output = session_final_assistant_message(session_events, args.max_output_chars)
if not trace_output:
trace_output = json_attr({"session_status": "recorded"})
trace_metadata = {
**main_trace_metadata(args),
"codex_session_jsonl": True,
"subagent_session": True,
"main_session_id": args.session_id,
"thread_id": thread_id,
"parent_thread_id": parent_thread_id,
"agent_nickname": agent_nickname,
"agent_role": agent_role,
"session_file": session_path,
"session_event_count": len(session_events),
}
trace_metadata = {
key: value for key, value in trace_metadata.items() if value not in (None, "")
}
trace_session_id = f"{args.session_id}:subagent:{thread_id}"
first_timestamp = (
session_event_timestamp(session_events[0], now)
if session_events
else iso_from_ns(now)
)
base_ns = ns_from_iso(first_timestamp) or now
event_start_nses = [
ns_from_iso(event.get("timestamp")) or base_ns + offset * 1_000_000
for offset, event in enumerate(session_events, start=2)
]
latest_event_start_ns = max(event_start_nses) if event_start_nses else base_ns
root_end = iso_from_ns(latest_event_start_ns + 1_000_000)
call_outputs = session_call_outputs(session_events)
function_call_ids = {
(event.get("payload") or {}).get("call_id")
for event in session_events
if event.get("type") == "response_item"
and isinstance(event.get("payload"), dict)
and event["payload"].get("type") == "function_call"
and event["payload"].get("call_id")
}
batch = [
ingestion_event(
"trace-create",
first_timestamp,
{
"id": trace_id,
"timestamp": first_timestamp,
"name": args.subagent_trace_name,
"input": trace_input,
"output": trace_output,
"sessionId": trace_session_id,
"environment": args.environment,
"metadata": trace_metadata,
"tags": ["doris-ai-review-subagent", "codex-session-jsonl"],
},
),
ingestion_event(
"span-create",
first_timestamp,
{
"id": root_observation_id,
"traceId": trace_id,
"name": "codex.subagent.review",
"startTime": first_timestamp,
"endTime": root_end,
"input": {"session_file": session_path, "thread_id": thread_id},
"output": {"final_message": trace_output},
"environment": args.environment,
"metadata": trace_metadata,
},
),
]
observation_count = 1
for offset, event in enumerate(session_events, start=2):
payload = event.get("payload") if isinstance(event.get("payload"), dict) else {}
if (
event.get("type") == "response_item"
and payload.get("type") == "function_call_output"
and payload.get("call_id") in function_call_ids
):
continue
shape = session_observation_shape(event, call_outputs, args.max_json_chars)
fallback_start_ns = base_ns + offset * 1_000_000
start_time = session_event_timestamp(event, fallback_start_ns)
end_ns = (ns_from_iso(start_time) or fallback_start_ns) + 750_000
body = {
"id": secrets.token_hex(16),
"traceId": trace_id,
"parentObservationId": root_observation_id,
"name": shape["name"],
"startTime": start_time,
"endTime": iso_from_ns(end_ns),
"input": shape["input"],
"output": shape["output"],
"environment": args.environment,
"metadata": {
**trace_metadata,
"session_event_type": event.get("type"),
"session_line": event.get("_line_number"),
},
}
for key, value in shape.get("metadata", {}).items():
if value not in (None, ""):
body["metadata"][key] = value
event_type = "generation-create" if shape["type"] == "generation" else "span-create"
if shape["type"] == "generation":
body["model"] = args.model
batch.append(ingestion_event(event_type, start_time, body))
observation_count += 1
return {
"trace_id": trace_id,
"session_id": trace_session_id,
"thread_id": thread_id,
"path": session_path,
"event_count": len(session_events),
"observation_count": observation_count,
"payload": {"batch": batch},
}
def build_subagent_session_payloads(args, main_events):
expected_thread_ids = receiver_thread_ids(main_events)
payloads = []
for path in session_jsonl_files(args.subagent_sessions_dir):
events = load_jsonl(path)
meta = session_meta(events)
if not is_subagent_meta(meta, expected_thread_ids):
continue
payloads.append(build_subagent_session_payload(args, path, events))
if len(payloads) >= args.max_subagent_sessions:
break
return payloads
def compact_json_bytes(payload):
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
def compact_context_event(event, max_chars):
compact = {}
for key in (
"event_type",
"line_number",
"item_id",
"item_type",
"status",
"server",
"tool",
"exit_code",
):
if event.get(key) not in (None, ""):
compact[key] = event.get(key)
if event.get("command"):
compact["command"] = truncate_text(event.get("command") or "", max_chars)
for key in ("arguments", "result", "error", "payload", "item"):
if key in event:
compact[key] = truncate_json(event.get(key), max_chars)
for key in ("aggregated_output", "text", "prompt"):
if event.get(key):
compact[key] = truncate_text(event.get(key) or "", max_chars, tail=True)
return compact
def shrink_event_for_payload(event, max_payload_bytes):
shrunk = json.loads(json.dumps(event, ensure_ascii=False))
body = shrunk.get("body") if isinstance(shrunk.get("body"), dict) else {}
event_name = body.get("name")
def candidate_with_limits(max_chars, max_context_events=None):
candidate = json.loads(json.dumps(shrunk, ensure_ascii=False))
candidate_body = candidate.get("body") if isinstance(candidate.get("body"), dict) else {}
input_object = candidate_body.get("input")
if isinstance(input_object, dict):
context_events = input_object.get("events_since_previous_agent_message")
if isinstance(context_events, list):
compact_events = [
compact_context_event(context_event, max_chars)
for context_event in context_events
if isinstance(context_event, dict)
]
if max_context_events is not None:
omitted_count = max(len(compact_events) - max_context_events, 0)
if max_context_events <= 0:
compact_events = []
else:
compact_events = compact_events[-max_context_events:]
if omitted_count:
input_object[
"events_since_previous_agent_message_omitted_count"
] = omitted_count
input_object["events_since_previous_agent_message"] = compact_events
if isinstance(input_object.get("previous_agent_message"), dict):
previous_message = input_object["previous_agent_message"]
previous_message["text"] = truncate_text(
previous_message.get("text") or "", max_chars
)
if input_object.get("turn_input"):
input_object["turn_input"] = truncate_text(
input_object.get("turn_input") or "", max_chars
)
if not (
isinstance(context_events, list)
or "previous_agent_message" in input_object
or "turn_input" in input_object
):
candidate_body["input"] = truncate_json(input_object, max_chars)
elif input_object not in (None, ""):
candidate_body["input"] = truncate_json(input_object, max_chars)
output_object = candidate_body.get("output")
if output_object not in (None, ""):
candidate_body["output"] = truncate_json(output_object, max_chars)
metadata = candidate_body.get("metadata")
if metadata not in (None, ""):
candidate_body["metadata"] = truncate_json(metadata, max_chars)
return candidate
for max_chars in (2_000, 1_000, 500, 200, 80):
candidate = candidate_with_limits(max_chars)
if json_payload_bytes({"batch": [candidate]}) <= max_payload_bytes:
return candidate
for max_context_events in (50, 20, 10, 5, 2, 1, 0):
for max_chars in (80, 40, 20, 10):
candidate = candidate_with_limits(max_chars, max_context_events)
if json_payload_bytes({"batch": [candidate]}) <= max_payload_bytes:
return candidate
raise RuntimeError(
"Litefuse ingestion event is too large after truncation: "
f"{json_payload_bytes({'batch': [event]})} bytes > {max_payload_bytes} bytes; "
f"type={event.get('type')}, name={event_name}"
)
def chunk_payload(payload, max_payload_bytes):
batch = payload.get("batch") or []
chunks = []
current = []
current_bytes = json_payload_bytes({"batch": current})
for event in batch:
event_payload = {"batch": [event]}
event_bytes = json_payload_bytes(event_payload)
if event_bytes > max_payload_bytes:
event = shrink_event_for_payload(event, max_payload_bytes)
event_payload = {"batch": [event]}
event_bytes = json_payload_bytes(event_payload)
candidate = {"batch": current + [event]}
candidate_bytes = json_payload_bytes(candidate)
if current and candidate_bytes > max_payload_bytes:
chunks.append(({"batch": current}, current_bytes))
current = [event]
current_bytes = event_bytes
else:
current.append(event)
current_bytes = candidate_bytes
if current:
chunks.append(({"batch": current}, current_bytes))
return chunks
def post_payload_once(endpoint, public_key, secret_key, payload, timeout_seconds):
auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
request = urllib.request.Request(
endpoint,
data=compact_json_bytes(payload),
headers={
"Content-Type": "application/json",
"Authorization": f"Basic {auth}",
},
method="POST",
)
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
body = response.read().decode()
detail = json.loads(body) if body else {}
errors = detail.get("errors") if isinstance(detail, dict) else None
if errors:
raise RuntimeError(f"Litefuse ingestion returned errors: {json_attr(errors)}")
return {
"status": response.status,
"success_count": len(detail.get("successes") or [])
if isinstance(detail, dict)
else 0,
}
def retry_payload_chunks_after_413(payload, request_size, max_payload_bytes):
batch = payload.get("batch") or []
if not batch:
raise RuntimeError(
"Litefuse ingestion returned 413 for an empty payload chunk"
)
next_limit = max(1_000, min(max_payload_bytes - 1, request_size // 2))
if len(batch) == 1:
event = shrink_event_for_payload(batch[0], next_limit)
return [({"batch": [event]}, json_payload_bytes({"batch": [event]}))]
return chunk_payload(payload, next_limit)
def retry_payload_chunks_after_transport_error(payload, request_size, max_payload_bytes):
batch = payload.get("batch") or []
if len(batch) <= 1:
return [(payload, request_size)]
next_limit = max(1_000, min(max_payload_bytes - 1, request_size // 2))
return chunk_payload(payload, next_limit)
def post_payload(
endpoint,
public_key,
secret_key,
payload,
max_payload_bytes,
timeout_seconds,
retry_attempts,
retry_sleep_seconds,
):
statuses = []
success_count = 0
request_sizes = []
payload_too_large_retry_count = 0
transport_retry_count = 0
chunks = chunk_payload(payload, max_payload_bytes)
while chunks:
chunk, request_size = chunks.pop(0)
try:
status = post_payload_once(
endpoint, public_key, secret_key, chunk, timeout_seconds
)
except urllib.error.HTTPError as exc:
if exc.code != 413:
raise
payload_too_large_retry_count += 1
chunks = (
retry_payload_chunks_after_413(
chunk, request_size, max_payload_bytes
)
+ chunks
)
continue
except (TimeoutError, urllib.error.URLError) as exc:
transport_retry_count += 1
if transport_retry_count > retry_attempts:
raise RuntimeError(
"Litefuse ingestion failed after transport retries: "
f"{type(exc).__name__}: {exc}"
) from exc
time.sleep(retry_sleep_seconds)
chunks = (
retry_payload_chunks_after_transport_error(
chunk, request_size, max_payload_bytes
)
+ chunks
)
continue
statuses.append(status["status"])
success_count += int(status.get("success_count") or 0)
request_sizes.append(request_size)
return {
"statuses": statuses,
"request_count": len(statuses),
"request_sizes": request_sizes,
"max_request_size": max(request_sizes) if request_sizes else 0,
"payload_too_large_retries": payload_too_large_retry_count,
"transport_retries": transport_retry_count,
"success_count": success_count,
}
def fetch_trace(base_url, public_key, secret_key, trace_id):
auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
request = urllib.request.Request(
f"{base_url.rstrip('/')}/api/public/traces/{trace_id}",
headers={"Authorization": f"Basic {auth}"},
method="GET",
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode())
def fetch_observations_v2(base_url, public_key, secret_key, trace_id):
auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
now = datetime.now(timezone.utc)
params = urllib.parse.urlencode(
{
"traceId": trace_id,
"fromStartTime": (now - timedelta(hours=1)).isoformat().replace("+00:00", "Z"),
"toStartTime": (now + timedelta(minutes=5)).isoformat().replace("+00:00", "Z"),
"fields": "core,basic,io,trace_context,model,usage",
"limit": "100",
}
)
request = urllib.request.Request(
f"{base_url.rstrip('/')}/api/public/v2/observations?{params}",
headers={"Authorization": f"Basic {auth}"},
method="GET",
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.loads(response.read().decode())
def fetch_observations_legacy(
base_url, public_key, secret_key, trace_id, max_pages=10
):
auth = base64.b64encode(f"{public_key}:{secret_key}".encode()).decode()
limit = 100
rows = []
last_payload = {}
for page in range(1, max_pages + 1):
params = urllib.parse.urlencode(
{"traceId": trace_id, "limit": str(limit), "page": str(page)}
)
request = urllib.request.Request(
f"{base_url.rstrip('/')}/api/public/observations?{params}",
headers={"Authorization": f"Basic {auth}"},
method="GET",
)
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.loads(response.read().decode())
last_payload = payload if isinstance(payload, dict) else {}
page_rows = observation_rows_from_v2(last_payload)
rows.extend(page_rows)
if len(page_rows) < limit:
break
return {**last_payload, "data": rows}
def observation_rows_from_v2(payload):
rows = payload.get("data") if isinstance(payload, dict) else None
if isinstance(rows, list):
return rows
return []
def observation_io_object(observation, field):
value = observation.get(field)
if isinstance(value, dict):
return value
if isinstance(value, str):
try:
parsed = json.loads(value)
except json.JSONDecodeError:
return {}
if isinstance(parsed, dict):
return parsed
return {}
def context_event_types(events_value):
if isinstance(events_value, list):
return [
event.get("item_type") or event.get("event_type")
for event in events_value
if isinstance(event, dict)
]
if isinstance(events_value, dict) and "truncated_json" in events_value:
return ["truncated_json"]
return []
def context_events_readback_ok(input_object):
context_window = input_object.get("context_window")
event_count = 0
if isinstance(context_window, dict):
try:
event_count = int(context_window.get("event_count") or 0)
except (TypeError, ValueError):
event_count = 0
events_value = input_object.get("events_since_previous_agent_message")
if isinstance(events_value, list):
return True
return event_count == 0 and events_value in (None, "", {})
def verify_trace(args, public_key, secret_key, trace_id):
last_diagnostic = {}
for _ in range(args.verify_attempts):
legacy_trace_error = ""
try:
legacy_detail = fetch_trace(args.base_url, public_key, secret_key, trace_id)
except Exception as exc:
legacy_detail = {}
legacy_trace_error = type(exc).__name__
try:
observations_payload = fetch_observations_legacy(
args.base_url, public_key, secret_key, trace_id
)
observations = observation_rows_from_v2(observations_payload)
read_source = "legacy_observations"
except Exception as exc:
try:
observations_payload = fetch_observations_v2(
args.base_url, public_key, secret_key, trace_id
)
observations = observation_rows_from_v2(observations_payload)
read_source = "v2_observations"
except Exception:
observations = legacy_detail.get("observations") or []
read_source = f"legacy_trace_fallback:{type(exc).__name__}"
observations_missing_io = [
observation
for observation in observations
if not (observation.get("input") and observation.get("output"))
]
step_observations = [
observation
for observation in observations
if observation.get("name") not in ("codex.review", "codex.turn")
]
agent_message_observations = [
observation
for observation in observations
if observation.get("name") == "codex.agent_message"
]
agent_message_input_objects = [
observation_io_object(observation, "input")
for observation in agent_message_observations
]
agent_message_input_keys = sorted(
{
key
for input_object in agent_message_input_objects
for key in input_object.keys()
}
)
agent_message_all_have_context_window = all(
bool(input_object.get("context_window"))
for input_object in agent_message_input_objects
)
agent_message_all_have_context_events = all(
context_events_readback_ok(input_object)
for input_object in agent_message_input_objects
)
agent_message_with_previous_count = sum(
1
for input_object in agent_message_input_objects
if bool(input_object.get("previous_agent_message"))
)
agent_message_with_turn_input_count = sum(
1
for input_object in agent_message_input_objects
if bool(input_object.get("turn_input"))
)
agent_message_context_event_counts = [
(input_object.get("context_window") or {}).get("event_count", 0)
for input_object in agent_message_input_objects[:20]
if isinstance(input_object.get("context_window"), dict)
]
agent_message_event_type_samples = [
context_event_types(
input_object.get("events_since_previous_agent_message")
)[:8]
for input_object in agent_message_input_objects[:5]
]
agent_message_structure_ok = (
agent_message_all_have_context_window
and agent_message_all_have_context_events
and agent_message_with_turn_input_count == 1
and (
agent_message_with_previous_count
== max(len(agent_message_observations) - 1, 0)
)
)
root_observation = next(
(observation for observation in observations if observation.get("name") == "codex.review"),
{},
)
trace_input = legacy_detail.get("input") or root_observation.get("input")
trace_output = legacy_detail.get("output") or root_observation.get("output")
last_diagnostic = {
"read_source": read_source,
"legacy_trace_input": bool(legacy_detail.get("input")),
"legacy_trace_output": bool(legacy_detail.get("output")),
"legacy_trace_error": legacy_trace_error,
"trace_input": bool(trace_input),
"trace_output": bool(trace_output),
"observation_count": len(observations),
"step_observation_count": len(step_observations),
"agent_message_count": len(agent_message_observations),
"agent_message_input_keys": agent_message_input_keys,
"agent_message_all_have_context_window": agent_message_all_have_context_window,
"agent_message_all_have_context_events": agent_message_all_have_context_events,
"agent_message_with_previous_count": agent_message_with_previous_count,
"agent_message_with_turn_input_count": agent_message_with_turn_input_count,
"agent_message_context_event_counts": agent_message_context_event_counts,
"agent_message_event_type_samples": agent_message_event_type_samples,
"observations_missing_io": [
observation.get("name") for observation in observations_missing_io[:20]
],
"observation_names": [observation.get("name") for observation in observations[:20]],
}
ok = all(
[
trace_input,
trace_output,
len(observations) >= args.min_observations,
len(step_observations) >= args.min_step_observations,
not observations_missing_io,
not agent_message_observations or agent_message_structure_ok,
]
)
if ok:
return {
"trace_input": True,
"trace_output": True,
"read_source": read_source,
"observation_count": len(observations),
"step_observation_count": len(step_observations),
"agent_message_count": len(agent_message_observations),
"agent_message_input_keys": agent_message_input_keys,
"agent_message_all_have_context_window": agent_message_all_have_context_window,
"agent_message_all_have_context_events": agent_message_all_have_context_events,
"agent_message_with_previous_count": agent_message_with_previous_count,
"agent_message_with_turn_input_count": agent_message_with_turn_input_count,
"agent_message_context_event_counts": agent_message_context_event_counts,
"agent_message_event_type_samples": agent_message_event_type_samples,
"observations_missing_io": [],
"observation_names": [
observation.get("name") for observation in observations[:20]
],
}
time.sleep(args.verify_sleep_seconds)
raise RuntimeError(
"Litefuse trace "
f"{trace_id} did not expose multi-step I/O in time; "
f"last_diagnostic={json.dumps(last_diagnostic, sort_keys=True)}"
)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--base-url", default="https://litefuse.cloud")
parser.add_argument("--endpoint", default=None)
parser.add_argument("--input-file", required=True)
parser.add_argument("--events-file", required=True)
parser.add_argument("--output-file", default="")
parser.add_argument("--trace-name", default="doris-ai-review")
parser.add_argument("--subagent-trace-name", default="doris-ai-review-subagent")
parser.add_argument("--subagent-sessions-dir", default="")
parser.add_argument("--session-id", required=True)
parser.add_argument("--repository", default="")
parser.add_argument("--workflow", default="")
parser.add_argument("--run-id", default="")
parser.add_argument("--pr-number", default="")
parser.add_argument("--head-sha", default="")
parser.add_argument("--base-sha", default="")
parser.add_argument("--model", default="gpt-5.5")
parser.add_argument("--reasoning-effort", default="")
parser.add_argument("--environment", default="github-actions")
parser.add_argument("--max-input-chars", type=int, default=200_000)
parser.add_argument("--max-output-chars", type=int, default=200_000)
parser.add_argument("--max-json-chars", type=int, default=40_000)
parser.add_argument("--max-context-json-chars", type=int, default=0)
parser.add_argument("--max-payload-bytes", type=int, default=4_000_000)
parser.add_argument("--max-subagent-sessions", type=int, default=100)
parser.add_argument("--post-timeout-seconds", type=int, default=120)
parser.add_argument("--post-retry-attempts", type=int, default=5)
parser.add_argument("--post-retry-sleep-seconds", type=int, default=5)
parser.add_argument("--verify", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--verify-attempts", type=int, default=24)
parser.add_argument("--verify-sleep-seconds", type=int, default=5)
parser.add_argument("--min-observations", type=int, default=3)
parser.add_argument("--min-step-observations", type=int, default=1)
return parser.parse_args()
def main():
args = parse_args()
endpoint = args.endpoint or f"{args.base_url.rstrip('/')}/api/public/ingestion"
if args.max_context_json_chars <= 0:
args.max_context_json_chars = args.max_json_chars
input_text = read_text(args.input_file, args.max_input_chars)
output_text = (
read_text(args.output_file, args.max_output_chars, tail=True, optional=True)
if args.output_file
else ""
)
events = load_jsonl(args.events_file)
trace_id, payload, observation_count = build_ingestion_payload(
args, input_text, output_text, events
)
subagent_payloads = (
build_subagent_session_payloads(args, events)
if args.subagent_sessions_dir
else []
)
result = {
"dry_run": args.dry_run,
"event_count": len(events),
"observation_count": observation_count,
"trace_id": trace_id,
"session_id": args.session_id,
"trace_name": args.trace_name,
"subagent_trace_count": len(subagent_payloads),
"model": args.model,
"reasoning_effort": args.reasoning_effort,
}
if args.dry_run:
result["batch_count"] = len(payload["batch"])
result["event_types"] = [event["type"] for event in payload["batch"][:10]]
chunks = chunk_payload(payload, args.max_payload_bytes)
result["request_count"] = len(chunks)
result["request_sizes"] = [request_size for _, request_size in chunks]
result["max_request_size"] = (
max(result["request_sizes"]) if result["request_sizes"] else 0
)
result["subagent_traces"] = []
for subagent_payload in subagent_payloads:
chunks = chunk_payload(subagent_payload["payload"], args.max_payload_bytes)
request_sizes = [request_size for _chunk, request_size in chunks]
result["subagent_traces"].append(
{
"trace_id": subagent_payload["trace_id"],
"session_id": subagent_payload["session_id"],
"thread_id": subagent_payload["thread_id"],
"path": subagent_payload["path"],
"event_count": subagent_payload["event_count"],
"observation_count": subagent_payload["observation_count"],
"batch_count": len(subagent_payload["payload"]["batch"]),
"request_count": len(chunks),
"request_sizes": request_sizes,
"max_request_size": max(request_sizes) if request_sizes else 0,
}
)
print(json.dumps(result, sort_keys=True))
return
public_key = os.environ["LANGFUSE_PUBLIC_KEY"]
secret_key = os.environ["LANGFUSE_SECRET_KEY"]
result["status"] = post_payload(
endpoint,
public_key,
secret_key,
payload,
args.max_payload_bytes,
args.post_timeout_seconds,
args.post_retry_attempts,
args.post_retry_sleep_seconds,
)
result["subagent_traces"] = []
for subagent_payload in subagent_payloads:
status = post_payload(
endpoint,
public_key,
secret_key,
subagent_payload["payload"],
args.max_payload_bytes,
args.post_timeout_seconds,
args.post_retry_attempts,
args.post_retry_sleep_seconds,
)
result["subagent_traces"].append(
{
"trace_id": subagent_payload["trace_id"],
"session_id": subagent_payload["session_id"],
"thread_id": subagent_payload["thread_id"],
"path": subagent_payload["path"],
"event_count": subagent_payload["event_count"],
"observation_count": subagent_payload["observation_count"],
"status": status,
}
)
if args.verify:
try:
result["verified"] = verify_trace(args, public_key, secret_key, trace_id)
except Exception as exc:
result["verification_error"] = str(exc)
print(json.dumps(result, sort_keys=True))
raise
print(json.dumps(result, sort_keys=True))
if __name__ == "__main__":
main()