diff --git a/codex_exec_json_all_formats.jsonl b/codex_exec_json_all_formats.jsonl new file mode 100644 index 0000000..64bad05 --- /dev/null +++ b/codex_exec_json_all_formats.jsonl @@ -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} \ No newline at end of file diff --git a/src/takopi/exec_render.py b/src/takopi/exec_render.py index dac2a14..594b5c4 100644 --- a/src/takopi/exec_render.py +++ b/src/takopi/exec_render.py @@ -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()) diff --git a/tests/test_exec_render.py b/tests/test_exec_render.py index 561dbdd..fc1d2dc 100644 --- a/tests/test_exec_render.py +++ b/tests/test_exec_render.py @@ -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):