feat: msgspec schemas for jsonl decoding (#37)

This commit is contained in:
banteg
2026-01-04 03:41:07 +04:00
committed by GitHub
parent 30fe5cef43
commit a0c16c325e
33 changed files with 2235 additions and 1148 deletions
-5
View File
@@ -1,5 +0,0 @@
{"type":"system","subtype":"init","session_id":"session_02","cwd":"/Users/banteg/dev/project","model":"sonnet","permissionMode":"manual","apiKeySource":"env","tools":["Bash","Read","Write"],"mcp_servers":[{"name":"approvals","status":"connected"}]}
{"type":"assistant","session_id":"session_02","message":{"id":"msg_10","type":"message","role":"assistant","content":[{"type":"text","text":"I need permission to run this command."}],"usage":{"input_tokens":80,"output_tokens":20}}}
{"type":"assistant","session_id":"session_02","parent_tool_use_id":"toolu_parent","message":{"id":"msg_11","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_9","name":"Bash","input":{"command":"git fetch origin main"}}]}}
{"type":"user","session_id":"session_02","message":{"id":"msg_12","type":"message","role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_9","content":"permission denied"}]}}
{"type":"result","subtype":"error","session_id":"session_02","total_cost_usd":0.001,"is_error":true,"duration_ms":2000,"duration_api_ms":1800,"num_turns":1,"result":"","error":"Permission denied","permission_denials":[{"tool_name":"Bash","tool_use_id":"toolu_9","tool_input":{"command":"git fetch origin main"}}]}
-8
View File
@@ -1,8 +0,0 @@
{"type":"system","subtype":"init","session_id":"session_01","cwd":"/Users/banteg/dev/project","model":"sonnet","permissionMode":"auto","apiKeySource":"env","tools":["Bash","Read","Write","WebSearch","Task"],"mcp_servers":[{"name":"approvals","status":"connected"}]}
{"type":"assistant","session_id":"session_01","message":{"id":"msg_1","type":"message","role":"assistant","model":"claude-3-5-sonnet","content":[{"type":"text","text":"I'll inspect the repo, then add notes."}],"usage":{"input_tokens":120,"output_tokens":45}}}
{"type":"assistant","session_id":"session_01","message":{"id":"msg_2","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls -la"}}],"usage":{"input_tokens":10,"output_tokens":5}}}
{"type":"user","session_id":"session_01","message":{"id":"msg_3","type":"message","role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":[{"type":"text","text":"total 2\nREADME.md\nsrc\n"}]}]}}
{"type":"assistant","session_id":"session_01","message":{"id":"msg_4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_2","name":"Write","input":{"path":"notes.md","content":"hello"}}]}}
{"type":"user","session_id":"session_01","message":{"id":"msg_5","type":"message","role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_2","content":"ok"}]}}
{"type":"assistant","session_id":"session_01","message":{"id":"msg_6","type":"message","role":"assistant","content":[{"type":"text","text":"Done. Added notes.md."}],"usage":{"input_tokens":20,"output_tokens":12}}}
{"type":"result","subtype":"success","session_id":"session_01","total_cost_usd":0.0123,"is_error":false,"duration_ms":12345,"duration_api_ms":12000,"num_turns":2,"result":"Done. Added notes.md.","usage":{"input_tokens":150,"output_tokens":70,"service_tier":"standard","server_tool_use":{"web_search_requests":0}},"modelUsage":{"sonnet":{"inputTokens":150,"outputTokens":70,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"webSearchRequests":0,"costUSD":0.0123,"contextWindow":200000}}}
+9
View File
@@ -0,0 +1,9 @@
{"type":"system","subtype":"init","uuid":"11111111-1111-1111-1111-111111111111","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","apiKeySource":"ANTHROPIC_API_KEY","cwd":"/home/alex/demo-project","tools":["Task","Bash","Read","Edit","Write","WebFetch","WebSearch"],"mcp_servers":[{"name":"github","status":"connected"},{"name":"sentry","status":"error"}],"model":"claude-sonnet-4-5-20250929","permissionMode":"default","slash_commands":["help","status","clear","compact","resume"],"output_style":"default","claude_code_version":"2.0.75","agents":["general-purpose","Plan","Explore"],"skills":["python","git"],"plugins":[{"name":"local-plugin-example","path":"/home/alex/.claude/plugins/cache/local-plugin-example"}]}
{"type":"user","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","parent_tool_use_id":null,"message":{"role":"user","content":[{"type":"text","text":"List the files in the current directory, then summarize what you see."}]}}
{"type":"assistant","uuid":"33333333-3333-3333-3333-333333333333","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","parent_tool_use_id":null,"message":{"id":"msg_02EXAMPLEASSIST","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[{"type":"text","text":"Sure - I-ll list the directory contents."},{"type":"tool_use","id":"toolu_01BASH_LS_EXAMPLE","name":"Bash","input":{"command":"ls","timeout":600000}},{"type":"tool_use","id":"toolu_02","name":"Write","input":{"file_path":"notes.md","content":"hello"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":123,"output_tokens":42}}}
{"type":"user","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","parent_tool_use_id":null,"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_01BASH_LS_EXAMPLE","content":[{"type":"text","text":"README.md\npyproject.toml\nsrc/\n"}],"is_error":false}]},"tool_use_result":{"stdout":"README.md\npyproject.toml\nsrc/\n","stderr":"","interrupted":false,"exit_code":0}}
{"type":"user","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","parent_tool_use_id":null,"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_02","content":"ok","is_error":false}]}}
{"type":"assistant","uuid":"44444444-4444-4444-4444-444444444444","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","parent_tool_use_id":null,"message":{"id":"msg_03EXAMPLEASSIST","type":"message","role":"assistant","model":"claude-sonnet-4-5-20250929","content":[{"type":"text","text":"I see README.md, pyproject.toml, and src/."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":130,"output_tokens":76}}}
{"type":"result","subtype":"success","uuid":"77777777-7777-7777-7777-777777777777","session_id":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa","duration_ms":2450,"duration_api_ms":2100,"is_error":false,"num_turns":2,"result":"I see README.md, pyproject.toml, and src/.","total_cost_usd":0.012345,"usage":{"input_tokens":130,"output_tokens":76,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"service_tier":"standard"},"modelUsage":{"claude-sonnet-4-5-20250929":{"input_tokens":130,"output_tokens":76,"service_tier":"standard"}},"permission_denials":[],"structured_output":null}
{"type":"system","subtype":"init","uuid":"aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb","session_id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","apiKeySource":"none","cwd":"/home/alex/demo-project","tools":["Bash","Read","Write"],"mcp_servers":[],"model":"claude-sonnet-4-5-20250929","permissionMode":"default","slash_commands":["help","status"],"output_style":"default"}
{"type":"result","subtype":"error_during_execution","uuid":"99999999-9999-9999-9999-999999999999","session_id":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb","duration_ms":1200,"duration_api_ms":800,"is_error":true,"num_turns":1,"total_cost_usd":0.001,"usage":{"input_tokens":40,"output_tokens":12,"service_tier":"standard"},"modelUsage":{"claude-sonnet-4-5-20250929":{"input_tokens":40,"output_tokens":12,"service_tier":"standard"}},"permission_denials":[{"tool_name":"Write","tool_use_id":"toolu_01WRITE_SECRET_EXAMPLE","tool_input":{"file_path":"/root/secret.txt","content":"hello\n"}}],"errors":["Permission denied: cannot write to /root/secret.txt"],"result":""}
-37
View File
File diff suppressed because one or more lines are too long
+21 -41
View File
@@ -1,43 +1,23 @@
{"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":"thread.started","thread_id":"0199a213-81c0-7800-8aa1-bbab2a035a53"}
{"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":"item.started","item":{"id":"item_0","type":"todo_list","items":[{"text":"Inspect repo structure","completed":false},{"text":"Run tests","completed":false},{"text":"Fix failing tests","completed":false}]}}
{"type":"item.updated","item":{"id":"item_0","type":"todo_list","items":[{"text":"Inspect repo structure","completed":true},{"text":"Run tests","completed":false},{"text":"Fix failing tests","completed":false}]}}
{"type":"item.completed","item":{"id":"item_0","type":"todo_list","items":[{"text":"Inspect repo structure","completed":true},{"text":"Run tests","completed":true},{"text":"Fix failing tests","completed":true}]}}
{"type":"item.started","item":{"id":"item_1","type":"command_execution","command":"pytest -q","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_1","type":"command_execution","command":"pytest -q","aggregated_output":"....\n","exit_code":0,"status":"completed"}}
{"type":"item.started","item":{"id":"item_2","type":"command_execution","command":"pytest -q","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_2","type":"command_execution","command":"pytest -q","aggregated_output":"....F\n","exit_code":1,"status":"failed"}}
{"type":"item.completed","item":{"id":"item_3","type":"file_change","changes":[{"path":"src/compute_answer.py","kind":"update"},{"path":"README.md","kind":"add"}],"status":"completed"}}
{"type":"item.completed","item":{"id":"item_4","type":"file_change","changes":[{"path":"src/compute_answer.py","kind":"update"}],"status":"failed"}}
{"type":"item.started","item":{"id":"item_5","type":"mcp_tool_call","server":"github","tool":"search_issues","arguments":{"q":"exec --json"},"result":null,"error":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_5","type":"mcp_tool_call","server":"github","tool":"search_issues","arguments":{"q":"exec --json"},"result":{"content":[{"type":"text","text":"Found 3 matches."}],"structured_content":{"matches":3}},"error":null,"status":"completed"}}
{"type":"item.started","item":{"id":"item_6","type":"mcp_tool_call","server":"github","tool":"search_issues","arguments":null,"result":null,"error":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"item_6","type":"mcp_tool_call","server":"github","tool":"search_issues","arguments":null,"result":null,"error":{"message":"tool timeout"},"status":"failed"}}
{"type":"item.completed","item":{"id":"item_7","type":"web_search","query":"codex exec --json schema"}}
{"type":"item.completed","item":{"id":"item_8","type":"reasoning","text":"Root cause: compute_answer() returned 0."}}
{"type":"item.completed","item":{"id":"item_9","type":"agent_message","text":"Updated src/compute_answer.py and tests pass."}}
{"type":"item.completed","item":{"id":"item_10","type":"error","message":"command output truncated"}}
{"type":"turn.completed","usage":{"input_tokens":1840,"cached_input_tokens":256,"output_tokens":732}}
{"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}
{"type":"turn.failed","error":{"message":"Aborted: required dependency `npm` is missing; cannot continue."}}
{"type":"error","message":"codex exec exited non-zero after turn.failed"}
+50
View File
@@ -0,0 +1,50 @@
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"Summarize README and list the main CLI commands."},{"type":"image","data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=","mimeType":"image/png"}],"timestamp":1767401000000}}
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"Summarize README and list the main CLI commands."},{"type":"image","data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=","mimeType":"image/png"}],"timestamp":1767401000000}}
{"type":"message_start","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"thinking_start","contentIndex":0,"partial":{"role":"assistant","content":[{"type":"thinking","thinking":""},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README"},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"thinking_delta","contentIndex":0,"delta":"Need to read README","partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README"},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"thinking_end","contentIndex":0,"content":"Need to read README for the CLI commands.","partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"text_start","contentIndex":1,"partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":""},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README"},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"text_delta","contentIndex":1,"delta":"I'll check the README","partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README"},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"text_end","contentIndex":1,"content":"I'll check the README for the CLI commands.","partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"toolcall_start","contentIndex":2,"partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"toolcall_delta","contentIndex":2,"delta":"{\"path\":\"README.md\"}","partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200},"assistantMessageEvent":{"type":"toolcall_end","contentIndex":2,"toolCall":{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}},"partial":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"toolUse","timestamp":1767401001200}}}
{"type":"message_end","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":1240,"output":182,"cacheRead":0,"cacheWrite":0,"totalTokens":1422,"cost":{"input":0.00124,"output":0.000364,"cacheRead":0.0,"cacheWrite":0.0,"total":0.001604}},"stopReason":"toolUse","timestamp":1767401001500}}
{"type":"tool_execution_start","toolCallId":"toolu_01HXYZREAD","toolName":"read","args":{"path":"README.md"}}
{"type":"tool_execution_update","toolCallId":"toolu_01HXYZREAD","toolName":"read","args":{"path":"README.md"},"partialResult":{"content":[{"type":"text","text":"Reading README.md..."}],"details":{"bytesRead":512}}}
{"type":"tool_execution_end","toolCallId":"toolu_01HXYZREAD","toolName":"read","result":{"content":[{"type":"text","text":"# Pi Coding Agent\n\nUsage:\n pi [options] [@files...] [messages...]\n\nOptions:\n --mode <mode>\n --print, -p\n"}],"details":{"path":"README.md","bytesRead":2048,"truncated":false}},"isError":false}
{"type":"message_start","message":{"role":"toolResult","toolCallId":"toolu_01HXYZREAD","toolName":"read","content":[{"type":"text","text":"# Pi Coding Agent\n\nUsage:\n pi [options] [@files...] [messages...]\n\nOptions:\n --mode <mode>\n --print, -p\n"}],"details":{"path":"README.md","bytesRead":2048,"truncated":false},"isError":false,"timestamp":1767401002500}}
{"type":"message_end","message":{"role":"toolResult","toolCallId":"toolu_01HXYZREAD","toolName":"read","content":[{"type":"text","text":"# Pi Coding Agent\n\nUsage:\n pi [options] [@files...] [messages...]\n\nOptions:\n --mode <mode>\n --print, -p\n"}],"details":{"path":"README.md","bytesRead":2048,"truncated":false},"isError":false,"timestamp":1767401002500}}
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":1240,"output":182,"cacheRead":0,"cacheWrite":0,"totalTokens":1422,"cost":{"input":0.00124,"output":0.000364,"cacheRead":0.0,"cacheWrite":0.0,"total":0.001604}},"stopReason":"toolUse","timestamp":1767401001500},"toolResults":[{"role":"toolResult","toolCallId":"toolu_01HXYZREAD","toolName":"read","content":[{"type":"text","text":"# Pi Coding Agent\n\nUsage:\n pi [options] [@files...] [messages...]\n\nOptions:\n --mode <mode>\n --print, -p\n"}],"details":{"path":"README.md","bytesRead":2048,"truncated":false},"isError":false,"timestamp":1767401002500}]}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767401004500}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767401004500},"assistantMessageEvent":{"type":"text_start","contentIndex":0,"partial":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767401004500}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands"}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767401004500},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Main CLI commands","partial":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands"}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767401004500}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":680,"output":128,"cacheRead":0,"cacheWrite":0,"totalTokens":808,"cost":{"input":0.00068,"output":0.000256,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000936}},"stopReason":"stop","timestamp":1767401004500},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs.","partial":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":680,"output":128,"cacheRead":0,"cacheWrite":0,"totalTokens":808,"cost":{"input":0.00068,"output":0.000256,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000936}},"stopReason":"stop","timestamp":1767401004500}}}
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":680,"output":128,"cacheRead":0,"cacheWrite":0,"totalTokens":808,"cost":{"input":0.00068,"output":0.000256,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000936}},"stopReason":"stop","timestamp":1767401004500}}
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":680,"output":128,"cacheRead":0,"cacheWrite":0,"totalTokens":808,"cost":{"input":0.00068,"output":0.000256,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000936}},"stopReason":"stop","timestamp":1767401004500},"toolResults":[]}
{"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"Summarize README and list the main CLI commands."},{"type":"image","data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=","mimeType":"image/png"}],"timestamp":1767401000000},{"role":"assistant","content":[{"type":"thinking","thinking":"Need to read README for the CLI commands."},{"type":"text","text":"I'll check the README for the CLI commands."},{"type":"toolCall","id":"toolu_01HXYZREAD","name":"read","arguments":{"path":"README.md"}}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":1240,"output":182,"cacheRead":0,"cacheWrite":0,"totalTokens":1422,"cost":{"input":0.00124,"output":0.000364,"cacheRead":0.0,"cacheWrite":0.0,"total":0.001604}},"stopReason":"toolUse","timestamp":1767401001500},{"role":"toolResult","toolCallId":"toolu_01HXYZREAD","toolName":"read","content":[{"type":"text","text":"# Pi Coding Agent\n\nUsage:\n pi [options] [@files...] [messages...]\n\nOptions:\n --mode <mode>\n --print, -p\n"}],"details":{"path":"README.md","bytesRead":2048,"truncated":false},"isError":false,"timestamp":1767401002500},{"role":"assistant","content":[{"type":"text","text":"Main CLI commands include `pi --mode <mode>` for output format and `pi --print` for non-interactive runs."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":680,"output":128,"cacheRead":0,"cacheWrite":0,"totalTokens":808,"cost":{"input":0.00068,"output":0.000256,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000936}},"stopReason":"stop","timestamp":1767401004500}]}
{"type":"auto_compaction_start","reason":"threshold"}
{"type":"auto_compaction_end","result":{"summary":"## Goal\nSummarize CLI usage.\n\n## Progress\n- Read README for CLI commands.","firstKeptEntryId":"b1b2c3d4-e5f6-7890-abcd-ef0123456789","tokensBefore":118432,"details":{"readFiles":["README.md"],"modifiedFiles":[]}},"aborted":false,"willRetry":false}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"List the latest release notes for the CLI."}],"timestamp":1767402000000}}
{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"List the latest release notes for the CLI."}],"timestamp":1767402000000}}
{"type":"message_start","message":{"role":"assistant","content":[],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":320,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":320,"cost":{"input":0.00032,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.00032}},"stopReason":"error","errorMessage":"Rate limit exceeded","timestamp":1767402001200}}
{"type":"message_end","message":{"role":"assistant","content":[],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":320,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":320,"cost":{"input":0.00032,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.00032}},"stopReason":"error","errorMessage":"Rate limit exceeded","timestamp":1767402001200}}
{"type":"turn_end","message":{"role":"assistant","content":[],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":320,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":320,"cost":{"input":0.00032,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.00032}},"stopReason":"error","errorMessage":"Rate limit exceeded","timestamp":1767402001200},"toolResults":[]}
{"type":"agent_end","messages":[{"role":"user","content":[{"type":"text","text":"List the latest release notes for the CLI."}],"timestamp":1767402000000},{"role":"assistant","content":[],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":320,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":320,"cost":{"input":0.00032,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.00032}},"stopReason":"error","errorMessage":"Rate limit exceeded","timestamp":1767402001200}]}
{"type":"auto_retry_start","attempt":1,"maxAttempts":3,"delayMs":2000,"errorMessage":"Rate limit exceeded"}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767402004200}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767402004200},"assistantMessageEvent":{"type":"text_start","contentIndex":0,"partial":{"role":"assistant","content":[{"type":"text","text":""}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767402004200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded"}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767402004200},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Retry succeeded","partial":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded"}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0.0,"output":0.0,"cacheRead":0.0,"cacheWrite":0.0,"total":0.0}},"stopReason":"stop","timestamp":1767402004200}}}
{"type":"message_update","message":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":410,"output":96,"cacheRead":0,"cacheWrite":0,"totalTokens":506,"cost":{"input":0.00041,"output":0.000192,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000602}},"stopReason":"stop","timestamp":1767402004200},"assistantMessageEvent":{"type":"text_end","contentIndex":0,"content":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export.","partial":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":410,"output":96,"cacheRead":0,"cacheWrite":0,"totalTokens":506,"cost":{"input":0.00041,"output":0.000192,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000602}},"stopReason":"stop","timestamp":1767402004200}}}
{"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":410,"output":96,"cacheRead":0,"cacheWrite":0,"totalTokens":506,"cost":{"input":0.00041,"output":0.000192,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000602}},"stopReason":"stop","timestamp":1767402004200}}
{"type":"turn_end","message":{"role":"assistant","content":[{"type":"text","text":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":410,"output":96,"cacheRead":0,"cacheWrite":0,"totalTokens":506,"cost":{"input":0.00041,"output":0.000192,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000602}},"stopReason":"stop","timestamp":1767402004200},"toolResults":[]}
{"type":"agent_end","messages":[{"role":"assistant","content":[{"type":"text","text":"Retry succeeded. Recent notes: 1) Added --mode json output. 2) Improved session export."}],"api":"openai-responses","provider":"openai","model":"gpt-5.1-codex","usage":{"input":410,"output":96,"cacheRead":0,"cacheWrite":0,"totalTokens":506,"cost":{"input":0.00041,"output":0.000192,"cacheRead":0.0,"cacheWrite":0.0,"total":0.000602}},"stopReason":"stop","timestamp":1767402004200}]}
{"type":"auto_retry_end","success":true,"attempt":1}
+158 -26
View File
@@ -11,11 +11,43 @@ from takopi.runners.claude import (
ENGINE,
translate_claude_event,
)
from takopi.schemas import claude as claude_schema
def _load_fixture(name: str) -> list[dict]:
def _load_fixture(
name: str, *, session_id: str | None = None
) -> list[claude_schema.StreamJsonMessage]:
path = Path(__file__).parent / "fixtures" / name
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
events = [
claude_schema.decode_stream_json_line(line)
for line in path.read_bytes().splitlines()
if line.strip()
]
if session_id is None:
return events
return [
event for event in events if getattr(event, "session_id", None) == session_id
]
def _decode_event(payload: dict) -> claude_schema.StreamJsonMessage:
data_payload = dict(payload)
data_payload.setdefault("uuid", "uuid")
data_payload.setdefault("session_id", "session")
match data_payload.get("type"):
case "assistant":
message = dict(data_payload.get("message", {}))
message.setdefault("role", "assistant")
message.setdefault("content", [])
message.setdefault("model", "claude")
data_payload["message"] = message
case "user":
message = dict(data_payload.get("message", {}))
message.setdefault("role", "user")
message.setdefault("content", [])
data_payload["message"] = message
data = json.dumps(data_payload).encode("utf-8")
return claude_schema.decode_stream_json_line(data)
def test_claude_resume_format_and_extract() -> None:
@@ -33,8 +65,18 @@ def test_claude_resume_format_and_extract() -> None:
def test_translate_success_fixture() -> None:
state = ClaudeStreamState()
events: list = []
for event in _load_fixture("claude_stream_success.jsonl"):
events.extend(translate_claude_event(event, title="claude", state=state))
for event in _load_fixture(
"claude_streamjson_session.jsonl",
session_id="aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
):
events.extend(
translate_claude_event(
event,
title="claude",
state=state,
factory=state.factory,
)
)
assert isinstance(events[0], StartedEvent)
started = next(evt for evt in events if isinstance(evt, StartedEvent))
@@ -47,8 +89,10 @@ def test_translate_success_fixture() -> None:
for evt in action_events
if evt.phase == "started"
}
assert started_actions[("toolu_1", "started")].action.kind == "command"
write_action = started_actions[("toolu_2", "started")].action
assert (
started_actions[("toolu_01BASH_LS_EXAMPLE", "started")].action.kind == "command"
)
write_action = started_actions[("toolu_02", "started")].action
assert write_action.kind == "file_change"
assert write_action.detail["changes"][0]["path"] == "notes.md"
@@ -57,34 +101,37 @@ def test_translate_success_fixture() -> None:
for evt in action_events
if evt.phase == "completed"
}
assert completed_actions[("toolu_1", "completed")].ok is True
assert completed_actions[("toolu_2", "completed")].ok is True
assert completed_actions[("toolu_01BASH_LS_EXAMPLE", "completed")].ok is True
assert completed_actions[("toolu_02", "completed")].ok is True
completed = next(evt for evt in events if isinstance(evt, CompletedEvent))
assert events[-1] == completed
assert completed.ok is True
assert completed.resume == started.resume
assert completed.answer == "Done. Added notes.md."
assert completed.answer == "I see README.md, pyproject.toml, and src/."
def test_translate_error_fixture_permission_denials() -> None:
state = ClaudeStreamState()
events: list = []
for event in _load_fixture("claude_stream_error.jsonl"):
events.extend(translate_claude_event(event, title="claude", state=state))
for event in _load_fixture(
"claude_streamjson_session.jsonl",
session_id="bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
):
events.extend(
translate_claude_event(
event,
title="claude",
state=state,
factory=state.factory,
)
)
started = next(evt for evt in events if isinstance(evt, StartedEvent))
completed = next(evt for evt in events if isinstance(evt, CompletedEvent))
warnings = [
evt
for evt in events
if isinstance(evt, ActionEvent) and evt.action.kind == "warning"
]
assert warnings
assert events.index(warnings[0]) < events.index(completed)
assert completed.ok is False
assert completed.error == "Permission denied"
assert completed.error is not None
assert "claude run failed" in completed.error
assert completed.resume == started.resume
@@ -120,13 +167,54 @@ def test_tool_results_pop_pending_actions() -> None:
},
}
translate_claude_event(tool_use_event, title="claude", state=state)
translate_claude_event(
_decode_event(tool_use_event),
title="claude",
state=state,
factory=state.factory,
)
assert "toolu_1" in state.pending_actions
translate_claude_event(tool_result_event, title="claude", state=state)
translate_claude_event(
_decode_event(tool_result_event),
title="claude",
state=state,
factory=state.factory,
)
assert not state.pending_actions
def test_translate_thinking_block() -> None:
state = ClaudeStreamState()
event = {
"type": "assistant",
"message": {
"id": "msg_1",
"content": [
{
"type": "thinking",
"thinking": "Consider the options.",
"signature": "sig",
}
],
},
}
events = translate_claude_event(
_decode_event(event),
title="claude",
state=state,
factory=state.factory,
)
assert len(events) == 1
assert isinstance(events[0], ActionEvent)
assert events[0].phase == "completed"
assert events[0].action.kind == "note"
assert events[0].action.title == "Consider the options."
assert events[0].ok is True
@pytest.mark.anyio
async def test_run_serializes_same_session() -> None:
runner = ClaudeRunner(claude_cmd="claude")
@@ -184,15 +272,30 @@ async def test_run_serializes_new_session_after_session_is_known(
"resume_marker = os.environ['CLAUDE_TEST_RESUME_MARKER']\n"
"session_id = os.environ['CLAUDE_TEST_SESSION_ID']\n"
"\n"
"init = {\n"
" 'type': 'system',\n"
" 'subtype': 'init',\n"
" 'uuid': 'uuid',\n"
" 'session_id': session_id,\n"
" 'apiKeySource': 'env',\n"
" 'cwd': '.',\n"
" 'tools': [],\n"
" 'mcp_servers': [],\n"
" 'model': 'claude',\n"
" 'permissionMode': 'default',\n"
" 'slash_commands': [],\n"
" 'output_style': 'default',\n"
"}\n"
"\n"
"args = sys.argv[1:]\n"
"if '--resume' in args or '-r' in args:\n"
" print(json.dumps({'type': 'system', 'subtype': 'init', 'session_id': session_id}), flush=True)\n"
" print(json.dumps(init), flush=True)\n"
" with open(resume_marker, 'w', encoding='utf-8') as f:\n"
" f.write('started')\n"
" f.flush()\n"
" sys.exit(0)\n"
"\n"
"print(json.dumps({'type': 'system', 'subtype': 'init', 'session_id': session_id}), flush=True)\n"
"print(json.dumps(init), flush=True)\n"
"while not os.path.exists(gate):\n"
" time.sleep(0.001)\n"
"sys.exit(0)\n",
@@ -252,8 +355,37 @@ async def test_run_strips_anthropic_api_key_by_default(tmp_path, monkeypatch) ->
"\n"
"session_id = 'session_01'\n"
"status = 'set' if os.environ.get('ANTHROPIC_API_KEY') else 'unset'\n"
"print(json.dumps({'type': 'system', 'subtype': 'init', 'session_id': session_id}), flush=True)\n"
"print(json.dumps({'type': 'result', 'subtype': 'success', 'is_error': False, 'result': f'api={status}', 'session_id': session_id}), flush=True)\n"
"init = {\n"
" 'type': 'system',\n"
" 'subtype': 'init',\n"
" 'uuid': 'uuid',\n"
" 'session_id': session_id,\n"
" 'apiKeySource': 'env',\n"
" 'cwd': '.',\n"
" 'tools': [],\n"
" 'mcp_servers': [],\n"
" 'model': 'claude',\n"
" 'permissionMode': 'default',\n"
" 'slash_commands': [],\n"
" 'output_style': 'default',\n"
"}\n"
"print(json.dumps(init), flush=True)\n"
"result = {\n"
" 'type': 'result',\n"
" 'subtype': 'success',\n"
" 'uuid': 'uuid',\n"
" 'session_id': session_id,\n"
" 'duration_ms': 0,\n"
" 'duration_api_ms': 0,\n"
" 'is_error': False,\n"
" 'num_turns': 1,\n"
" 'result': f'api={status}',\n"
" 'total_cost_usd': 0.0,\n"
" 'usage': {'input_tokens': 0, 'output_tokens': 0},\n"
" 'modelUsage': {},\n"
" 'permission_denials': [],\n"
"}\n"
"print(json.dumps(result), flush=True)\n"
"raise SystemExit(0)\n",
encoding="utf-8",
)
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
from pathlib import Path
import pytest
from takopi.schemas import claude as claude_schema
def _fixture_path(name: str) -> Path:
return Path(__file__).parent / "fixtures" / name
def _decode_fixture(name: str) -> list[str]:
path = _fixture_path(name)
errors: list[str] = []
for lineno, line in enumerate(path.read_bytes().splitlines(), 1):
if not line.strip():
continue
try:
decoded = claude_schema.decode_stream_json_line(line)
except Exception as exc:
errors.append(f"line {lineno}: {exc.__class__.__name__}: {exc}")
continue
_ = decoded
return errors
@pytest.mark.parametrize(
"fixture",
[
"claude_streamjson_session.jsonl",
],
)
def test_claude_schema_parses_fixture(fixture: str) -> None:
errors = _decode_fixture(fixture)
assert not errors, f"{fixture} had {len(errors)} errors: " + "; ".join(errors[:5])
+44
View File
@@ -0,0 +1,44 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from takopi.schemas import codex as codex_schema
def _fixture_path(name: str) -> Path:
return Path(__file__).parent / "fixtures" / name
def _decode_fixture(name: str) -> list[str]:
path = _fixture_path(name)
errors: list[str] = []
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
try:
json.loads(line)
except Exception as exc:
errors.append(f"line {lineno}: invalid JSON ({exc})")
continue
try:
codex_schema.decode_event(line)
except Exception as exc:
errors.append(f"line {lineno}: {exc.__class__.__name__}: {exc}")
return errors
@pytest.mark.parametrize(
"fixture",
[
"codex_exec_json_all_formats.jsonl",
],
)
def test_codex_schema_parses_fixture(fixture: str) -> None:
errors = _decode_fixture(fixture)
assert not errors, f"{fixture} had {len(errors)} errors: " + "; ".join(errors[:5])
+26 -26
View File
@@ -1,5 +1,21 @@
import json
from takopi.events import EventFactory
from takopi.model import ActionEvent
from takopi.runners.codex import translate_codex_event
from takopi.schemas import codex as codex_schema
def _decode_event(payload: dict) -> codex_schema.ThreadEvent:
return codex_schema.decode_event(json.dumps(payload))
def _translate_event(payload: dict) -> list:
return translate_codex_event(
_decode_event(payload),
title="Codex",
factory=EventFactory("codex"),
)
def test_translate_mcp_tool_call_summarizes_structured_content() -> None:
@@ -20,7 +36,7 @@ def test_translate_mcp_tool_call_summarizes_structured_content() -> None:
},
}
out = translate_codex_event(evt, title="Codex")
out = _translate_event(evt)
assert len(out) == 1
assert isinstance(out[0], ActionEvent)
summary = out[0].action.detail["result_summary"]
@@ -36,38 +52,19 @@ def test_translate_mcp_tool_call_summarizes_null_structured_content() -> None:
"type": "mcp_tool_call",
"server": "docs",
"tool": "search",
"arguments": None,
"result": {"content": [], "structured_content": None},
"error": None,
"status": "completed",
},
}
out = translate_codex_event(evt, title="Codex")
out = _translate_event(evt)
assert len(out) == 1
assert isinstance(out[0], ActionEvent)
assert out[0].action.detail["result_summary"]["has_structured"] is False
def test_translate_mcp_tool_call_summarizes_legacy_structured_key() -> None:
evt = {
"type": "item.completed",
"item": {
"id": "item_3",
"type": "mcp_tool_call",
"server": "docs",
"tool": "search",
"result": {"structured": {"matches": 3}},
"error": None,
"status": "completed",
},
}
out = translate_codex_event(evt, title="Codex")
assert len(out) == 1
assert isinstance(out[0], ActionEvent)
assert out[0].action.detail["result_summary"]["has_structured"] is True
def test_translate_mcp_tool_call_missing_error_is_ok() -> None:
evt = {
"type": "item.completed",
@@ -76,18 +73,20 @@ def test_translate_mcp_tool_call_missing_error_is_ok() -> None:
"type": "mcp_tool_call",
"server": "docs",
"tool": "search",
"arguments": None,
"status": "completed",
"result": {"content": []},
"result": {"content": [], "structured_content": None},
"error": None,
},
}
out = translate_codex_event(evt, title="Codex")
out = _translate_event(evt)
assert len(out) == 1
assert isinstance(out[0], ActionEvent)
assert out[0].ok is True
def test_translate_command_execution_allows_missing_exit_code() -> None:
def test_translate_command_execution_allows_null_exit_code() -> None:
evt = {
"type": "item.completed",
"item": {
@@ -95,11 +94,12 @@ def test_translate_command_execution_allows_missing_exit_code() -> None:
"type": "command_execution",
"command": "ls -la",
"aggregated_output": "",
"exit_code": None,
"status": "completed",
},
}
out = translate_codex_event(evt, title="Codex")
out = _translate_event(evt)
assert len(out) == 1
assert isinstance(out[0], ActionEvent)
assert out[0].ok is True
+2 -2
View File
@@ -218,7 +218,7 @@ async def test_codex_runner_preserves_warning_order(tmp_path) -> None:
"import sys\n"
"\n"
"sys.stdin.read()\n"
"print(json.dumps({'type': 'error', 'message': 'warning one', 'fatal': False}), flush=True)\n"
"print(json.dumps({'type': 'error', 'message': 'warning one'}), flush=True)\n"
f"print(json.dumps({{'type': 'thread.started', 'thread_id': '{thread_id}'}}), flush=True)\n"
"print(json.dumps({'type': 'item.completed', 'item': {'id': 'item_0', 'type': 'agent_message', 'text': 'ok'}}), flush=True)\n",
encoding="utf-8",
@@ -335,7 +335,7 @@ async def test_codex_runner_includes_stderr_reason(tmp_path) -> None:
assert completed.ok is False
assert completed.error is not None
assert "codex exec failed (rc=1)." in completed.error
assert "\n\nNot inside a trusted directory" in completed.error
assert "Not inside a trusted directory" not in completed.error
@pytest.mark.anyio
+99 -72
View File
@@ -11,11 +11,26 @@ from takopi.runners.opencode import (
ENGINE,
translate_opencode_event,
)
from takopi.schemas import opencode as opencode_schema
def _load_fixture(name: str) -> list[dict]:
def _load_fixture(name: str) -> list[opencode_schema.OpenCodeEvent]:
path = Path(__file__).parent / "fixtures" / name
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
events: list[opencode_schema.OpenCodeEvent] = []
for line in path.read_bytes().splitlines():
if not line.strip():
continue
try:
events.append(opencode_schema.decode_event(line))
except Exception as exc:
raise AssertionError(
f"{name} contained unparseable line: {line!r}"
) from exc
return events
def _decode_event(payload: dict) -> opencode_schema.OpenCodeEvent:
return opencode_schema.decode_event(json.dumps(payload).encode("utf-8"))
def test_opencode_resume_format_and_extract() -> None:
@@ -59,9 +74,6 @@ def test_translate_success_fixture() -> None:
assert completed.resume == started.resume
assert completed.answer == "```\nhello\n```"
assert completed.usage is not None
assert "tokens" in completed.usage
def test_translate_missing_reason_success() -> None:
state = OpenCodeStreamState()
@@ -74,7 +86,6 @@ def test_translate_missing_reason_success() -> None:
fallback = runner.stream_end_events(
resume=None,
found_session=started.resume,
stderr_tail="",
state=state,
)
@@ -82,14 +93,13 @@ def test_translate_missing_reason_success() -> None:
assert completed.ok is True
assert completed.resume == started.resume
assert completed.answer == "All done."
assert completed.usage is not None
def test_translate_accumulates_text() -> None:
state = OpenCodeStreamState()
events = translate_opencode_event(
{"type": "step_start", "sessionID": "ses_test123", "part": {}},
_decode_event({"type": "step_start", "sessionID": "ses_test123", "part": {}}),
title="opencode",
state=state,
)
@@ -97,20 +107,24 @@ def test_translate_accumulates_text() -> None:
assert isinstance(events[0], StartedEvent)
translate_opencode_event(
{
"type": "text",
"sessionID": "ses_test123",
"part": {"type": "text", "text": "Hello "},
},
_decode_event(
{
"type": "text",
"sessionID": "ses_test123",
"part": {"type": "text", "text": "Hello "},
}
),
title="opencode",
state=state,
)
translate_opencode_event(
{
"type": "text",
"sessionID": "ses_test123",
"part": {"type": "text", "text": "World"},
},
_decode_event(
{
"type": "text",
"sessionID": "ses_test123",
"part": {"type": "text", "text": "World"},
}
),
title="opencode",
state=state,
)
@@ -118,11 +132,13 @@ def test_translate_accumulates_text() -> None:
assert state.last_text == "Hello World"
events = translate_opencode_event(
{
"type": "step_finish",
"sessionID": "ses_test123",
"part": {"reason": "stop", "tokens": {"input": 100, "output": 10}},
},
_decode_event(
{
"type": "step_finish",
"sessionID": "ses_test123",
"part": {"reason": "stop", "tokens": {"input": 100, "output": 10}},
}
),
title="opencode",
state=state,
)
@@ -140,22 +156,24 @@ def test_translate_tool_use_completed() -> None:
state.emitted_started = True
events = translate_opencode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "bash",
"state": {
"status": "completed",
"input": {"command": "ls -la"},
"output": "file1.txt\nfile2.txt",
"title": "List files",
"metadata": {"exit": 0},
_decode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "bash",
"state": {
"status": "completed",
"input": {"command": "ls -la"},
"output": "file1.txt\nfile2.txt",
"title": "List files",
"metadata": {"exit": 0},
},
},
},
},
}
),
title="opencode",
state=state,
)
@@ -175,22 +193,24 @@ def test_translate_tool_use_with_error() -> None:
state.emitted_started = True
events = translate_opencode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "bash",
"state": {
"status": "completed",
"input": {"command": "exit 1"},
"output": "error",
"title": "Run failing command",
"metadata": {"exit": 1},
_decode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "bash",
"state": {
"status": "completed",
"input": {"command": "exit 1"},
"output": "error",
"title": "Run failing command",
"metadata": {"exit": 1},
},
},
},
},
}
),
title="opencode",
state=state,
)
@@ -209,21 +229,23 @@ def test_translate_tool_use_read_title_wraps_path() -> None:
path = Path.cwd() / "src" / "takopi" / "runners" / "opencode.py"
events = translate_opencode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "read",
"state": {
"status": "completed",
"input": {"filePath": str(path)},
"output": "file contents",
"title": "src/takopi/runners/opencode.py",
_decode_event(
{
"type": "tool_use",
"sessionID": "ses_test123",
"part": {
"id": "prt_123",
"callID": "call_abc",
"tool": "read",
"state": {
"status": "completed",
"input": {"filePath": str(path)},
"output": "file contents",
"title": "src/takopi/runners/opencode.py",
},
},
},
},
}
),
title="opencode",
state=state,
)
@@ -255,11 +277,16 @@ def test_step_finish_tool_calls_does_not_complete() -> None:
state.emitted_started = True
events = translate_opencode_event(
{
"type": "step_finish",
"sessionID": "ses_test123",
"part": {"reason": "tool-calls", "tokens": {"input": 100, "output": 10}},
},
_decode_event(
{
"type": "step_finish",
"sessionID": "ses_test123",
"part": {
"reason": "tool-calls",
"tokens": {"input": 100, "output": 10},
},
}
),
title="opencode",
state=state,
)
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from pathlib import Path
import pytest
from takopi.schemas import opencode as opencode_schema
def _fixture_path(name: str) -> Path:
return Path(__file__).parent / "fixtures" / name
def _decode_fixture(name: str) -> list[str]:
path = _fixture_path(name)
errors: list[str] = []
for lineno, line in enumerate(path.read_bytes().splitlines(), 1):
if not line.strip():
continue
try:
opencode_schema.decode_event(line)
except Exception as exc:
errors.append(f"line {lineno}: {exc.__class__.__name__}: {exc}")
return errors
@pytest.mark.parametrize(
"fixture",
[
"opencode_stream_success.jsonl",
"opencode_stream_success_no_reason.jsonl",
"opencode_stream_error.jsonl",
],
)
def test_opencode_schema_parses_fixture(fixture: str) -> None:
errors = _decode_fixture(fixture)
assert not errors, f"{fixture} had {len(errors)} errors: " + "; ".join(errors[:5])
+12 -3
View File
@@ -1,4 +1,3 @@
import json
from pathlib import Path
import anyio
@@ -6,11 +5,21 @@ import pytest
from takopi.model import ActionEvent, CompletedEvent, ResumeToken, StartedEvent
from takopi.runners.pi import ENGINE, PiRunner, PiStreamState, translate_pi_event
from takopi.schemas import pi as pi_schema
def _load_fixture(name: str) -> list[dict]:
def _load_fixture(name: str) -> list[pi_schema.PiEvent]:
path = Path(__file__).parent / "fixtures" / name
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
events: list[pi_schema.PiEvent] = []
for line in path.read_text().splitlines():
if not line.strip():
continue
try:
decoded = pi_schema.decode_event(line)
except Exception as exc:
raise AssertionError(f"{name} contained unparseable line: {line}") from exc
events.append(decoded)
return events
def test_pi_resume_format_and_extract() -> None:
+39
View File
@@ -0,0 +1,39 @@
from __future__ import annotations
from pathlib import Path
import pytest
from takopi.schemas import pi as pi_schema
def _fixture_path(name: str) -> Path:
return Path(__file__).parent / "fixtures" / name
def _decode_fixture(name: str) -> list[str]:
path = _fixture_path(name)
errors: list[str] = []
for lineno, line in enumerate(path.read_text().splitlines(), 1):
if not line.strip():
continue
try:
pi_schema.decode_event(line)
except Exception as exc:
errors.append(f"line {lineno}: {exc.__class__.__name__}: {exc}")
return errors
@pytest.mark.parametrize(
"fixture",
[
"pi_stream_success.jsonl",
"pi_stream_error.jsonl",
"pi_print_mode_events.jsonl",
],
)
def test_pi_schema_parses_fixture(fixture: str) -> None:
errors = _decode_fixture(fixture)
assert not errors, f"{fixture} had {len(errors)} errors: " + "; ".join(errors[:5])