fix: handle legacy item types in exec render

This commit is contained in:
banteg
2025-12-29 18:27:05 +04:00
parent c34fdfc923
commit e57e4fd04a
3 changed files with 79 additions and 2 deletions
+43
View File
@@ -0,0 +1,43 @@
{"type":"error","message":"Failed to load optional config file ~/.codex/local.toml (ENOENT); continuing with defaults","code":"CONFIG_NOT_FOUND","fatal":false}
{"type":"thread.started","thread_id":"thread_01JHEM1P9M8Z7Y2YQJ4G6N2C3D","cli_version":"0.56.0","model":"gpt-5-codex","sandbox_mode":"workspace-write","cwd":"/home/user/project"}
{"type":"turn.started","turn_id":"turn_01JHEM1P9M8Z7Y2YQJ4G6N2C3E"}
{"type":"item.started","item":{"id":"item_0001","type":"todo_list","items":[{"text":"Inspect repo structure","completed":false},{"text":"Run tests","completed":false},{"text":"Fix failing tests","completed":false},{"text":"Summarize changes","completed":false}]}}
{"type":"item.updated","item":{"id":"item_0001","type":"todo_list","items":[{"text":"Inspect repo structure","completed":true},{"text":"Run tests","completed":false},{"text":"Fix failing tests","completed":false},{"text":"Summarize changes","completed":false}]}}
{"type":"item.completed","item":{"id":"item_0001","type":"todo_list","items":[{"text":"Inspect repo structure","completed":true},{"text":"Run tests","completed":true},{"text":"Fix failing tests","completed":true},{"text":"Summarize changes","completed":false}]}}
{"type":"item.started","item":{"id":"item_0002","type":"web_search","query":"python jsonlines parser handle unknown fields"}}
{"type":"item.completed","item":{"id":"item_0002","type":"web_search","query":"python jsonlines parser handle unknown fields"}}
{"type":"error","message":"Web search disabled by policy; returned cached results only","code":"WEB_SEARCH_POLICY","fatal":false}
{"type":"item.started","item":{"id":"item_0003","type":"mcp_tool_call","server":"github","tool":"search_issues","status":"in_progress"}}
{"type":"item.updated","item":{"id":"item_0003","type":"mcp_tool_call","server":"github","tool":"search_issues","status":"completed"}}
{"type":"item.completed","item":{"id":"item_0003","type":"mcp_tool_call","server":"github","tool":"search_issues","status":"completed"}}
{"type":"item.started","item":{"id":"item_0004","type":"command_execution","command":"pytest -q","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.updated","item":{"id":"item_0004","type":"command_execution","command":"pytest -q","aggregated_output":"....F\n","exit_code":null,"status":"in_progress"}}
{"type":"item.updated","item":{"id":"item_0004","type":"command_execution","command":"pytest -q","aggregated_output":"....F....\nFAILURES\n_________________________________ test_beta __________________________________\nE AssertionError: expected 42, got 0\n","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_0004","type":"command_execution","command":"pytest -q","aggregated_output":"....F....\n\nFAILURES\n_________________________________ test_beta __________________________________\nE AssertionError: expected 42, got 0\n\n=========================== short test summary info ===========================\nFAILED tests/test_beta.py::test_beta - AssertionError: expected 42, got 0\n1 failed, 11 passed in 0.98s\n","exit_code":1,"status":"failed"}}
{"type":"item.completed","item":{"id":"item_0005","type":"file_change","changes":[{"path":"src/compute_answer.py","kind":"update"},{"path":"tests/test_beta.py","kind":"update"}],"status":"completed"}}
{"type":"item.started","item":{"id":"item_0006","type":"command_execution","command":"pytest -q","aggregated_output":"","status":"in_progress","exit_code":null}}
{"type":"item.updated","item":{"id":"item_0006","type":"command_execution","command":"pytest -q","aggregated_output":"............\n","status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_0006","type":"command_execution","command":"pytest -q","aggregated_output":"............\n12 passed in 1.23s\n","status":"completed","exit_code":0}}
{"type":"item.started","item":{"id":"item_0007","type":"reasoning","text":"Root cause: compute_answer() returned 0. Updated logic to return 42 for the valid input path. Re-ran pytest to confirm all tests pass."}}
{"type":"item.completed","item":{"id":"item_0007","type":"reasoning","text":"Root cause: compute_answer() returned 0. Updated logic to return 42 for the valid input path. Re-ran pytest to confirm all tests pass."}}
{"type":"item.started","item":{"id":"item_0008","type":"agent_message","text":"I found the failing assertion in "}}
{"type":"item.updated","item":{"id":"item_0008","type":"agent_message","text":"I found the failing assertion in tests/test_beta.py and updated src/compute_answer.py to return the expected value."}}
{"type":"item.completed","item":{"id":"item_0008","type":"agent_message","text":"I found the failing assertion in tests/test_beta.py and updated src/compute_answer.py to return the expected value (42). After the change, `pytest -q` reports 12 passed."}}
{"type":"turn.completed","usage":{"input_tokens":1840,"cached_input_tokens":256,"output_tokens":732},"latency_ms":8421}
{"type":"turn.started"}
{"type":"item.started","item":{"id":"item_0009","type":"command_execution","command":"npm test","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_0009","type":"command_execution","command":"npm test","aggregated_output":"sh: npm: command not found\n","exit_code":127,"status":"failed"}}
{"type":"item.completed","item":{"id":"item_0010","type":"error","message":"Command `npm` not found in PATH (exit 127)."}}
{"type":"turn.failed","error":{"message":"Aborted: required dependency `npm` is missing; cannot continue."},"exit_code":1}
{"type":"error","message":"codex exec exited non-zero (1) after turn.failed"}
{"type":"thread.started","thread_id":"thread_legacy_7f9c2d3e"}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_l_0001","type":"agent_message","item_type":"assistant_message","text":"Legacy schema example: hello (item_type=assistant_message)."}}
{"type":"item.completed","item":{"id":"item_l_0002","item_type":"command_execution","command":"echo legacy","output":"legacy\n","exit_code":0,"status":"completed"}}
{"type":"turn.completed","usage":{"input_tokens":12,"output_tokens":9}}
{"type":"thread.started","thread_id":"thread_future_01JK0Y6F8K6C7R3N1MGZ9G9A2B"}
{"type":"turn.started"}
{"type":"item.completed","item":{"id":"item_f_0001","type":"tool_call","name":"my_custom_tool","arguments":{"foo":"bar","n":3},"status":"completed","result":{"ok":true}}}
{"type":"item.completed","item":{"id":"item_f_0002","type":"file_change","changes":[{"path":"README.md","kind":"add"}],"status":"failed","error":"permission denied"}}
{"type":"turn.rate_limited","retry_after_ms":1200}
{"type":"turn.completed","usage":null}
+7 -2
View File
@@ -90,14 +90,19 @@ def format_event(
return last_item, [f"stream error: {event['message']}"], None, None
case "item.started" | "item.updated" | "item.completed" as etype:
item = event["item"]
item_num = extract_numeric_id(item["id"], last_item)
item_type = item.get("type") or item.get("item_type")
if item_type == "assistant_message":
item_type = "agent_message"
if item_type is None:
return last_item, [], None, None
item_num = extract_numeric_id(item.get("id"), last_item)
last_item = item_num if item_num is not None else last_item
prefix = f"{item_num}. "
if escape_markdown and item_num is not None:
# Avoid ordered-list parsing which renumbers items in MarkdownIt/CommonMark.
prefix = f"{item_num}\\." + " "
match (item["type"], etype):
match (item_type, etype):
case ("agent_message", "item.completed"):
lines.append("assistant:")
lines.extend(indent(item["text"], " ").splitlines())
+29
View File
@@ -10,6 +10,9 @@ def _loads(lines: str) -> list[dict]:
FIXTURE_PATH = Path(__file__).resolve().parent / "fixtures" / "codex.jsonl"
ALL_FORMATS_FIXTURE_PATH = (
Path(__file__).resolve().parent.parent / "codex_exec_json_all_formats.jsonl"
)
SAMPLE_STREAM = """
{"type":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
@@ -62,6 +65,32 @@ def test_render_event_cli_real_run_fixture() -> None:
assert out[-1] == "turn completed"
def test_render_event_cli_all_formats_fixture() -> None:
events = _loads(ALL_FORMATS_FIXTURE_PATH.read_text(encoding="utf-8"))
last_turn = None
out: list[str] = []
for evt in events:
last_turn, lines = render_event_cli(evt, last_turn)
out.extend(lines)
assert "thread started" in out
assert "turn started" in out
assert any(line.startswith("stream error:") for line in out)
assert any(line.startswith("4. ▸ `pytest -q`") for line in out)
assert any("✗ `pytest -q` (exit 1)" in line for line in out)
assert any("searched: python jsonlines parser handle unknown fields" in line for line in out)
assert any("tool: github.search_issues" in line for line in out)
assert any("updated `src/compute_answer.py`" in line for line in out)
assert any(
line.startswith(
"turn failed: Aborted: required dependency `npm` is missing; cannot continue."
)
for line in out
)
assert "assistant:" in out
assert any("Legacy schema example" in line for line in out)
def test_progress_renderer_renders_progress_and_final() -> None:
r = ExecProgressRenderer(max_actions=5)
for evt in _loads(SAMPLE_STREAM):